@fnndsc/brasa 0.33.1 → 0.33.2

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.
@@ -1 +1 @@
1
- {"hash":"455a27"}
1
+ {"hash":"84821d"}
package/dist/notes.json CHANGED
@@ -1 +1 @@
1
- {"package":"@fnndsc/brasa","version":"0.33.1","built":"2026-10-07T12:28:03.321Z","releases":[{"version":"0.33.1","date":"2026-10-07","merge":"455a27d24292","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [c5e033d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [71fe8a5]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [de04249] - @fnndsc/orrery@0.4.0","body":"","internal":true}]},{"version":"0.33.0","date":"2026-10-05","merge":"ecfe0e091342","bump":"minor","changes":[{"hash":"5a0eef5","headline":"The session can say what the installed releases changed.","body":"`notes` lists the newest release — one row per change, the package named — and `notes --since N|DATE`, `--long` and `--all` reach further; `help notes` is the same; `/usr/share/doc/NEWS` holds every release. The notes are read from the packages installed where the session runs, so they describe the code that is running.","internal":false},{"hash":null,"headline":"Updated dependencies [5a0eef5] - @fnndsc/menu@0.19.0 - @fnndsc/cumin@3.27.5","body":"","internal":true}]},{"version":"0.32.1","date":"2026-10-05","merge":"f194de9ca69f","bump":"patch","changes":[{"hash":"5b64328","headline":"Bare `help` has a \"Games and utilities\" group: fortune, cal, weather, motd, and a `help games` row saying how many more small tools and games the /usr/games shelf holds (the count read from the shelf).","body":"General keeps help, date, exit and `!`. And `sl` flies the Enterprise: the Constitution-class refit side view from the Star Trek ASCII Art blog (re-use granted with attribution, kept in the source).","internal":false}]},{"version":"0.32.0","date":"2026-10-05","merge":"ff1316b2d9fb","bump":"minor","changes":[{"hash":"6901a81","headline":"The games shelf (#892): `help games` lists `/usr/games` by category and `ls /usr/games` walks it;","body":"the text toys (cowsay with the ChRIS brain, cowthink, figlet, banner, lolcat, rev, tac, yes, seq, factor, primes, shuf, roll, rot13, morse, calc, units), the lab's own (wtf, chrisfetch, who, uptime, ping, file, xxd, strings, sha256sum, md5sum, qr, say), time and sky (pom, stardate, ddate, sunrise; the chimes timer, leave, stopwatch on the ambient channel), the showpieces (sl, cmatrix, rain, asciiquarium) and the games (tetris, snake on a surface with a canvas; quiz, hangman, 2048 by question anywhere). A pipe segment naming a builtin now runs in the kernel, so `fortune | cowsay` works in a browser; a segment the surface cannot run is refused in words.","internal":false}]},{"version":"0.31.5","date":"2026-10-04","merge":"8b4c28c80fc2","bump":"patch","changes":[{"hash":"73c0087","headline":"A PACS series that arrives is no longer shown as ERROR.","body":"The retrieve watch always asks CUBE before it gives up on a series (with no retries, the default, it never did), a series not seen to finish and not yet in CUBE is UNCONFIRMED with its reason rather than ERROR, and the watch waits a minute (not fifteen seconds) for a PACS slow to begin. Progress messages carry an optional `reason`, shown on the ARGUS badge and in pull's summary. In ARGUS a fresh answer finding a series home in CUBE replaces a pull that ended badly, and a pull waiting on its first file paces again instead of drawing a still empty bar. After twenty seconds of silence a pull checks CUBE and then asks whether to keep waiting (a surface that cannot be asked waits a minute). ARGUS runs the PACS pane's pull and query as the operator's own acts rather than instrument traffic, so they can ask.","internal":false},{"hash":null,"headline":"Updated dependencies [73c0087] - @fnndsc/salsa@3.21.1 - @fnndsc/menu@0.18.0 - @fnndsc/cumin@3.27.4","body":"","internal":true}]},{"version":"0.31.4","date":"2026-10-04","merge":"e57ca8f142f0","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [baec61d] - @fnndsc/orrery@0.3.0","body":"","internal":true}]},{"version":"0.31.3","date":"2026-10-03","merge":"607f07da0f3b","bump":"patch","changes":[{"hash":"ff80b59","headline":"A shared feed's `data` opens again.","body":"Listing a /proc link into a feed another user owns walked the owner's home asking for links, and that refusal, a side question, marked the whole `ls` an error though its listing came. chili's path walk no longer leaves its link probe's refusal on the error stack; it remembers the parent it could not read instead. When a listing then fails, `ls` says so after the reason (`ls: links in '/home/<owner>' could not be read; the path was walked as written`), because a link there, unseen, may be why the path was not found; when the listing succeeds nothing is said. ARGUS's files panel draws a listing the kernel sent even when the command is marked an error, and a refused press reads every line the kernel said, so the note reaches the bar too.","internal":false},{"hash":null,"headline":"Updated dependencies [ff80b59] - @fnndsc/chili@3.7.1","body":"","internal":true}]},{"version":"0.31.2","date":"2026-10-03","merge":"b737b6d5269e","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [51315ec]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [33e4f1d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fe4b6b4]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2d26368] - @fnndsc/menu@0.17.0 - @fnndsc/cumin@3.27.2","body":"","internal":true}]},{"version":"0.31.1","date":"2026-10-03","merge":"ff157a55d5ed","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [685ed59] - @fnndsc/menu@0.16.0 - @fnndsc/cumin@3.27.1","body":"","internal":true}]},{"version":"0.31.0","date":"2026-10-03","merge":"30fbc2abbc84","bump":"minor","changes":[{"hash":"c1cf57e","headline":"An edit crosses the wire to a browser (#831, slice 2).","body":"The daemon's `edit` request now names the file being edited (`path`), and a surface may answer `opened`: it took the file into an editor that stays open, so the command saves nothing and says `(opened in the editor)`; each save made in that editor will run as its own command line. The argus client hands an edit to a host editor hook and answers at once that it opened, or refuses in words when it has none (the hook is installed by the editor pane, slice 3). `feed note edit N` is now `edit /proc/jobs/feed_N/note`: a terminal's editor and a browser's pane open the same text and save it the same way, and the title is set with `feed note N --title`.","internal":false},{"hash":"1c8b92d","headline":"A feed's name and sharing, whole, in the kernel (#853, slice 1).","body":"`/proc/jobs/feed_N/title` is writable: its first line renames the feed (`touch --withContents=`, `edit`), and the roster says so at once. `setfacl` speaks every entry CUBE has: `-m u:<user>:r`, `-m g:<group>:r`, `-m o::r` (public) and `-m o::-` (private again); `-x u:<user>` and `-x g:<group>` withdraw a grant (a feed that held none is said). `chmod o+r` / `o-r` is the other entry spelled as a mode. `getfacl` shows users, groups and `other::`. A feed is also named by `/proc/jobs/feed_N`. `feeds share` is retired and refuses by name; SHARE's question no longer says a grant is permanent; the access and tag verbs get a help category. cumin gains `feedAccess_read`, `feedShare_group`, `feedShare_revoke` and `feed_rename`.","internal":false},{"hash":"e20fb09","headline":"A feed's note is a file (#831, slice 1b): `/proc/jobs/feed_N/note` lists beside `status` and `title`, `cat` reads it and `touch --withContents` writes it whole (its title stands), and `edit /proc/jobs/feed_N/note` opens it in the surface's editor and saves it back through the projection — a projected file is never uploaded into CUBE's store.","body":"`feed note edit` now edits through the surface's editor seam (`localEdit`) instead of spawning `$EDITOR` itself, so a browser surface can offer its own editor; a surface without one refuses by name. Fixed on the way: an empty `# Title:` line no longer takes the note's first line as its title.","internal":false},{"hash":"ddbd551","headline":"A feed's tags are kernel-covered (#831, slice 1a): cumin's `feedTags_list`, `feedTag_add` (the user's tag of that name, made if missing;","body":"idempotent) and `feedTag_remove` (the tagging goes, the tag stays); brasa speaks them as an extended attribute — `getfattr feed_12` dumps them, `setfattr -n tag -v urgent feed_12` hangs one, `setfattr -x tag -v urgent feed_12` takes it off, `setfattr -x tag feed_12` takes every tag off. A feed is named by id, feed_N, a /feeds/ path or /proc/jobs/feed_N; another attribute or flag is refused by name.","internal":false},{"hash":"32e3919","headline":"`ls -l` shows a feed's tags (#831, slice 1c): a long listing that holds feeds (`feed_N` rows, as in `/proc/jobs` or `~/feeds`) prints each feed's tags as `#tags` after its title.","body":"The tags come from one map for the whole listing — the user's tags, then each tag's feeds — so a thousand feeds cost as many reads as the user has tags, never one per feed; the map is kept a minute and forgotten whenever a tag is added or removed here. A listing with no feed in it reads nothing.","internal":false},{"hash":"59399fe","headline":"Tags are a vocabulary of folders under `/proc/tags` (#831, slice 4a).","body":"`ls /proc/tags` lists your tags (an unworn one too); `ls /proc/tags/<tag>` lists the feeds wearing it as links to `/proc/jobs/feed_N`, and `cd` through one lands in the feed. `mkdir /proc/tags/<name>` makes a tag, `rmdir` deletes one no feed wears (\"Directory not empty\" otherwise), and `mv` renames one. `setfattr -n tag -v <name>` no longer makes a tag: naming one that does not exist is an error naming the cure (`mkdir /proc/tags/<name>`). `rm` under `/proc/tags` is refused and names the verb that does the job. New builtin `rmdir` (empty folders only, as on Linux, on the store and in projections); `mkdir` and `mv` route a projection's paths to it, and a projection that makes or renames nothing refuses by name (\"Read-only file system\", \"Invalid cross-device link\"). The old read-only `/tags` projection is retired. cumin gains `tags_index`, `tag_create`, `tag_delete`, `tag_rename` over one cached index of the user's tags and their feeds.","internal":false},{"hash":"51da853","headline":"ARGUS shows and changes a feed's tags (#831, slice 4b).","body":"The kernel's roster model (`feed.list`) carries each feed's tags from the cached tags index (one read per tag, never per feed). The runs roster marks a feed with its tags after its title, in one theme hue; a press on a mark filters the roster by it (`tag:`, a new filter-only term in the listing façade), and its × on the indicated row takes it off. A feed row offers NOTE (the editor on `/proc/jobs/feed_N/note`) and TAG. TAG asks a new `choose` question: your tags as pills (the worn ones dimmed), a field for a new name, DONE to end; each pick runs `setfattr`, a new name `mkdir /proc/tags/<name>` first, all as visible lines, and the question comes back with the tag held. A graph on stage reads its feed's note and tags when entered and shows ADD NOTE or EDIT NOTE, TAG and the marks under BACK, re-read after an editor save. A note is never read for the roster (CUBE's feed list says nothing about notes: ChRIS_ultron_backEnd#737).","internal":false},{"hash":"e057d20","headline":"ARGUS edits a file in a pane (#831, slice 3).","body":"The kernel's `edit <file>` (and `feed note edit N`) opens an EDIT pane beside the focused one: CodeMirror 6 as a guest field, loaded on demand and painted from the theme's tokens; SAVE and REVERT on the mode frame, lit only while the field differs from the file; the bar says DIRTY or SAVED; Ctrl-S saves and Esc gives the keyboard back. Every save is the line the operator could have typed, `touch --withContents='<text>' '<path>'`, echoed and run, so the transcript holds each save and a feed's note is written through its projection. A second `edit` of the same file finds its pane and keeps unsaved changes; a desktop card brings the pane back on its file. brasa fixes found on the way: the semicolon splitter honours a backslash escape (an escaped quote no longer ends the quoting, so a later `;` inside it no longer splits the line), and an empty value is a value (`--withContents ''` writes an empty file instead of touching the empty word as a path).","internal":false},{"hash":"2826f2b","headline":"A text file offers EDIT (#831, slice 3b).","body":"A file row whose extension is not on the binary list offers EDIT in ARGUS, a feed's note seen through `/proc` included; it runs `edit '<path>'` as a visible line and the kernel opens the editor pane, asking first for a file of a megabyte or more. The binary list moved into the contract package (`EDIT_BINARY_EXTENSIONS`, `path_isEditable`, `EDIT_CONFIRM_BYTES` in @fnndsc/menu), read by the kernel's `edit` for its refusal and by the surface for its offer, so the two cannot disagree. `feed note edit` help now says it edits in the surface's editor. New `docs/cube-feed-coverage.adoc`: each part of a CUBE feed, the kernel verb or path that reaches it, and the known gaps (rename, group permissions, revoking a grant, making a feed private again).","internal":false},{"hash":null,"headline":"Updated dependencies [2826f2b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c1cf57e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c8b92d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [e20fb09]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [ddbd551]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [32e3919]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [59399fe]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [51da853] - @fnndsc/menu@0.15.0 - @fnndsc/cumin@3.27.0 - @fnndsc/salsa@3.21.0 - @fnndsc/chili@3.7.0","body":"","internal":true}]},{"version":"0.30.0","date":"2026-10-01","merge":"0491d58506dd","bump":"minor","changes":[{"hash":"4864a34","headline":"`netstat`: the wire is a readout.","body":"The kernel keeps a ledger of every request it makes to CUBE (Node's diagnostics channels, nothing patched) — count since the session or the last reset, by endpoint family with time spent, the last few timed — and `netstat [-n <count>] [-r]` reads it as text and as the `net.stats` model.","internal":false},{"hash":"51f1ca9","headline":"`cd` into a virtual directory lists it through the listing façade — the cache the boot warmed and `ls` fills serves at once, a stale entry is refreshed behind the prompt — instead of fetching the whole of `/bin` from CUBE on every `cd`.","body":"","internal":false},{"hash":"c9c018b","headline":"`cd` reads the parent from the listing cache before asking CUBE: every `cd` listed the parent from CUBE (three requests) to learn whether the entry was a link, though the parent was the listing on screen.","body":"","internal":false},{"hash":"c371b47","headline":"A feed the roster gains after the topology sweep lands its jobs on its own: every roster sync (the watcher's delta, the full walk, a restored roster brought into service) queues the feeds it leaves without topology, and one detached walk at a time lands them behind the prompt — so a feed created after the sweep, or restored from a roster whose shard was never written, shows SIZE and TIME in the RUNS listing without anyone opening it.","body":"`proc stat` names the feeds still landing.","internal":false},{"hash":null,"headline":"Updated dependencies [fbb1579]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [576b948]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4864a34]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7fb4c21]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c371b47] - @fnndsc/cumin@3.26.0 - @fnndsc/menu@0.14.0 - @fnndsc/salsa@3.20.1","body":"","internal":true}]},{"version":"0.29.1","date":"2026-10-01","merge":"1b77ac182c25","bump":"patch","changes":[{"hash":"cbd71cc","headline":"A refused file read says why: the kernel names the status and reason (403 not yours to read, 404 no such file, 502 CUBE could not serve it), `/vfs` answers with them instead of 404 for everything, and an image pane that lost a slice reads the reason out.","body":"`porter --end <who>` ends one session so the next login boots on the current kernel.","internal":false}]},{"version":"0.29.0","date":"2026-09-26","merge":"a1a7cf9a2b2d","bump":"minor","changes":[{"hash":"fbd3f0a","headline":"The session lays the universe out itself: once the index is whole the daemon runs orrery's engines for every layout in a worker thread and keeps the places (`proc layout <name>` reports `laying` progress meanwhile).","body":"orrery is published; `layoutInput_build` builds the engines' input one way for browser and session, and the universe graph builders live in `@fnndsc/menu`. ARGUS waits on the session's progress instead of settling the same space, keeps places only for the view on stage (SHAPES apart), redraws a view already seen where it stood, and gives the session only default-physics places.","internal":false},{"hash":null,"headline":"Updated dependencies [fbd3f0a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fbd3f0a] - @fnndsc/orrery@0.2.0 - @fnndsc/menu@0.13.0 - @fnndsc/cumin@3.25.0 - @fnndsc/salsa@3.20.0","body":"","internal":true}]},{"version":"0.28.0","date":"2026-09-26","merge":"70016c84b4f0","bump":"minor","changes":[{"hash":"0f9ae74","headline":"The universe laid out once per identity: `proc layout <name>` answers where a surface last laid the universe out under a layout, and `proc layout put <name> <places>` keeps it beside the index checkpoint.","body":"ARGUS puts each settled layout and, in a browser that keeps none, draws from the session's copy at once, settling only feeds new since.","internal":false},{"hash":"d1da8ac","headline":"`proc universe` carries what each feed's data is (`data`: format, and for DICOM modality and series description) once the index has read it.","body":"The reader follows a copy job's links (to files and to folders), reads newest feeds first, four at a time, gives a feed that never answers 45 s before moving on, records a DICOM-named file whose header will not read as DICOM with the reason, and stamps each record with its reader's version so a better reader reads feeds again.","internal":false},{"hash":"d8e160c","headline":"`proc universe` says when each feed was made (`createdAt`, ISO 8601), the order a replay reveals the space in;","body":"a model from an older daemon reads it as empty.","internal":false},{"hash":null,"headline":"Updated dependencies [e7f82dd]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8602c78]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a8d0d99]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [f571a14]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d1da8ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [0f9ae74]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d8e160c] - @fnndsc/cumin@3.24.0 - @fnndsc/salsa@3.19.0 - @fnndsc/menu@0.12.0","body":"","internal":true}]},{"version":"0.27.0","date":"2026-09-24","merge":"43521c122e64","bump":"minor","changes":[{"hash":"53d43f6","headline":"`mkdir` follows POSIX: `-p`/`--parents` makes missing parents and accepts an existing folder;","body":"without it a missing parent is \"No such file or directory\" and an existing folder \"File exists\". `rm` says \"Is a directory\" for a directory without `-r`, reads `--recursive`/`--force`/`--interactive` whole, and stays silent on `-f` of a missing operand. `mkdir`, `rm`, `cp`, `mv` and `touch` refuse an option they do not have, by name, instead of skipping it or taking it for a path. argus ensures `~/gather` with `mkdir -p`.","internal":false}]},{"version":"0.26.0","date":"2026-09-23","merge":"4ce95fee1d4a","bump":"minor","changes":[{"hash":"7b89dcc","headline":"brasa: `pacs query --series` answers one row per series (each carrying its study's columns) on the screen and in the CSV, and alone shows the series table.","body":"`--table` shows the answer's rows aligned: the CSV's columns, for a screen, nothing cut. `--csv --table` is refused by name (two formats for one screen); `--csv-to <path> --table` writes the CSV and shows the table. A flag the command does not know is refused by name instead of being dropped in silence. `--anon` is in the help.","internal":false}]},{"version":"0.25.0","date":"2026-09-22","merge":"c1b0be0099d8","bump":"minor","changes":[{"hash":"dac2d96","headline":"brasa: `motd [surface]`, the greeting a session gives on arrival — who you are, how many feeds you hold, how many jobs have run and what share failed, what is running now, from the session's own index at no cost;","body":"\"so far\" while the index warms; ends with a fortune. A surface may name itself (`motd ARGUS`).","internal":false},{"hash":"c82f514","headline":"brasa: `proc universe` names each feed.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [247bf09]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [dac2d96]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50f8a4e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50f8a4e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c82f514]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c82f514] - @fnndsc/cumin@3.23.1 - @fnndsc/menu@0.11.0","body":"","internal":true}]},{"version":"0.24.0","date":"2026-09-21","merge":"576426874567","bump":"minor","changes":[{"hash":"e74ce1d","headline":"brasa: the pronouns a chain needs, and claims that look where a plugin files its output.","body":"Built while writing the first battery of manifests, and each one is a thing a real workflow could not say before:\n\n`${run.place}` — where the last run writes. A chain works on what the previous act produced, and only the kernel knows where CUBE put it; a manifest cannot spell a path that holds an instance id it never saw. With it, `cd ${run.place}` chains one plugin onto another's output.\n\n`expect path <p> count --deep` and a tag claim that looks beneath the path it was given. A plugin files its output in a tree — pfdicom writes under `share/incoming/<input tree>` — so a claim made at the node's own folder was false about a run that had worked perfectly. `--deep` is declared a boolean flag, because undeclared it took the comparison as its value and the claim lost its predicate.\n\nA dry run leaves a pronoun it cannot know as it was written, rather than refusing: nothing has run to give `${run}` a value, and refusing there made `--dry-run` useless for exactly the manifests that chain. `play` also reads a manifest from the engine's own disk when the ChRIS filesystem has no such file, which is how a repository's battery runs without uploading itself first — and it drains the CFS miss it went looking for, which was otherwise left behind and made a clean play exit non-zero.\n\nA claim now prints as it was MADE, narrowing included: a ✓ reading `count gt 0` for a claim about `*.nii*` says the wrong thing held, and a battery is read by people who were not there.","internal":false},{"hash":"1b2f7f7","headline":"brasa: `expect` — a claim the session can refuse.","body":"A workflow that only acts is a demonstration. `expect` is what makes one a test: it states a claim about the session and refuses, non-zero, when the claim does not hold. Because it is a verb rather than a harness, a workflow asserts wherever it is played — in CI, at a console, or in someone else's browser after they were handed the file.\n\n`expect gather size eq 14`, `expect feed 4599 status eq finishedSuccessfully`, `expect run 601152 status eq finishedSuccessfully --within 20m`, `expect path <p> count --matching '*.nii*' gt 0`, `expect path <p> tags PatientID eq anon-001`. Comparisons are `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `notContains` and `matches`.\n\nTime lives in this verb and nowhere else: `--within` keeps asking until the claim holds or the deadline passes, which is why no other verb needs a timeout and why the language has no sleep — a sleep asserts nothing and lies about how long the work took. A failed claim prints what it wanted, what it got, and how long it waited.\n\nTwo decisions worth naming. A subject that cannot answer — a feed this session has never indexed, a path holding no readable DICOM — is reported as NOT KNOWING rather than as a false claim, because those are different facts. And a DICOM tag that varies across a folder answers with every value it takes, so a claim about \"the\" value cannot pass by reading only the first file; that is what makes the tag check an honest proof that an anonymization ran.\n\nThird slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"3320487","headline":"brasa: the cohort is a kernel subject.","body":"`gather list|add|remove|clear|name` holds the set a session is working on, so building a cohort no longer requires a surface to press on. Membership is keyed by PATH: a PACS series still at the modality, a directory of uploads and a single file are all addressable, so one identity rule covers every kind, gathering the same thing twice merges rather than doubles, and `gather remove` takes the same operand whatever it is removing — or the index the operator just read back.\n\nThe cohort lives where the surface already kept it, `~/gather/current.json`, in the shape the surface already wrote, so the two read each other's cohorts with no migration: the kernel fills in what an older record left out (a member written before kinds existed is a series; one written before paths were identity carries its address in `seriesUID`), and preserves the fields it has no name for, because a surface knows things about a series that the kernel does not and a kernel write must not quietly forget them. What a gathered path IS is asked of the filesystem rather than guessed from its spelling, and the holding directory is made and its listing invalidated on every write — a cohort the next `ls` cannot see is the staleness a CSV export already taught once.\n\nThis is the first slice of the manifest work (`docs/manifest.adoc`): a workflow can now gather.","internal":false},{"hash":"a010cb0","headline":"brasa: `play` — a manifest runs, and plays.","body":"A manifest is a text file of the lines an operator could have typed. `play` reads it and feeds those lines to the session one at a time, through the same path a typed line takes — there is no second runtime, no driver and no selector language. Because every verb lowers to a line and every line's answer is a typed model the surface renders, a manifest played at a surface PLAYS: the PACS pane fills, the cohort's face counts up, a viewer opens. Played headlessly the same file prints the same envelopes and exits 0 or non-zero. Neither run knows which it is.\n\nA file says what it is (`@name`, `@description`) and what it needs (`@param NAME`, `@param NAME = default`). Everything it needs is settled BEFORE the first line runs: a manifest that gets half way and then asks has already changed the session. Lines may refer to what the session just did through a closed set of pronouns — `${gather}`, `${gather.first}`, `${gather.3}`, `${gather.size}`, `${feed}`, `${run}`, `${query}`, `${cwd}` — and a pronoun the session cannot answer refuses by name rather than expanding to nothing.\n\nA member of a cohort has two addresses and the difference matters: where it was gathered FROM, which `pull` takes, and where it LANDED, which `image` and a run take. `${gather.first}` is the first; `${gather.first.place}` is the second, and says so when a member is not in CUBE yet.\n\n`--pace` waits between lines, for watching a play at a surface. `--dry-run` expands everything and shows the lines without running them. `--step` is refused by name, because a keypress needs a terminal and a daemon has none. A refused line stops the play, naming the file and line, and the play exits non-zero — the second half of a workflow whose first half failed is noise.\n\nFourth slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"8a8dc25","headline":"A patient answers to a number: `@PAT001`.","body":"A PACS answer asked of two patients showed `1` beside each — the patient level had no kind, so it counted itself dim per group. Patients are now a kind of their own: `PAT001`, `PAT002` in their own sequence, lit on the pane, and `pull @PAT001` or `gather add @PAT001` hands a verb every series of every study of theirs, as the row's own GATHER does. A patient has no path of its own, so its address is minted from what names it (`patientAddress_of`, in the wire package, the same string on both sides); a patient the PACS answered nothing for has nothing to hand and wears no number.","internal":false},{"hash":"03e0f87","headline":"brasa: a row can be named by its number.","body":"An operator reads a listing and wants the third row. Saying which row meant repeating its address — for a PACS series, a line of text nobody types by hand — so acting on what you can see was a surface gesture, and a scripted session could not do it at all.\n\nNow a command that answers with rows records them as the session's last answer, and `@3` names one: `gather add @2,3,6`, `image @1`, `gather remove @15,16`. Lists and ranges read as they look (`@2-4`), and because the index is resolved by the one expander, every verb takes one without knowing anything about it.\n\nWhat gets numbered is the MODEL, not a rendering of it, so a console table and a graphical pane count the same rows — an index that meant different things on different surfaces would be worse than no index. What a row is worth is declared per listing (`fs.listing` and a PACS answer give paths; the cohort gives its members' paths), so a verb receives the operand it already takes.\n\nThe sigil is what keeps it honest: a directory called `2` is a path and `@2` never is, `'@2'` in quotes is text, and `user@host` is not an index because only a number at the start of a word is one. A line that acts on a number says which listing it counted (`from: ls ~/uploads · 7 rows`), and a number past the end refuses with how many rows there are rather than acting on the wrong one. An answer with no rows does not replace one that has them: an empty `ls` should not un-number the listing just read.\n\nNumbering lives as long as the session, so at a daemon it spans commands and at `chell -c` it spans the one line — `ls ~/uploads; gather add @1` is the one-shot form.","internal":false},{"hash":"1bdf876","headline":"A row wears the number it answers to.","body":"A row of the session's last answer can be named by the number beside it — `gather add @2,3,6`, `image @1` — so the number is now DRAWN beside it. The listing façade mints a leading index pill for any level that says how its rows are addressed, and no pane draws a number itself.\n\nWhat is numbered is the kernel's ANSWER rather than a rendering of it, so a console table and a graphical pane count the same rows. The lit number is found BY ADDRESS, not by counting down the screen, so a sorted or filtered listing keeps each row's own number instead of renumbering under the operator.\n\nEvery listing leads with the column and every row counts itself; only the rows the kernel reaches wear a LIT number. In a PACS answer that is the series, while the patient and study levels above count themselves dim — the distinction is the hue, never the digits, so a number nothing answers to cannot be mistaken for one that does. Numbers are zero-padded to the widest in their group, so a fourteen-series answer reads 01…14 and the column stays a column. The lit hue is the pane's FRAME hue (`--listing-index-hue`, set per pane), because the pill is chrome and not content: an orange pill beside the browser's orange kind glyph read as one blob and the number stopped being a number.\n\nThe surface learns which listing is numbered from a retained `numbered` message: brasa announces it as the answer changes, calypso relays it and replays it to a surface attaching late, and the addresses travel with it (capped, past which a long listing is numbered in the kernel and unnumbered on screen). The same shape regard already travels in.\n\nLaw: `a-row-wears-the-number-it-answers-to`.","internal":false},{"hash":"854ba2e","headline":"brasa: a verb that starts work waits for it.","body":"A run returned the moment CUBE accepted it, which reads as completion and is not: the next line of a script worked on output that did not exist yet, and the only way to sequence work was a sleep — a lie about time. Running a plugin from `/bin`, `plugin run` and `pipeline run` now wait until every node they scheduled reaches a status it does not leave, and report what happened: how long it took, and which node failed when one did (a failed run exits non-zero, so a script can be believed).\n\n`--detach` asks for the handle instead, which is what these verbs did before; `pull` gained `--detach` as the language's word for the `--nowait` it already had, and still answers to both. A cancellation — Esc at a surface, Ctrl-C at a console — DETACHES rather than kills: the run continues and its handle is printed, which is `--detach` asked for later. Progress goes out on the progress channel throughout, so a surface shows the work moving rather than a frozen line, and losing touch with CUBE is reported as not knowing rather than as failure, because a run whose status cannot be read has not failed.\n\nA pipeline schedules several nodes at once, so a wait is over the SET: it ends when all of them have settled, not when the first does.\n\nSecond slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"80ddce6","headline":"brasa: `record` — a session writes itself down as a manifest.","body":"The workflows worth sharing are the fiddly ones nobody will retype correctly. `record start <manifest>` … `record stop` captures the lines the session runs and writes them as a manifest someone else can play. It costs almost nothing because the lowering was already the law: a press on a surface and a line at a console arrive at the same place, so both are captured alike.\n\nA recording is a TRANSCRIPT and stays one. It never invents expectations — a file that guessed at assertions would be a fiction, and adding `expect` lines is the author's act, the one that turns a recording into a test. It does not film its own camera (`record` and `play` lines are not captured), and it does not capture the lines a played manifest runs, which are already written down in the file being played.\n\nWhat makes a recording personal is its identifiers, so the ones a PACS line wears on its face — `PatientID`, `AccessionNumber`, `PatientName` — are offered as parameters, written as defaults so the file still plays exactly as recorded. The same value gets one name wherever it appears and a second distinct value gets a name of its own, so a workflow over two patients does not collapse into one. A value stops at an underscore or a slash: a query's projection folder is `PatientID:1279049_qid:3125_owner`, and a looser match would have parameterized the query id along with the MRN.\n\nFifth slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"5b520c7","headline":"An index says what it counts.","body":"`@2` said nothing about what it counted — in a PACS answer every study read \"1\", being the only study under its patient — so a bare number is no longer an index. A handle names its KIND and its place in that kind's sequence: `@SER3`, `@STD001`, `@FIL2,3,7`, `@DIR2-4`. One sequence per kind runs across the whole answer, so a handle is a name rather than a position and a sorted listing does not renumber it; zero padding is how the pill draws it, not something to type.\n\nWhat a row hands over differs by kind. A series, a file or a folder hands over its path; a STUDY hands over its series — what the surface's GATHER on a study hands over — so `gather add @STD001` and `pull @STD001` act on the whole study from a console exactly as a press does. A verb refuses a kind it does not take, by name and at expansion: `image @STD001` says \"image takes a series or a folder or a file; STD001 is a study\" instead of resolving to a path and failing three steps later for a reason that names nothing typed.\n\nOn the surface the pill draws the handle, lit in the pane's frame hue on the rows the session reaches, and a study row wears `STD001` where it read a meaningless \"1\". The `numbered` message carries each row's kind, place and address, so a pane finds its rows by address and draws the code the operator would type.","internal":false},{"hash":"ca3e39b","headline":"brasa: one expander, owned by the tokenizer.","body":"There were two, over one syntax. The dispatcher substituted the environment AFTER tokenizing, so a single-quoted `$HOME` expanded anyway and quoting could protect nothing. `play` substituted parameters and session pronouns BEFORE tokenizing, so a gathered path with a space had to be quoted by hand and an unresolved pronoun could fall through to the other expander. Two sources of truth that composed by accident.\n\nNow the tokenizer records references with the quoting they were written in, and one resolver answers them: the session's reserved pronouns (`gather`, `gather.first.place`, `feed`, `run`, `run.place`, `query`, `cwd`), then the played manifest's parameters, then the environment. `play` no longer substitutes anything; a manifest that declares a `@param` with a reserved name is refused when the file is read.\n\nThe shell's rules, kept, because the stack claims to wear them: a bare reference carrying several values becomes several operands, a double-quoted one stays a single operand, a single-quoted `$` is text, and an expanded value never globs afterwards. The command word expands too — a manifest names its plugin as a parameter so the same workflow runs where the builds differ.\n\nA reference nothing answers now REFUSES the line, naming it and where it looked, rather than expanding to nothing or standing as text: `rm -rf ${DIR}/scratch` with `DIR` unset is how a script deletes the wrong thing. A dry run is the one exception, having run nothing yet.\n\nBehaviour changes worth knowing: `'$HOME'` in single quotes no longer expands (it did, wrongly), and an unset reference refuses where it used to be left as literal text.","internal":false},{"hash":"7bba167","headline":"The door hands a token: a session can be started by a front that already logged the operator in.","body":"Today a session is started by the operator, at a terminal, with a password: `chell user@url -p … --daemon`. A login front on a shared host — the porter, the display manager the browser surface needs — exchanges the password for a CUBE token itself and holds the token, never the password. This is the seam that lets it start a session with what it holds.\n\n- cumin `connection_connectWithToken({ user, url, token })`: proves the token against the server BEFORE writing anything, so a refusal leaves the saved context alone; never exits the process; the refusal travels in the outcome, not on the error stack.\n- brasa `sessionConnect_withToken(user, url, token)`: the headless connect beside `sessionConnect_fromSaved`, setting the context the way a credentialed boot does and leaving the working directory as it was.\n- chell `--auth-token-stdin`: the token comes in on stdin, one line, never on argv where `ps` shows it to the host. Refuses by name: without a `<user>@<url>`, beside `--password`, or with no line on the stream. The boot row reads `Connect Connected to <url> (token)`.\n- chell `--daemon` off a TTY no longer spawns a console onto its pipe — a boot ends at a login only where there is a terminal to log in on; otherwise the daemon says so and keeps listening, as the standalone `calypso` binary already did.\n- menu `@fnndsc/menu/logo`: the mise brain and its frame renderer move from the kernel to the wire package, decoding with `atob` and touching no Node builtin, so a browser can draw the same brain a terminal boot does. brasa re-exports it; chell and calypso keep their import.\n\nExemplar `14_tokenLogin` starts a daemon this way against a live CUBE, off a TTY, in isolated directories, and proves the Connect row and a live berth.","internal":false},{"hash":"fc4eb7e","headline":"brasa: `weather [place] [-u|--units metric|imperial] [-d|--days N]` — the conditions now and a short forecast for a place, from Open-Meteo (no key, no account), geocoded by name;","body":"Boston when no place is given, metric unless `--units imperial`. A kernel command, so a TTY and ARGUS show the same report; the typed `sys.weather` model carries the numbers beneath it. Unknown flags refused by name; an unreachable service or an unknown place answers in one line.","internal":false},{"hash":"43b9440","headline":"The space of everything run here: the wait is drawn with what is known, and UNIVERSE is a face.","body":"While the job index warms, RUNS-02 had nothing to show but a refusal and a moving figure. Now the session reports each feed as the index reads it — its jobs collapsed by plugin per place in its pipeline (a fan of three hundred conversions is one node of 300), with a status on every node — on the prompt context (`procWarmup.landed`), and the pane draws them as they arrive: every feed its own small DAG, floating free like molecules in a solution, counts as weight and status as hue, feeds of one pipeline shape pulled together by an unseen anchor so the space settles into constellations. Every node is real. When the index is whole the roster takes over, as before.\n\n`proc universe` answers the same picture from the cache at any time, whole or warming, and says which; the dashboard's UNIVERSE tile opens it on the RUNS canvas. A feed's status words are now the DAG's own (`finishedWithError`, `started`, …) wherever cumin derives them from counts, so a feed on a surface wears the hue its jobs would.","internal":false},{"hash":"3c6a0d3","headline":"ARGUS and kernel: the gather gesture is reachable, and an exported table is visible where it landed.","body":"Three faults found by running the operator's own flow against a live PACS.\n\n**GATHER SHOWN was invisible.** It was put on the results mode frame, which is the closed spine at rest: measured live, 22px wide and `visibility: hidden`. It now stands beside EXPORT CSV on the command row, where the verbs that act on the whole answer already live.\n\n**GATHER was offered only once a series was home**, so on a fresh answer, where nothing is home, the gesture did not exist. A cohort is a set of targets and the feed it roots is made by a pull over its members, so a series is gatherable once it can be named: a PACS path, or the fact that it has landed. Query, filter, take what matched, fetch it as a set. PULL still means bring this one now.\n\n**The export was not broken; the listing was stale.** The CSV writer wrote straight to CUBE and never invalidated the folder's cached listing, which every other fs verb does — so `cat` returned the file while `ls` and every browser showed the folder without it. The write now invalidates the folder it wrote into, and the one that shows a folder it had to create. The WROTE readout also opens that folder when pressed.","internal":false},{"hash":null,"headline":"Updated dependencies [e74ce1d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8a8dc25]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1bdf876]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3320487]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a010cb0]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5b520c7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7bba167]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c169d7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [43b9440] - @fnndsc/salsa@3.18.2 - @fnndsc/menu@0.10.0 - @fnndsc/chili@3.6.7 - @fnndsc/cumin@3.23.0","body":"","internal":true}]},{"version":"0.23.2","date":"2026-09-17","merge":"9ed4e1e6d106","bump":"patch","changes":[{"hash":"48e112d","headline":"Kernel: a DICOM address is followed to the bytes behind it.","body":"A `pl-dircopy` of a PACS pull does not copy the files: its output folder is a link, and the DICOM under it is stored beneath `/SERVICES/PACS`. `dcm series` and `dcm tags` resolved the path they were given against the cwd and stopped there, so the logical name matched no file id and every slice of a pulled series was unreadable from the feed it was pulled into. The listing showed the files; nothing could open one. The same series read fine at its PACS address, and `cd` into the folder — which does follow links — made the very same command work on the very same file.\n\nBoth commands now resolve the address the rest of the file verbs already do: a projection through the provider that owns it, then the link walk. `file_read`, the door behind the browser's byte route, follows a node's data link the same way when the folder it lands on is itself a link. Resolution is best effort, so a path that resolves no further stands as it was and the command still refuses by its own name.","internal":false}]},{"version":"0.23.1","date":"2026-09-17","merge":"96133a6d86a6","bump":"patch","changes":[{"hash":"3f96d86","headline":"A daemon comes up where the operator left it.","body":"A credentialed boot used to write `/` into the working-directory context after connecting, so every restart began at the root whatever was stored; it no longer touches the directory, and with nothing stored the kernel's home default (`/home/<user>`, now exported as `homePath_of`) applies to the boot context too.","internal":false},{"hash":"6d8a595","headline":"`pipeline run` answers with the `run.scheduled` model a plugin run answers with (pipeline name, first instance, feed), so a surface lights the feed it landed in without reading the id out of the text.","body":"","internal":false},{"hash":"a4a38b1","headline":"A file under a `/proc` projection can be read.","body":"The proc provider resolved a node's `data` to the CFS file behind it and then handed that file back to the dispatcher, whose default provider is the host filesystem — which refused it; it now reads the target through `fileContent_get`, the door that knows both worlds. The engine's `file_read` (the daemon's `/vfs` route) resolves the path it is given, so a surface can read a file at the address the session uses for it.","internal":false},{"hash":"0d99ec5","headline":"`pull --new-feed` answers with a `feed.created` model beside its text: the feed it made, its pl-dircopy root instance, the owner, and the root's data folder, so a surface binds to the place rather than parsing the id and path out of the lines.","body":"","internal":false},{"hash":"480632d","headline":"A scheduled run answers with a typed model.","body":"Running a plugin from the shell now returns a `run.scheduled` envelope model (the executable, the instance, the feed it landed in, whether that feed is new, the output path) beside the lines it already printed, so a surface can point at what it started rather than parse the console.","internal":false},{"hash":"b4d9282","headline":"A first session begins in the identity's home directory.","body":"Before the context had stored a working directory the session answered `/`, which is a place nobody works in; it now answers `/home/<user>`, as a shell would. A stored directory is always honoured, the root included.","internal":false},{"hash":"b337285","headline":"`image` is a kernel command, but it was missing from the two places that enumerate commands for the operator: tab-completion (offered from the help registry) and the grouped `help` listing (grouped by a hardcoded category map that had no imaging category).","body":"It had a help entry, so `image --help` worked, but the entry carried no summary and belonged to no category, so it never appeared in `help`. Now `dcm` and `image` share an `Imaging` category in the listing, the `image` entry carries a one-line summary, and `image`/`dcm` take path-argument completion like the other path commands — so `ima⇥` completes to `image` and `image ~/up⇥` completes a path.","internal":false},{"hash":"ab127ea","headline":"`plugininstance list` answers with a `plugininstance.list` model beside its table: the listed instances as typed rows (id, plugin name and version, feed, owner, status, start), the fields the listing selected.","body":"A surface reads its run history rather than scraping it.","internal":false},{"hash":null,"headline":"Updated dependencies [a4a38b1] - @fnndsc/salsa@3.18.1","body":"","internal":true}]},{"version":"0.23.0","date":"2026-09-15","merge":"7717a54fa4b1","bump":"minor","changes":[{"hash":"9cf2ea2","headline":"`image <path>`'s text reflection now carries a thumbnail of the middle slice, so a TTY sharing the session shows the picture, not only the facts.","body":"The kernel reads the slice's pixels (`dicomSlice_gray` in salsa, uncompressed transfer syntaxes via dcmjs), window/levels to 8-bit with MONOCHROME1 inverted, and box-downsamples to a cell grid; brasa renders it as an ASCII ramp on any pipe or ANSI truecolour half-blocks (`▀`) on a colour terminal. Colour is a new declared surface capability: ARGUS declares it (its DOM console renders ANSI), chell reads its own terminal through chalk, a bare pipe gets the ramp. Compressed pixels say so in one line rather than pulling a codec into the kernel; an unreadable slice leaves the facts and no picture. The ASCII ramp is always the floor.","internal":false},{"hash":"d133ef6","headline":"`image <path>` is a kernel command, not a surface verb.","body":"A new brasa builtin resolves what a path is — a DICOM series folder, a study, a `.dcm`, or a NIfTI/MGZ volume — and emits a typed `image.view` intent (schema and `IMAGE_MODEL_KINDS` in `@fnndsc/menu`) that every surface renders in its own way: ARGUS opens a rendered pane from the intent, a TTY prints the reflection the command renders (for a series, the same facts `dcm series` shows). So the same `image ~/uploads/sag-anon` works from a browser and from a terminal sharing one CALYPSO session. The surface keeps only the subverbs that drive a pane already on the field (`image layout|slice|wl|colormap|save|tags|load|guard|ghost`), declared shared so `image <path>`, `image --help`, and `image` alone fall through to the session; help lives in the kernel's registry. Opening an image no longer drifts into a surface-only verb.","internal":false},{"hash":"c70e11e","headline":"A landed series says where it was filed, and the image pane answers the press at once.","body":"A retrieve is confirmed by count and filed by CUBE a beat later, so the pull's \"done\" never knew the folder and a surface offering IMAGE on the row waited for the next query. The progress message now carries an optional `path`, `pull` resolves each landed series' folder before its channel closes and says it on the same channel, and the ARGUS PACS row takes the folder from the landing: IMAGE appears the moment the pull says the series is home. In the same breath, `image <path>` puts the pane on stage before the kernel is asked, with `OPENING <name>` on the field and on the bar, so a header read that takes seconds is never seconds of nothing; what could not open is named on the field, and counted loads say their percentage beside the count.","internal":false},{"hash":null,"headline":"Updated dependencies [9cf2ea2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d133ef6]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c70e11e] - @fnndsc/salsa@3.18.0 - @fnndsc/menu@0.9.0 - @fnndsc/cumin@3.22.1","body":"","internal":true}]},{"version":"0.22.0","date":"2026-09-10","merge":"bbff653b165d","bump":"minor","changes":[{"hash":"163e4b1","headline":"`pacs query --anon` stands in for what an answer says about a person, for demonstrating against a live hospital PACS.","body":"Record numbers and accessions become salted tokens, a name becomes a stand-in name of the same DICOM shape, a study date falls back to the first of its month and a birth date to its year. The substitution happens on the decoded payload, so the model a surface renders, the console table and any CSV all say the same thing. The salt is made once per daemon and never written down, which makes tokens stable through one demo and meaningless after it: an unsalted digest of a seven-digit record number is a second's work to reverse. DICOM identifiers are left alone, since they are never shown and are what the held-state reconciliation matches on.","internal":false},{"hash":"aa923c6","headline":"A replayed PACS query no longer costs a round trip per series.","body":"Reconciling a stored answer against what CUBE already holds asked about every series in turn, two requests each, and held the whole answer back until the last one replied: a patient's history of 299 series took 20.6 seconds against a live CUBE, so a replay — whose entire purpose is not to trouble the wire — felt exactly like a fresh query. CUBE indexes a stored series by its study, so the reconciliation is now one page per study, with file counts fetched only for the series actually home. The same question costs 3.1 seconds. `seriesStorage_resolveMany` is the new bulk resolver; the per-series resolver stays for the paths that need it and for a study the answer cannot name.","internal":false},{"hash":"59aa79b","headline":"A run now follows itself until it settles, so the header's pulse tells the truth without a pane open on the feed.","body":"A new feed entered the cache with its jobs scheduled and nothing revisited them, so RUNNING stayed at zero through the operator's own run and SCHEDULED only ever climbed — one header read `SCHEDULED 90` with nothing running. Starting a run takes out a watch on that feed; the sampler already knew to stop when the feed settles. The watch is taken where a run is started, never inside the cache-writing helper, which would make recording a feed quietly begin visiting CUBE.","internal":false},{"hash":null,"headline":"Updated dependencies [163e4b1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aa923c6] - @fnndsc/salsa@3.17.0 - @fnndsc/cumin@3.22.0","body":"","internal":true}]},{"version":"0.21.0","date":"2026-09-10","merge":"b11d56847aad","bump":"minor","changes":[{"hash":"af2df66","headline":"New `dcm` builtin: `dcm series <folder>` renders a folder as a series and carries the `dicom.series` model;","body":"`dcm tags <file|folder> [--all] [--filter <text>]` renders a grouped tag listing, or a folder's constant and varying tags, and carries `dicom.tags`. Folder listings read every file up to 512 then sample evenly, and the text says how many were read of how many, and which were refused.","internal":false},{"hash":"7c793ad","headline":"`pacs query` fills each pulled series' `folderPath` from the storage state it already resolves.","body":"","internal":false},{"hash":"0eb202f","headline":"`dcm tags --filter` descends into sequence items, so a needle inside a referenced-image sequence keeps the sequence on screen.","body":"","internal":false},{"hash":"183bf02","headline":"`rm` treats an abandoned question as an answer, not a failure.","body":"A confirmation that comes back with no answer — the operator pressed Esc, or the surface lost its ability to ask — used to abort the per-file `-i` walk with `rm: cannot remove '<path>'`, an error over a file nothing had touched, and left every file behind it silently unoffered. It now skips that file by name, says why, stops asking, and says how many it left alone. The one-question `-I` path keeps everything as before and now states the reason too, so a surface that has quietly lost its voice is not reported as an operator who declined.","internal":false},{"hash":null,"headline":"Updated dependencies [7c793ad]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [af2df66]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [af2df66]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [b9ec3aa] - @fnndsc/menu@0.8.0 - @fnndsc/salsa@3.16.0 - @fnndsc/cumin@3.21.1","body":"","internal":true}]},{"version":"0.20.0","date":"2026-09-09","merge":"3a263f013a83","bump":"minor","changes":[{"hash":"3d11f59","headline":"Boot readout, second pass.","body":"A step that starts as `[PENDING]` (or falls to `[RETRY]`) is now rewritten in place when its outcome lands, the way a boot screen settles a row rather than repeating it beneath, so a settled step leaves one `[ OK ]`/`[FAIL]` row and no open pending row. The brain animation and the readout now share one stdout row counter, so neither miscounts the other's scrolling; a row that has scrolled off the screen, or an outcome taller than the pending row it replaces, appends as before. The daemon banner paints each package in three parts (its name bright, the rest of its backronym plain, the version dim in one column) and carries the build hash on its own CALYPSO row, so the redundant headline line is gone; a remote surface paints the daemon's stack the same way.","internal":false},{"hash":"9c320a0","headline":"The boot ends at a login.","body":"An interactive `chell --daemon` no longer rests on the console face: once the daemon is listening it puts an ordinary `chell --remote --attach` surface on its own terminal, as a child on the wire, so the boot log runs straight into a prompt. `exit` detaches and leaves the daemon running, Enter attaches again, Ctrl-C at the idle terminal stops the daemon, and whatever the daemon writes while a surface holds the terminal is held (calypso `consoleCage_start`/`consoleCage_stop`) and printed on detach. Every chell REPL, local or remote, now guards its idle prompt: a stray line (another surface's output, a late warm-up) lands on its own line with the prompt redrawn beneath it.\n\nThe boot readout is lifted with it. A warm-up step running behind the prompt printed its pending row twice and never reported an outcome; it now prints once as `[PENDING]` and settles on the daemon's readout as `[ OK ]` with what it cached or `[FAIL]` with why. Every count takes its noun in the right number (`1 PACS query`, `3 feeds`) through a new brasa `count_noun`. The daemon banner writes every package out in full (brasa `stackBanner_build`/`stackBanner_compose`), adds an ARGUS row when a web surface is served, and drops the welcome; the remote surface banners the daemon's stack the same way. A daemon bound on every interface advertises its fully qualified host name.","internal":false},{"hash":"d89e31b","headline":"A feed-list row now carries `jobsErrored` (the errored-or-cancelled count, from the proc cache's per-status counters), so a surface can fill an errored feed's progress bar to the work that actually succeeded — `jobsDone - jobsErrored` over `jobsTotal` — rather than run it full in the error hue.","body":"The RUNS roster uses it; a full red bar said nothing a red mark would not.","internal":false},{"hash":null,"headline":"Updated dependencies [711e1b3]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [612c052]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d89e31b] - @fnndsc/cumin@3.21.0 - @fnndsc/salsa@3.15.0 - @fnndsc/menu@0.7.0","body":"","internal":true}]},{"version":"0.19.0","date":"2026-09-09","merge":"4b93ef54c7d2","bump":"minor","changes":[{"hash":"49634f2","headline":"feat(brasa): the index snapshot the daemon heartbeats carries CUBE's pace","body":"","internal":false},{"hash":"2545ffa","headline":"feat(brasa): the index snapshot carries the lab's pulse — jobs running and waiting, and the feeds that errored — derived from the roster's counters","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [49634f2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [49634f2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2545ffa] - @fnndsc/cumin@3.20.0 - @fnndsc/menu@0.6.0","body":"","internal":true}]},{"version":"0.18.0","date":"2026-09-09","merge":"bb6d0d575ad0","bump":"minor","changes":[{"hash":"6cd5ab1","headline":"feat(brasa): `feed diagram`, `feed tree`, wire `feed.dag` and `proc refresh <feed>` answer at once with `feed.indexing` when the feed is cold;","body":"a feed watch holds the floor while its topology is indexing, never settles on an empty topology, and is kicked by the cache the moment the walk lands","internal":false},{"hash":"57ba039","headline":"feat(brasa): the prompt context names every feed walk in flight, not only the earliest","body":"","internal":false},{"hash":"98ab832","headline":"feat(brasa): `proc feeds` and `proc jobs list` answer from the cache at once and start the roster sync off the session's lane;","body":"the prompt context names the walk while it runs","internal":false},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [adf8cc1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [adf8cc1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832] - @fnndsc/cumin@3.19.0 - @fnndsc/menu@0.5.0 - @fnndsc/salsa@3.14.0","body":"","internal":true}]},{"version":"0.17.0","date":"2026-09-07","merge":"afa00444c12e","bump":"minor","changes":[{"hash":"a7f057c","headline":"feat: a value-taking flag given no value asks for it","body":"`pacs query … --csv-to` with nothing after it now means \"ask me where\". Before, such a flag was silently ignored: the operator asked for a table and got none, which is worse than either answering or refusing.\n\nNo sigil was needed. `?` was the obvious spelling and is already a glob in `string_checkHasWildcard`, so it would have expanded against the VFS; a flag that takes a value and is given none is unambiguous on its own, and the rule now generalises to every flag in the stack without another convention.\n\nThe ask carries what it wants — a `path`, with the session's own cwd as its anchor, a suggested basename, and `EXPORT HERE` as the word its committing control should read. The anchor is a fact rather than a guess: inventing a directory means creating one behind the operator's back. And it is raised only once there is something to write, since a question about a file that may never exist is asked too early.\n\nTwo rules ride with it, both in the daemon, both about who may interrupt an operator:\n\n- **A command marked `instrument` may never ask.** A pane's silent refresh or an ambient cycler raising a question is refused outright — the operator did not issue that command and cannot answer for it.\n- **One question at a time per surface.** A second is refused by name rather than queued, because a queued question is one whose command the operator has forgotten issuing.\n\nAn abandoned ask is not a failed query: the answer stands and only the writing does not happen.\n\n`prompt_current` now takes the whole request rather than a message and a flag, and the daemon relays `wants`, `path` and `commit` to the surface. `repl_confirm` and `repl_questionPath` join the kernel's ask helpers, so a caller states the kind once and every surface reads the same intent.","internal":false},{"hash":"da15f3b","headline":"feat(argus): the surface can be asked","body":"argus refused every question the session put to it — `the argus surface cannot answer prompts`, with `hiddenInput: false` as the stated reason. So `sudo` dead-ended in the web surface, a confirmation dead-ended, and no control could ask for a value the operator had not already typed.\n\nIt answers now, in the **console**: where the session speaks, and where the scrollback keeps what was asked, so an operator can look back at the question as well as the answer.\n\n- **`text`** takes an inline line.\n- **`secret`** masks the field and enters the transcript as dots — never its text, never its length — which is what lets `hiddenInput: true` be declared honestly.\n- **`confirm`** puts YES and NO capsules beside the question, because a control that reads as what it does beats a letter an operator has to know to type. Typing `y` still works for a hand already on the keys.\n- **`path`** is answered here too for now, with its suggestion offered so answering is a rename rather than a whole path typed out. The errand that walks a browser for it is the next slice; until then a location is answerable rather than refused.\n\nA question outranks the command line while it is open: an answer is never dispatched as a line, and a queued line never jumps ahead of it. **Esc abandons**, which is an answer of its own — the command is told, and reports what it did not do.\n\nTwo asks that never travelled were routed through the surface while here. `rm -i` built its own readline against `process.stdin`, so under a daemon its confirmation was put to whoever started the daemon rather than to the operator who typed the command — they would have waited forever for a question asked of somebody else's terminal. `upload`'s confirmation was untyped, and is now a yes/no like the download's.\n\nVerified live: `sudo` in argus asks for an administrator username, masks the password, leaks nothing to the transcript, and abandons cleanly. Law `a-question-is-answered-where-the-session-speaks`, smoke-enforced.","internal":false},{"hash":"349d7d2","headline":"feat: a missing destination is asked for, not refused with a usage line","body":"`mv foo` names what to move and never says where. Until now that was a usage line — telling an operator the shape of a command they had just typed correctly enough to be understood, instead of asking them the one thing they had not said. A required operand with no value is the same sentence as a value-taking flag with no value, and now gets the same answer: `mv` and `cp` ask.\n\nThe ask is the typed `path` ask that already exists, so neither verb knows which surface it is talking to: a terminal renders it as a line, and a surface that browses borrows a files pane for it. It opens where the source already lives, wants a directory when there is more than one source, and its committing control reads `MOVE HERE` or `COPY HERE`.\n\nIt offers no default name. The only name `mv` could propose is the source's own, and the first live run took Enter as exactly that and moved a file onto itself. The rule that came out of it holds for every path ask, and the terminal now implements it directly: the **anchor** says where to look, the **suggestion** says what to offer, and a verb with nothing to offer offers nothing — Enter on such a question answers nothing rather than answering with the question.\n\nAn abandoned ask moves nothing and says so (`mv: no destination given; nothing moved.`), rather than failing with a description of `mv`.","internal":false},{"hash":"02ef648","headline":"feat: a write is checked before it happens, and says what it did","body":"A chooser can hand a write a place that cannot be used, so `--csv-to` stopped trusting the path it is given:\n\n- **A provider path is refused by name.** `/net/pacs/x.csv` is somewhere to browse, not somewhere a file lands, and CUBE's own refusal talks about an upload, which says nothing about why.\n- **A missing folder is made, and said** — a directory appearing without a word is the silent side effect this replaces. argus's EXPORT CSV no longer runs `mkdir ~/audits` behind the operator's back; it lowers to `--csv-to` with no value, so the destination is asked for.\n- **An existing table is never overwritten in silence.** It takes `--force`, and the replacement is reported.\n- **The pane states the path** the kernel reported writing, rather than leaving it to the console alone.\n\nThree things the live run turned up that no mock would have shown:\n\n- **`files_listAll` answers null for an empty folder AND for one that is not there**, so existence can never be inferred from it. Doing so reported a folder \"made\" that already existed, and a file \"already there\" in a folder that did not exist at all. Existence is asked of `files_path_isDirectory` now.\n- **CUBE answers a re-upload over a path it already holds with a 500, thrown.** An unhandled throw in a builtin does not stop at the command: under a daemon it takes the process, and every surface attached to it. The write is wrapped — a store's bad day is a refusal, not an outage.\n- **Deletion is asynchronous**, so removing a file and writing in the same breath races the store and answers 500 again — with the operator's old file already gone. `--force` now removes, waits for the path to stop resolving, writes, and confirms by listing afterwards, because a write reported without confirmation was seen to leave nothing behind.\n\nBoth CUBE behaviours are recorded in `docs/CUBE-gaps.adoc`, with what the API could offer instead.","internal":false},{"hash":"116e8ba","headline":"feat(listing): progress is a trait of a row, and a row can carry verbs","body":"The operator's observation: progress is not a PACS quirk. A feed row should say how far its work has got without anyone opening it, and every level should report — a series its own pull, a study the sum of its series, a patient the sum of its studies.\n\n**The wire could not say it.** The `feed.list` model carried id, title, owner, status, created and two totals that need resident topology, so a roster genuinely could not know a feed's progress. CUBE's own job counters were already on the process cache, so the kernel now derives `jobsDone` and `jobsTotal` from them — settled meaning finished, errored or cancelled — and they travel with the feed row rather than waiting for topology. Kernel, wire, surface, in that order.\n\n**Progress aggregates by addition, not by average.** A study's progress is the sum of its series'. An average would let one finished series of a hundred files outweigh a stalled one of ten thousand.\n\n**A row with nothing scheduled still gets a track**, dimmed. The absence of a bar reads as \"no such thing\"; a dim track reads as \"nothing has happened yet\", which is the truth and the more useful statement.\n\n**Actions are not traits.** A trait says what a row is under some column; an action is a verb applied to it, and it sits outside the column grid because it answers to no cap. Capsules stop click propagation, so pressing one is not also activating the row.\n\n**Expansion has two modes**, declared rather than assumed: `replace` leaves the parent behind, `fold` keeps it on stage with the child inside. PACS exercises both in the next slice.\n\nThe runs roster gains NODES and PROGRESS, which widened its positional track list from seven columns to nine.\n\nOne smoke assertion changed, and deliberately: it counted seven cells per row as a literal. It now counts cells against the number of caps, which is the invariant that actually protects a positional grid and needs no edit when a column is added.","internal":false},{"hash":"ba1e5b4","headline":"feat: a PACS answer can be written as a table a spreadsheet reads","body":"`--csv` renders the answer; `--csv-to <cfs-path>` writes it into ChRIS storage; argus's EXPORT CSV lowers visibly to the second, as GATHER's SAVE already lowers, so the operator reads the command that ran.\n\nTwo flags rather than one with an optional value: `--csv PatientID:1234` cannot tell a destination from a query expression, and guessing wrong writes a file named after a patient.\n\n**Why a CFS destination exists.** A local terminal needs none — `--csv > audit.csv` writes the operator's own disk, because engine and operator share a filesystem. A detached surface does not: the engine runs on the daemon's host, so a redirect lands on somebody else's machine (#415, where `>` also turns out to bypass the `engineFilesystem` capability built to prevent exactly that). A cohort's MRNs and study descriptions stay inside ChRIS, and the file browser or `download` retrieves them.\n\n**The table's shape carries the level above's doctrine**: a row per study, and a row for every patient that owns none. A table built from studies alone would silently drop the misses, which are what an audit is usually asking about. `ANSWERED` carries an ISO timestamp rather than `3 MONTHS AGO` — a spreadsheet sorts dates; a phrase is for a human glance.\n\nEvery cell is quoted and embedded quotes doubled, the rule chili's `--csv` has always followed, now stated as a function rather than left inside a handler nothing else can call — so a study description like `MRI BRAIN, W/ AND W/O \"GAD\"` survives the trip.","internal":false},{"hash":"404f5e3","headline":"feat: a PACS question that names several patients becomes several questions","body":"A PACS will not match a list. `PatientID:4356325\\4433255` — the DICOM multi-value form — returns nothing from a real PACS, and the standard agrees: _List of UID Matching_ is defined only for attributes whose VR is `UI`, and `PatientID` is `LO`. So asking after two hundred MRNs is two hundred C-FINDs, and the fan-out is forced rather than chosen.\n\n**The inline comma list is the operator's own syntax, now accepted.** `query PatientID:1234,4532,6654` is three questions and one table; `query PatientID:1234,StudyDate:20240101` still means what it always meant. There is nothing to disambiguate — every genuine term carries a colon, so a bare segment can only be another value for the key before it. Several multi-valued keys fan out over their cross-product, and above 32 the command refuses by name rather than launching hundreds at a shared clinical system.\n\n**`--patients <list|@file>` carries a real cohort.** `@file` names a file in ChRIS storage, read through the session's own path and never the engine's host disk, so the flag behaves identically from a local shell, a remote shell and a browser — and a list on somebody's laptop reaches it through `upload`, the gated door. One MRN per line; blanks and `#` comments ignored.\n\n**Four questions in flight**, in one constant. Not operator-settable: nobody inside mise knows the right number for a given hospital, and a flag that can hurt a shared clinical system will eventually be set to 50.\n\n**A failure is not a miss.** A question that could not be asked is recorded `unasked` with its reason, never as zero studies. A server that timed out has told us nothing; a PACS that answered with nothing has told us something, and a clinician acts on the difference. The table says `FOUND 2 · NONE 1 · UNASKED 0`, and the model carries every row.\n\n**Replay applies per patient**, so a cohort's already-asked MRNs never leave the building — verified live against rows answered eight months ago.\n\nTwo things found live while proving it:\n\n- **CUBE refuses a second PACSQuery with a title it already holds for that server.** A fan-out under one title had every question after the first come back `You have already registered a PACS query with title=…`, which a less careful client would have rendered as \"no imaging\". Each question now carries its own title. Recorded in `docs/CUBE-gaps.adoc`.\n- **`chell -c` and `chell -f` had no replay at all.** A one-shot skips the boot warm-up — correct — but that left the replay index neither restored nor written, so a scripted cohort re-asked the PACS every single time, and the audit workflow the fan-out exists for was the one workflow replay never reached. A one-shot now primes the index from the checkpoint a previous run paid for, and flushes what it learned before exiting, since the debounced writer's timer never fires in a process that short.","internal":false},{"hash":"7181f7e","headline":"feat: a PACS question can be put to several servers at once","body":"`--pacsserver a,b` asks each named server and unions the answers into one listing. It is the same fan-out a cohort uses, discriminated by a different column: SERVER rides every study and every patient row, so two answers can be told apart, sorted and filtered.\n\n**A row names its server only when more than one could have answered.** On a single-server query the column would repeat one value down the page and tell an operator nothing.\n\n**There is no sweep-everything.** The CUBE this was designed against carries thirteen registered servers, several reading as one-off or per-person registrations; an `--all` would mean thousands of C-FINDs mostly into endpoints of unknown liveness. A fan-out is always something the operator named.\n\n**A server that could not be reached is `unasked`, not empty.** Verified live: naming a server that does not resolve leaves that row `—` with its reason while the server that did answer still reports what it found. A server that could not be reached has told us nothing about that patient; rendering it as a zero would say the opposite.\n\nThe reason on such a row is stripped of the error stack's debugging prefix where it becomes model data, since it is read in a terminal table and on a graphical surface alike.\n\nServers are keyed by their canonical identifier — what CUBE files a query under, and therefore what the replay index matches on, so a question already asked of one server replays while the same question to another is asked fresh.","internal":false},{"hash":"a245b5f","headline":"feat: choosing which PACS to ask is a strip you pick from, never a sweep","body":"SERVER became a column of the study listing, and the law that places a form's fields put its control there with no separate decision to make: the cell stands in SERVER's column, wearing the form's own label.\n\nIt is a **state readout you press** — `PACSDCM`, `PACSDCM +2` — that unfolds a strip of segments beneath the form, one per registered server, lit when included; the field's touch or Esc retracts it, the same retraction grammar the mode frames already use. Not a dropdown: LCARS has no popup layer and should not grow one, since a floating menu is exactly the window chrome this grammar rejects — and a strip of the pane's own width carries thirteen servers where a grid track could not carry three.\n\n**There is no ALL, and there will not be one.** Thirteen registrations of unknown liveness would mean thousands of C-FINDs mostly into the void. A fan-out is always something the operator named.\n\n**One chosen is a context; several is a query.** One lowers to a visible `pacs connect`, so the session moves and the linked terminal's prompt follows. Several lowers to `--pacsserver a,b` in the editable line and moves nothing. The prompt changing, or not, is the honest tell.\n\n**The list comes from the kernel, not from CUBE.** `pacs list` now carries a `pacs.servers` model beside the text it always printed — id, identifier, and which one the session is on. A surface asking CUBE itself would be reading a different CUBE from the one its commands run against. Liveness is deliberately absent: CUBE registers servers, it does not test them, and a field that looked like health would be a claim nobody checked.\n\nTwo things found live: the PACS pane's own commands had no way back to it — the DAG pane's already did — so `pacs list` answered into the void; and the document's retraction listener closed the strip with the very press that opened it.\n\nLaw `a-server-is-named-never-swept`, smoke-enforced.","internal":false},{"hash":"9e2a9dd","headline":"feat: a verb that acts on the place rides the frame — MKDIR and UPLOAD","body":"Two verbs act on the PLACE the field holds rather than on any row, so they ride the field's own frame beside HOME and BACK, and they appear on a browser's frame and nowhere else — there is no directory to make in a list of feeds.\n\n**MKDIR** asks for a name in the console and makes it where the field points, not where the session's cwd happens to be: a rooted browser is showing a place of its own, and a verb that acted on the session's place instead would make the folder somewhere the operator is not looking.\n\n**UPLOAD** is the verb this surface could not previously speak at all. `upload` reaches the daemon's disk, which a browser has never seen. So the operator's own picker chooses the file, and the bytes travel over the daemon's `/vfs` route — now answering `POST` as well as `GET` — where the **engine** writes them through the kernel. No surface talks to CUBE, the same attach token gates the write as gates the read, and the body is capped. The console keeps the account, because a gesture the surface performs itself still owes the transcript what it did.\n\nNew seam: `Engine.file_write(path, bytes)`, the twin of `file_read`. It writes through salsa's own create and **invalidates the listing it changed**, as every writing builtin does — without that the browser asks for the folder again and is served the folder as it was before the delivery, which reads as an upload that silently did nothing.","internal":false},{"hash":"5a339f0","headline":"feat: a plugin is data — the manual becomes a projection of the model","body":"A plugin's substance existed only as text. `cat /bin/<entry>` fetched the plugin, formatted a manual, and that manual was the whole of what any surface could have — a paragraph nothing downstream can act on: not a card, not a parameter list, not a form, not an export.\n\nThe kernel now builds the model first and renders the manual from it. `plugin info pl-dcm2niix-v2.0.0` answers with a `plugin.info` model carrying identity, the authoring facts, and every declared parameter; `cat /bin/pl-dcm2niix-v2.0.0` prints exactly the text it printed before, now as one projection of that model rather than a second independent scrape that can drift from it.\n\n**Parameters carry the flag as it is typed.** A plugin's own `flag` when it declares one, `--name` otherwise — a form built from the name alone would spell `--inputFile` where the plugin wants `-i`.\n\n**The parameter list is drained to exhaustion.** `getPluginParameters({ limit: 100 })` was one of the silent truncations catalogued in #401: a plugin declaring more lost the tail and said nothing about it. `pluginParameters_drain` walks to the end, and a live exemplar checks the model's count against the count CUBE itself reports — a client that both fetches and counts can agree with itself while being wrong.\n\ncumin gains `plugin_find` and `pluginParameters_drain` on the typed contract, so the `/bin` reader no longer reaches past it to the raw client.\n\nNo surface change: this is the wire fact a plugin's one-node graph will read.","internal":false},{"hash":"a8449bc","headline":"feat: the roster shares, and a feed can be removed","body":"`setfacl` grants to an identity and applies to a feed, so the feed roster is where sharing belongs — a browser row offers SHARE only because the path it holds names a feed.\n\nThe roster's rows learn the split the browser's already have: a click indicates, a double-click enters, and the action track is reserved on every row so a roster with verbs does not jump when one row starts speaking.\n\n- **SHARE** asks who, and the question itself says a grant cannot be taken back. The irreversibility is stated where the grant is made rather than discovered afterwards, and because the sentence lives in the kernel's ask, every surface says it.\n- Beside the verb, the row **reads back who already holds it** — a readout, not a verb: it says what the grant would be adding to.\n- **DELETE** lowers to `feed rm`, which is new: the kernel could delete a feed but no shell verb could. It asks first by default, naming the feed and what goes with it, and takes `-f` for a caller that has already asked its own question.\n\n`feed rm [<feed>] [-f]` joins the feed subcommands.","internal":false},{"hash":"8446459","headline":"feat: a row is indicated before it is acted on","body":"The browser's rows have their verbs. Two problems had to be solved before they could:\n\n**There was no indicate gesture.** A click entered a directory, opened a file, opened a `/bin` entry — nothing meant \"this one\" without also meaning \"go\". A click now indicates and a double-click activates: the split the DAG scene already teaches, and what every file manager does. Only a listing that hides its verbs learns it — a browser given no verbs (a node's overlay) keeps its single click, since asking for a second click while offering nothing for the first is a worse bargain than the one it replaced.\n\n**A row that grew when indicated would make the listing jump.** The action track is reserved on every row at the capsule's own height, declared once and read by the track and the capsules alike, so what changes when a row speaks is what the track holds and never the geometry around it.\n\nEach verb lowers to a command the operator can read in the transcript:\n\n- **DELETE** → `rm -i` (`rm -ri` for a directory), so the kernel raises the confirmation and one confirmation grammar serves every surface.\n- **MOVE** / **COPY** → a one-operand `mv` / `cp`, whose missing destination is the ask that opens the errand.\n- **DOWNLOAD** → the file, as before.\n- **SHARE FEED n** → on any row whose path holds a feed, naming the feed because CUBE grants a feed and never a file, with the grants that already exist read out beside it.\n\nA `/bin` row is offered nothing: an executable the catalogue lists is not a file in a store.\n\n`setfacl <feed>` with no entry now **asks who to share it with**. A path and no entry names what to share and not with whom, which is a question rather than a usage error — the same law that made `mv foo` ask. Abandoning it shares nothing and says so.","internal":false},{"hash":"17964a9","headline":"feat: SELECT is a mode, and a selection has verbs","body":"A mode describes how the field behaves, and this one changes what a click means. While SELECT is on a click gathers a row instead of indicating it, row verbs stand down — a row is not indicated then, it is selected — and the bar reads `SELECT · 2 SELECTED`, adding `· 1 SHOWN` when a filter hides some of what was gathered, since a verb acts on the selection and not on what is on screen.\n\nThe selection belongs to the **field**: it survives a filter (which is how a selection gets built in a folder of hundreds) and the same rows arriving again; it is cleared by navigation, because a selection that follows the operator elsewhere is one they can act on without seeing; and leaving the mode clears nothing. Esc leaves SELECT before it retreats anywhere — but never while a question is open, since abandoning a question is an answer and one press must not answer two things.\n\nIts verbs ride the frame, and each is ONE command the operator could have typed:\n\n- `rm -rI \"a\" \"b\"` — **new `-I`**: one question for the whole list, naming how many, rather than one per file. A refusal removes nothing at all; there is no half of a set.\n- `mv -t` / `cp -t` — **new `-t <dir>`**: names a target directory so every operand is a source, and given no value it asks, wanting a directory.\n- `setfacl` over the **distinct feeds** the selection touches, which asks who once for the set.\n\n## A defect this bought, and the guard for it\n\nThe live run found the surface lowering a two-file selection to `mv a b` — which the shell correctly reads as \"rename a onto b\", and which moved one file of the pair onto the other. Chasing it turned up something worse and older: **a move onto a path the store already holds leaves a row CUBE's own API cannot serve, and one such row makes every listing of that folder fail** — the poisoning first seen in #462, now reproducible in three commands.\n\n`mv` refuses that move by name instead of attempting it, which costs one listing call and keeps the folder readable, and a failed move now reports the reason the kernel gave rather than a bare \"Failed to move\".\n\nAlso: a browser re-lists after a verb that changed the folder it is showing (`fs.rm`, `fs.mv`, `fs.cp`) — `rm` reports what it removed, not where it removed it from, and a listing that keeps showing rows that are gone is a listing lying about the store.","internal":false},{"hash":"78d85ae","headline":"feat(argus): a location is asked for by borrowing a browser","body":"An ask is never a box. A `path` question opens the instrument that already shows that space: a **new** files pane beside the pane that asked — never an existing browser, since hijacking one loses the operator's place — anchored where the ask said, closing when the errand ends either way.\n\nIts controls ride a bar of its own across the top of the pane: the question as a caption, the composed path as an editable field, **MKDIR** for a folder that does not exist yet, and one verb that commits, reading the word the kernel sent (`EXPORT HERE`). The grill had put those on the mode frame; building it showed why they cannot live there — the frame is a narrow rail against the spine, right for a column of capsules and hopeless for a caption and a path, and it answers to what the _field_ holds, which an errand does not change.\n\nThree defects the live run turned up, each fixed here:\n\n- **The errand opened an empty browser, forever.** The daemon runs commands one at a time, so the listing that would answer the question queued behind the command that asked it: the answer waiting on the browsing, the browsing waiting on the answer. An instrument command now runs **beside** a command waiting on a question — the natural twin of the rule that an instrument may never ask. A pane's own read neither asks nor waits, and the daemon saves and restores the executing command rather than clearing it, so the outer command's output still finds its way home.\n- **The errand split beside the focused pane**, which can be one the current preset does not hold. A split beside a pane that is not in the tree fails silently, and an errand that never opens is a question asked of nobody. The host is now a leaf that is actually on stage.\n- **The anchor could be somewhere nothing can be written.** A session sitting in `/bin` or `/proc` is browsing a provider, not standing where a file lands, so the ask falls back to home rather than offering a destination that is refused the moment it is committed.\n\nLaw `an-ask-borrows-an-instrument`, smoke-enforced. Two consecutive full smoke runs, 103 checks.","internal":false},{"hash":"8312a79","headline":"feat(chell): a terminal answers the same typed question","body":"A terminal has one instrument — a line — so a location ask cannot borrow a browser the way a graphical surface does. It renders into the line instead: the composed default in brackets, and Enter takes the offer.\n\n```\nWhere should the table go? [/home/rudolphpienaar/pacs-2026-09-07.csv]\n```\n\nThe answer a terminal commits is then the same answer an errand would have committed, without either surface knowing the other exists — which is the point of putting the _kind_ on the wire rather than a rendering. A yes/no says which letters it takes and offers no default, because there is no safe guess. A prompt from a daemon that predates typed asks reads exactly as it always did.\n\nExemplar 12 closes the epic by driving the whole path through the kernel with a scripted surface: a value-taking flag given no value asks and the question says what it wants; the answer is what happens next and the file lands where it said; abandoning writes nothing and the command says so; and a session sitting in a provider path is not offered as a destination.\n\nIt caught the epic's last defect: **an abandoned ask was throwing the answer away with the writing.** The model crosses either way now — the operator waited for that answer, and only the writing of it did not happen.","internal":false},{"hash":"3df1c41","headline":"docs(calypso): say calypso where the component is meant, and make it typeable","body":"Every other part of the stack is a noun with a role — cumin, salsa, chili, brasa, menu, chell, argus. The session supervisor was the odd one out, described by a flag on another program, so the prose said \"the daemon\" for something that already has a name, a package, a binary and a doctrine document.\n\nHost control is _calypso's_ policy; berths are calypso's; the wire is calypso's. \"Grant calypso host access\" says whose policy changed. \"Grant the daemon\" does not.\n\nOperator-facing text now names it: the flag help, the not-running message, the already-running refusal, the listening banner, the attach errors, the several-sessions chooser, and the host-control sentences — `upload` refusing without the `files` tier, the HOST banner, and argus's HOST lamp tooltip.\n\nThe start hints used to read `chell --daemon <user>@<url>` while the prose said \"run calypso\". They now say `calypso <user>@<url>`, which is a real command, since calypso ships its own binary. The getting-started guide leads with what calypso _is_, then gives both ways to start it — standalone from a saved session, or through chell logging in fresh — because they genuinely differ and one does not replace the other.\n\n**Daemon survives where it is correct**: a mode name, an anchor, a make target, a background process. Code symbols are untouched — `daemon_launch` and its kin describe something that really is a daemon, and churning them buys nothing an operator sees. Command lines inside code blocks were left alone, so nothing became a command that does not exist.","internal":false},{"hash":"f5c16fe","headline":"fix: a listing says what it could not read, and cat reports instead of crashing","body":"**A listing that could not read part of itself says so.** `ls` asks a folder for its directories, its files and its links, and a refused sub-listing was being dropped for looking like an empty one — so the home root whose file listing CUBE refuses rendered its folders alone, as though that were everything. The provider now names what it could not read (`Cannot fully list <path>: could not read files (Internal server error)`) and the listing carries that reason with the entries it did get.\n\n**`files_listAll`'s `null` meant three things** — an empty folder, a folder that is not there, and a folder the server would not describe — and nothing above it could behave correctly on one word that means all three. `files_listOutcome` says which: `listing`, `empty`, `missing`, `refused`. `files_listAll` stays as the lossy wrapper, but a refusal now throws rather than passing for absence.\n\n**A file is deleted by its id**, not by finding it in a listing first. The folder whose listing the server refuses is exactly the one an operator needs to clear, and a delete that walks the parent cannot help there. `chrisIO.file_deleteById` is the new door; other asset kinds still resolve through the group.\n\n**`cat` reports an unreadable path instead of ending the session.** A read that threw — a path that cannot be resolved, a server that refuses — escaped as an unhandled rejection that dumped an axios request object, auth header included, and killed the process. Each path is now resolved and read inside its own guard: the failure is reported like any other unreadable file, the rest of the line still runs, and nothing is dumped.","internal":false},{"hash":"ec3ef9f","headline":"fix: a listing shows what is there — no silent page limits","body":"`ls /net/pacs/queries` answered with 100 of 1,817 stored queries and said nothing about the other 1,717. That was one symptom of a pattern: callers reaching a single-page list call with a literal limit and returning the page as though it were the collection.\n\n**The default is now the collection.** A caller that names no limit gets a walk to exhaustion, not the first twenty. A caller that names one gets exactly that page, marked `incomplete` with what it is showing of what exists — a bound the surface can state rather than a truncation nobody sees.\n\n**A walk that stops says where.** CUBE fails some collection queries past an offset (measured: `userfiles` answers to offset 800 and returns 500 at 1000). The walk keeps what it gathered and reports where it stopped, rather than handing back a prefix that reads as an ending. A walk whose _first_ page fails is still a failure, since a listing that never started is not a short listing.\n\n**Three callers fixed:**\n\n- `pacs status` searched the first 200 queries for a matching expression, so anything older answered \"not found\". It walks now — verified live against a log of 1,897, finding query 3.\n- `getfacl` read a feed's first 100 grants. A hundredth name is not a natural place for that answer to stop.\n- A `ts` node's join edges are read from its parameters, which were fetched one page deep. A node with more parameters than a page silently lost its edges — a graph drawn short.\n\n**And a gate**, `npm run lint:listings`, in CI: a literal `limit` above one reaching a single-page list call fails the build unless the line says `listing-bound:` and why. Two bounds are declared today; both are exact-path lookups, not collections.","internal":false},{"hash":"6234d55","headline":"test: a live exemplar proves a cohort answers for every patient asked","body":"Exemplar 11 drives the whole PACS plural path through `command_dispatchEnvelope` — the same entry chell uses — against a live CUBE and a real PACS.\n\nWhat it pins, in the order an operator would care:\n\n- **Every MRN asked has a row.** It counts rows, not hits: the answer an audit wants is usually the patients a hit-list would hide.\n- **Replay applies per patient.** The second ask of the same cohort is served from stored answers for the patient with imaging — same query id, provenance saying so — while the rows that found nothing are asked again, because an absence decays where a hit does not.\n- **A failure is not a miss.** Asked of a server CUBE does not register, the rows read `unasked` with a readable reason rather than zero studies. This is the property that needs a live PACS: a mock would only agree with whatever the code already does.\n- **The CSV round-trips.** `--csv-to` writes into CFS, `cat` reads it back, every row carries the same ten columns, and the misses are in the file.\n\nIt needs no new fixture: the designated test accession names a study, that study names a patient, and that patient is the MRN known to have imaging. Every PACSQuery the run creates is deleted and the file removed, so the CUBE ends as it began. Self-skips with exit 2 where no PACS fixture is configured, and joins the nightly e2e list.\n\nOne fix it drew out: the query model named its server with the raw `--pacsserver` argument, so a table asked with `--pacsserver 1` reported the server as `1`. The model now carries the canonical identifier CUBE files queries under, which is what `PACSDCM` means to an operator reading a CSV.","internal":false},{"hash":"17964a9","headline":"fix: no write goes to a path the store already holds","body":"A write onto an occupied path leaves a row CUBE's own API cannot serve, and one such row makes **every** listing of that folder fail — the damage first seen as \"the home root cannot list its files\". It is reproducible in three commands, and all three ordinary write routes did it:\n\n- `mv a b` where `b` exists — its own PUT of `upload_path`.\n- `cp a b` where `b` exists — the copy uploads to an occupied name, and reported _success_ while doing it.\n- `upload a.txt` where `a.txt` exists — and this is the one that made the original rows. CUBE renames a colliding upload (`a.txt` → `a_VlIxMSp.txt`); mise then PUT the wanted path back onto the file, to undo a rename it read as spurious. That PUT is the damage.\n\nEach route now asks first, which costs one listing call and keeps the folder readable:\n\n- `mv` and `cp` refuse by name — `Destination exists: <path> — mise cannot overwrite a file; remove it first` — and a failed move or copy now reports the kernel's own reason instead of a bare `Failed to move` / `Failed to copy`.\n- `upload` renames back only when the wanted path is genuinely free, which is the case that rule was written for (a path deleted and not yet committed). Otherwise it keeps the name CUBE gave the file and says so: `'<path>' already exists — the upload landed as '<other>' rather than overwriting it`. A probe that cannot answer counts the path as taken, since the cost of guessing wrong is the operator's folder.\n\nThe deliberate replace (`--csv-to --force`) is unaffected: it removes the file, waits for the path to stop resolving, then writes to a path that is free.\n\n`docs/CUBE-gaps.adoc` carries the reproduction, the mechanism and the client policy; upstream is ChRIS_ultron_backEnd#732.","internal":false},{"hash":null,"headline":"Updated dependencies [f5c16fe]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [116e8ba]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [ec3ef9f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7181f7e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d2a4315]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a245b5f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5a339f0]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [17964a9]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [62761a3]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [17964a9] - @fnndsc/salsa@3.13.0 - @fnndsc/cumin@3.18.0 - @fnndsc/menu@0.4.0 - @fnndsc/chili@3.6.6","body":"","internal":true}]},{"version":"0.16.1","date":"2026-09-05","merge":"0422da0acacf","bump":"patch","changes":[{"hash":"e3948f1","headline":"fix(release): publish `@fnndsc/menu`, without which nothing else installs","body":"`@fnndsc/menu` was marked private, so changesets versioned it and then skipped publishing it. cumin, brasa, chell and calypso all depend on it, so every published version since menu was extracted has been uninstallable from a clean registry:\n\n```\nnpm install @fnndsc/brasa\nnpm error 404 The requested resource '@fnndsc/menu@*' could not be found\n```\n\nIt worked in development only because the workspace resolves menu locally, and it went unnoticed because nothing ever installed these packages from npm alone.\n\nmenu is an ordinary library — the wire schemas and types three published packages import. Its manifest was already publish-ready and identical in shape to its siblings; only the flag stood in the way. It is dropped.\n\nThe dependency range is pinned to `^0.3.0` at the same time. `\"*\"` was an artefact of menu being private, since changesets does not rewrite ranges for packages it will not publish, and `\"*\"` on a published dependency means any future breaking change applies silently.\n\n`argus` stays private, and correctly so: it is a browser bundle served from calypso's web root, not a library anyone imports.","internal":false},{"hash":null,"headline":"Updated dependencies [e3948f1] - @fnndsc/menu@0.3.1 - @fnndsc/cumin@3.17.1","body":"","internal":true}]},{"version":"0.16.0","date":"2026-09-04","merge":"f25e3f4babc4","bump":"minor","changes":[{"hash":"5dc064e","headline":"feat(boot): warm-up leaves the gate, and a failure that leaves it is still heard","body":"Boot blocked on four prefetches in front of the prompt. Measured against a live CUBE, `/PUBLIC` costs 8.4 seconds and `/SHARED` 9.3 — roughly eighteen seconds an operator spent watching a prompt that was already theirs, buying freshness the stale-serve path delivers a moment later anyway, since the checkpoint restore has already put those listings in the cache.\n\nBoot now blocks only on `/bin`, which completion cannot work without: an empty completer reads as a broken prompt rather than a fast one. Groups, Feeds, Public and a newly added `/SHARED` step warm behind the prompt under a new `PENDING` boot status. They keep their bounded retry policy; a transient failure should be retried before it is announced.\n\n`/SHARED` had no step at all before. That is where another identity's work becomes visible, and with nothing to fail, a CUBE that stopped serving shared paths stayed silent until somebody went looking.\n\n**A deferred step leaves the boot failure gate**, so its failure can no longer stop a daemon binding — and a boot readout has scrolled away by the time it arrives. The failure is held until a later attempt succeeds and carried on the prompt context, so every surface says it: chell's prompt reads `[warm-up failed: groups]`, and argus names it on the JOBS readout in mars with the reason on hover. Named rather than counted, because \"Groups\" tells an operator which capability is degraded where \"1 warm-up failed\" only tells them to go looking.\n\nNothing reports a deferred completion. A warm that finishes and changes nothing is not news.\n\nCarries AEGIS law `deferred-warmup-failure-persists` with its smoke, which drives the surface's real prompt-context path rather than asserting a stub.","internal":false},{"hash":"eaf6c67","headline":"feat(acl): a feed's access list, in the shell's own verbs","body":"An exemplar needed to share a feed and mise had no wrapper, so it reached past the stack to CUBE's REST. That is backwards: a missing wrapper is the work. A capability living in one caller's private REST call is invisible to every other surface and untested by the kernel's suite.\n\nThe kernel gains `feed_share` and `feedShares_list`, over the client's own `addUserPermission` and `getUserPermissions` — which existed all along, so no raw REST was ever needed.\n\nIts shell face is `setfacl` and `getfacl`, not a `share` verb. Granting an identity access to a thing is an access control entry, and that is a verb a terminal already knows; \"share X with Y\" is a sentence, and reads as a natural-language assist rather than a shell:\n\n```\nsetfacl -m u:someone:r /home/me/feeds/feed_12\ngetfacl /home/me/feeds/feed_12\n # file: home/me/feeds/feed_12\n user::rw-\n user:someone:r--\n```\n\nA feed is named by id, by the `feed_N` a listing shows, or by any path holding one, so a path under `/SHARED` resolves by the same rule as one under a home folder. Group and other entries are refused rather than quietly read as users, an entry granting no read is refused because reading is what a CUBE share conveys, and `-x` is refused **by name** — CUBE models the grant but mise has no revocation to offer, and a shell that appears to strip an entry it cannot strip is worse than one that says so.\n\nThe grammar sits in its own dependency-free module, as `cat`'s does: the engine graph cannot be loaded under jest, so a grammar inside the builtin is a grammar nothing tests.","internal":false},{"hash":"73aa61a","headline":"The feed roster (`proc feeds`, the RUNS pane) reports each resident feed's total output size and wall span, derived from the cache with no wire;","body":"`cd` into a CFS link now follows it to its target, and a refusal names that target.","internal":false},{"hash":"75f1e5f","headline":"fix(boot): a step's label stops moving when it finishes","body":"A boot step is announced while it runs and again when it settles, and the two lines disagreed about where the label starts. The running line hardcoded a five-space indent against a comment asserting `[ OK ] ` was seven characters wide; the finished line pads its tag to the widest tag and adds a space. The label therefore jumped a column as each step resolved, and the plain non-interactive log was out by three rather than one.\n\nThe indent is now derived from the host's own tag width and from whether a spinner glyph precedes the text, since an animated line draws a frame and a space of its own and owes only the remainder while a plain log line owes the whole column. `chell` passes the width it actually renders with, so a longer status added later moves both lines together.\n\nThe arithmetic lives in its own import-free module, because the wider engine graph cannot be loaded under jest and an untestable alignment rule is how the first version came to be wrong.","internal":false},{"hash":"4f034b9","headline":"Host control: `chell --daemon --host-control[=shell,files,pipes]` lets the daemon declare capabilities of its own — `!` runs on the daemon host, pipe segments run there, `upload`/`download` reach its disk — off by default, refused on a non-loopback bind without `--expose-host-control`, and annunciated everywhere (attach ack `hostControl`, the daemon face, the prompt's HOST segment, a remote shell's banner).","body":"Without the `files` tier, `upload` under a daemon now refuses instead of reading the daemon host's disk.","internal":false},{"hash":"aa25502","headline":"The process cache records the compute resource each plugin instance ran on (from the CUBE list row), and the `feed.dag` model carries it per node (`mixed` for a group whose members ran on different resources), so a surface can hue a graph by where its work ran.","body":"","internal":false},{"hash":"f8d1b1c","headline":"Index movement is annunciated: a feed's first-visit topology load (`feed 812 indexing: 3400/20000 17%`) and roster arrivals (`+feed 812`, feeds created since or newly shared) reach the prompt context's `procWarmup` segment — `feed`, `arrived`, and `sweeping` so a renderer can tell a sweep from a load — and the chell prompt renders both.","body":"The process cache keeps the two registers (`feedLoad_progress/clear/get`, `arrivals_note/recent`); the salsa feed walk and roster syncs feed them.","internal":false},{"hash":null,"headline":"Updated dependencies [f73e6c2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5dc064e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3afaa65]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5b4b7db]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [eaf6c67]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [73aa61a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4f034b9]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aa25502]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [f8d1b1c]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aaf0159]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [85c6813] - @fnndsc/cumin@3.17.0 - @fnndsc/menu@0.3.0 - @fnndsc/salsa@3.12.1","body":"","internal":true}]},{"version":"0.15.0","date":"2026-09-02","merge":"e4e14b714f31","bump":"minor","changes":[{"hash":"920e0ac","headline":"The `feed.dag` wire model is now the COLLAPSED projection: isomorphic sibling subtrees merge into one ×N node (the terminal tree's own collapse transform), so a massive fan-out crosses the wire and reaches a rendering surface as its shape, not its census.","body":"Each collapsed node keeps a real representative instance (id and data address), wears its worst member's status (an error anywhere in the group is visible on the node that stands for it), sums member metrics, and carries a tally with jump-to-error anomaly ids.","internal":false},{"hash":"c716624","headline":"Directory listings survive a restart.","body":"The listing cache is checkpointed (identity-keyed file under `~/.cache/chell/vfs/`, throttled writes) and restored at boot with each entry's original timestamp, so a restored listing is exactly as stale as it really is. Stale handling itself is fixed: the listing path used to serve any cached entry regardless of its TTL (a listing never refreshed until eviction or `ls -f`). Now a fresh entry serves as is; a stale one is served at once and revalidated behind itself when a host can carry the refresh (the daemon publishes the fresh `fs.listing` on the ambient bus, marked `fresh`), and is refetched in line at a plain console. `ls` models carry `fresh` per listing; `vfs.listing_get` exposes the listing with its freshness.","internal":false},{"hash":"97af423","headline":"Watches: a surface can keep a running feed live.","body":"New wire pair `watch` / `unwatch` (subject = `/proc/jobs/feed_N`, owned per surface, released on detach) and a `watched` report (`live` | `settled` | `stale`). While anyone watches a feed the engine samples it on an adaptive cadence (3 s while it changes, backing off to 30 s when quiet), and whenever a visit changes the cache it publishes the refreshed `feed.dag` model to every surface as a session-bus envelope from the `daemon` surface, off the scrollback. A feed that settles reports `settled` and the watch ends; a failed sample reports `stale` and keeps trying. `proc watch <feed>` / `proc unwatch <feed>` are the console forms (`proc watch` lists). The engine gains an ambient event bus for events it originates on its own. Feed visits within one second of each other now share one sync, and `feedVisit_sync` reports whether it succeeded.","internal":false},{"hash":"cece0dc","headline":"`/proc` freshness is now visit-driven.","body":"A revisit to `/proc/jobs/feed_N` (or a `feed diagram` re-render) is a delta, not a re-crawl: one feed-row fetch, and only when the job counters moved, a `min_end_date` walk of the nodes created or finished since the last visit plus an `active=true` sweep of the nodes still running. Nodes a dynamic pipeline spawns after the first load now appear on the next visit; previously they were invisible until `proc refresh`. Settled feeds are re-checked at most once per ten minutes, so work appended to a finished feed is still seen. A `/proc/jobs` visit (and `proc feeds` / `proc jobs list`) picks up feeds newer than the highest known id, and walks the whole index once the roster is older than ten minutes, so a feed shared later still appears.","internal":false},{"hash":null,"headline":"Updated dependencies [920e0ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7a0f06e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c716624]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [cece0dc]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [97af423] - @fnndsc/cumin@3.16.0 - @fnndsc/salsa@3.11.0 - @fnndsc/menu@0.2.0","body":"","internal":true}]},{"version":"0.14.1","date":"2026-09-01","merge":"ee45cef0a1d0","bump":"patch","changes":[{"hash":"1bc0953","headline":"`ls` on a ChRIS link (`~/public`, `~/shared`) follows the link instead of rendering the link entry itself: the parent-cache leaf shortcut no longer captures links, so resolution falls through to the dispatcher's PathMapper.","body":"Native listings are also name-deduplicated (CUBE's links search can return the same row twice, observed on /PUBLIC).","internal":false},{"hash":null,"headline":"Updated dependencies [44ba77a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1bc0953] - @fnndsc/cumin@3.15.1 - @fnndsc/salsa@3.10.1","body":"","internal":true}]},{"version":"0.14.0","date":"2026-09-01","merge":"77fa3b4e5149","bump":"minor","changes":[{"hash":"85791b3","headline":"`config write <cfs-path> <base64>` — the durable layer's write path.","body":"Per-user surface state (argus desktops, and the config-in-CFS family to\ncome) lives as files under `~/.config/...` in the user's CUBE home. Reads\nwere always `cat`; this adds the write: content arrives base64-encoded,\nstages through a host temp file, and rides the same upload\n(replace-in-place on an existing document) the edit flow uses. Documents\nare notes, not datasets: 256KB limit.","internal":false}]},{"version":"0.13.0","date":"2026-08-31","merge":"03f466be83ce","bump":"minor","changes":[{"hash":"0adb4f2","headline":"The daemon terminal gets a resting state: the console face.","body":"The boot-phase brain animation repainted by cursor arithmetic against a\nscrolling buffer — counting rows printed beneath it and jumping up — so a\nretry warning landing mid-frame interleaved with the art, and the pulse had\nto kill itself the moment output scrolled the logo away: the brain went\nsilent exactly when the daemon came alive.\n\nBoot is unchanged and text-first: the brain prints, pulses while\ncredentials are checked, and every address and token scrolls into ordinary\nscrollback. What is new is what happens when boot completes on a TTY: the\ndaemon switches to the alternate screen buffer — the same screen vim and\nhtop own — where the brain pulses forever at fixed coordinates over an\nidentity panel (identity, wire, ARGUS URL, token, berth, attach line,\nuptime, attached surfaces, index counts), with the last few log lines caged\nin a dim strip beneath. The pulse is honest: idle is a slow shimmer, an\nexecuting command quickens it, a surface attaching flares it.\n\nEsc or `q` drops to the normal buffer, restored byte-perfect with the boot\nlog intact and the lines captured while the face was up flushed beneath it;\nany key returns. Ctrl-C stops the daemon from either mode. Off a TTY\n(systemd, nohup) nothing changes: sequential logging, as before.\n\nThe brain art and its frame renderer moved from chell to brasa\n(`logo_frameRender`, `logo_linesRender`, and the new `logoRows_count` /\n`logoColumns_count`), so any surface can draw it; chell keeps only its\nboot-terminal animation host. `daemon_launch` now returns the launched\ndaemon's addresses and handle, which both launch paths (`chell --daemon`\nand the standalone `calypso` binary) feed to the face.","internal":false},{"hash":"31c2a50","headline":"`pipeline diagram` and `feed diagram` now carry their graphs as typed envelope models: `pipeline.diagram` (the authored topology) and `feed.dag` (the live instance graph with statuses and each node's `/proc` data address).","body":"The rendered trees are unchanged; a graphical surface reads the model where a terminal reads the text — one command, two projections.","internal":false},{"hash":"9ed68cd","headline":"`download` no longer writes to the daemon host's disk.","body":"File delivery is now a surface capability, like prompting, piping and editing already were.\n\nThe builtin resolved its destination with `path.resolve()` inside the engine,\nso the bytes landed on whatever machine hosted the engine. For a local shell\nthat is right — the engine is in-process and the operator's disk is the\nengine's disk. Under a daemon it put files on a machine nobody attending the\nsession was sitting at, and from a browser the question \"download to where?\"\nhad no answer at all.\n\n`Surface` gains `fileDeliver`, and `SurfaceCapabilities` gains `fileDelivery`\nalongside `engineFilesystem` — which says whether a path the engine resolves is\na path this surface's operator can open. Only an in-process local shell claims\nit. When it is false, `download` hands the file to the surface, which puts it\nsomewhere its operator can actually reach: the client's disk for\n`chell --remote`, the download manager for `argus`.\n\nOnly the request crosses the wire. Each surface fetches the bytes itself\nthrough the daemon's existing token-gated `/vfs` route, so a DICOM series is\nnot base64'd across a channel meant for session state — the intent travels\nthrough the vocabulary and the bytes travel through the byte route.\n\nThe local path is unchanged: a local `chell` still uses the existing transfer\nmachinery, with its globs, directory walks and progress reporting.\n\nA directory has no bytes to hand over, and what to do about that depends on\nwhat the surface has — a third capability, `localFilesystem`, declared in the\nattach handshake and answered by the surface rather than by the daemon. A shell\nowns a filesystem wherever it runs, so `chell --remote` receives the tree file\nby file and gets the folder it asked for. A browser owns no directory and can\ntake files only one at a time, so several hundred DICOM instances would be\nseveral hundred saves; for that surface alone a directory is archived into a\nsingle CUBE file first, through\nthe registered `zip v20240311` pipeline — `pl-dircopy` into a zip plugin, the\nsame mechanism the ChRIS web UI has used for years, but living in `brasa` where\nevery surface reaches one implementation instead of each client re-deriving the\nsequence. `CHRIS_ARCHIVE_PIPELINE` names a different pipeline where a\ndeployment registered one. When it is absent, the failure says which pipeline\nis missing and that it can be registered from the store.\n\nThe archive run announces itself rather than creating a feed silently, because\nit is a workaround for CUBE having no directory-archive route: issue #233.\n\n`upload` has the mirror problem — it reads from the engine host's disk — and is\nnot addressed here, because the browser direction needs a file picker. See\nissue #232.","internal":false},{"hash":"e58bf58","headline":"The wire contract moves out of `calypso` into a package of its own, `@fnndsc/menu`.","body":"A surface author should depend on the contract, not on the daemon that happens\nto serve it. Until now the contract was a subpath of `@fnndsc/calypso`, so a\nthird-party surface took a dependency on the session host to learn the shape of\na result. `menu` imports nothing from the stack and sits below `cumin`, so both\nthe engine that produces envelopes and the browser that renders them can load\nit.\n\nTwo vocabularies the contract narrows to moved with it, because they were\ndeclared above the thing that describes them: the structured-progress values\n(previously `@fnndsc/brasa/progress`) and the prompt-facing process-index state\n(previously `@fnndsc/cumin/proc-prompt`). Both are re-exported from their old\nhomes, so existing importers are unaffected; `@fnndsc/cumin/proc-prompt` as a\nsubpath is gone, and its types are available from `@fnndsc/cumin` directly.\n\n`CommandEnvelope` is now inferred from the schema that validates it. The engine\nand the wire previously carried separate declarations of the same shape, tied\ntogether by a compile-time assertion that one stayed assignable to the other; a\nsingle inferred type makes that drift impossible rather than detected.\n`@fnndsc/cumin` re-exports the name, so nothing that imports it changes.\n\n`@fnndsc/calypso/protocol` no longer exists as a subpath. Import\n`@fnndsc/menu`. The names remain re-exported from `@fnndsc/calypso` itself for\nnow, since most of the stack has always reached them there.\n\nThis is the scaffold for envelope Phase-2 — a typed result model for every\ncommand — recorded in `docs/menu.adoc`. `menu` itself is unpublished until that\nwork lands, so thirty payload shapes can settle without forcing a release each\ntime.","internal":false},{"hash":"e062dbf","headline":"Two more commands speak in models: `proc feeds` (now listing the whole cache-resident roster when unqueried) carries a `feed.list` model, and `pacs query` carries a `pacs.query` model — studies and series with their instance UIDs and pullable VFS paths.","body":"Terminal renderings are unchanged.","internal":false},{"hash":"28b9a9f","headline":"Regard: the session learns what the operator is indicating.","body":"The wire contract gains one message, `regard`, travelling both directions\nunder one shape: a surface reports the addressable thing the operator most\nrecently indicated (a file clicked in a browser, a DAG node selected), and\nthe daemon retains it as session truth — last write wins — mirrors it into\nthe engine, and rebroadcasts it to every attached surface. A late attacher\nreceives the retained value with its ack, so spawn-then-see workflows start\nseeing immediately.\n\nThe value is an address in the namespace plus the model kind it was\nindicated through, never view-space coordinates: what has no address is view\nstate and stays surface-side. The brasa session retains the value behind\n`regard_get`/`regard_set`, so engine-side consumers can answer \"what is the\noperator regarding\" without any surface geometry crossing the seam. Design\nrecord: apps/argus/docs/aegis.adoc.","internal":false},{"hash":"8842ab4","headline":"Indeterminate progress now crosses the daemon wire as typed facts rather than terminal escapes.","body":"The spinner used to write `\\r\\x1b[K<frame>` and cursor hide/show to the status\nchannel twelve times a second, so every attached surface received terminal\nchoreography whether or not it was a terminal — a web surface had to emulate a\ncharacter grid to recover the meaning, and got it subtly wrong. It now announces\n`operation: 'task'`, `kind: 'inspection'`, `phase: 'working'` with a label, and\ncloses with `phase: 'complete'`. Each surface draws waiting in its own idiom.\n\nOnly state changes cross the wire: frames and elapsed counters are the\nrenderer's, so a spin of any length costs two events instead of dozens per\nsecond. `chell` gained the elapsed counter its spinner used to bake into the\nlabel, and `argus` gained a full progress renderer — indeterminate work spins,\ncounted work fills a bar — which also surfaces the download progress it had\nbeen silently discarding.\n\nThe `operation` and `phase` enums gained `task` and `working`. Every enum on\nthe progress message now degrades on an unknown value instead of failing the\nparse and dropping the message whole: `operation` to `task`, `phase` to\n`working`, `status` to `unknown`, `kind` and `unit` to absent. That makes good\nthe contract's promise that change within a major is additive — for future\nadditions, since the fallback lives in the build doing the reading.\n\nThe spinner keeps its call signature, so its callers are unchanged. Its\n`showTiming` and `clearLine` arguments are now ignored: both are rendering\ndecisions. It also no longer inspects `process.stdout.isTTY` before announcing,\nwhich had suppressed progress inside the daemon, where the engine's own stdout\nis never a terminal but the attached surface may well be able to draw.","internal":false},{"hash":"1d600ac","headline":"An archive that cannot run no longer leaves a feed behind, and says the right reason for not running.","body":"Live testing found both. `download <dir>` from a browser reported that the\n`zip v20240311` pipeline was not registered, directly below a line explaining\nthat one of its nodes was not registered on the target compute environment. The\npipeline was present; the advice to fetch it from the store was wrong. Every\n`pipeline_run` failure had been collapsed into the one message.\n\n`pipeline_readiness` in salsa answers whether a pipeline exists and could be\nprepared, distinguishing _unregistered_ from _registered but unpreparable_ —\ndifferent problems calling for different actions.\n\nPreparing a pipeline needs no previous instance, so readiness is now checked\n_before_ the feed is created. The failed run above had already created a feed\nthat nothing then used, leaving litter in a feed list and a copy in the compute\ngraph asserting an analysis that produced nothing.\n\nA run that fails after its feed exists now removes it, and names the feed when\nremoval fails. A run that merely exceeds its time limit is left alone, since it\nmay yet finish and deleting would remove the feed from under a running job.","internal":false},{"hash":"1c195a7","headline":"Execution metrics ride the warmup for free: node differentiation lands.","body":"Every CUBE plugin-instance list row already carries `start_date`,\n`end_date`, and `size` — the same rows warmup and status refresh page\nthrough. They are now typed on the contract, captured into the proc cache\n(`ProcInstance.startedAt/finishedAt/outputBytes`, merged defined-only so a\nrefresh never erases what warmup saw), persisted by the checkpoint, and\nprojected by `feed diagram` onto each node's `metrics` (wall-clock\n`computeSeconds`, `dataBytes`). Zero new CUBE calls at any point.\n\nThe molecule rendering scales by them (a SCALE pill flips between wall\ntime and output bytes, re-projecting the remembered model locally), and\ntimestamp-true pulse replay becomes possible.","internal":false},{"hash":"87f7c59","headline":"The `pacs.query` model now says what CUBE already holds: each series carries a `pulled` flag and file count (one bounded sweep of single-attempt lookups), and studies carry their own VFS path so a surface can pull a whole study.","body":"","internal":false},{"hash":"3125517","headline":"A dropped retrieve watch is no longer reported as a failed pull.","body":"Pulling a 22-series study reported `0/22 series complete` with every series\nmarked `ERROR` — and the CUBE path report printed immediately below it listed\nfour of those same series with real paths and file counts. The retrieves were\nfine; the watch had died.\n\nOne websocket failure marked every in-flight series `error`, because\n`RetrieveStatus` had no value meaning _the client stopped watching and does not\nknow the outcome_. The code knew the difference — a comment in `pull` says a\nwatch failure \"is usually cosmetic, the PACS keeps pushing and CUBE keeps\nregistering after detach\" — but nothing downstream acted on it.\n\nA lost watch now marks its series `unconfirmed`. The confirmation loop, which\nalready asks CUBE about series whose confirmation went missing, now asks about\nthese too: a series CUBE reports as stored is `pulled`, with its file count and\npath, whatever the watch managed to see.\n\nWhat remains unconfirmed is reported as unknown rather than lost — `? … [WATCH","internal":false},{"hash":"b39b584","headline":"A dropped retrieve watch reconnects instead of giving up.","body":"The LONK socket is only how a client watches a retrieve; the retrieve itself\nruns on the server and is unaffected by the socket dying. Losing the view was\nnonetheless the end of the watch — which is why a 22-series study failed where\na 2-series one did not: a longer retrieve gives the socket more chances to\ndrop.\n\nA dropped socket is now reopened, up to three times, and re-subscribed to the\nseries still in flight. Nothing is re-fired: those retrieves were never lost.\nThe reconnection is announced, because a silent one during a long pull is\nindistinguishable from a stall.\n\nOnly when reconnection is exhausted does the watch stop, and it still records\nthe remaining series as `unconfirmed` rather than failed.","internal":false},{"hash":null,"headline":"Updated dependencies [1d600ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a78b5ee]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c195a7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [e58bf58]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [56ade16]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2bb9c29]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3125517]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2c38d8b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [b39b584] - @fnndsc/salsa@3.10.0 - @fnndsc/cumin@3.15.0","body":"","internal":true}]},{"version":"0.12.0","date":"2026-08-26","merge":"6d1e218fbad2","bump":"minor","changes":[{"hash":"0ad04f2","headline":"The engine can now hand raw file bytes to a hosting daemon: brasa's `BrasaEngine` gains an optional `file_read(filePath)` that resolves a ChRIS VFS path to a `Buffer` through chili's binary cat.","body":"Calypso's daemon exposes it as a token-gated `/vfs?path=&token=` HTTP route with extension-derived content types, letting a web surface render images and other binary content that a text transcript cannot carry.","internal":false}]},{"version":"0.11.1","date":"2026-08-25","merge":"752c986dcf84","bump":"patch","changes":[{"hash":"b0b478b","headline":"PACS payload and path-grammar consolidation.","body":"Cumin gains `dicomPayload`, the one home for DICOM query-payload interpretation (tag unwrapping including the DICOM-JSON `{Value: [...]}` form, study and series array location, UID lookups), replacing four diverged private copies. Salsa's `pacsHelpers` becomes the single authority for the PACS folder grammar, adding `folderUID_get`, `queryLabel_extractFromFolder`, and `queryFolderName_build`; the query path a surface builds now always matches the name the listing shows, including the title fallback the old builder lacked. Brasa's `pacsUtils` and `query` builtins consume the shared helpers; `pacs_tagValueExtract` remains as a compatibility alias.","internal":false},{"hash":null,"headline":"Updated dependencies [b0b478b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [bb4e06a] - @fnndsc/cumin@3.12.0 - @fnndsc/salsa@3.8.0","body":"","internal":true}]},{"version":"0.11.0","date":"2026-08-24","merge":"fbb9d089711b","bump":"minor","changes":[{"hash":"69b0617","headline":"Typed chell API, third tranche: `cp` and `mv` join the facade with their existing `fs.cp`/`fs.mv` models.","body":"Fixing what their first typed run surfaced: file rename was silently broken against current CUBEs (the PUT field drifted from `path` to `upload_path`); cumin now sends the current field and falls back for older servers.","internal":false},{"hash":"7ed8e1c","headline":"Typed chell API, second tranche: `ls` and `cat`.","body":"`ls` gains the `fs.listing` model (entries per target as data), `cat` joins with its existing `fs.cat` outcomes and content in the rendered channel, and both enter the same cores the parsed builtins enter. Programmatic `cat` never injects syntax highlighting.","internal":false},{"hash":"7dc3bca","headline":"The typed chell API, first tranche.","body":"`chellApi_create()` returns the shell's vocabulary as function calls: `pwd`, `cd`, `mkdir`, `touch` and `rm` take typed options, enter the same per-command cores the parsed builtins enter, and return envelopes whose model slot is typed per command (`TypedEnvelope<K>` over the new `FsModelMap`). No command line is assembled or parsed anywhere on the path.","internal":false},{"hash":null,"headline":"Updated dependencies [14f8ee8]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4da2673]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [69b0617] - @fnndsc/cumin@3.11.0 - @fnndsc/salsa@3.7.0 - @fnndsc/chili@3.6.5","body":"","internal":true}]},{"version":"0.10.1","date":"2026-08-24","merge":"1bc0f7746067","bump":"patch","changes":[{"hash":"5285bd5","headline":"Cast burndown to the adapter floor.","body":"The wire contract gains the pipelines surface (`pipeline_get` with piping items and plugin metadata handles, `pipelineSourceFilesPage_get`); salsa's pipeline modules, feed joins, and brasa/chili call sites migrate onto typed accessors and honest converters. The repository's `as unknown as` count is now 2, both inside the licensed adapter seam, and the CI ratchet holds it there. chell progress bars also draw on the renderer's configured stream instead of assuming stdout.","internal":false},{"hash":null,"headline":"Updated dependencies [5285bd5]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [919648f] - @fnndsc/cumin@3.10.0 - @fnndsc/salsa@3.6.1 - @fnndsc/chili@3.6.4","body":"","internal":true}]},{"version":"0.10.0","date":"2026-08-21","merge":"364c31576aa7","bump":"minor","changes":[{"hash":"22db63f","headline":"Make PACS pulls observable and recoverable.","body":"A new `pacs status <expression | path | queryId>` reports one line per series: a fill bar of files registered in CUBE against the PACS-reported instance count, the derived state, and the in-CUBE `/SERVICES` folder for every landed series; an expression reuses the caller's most recent matching query instead of minting a new record per check. `pull` is now idempotent: series already fully registered in CUBE are skipped before firing, so re-running the same pull fetches exactly the missing series. Retrieve firing gets three attempts with backoff and is bounded to four concurrent creations, replacing the all-at-once stampede that overloaded CUBE and silently lost retrieves. A series whose firing still fails is reported as `FAILED TO FIRE, will not arrive; re-run pull`, distinct from watch-side failures, which now point at `pacs status` since their transfers usually complete server-side anyway. Cumin's status report gains the per-series storage folder (`seriesStorage_resolve`). The failure modes and operator playbook are documented in docs/pacs-pull-recovery.adoc.","internal":false},{"hash":"590a943","headline":"Converge the two PACS retrieve watchers onto one engine.","body":"pull's battle-tested LONK machinery (bounded, retried firing; stall/timeout/no-activity detection; storage confirmation; refire loop) moves into salsa as the presentation-free `retrieve/watch` module; the `pull` builtin becomes a thin consumer rendering engine events onto its sink, and the PACS VFS provider's `cp` drops its 5-second polling watcher for the same engine, gaining firing retry, LONK push, and the idempotency skip. The convergence surfaced and fixed two latent defects that had made PACS `cp` unusable on real paths: the source parser assumed paths without the `/queries/` segment and rejected every real listing path, and the file downloader only consulted the userfiles collection, so PACS files under `/SERVICES` read as \"not found\"; it now falls back to the PACS files collection. Supporting dedup: cumin gains `retry_untilValue` (the one bounded-backoff loop) and `seriesStorage_resolve` gains bounded re-probing, replacing the per-package storage-resolver copies; the `_qid:` parser is now shared from salsa.","internal":false},{"hash":"b2f5ad3","headline":"Greet every surface with the stack's identity.","body":"Builds now record the short git hash they were produced from (`dist/buildinfo.json`, written by the new `scripts/buildinfo.mjs` build step), and brasa exposes it through `buildHash_get` alongside `welcomeLine_build`/`welcomeLine_compose`, which render a banner of the form `ChELL Executes Layered Logic, v 5.3.0 (886f09). Welcome.` An interactive chell session prints the banner and a short fortune at boot, the calypso daemon announces its banner plus an aligned version line for every stack layer (chell, brasa, chili, salsa, cumin, calypso) when it starts listening, and the attach handshake gains an optional `stack` field carrying all six versions and the build hash, so a remote surface banners the daemon's full reported stack rather than its local install's. `fortune_random` is exported for reuse, with an optional line-count bound so banners favour short cookies.","internal":false},{"hash":"2e60785","headline":"Cache the `/etc/group` projection per CUBE connection for five minutes, invalidate it after ChELL membership changes, and show semantic inspection progress while an uncached projection is resolving.","body":"","internal":false},{"hash":"66bc932","headline":"Add ChELL group membership commands and include current CUBE usernames in the live `/etc/group` projection.","body":"","internal":false},{"hash":"8ec8a4b","headline":"Add explicit, command-scoped CUBE elevation with `sudo <command>`.","body":"The active surface collects an administrator identity and hidden password; a temporary CUBE client runs only the nested command, then the normal session is restored. Group membership and plugin registration now suggest a copyable `sudo` rerun after an authorization failure, and plugin registration no longer owns a separate interactive admin prompt.","internal":false},{"hash":"d0ac04f","headline":"Preserve failure causes and tighten cache contracts.","body":"Errors that were replaced by generic messages now carry their real cause: controller and handler creation failures name the underlying error instead of \"no ChRIS context\", `cat` shows the actual fetch failure, docker command failures surface stderr detail, context-set errors actually print, settings load/save failures are announced instead of silently using defaults or losing changes, unreadable berth and discovery files are named instead of reading as \"no daemon\", token-file read errors are distinguished from the logged-out state, and resource deletes report why they failed. Every remaining deliberate error absorption now carries a dated adjudication comment. Cache contracts: the PACS provider's decoded-query cache caches settled results only and is size-bounded; the object-context factory gains an eviction hook (`objContext_evict`) for deleted plugin or feed ids; `mv` and `cp` invalidate the whole affected listing subtree; and a new PACS query invalidates the cached `/net/pacs/queries` listing so it appears in the next `ls` immediately.","internal":false},{"hash":"886f09c","headline":"Fix stale directory state in long-lived daemons.","body":"Folder object contexts are no longer cached in the cumin context factory: folder paths are not stable identities in CUBE (deleting and re-uploading a directory assigns a new folder id), so a group bound at first touch kept serving the dead folder for the life of the process, which in a calypso daemon meant a freshly uploaded directory listed as permanently empty and `ls -f` could not recover it. Plugin and feed contexts keep their stable-id cache. The listing cache gains `cache_invalidateTree`, and `rm` and directory uploads now invalidate the whole affected subtree, so a delete-and-re-upload cycle cannot serve still-fresh nested listings from the old tree. The salsa native provider also distinguishes a missing folder from an empty one: when every sub-listing comes back empty it probes the parent, and a nonexistent path reports \"No such file or directory\" instead of rendering as an empty directory.","internal":false},{"hash":"50e9bb1","headline":"Make CUBE group membership commands name-first and batch-capable.","body":"`group members`, `group inspect`, `group adduser`, and `group removeuser` now accept an exact group name or a numeric ID; add and remove accept multiple usernames, report every result, and make already-satisfied membership changes successful no-ops.","internal":false},{"hash":"d0ac04f","headline":"BEHAVIOR CHANGE: failures no longer read as empty results.","body":"Commands that previously printed nothing and exited 0 when a fetch failed now report the error and exit non-zero, following the codebase's `Result<T>` pattern throughout. Affected surfaces: listing a PACS study or series that does not exist errors instead of showing an empty directory; `store list`/`store search` and `compute list` error when the store or CUBE is unreachable instead of showing no entries; plugin registration no longer defaults to the `host` compute resource when the compute fetch failed; `rm` on a feed fails when running jobs could not be cancelled instead of claiming success; a recursive scan that cannot list a subtree reports the exclusion instead of silently omitting it; batch job-status gaps and disconnected status fetches push visible warnings; the native VFS distinguishes \"folder absent\" from \"could not verify\" so probe failures no longer masquerade as missing directories; `cp` fails when path resolution fails rather than proceeding with a guessed path; join-edge resolution retries after a transient failure instead of permanently recording no joins; and the remote client reports invalid daemon messages instead of silently dropping them. Exit codes are now truthful end to end: `ls` reports an error status when any listing fails instead of aggregating failures into success, and `chell -c` derives its exit code from the command's envelopes, so a failed one-shot command exits non-zero even when the builtin did not set an exit code itself.","internal":false},{"hash":"ac69b3e","headline":"Run ChELL shell escapes on the originating surface, including remote clients, instead of exposing the CALYPSO daemon host process and filesystem.","body":"","internal":false},{"hash":"0e92d4d","headline":"Persist and reconcile daemon `/proc` checkpoints, fix wildcard listing of virtual executables, keep remote admin prompts on their originating surface, add a Unix-style `id` builtin for the current CUBE UID/GID projection and group memberships, and make versioned-plugin help, parameters, and README output compose correctly through terminals, pipes, and redirects.","body":"","internal":false},{"hash":"fa81126","headline":"Retain failed `/proc` topology sweep state and add `proc retry` to continue at the failed page without repeating already successful pagination work.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [2e60785]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [66bc932]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8ec8a4b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d0ac04f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [886f09c]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50e9bb1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d0ac04f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [22db63f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [0e92d4d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [590a943]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fa81126] - @fnndsc/salsa@3.6.0 - @fnndsc/cumin@3.9.0 - @fnndsc/chili@3.6.2","body":"","internal":true}]},{"version":"0.9.9","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Keep `cat /bin/<pipeline>` immediate with a cache-only executable summary;","body":"move complete registered invocation YAML and delayed inspection progress to `pipeline manifest <specifier>` and `<pipeline> --manifest`.","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/salsa@3.5.5","body":"","internal":true}]},{"version":"0.9.8","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Emit delayed semantic progress when an exact `/bin` Pipeline manifest read takes longer than 300 milliseconds, while leaving fast cache hits silent.","body":"","internal":false}]},{"version":"0.9.7","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Resolve exact `/bin` Pipeline reads without global Pipeline enumeration or per-node hosted-plugin metadata requests, cache repeat reads, and render Pipeline topology in linear time.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/salsa@3.5.4","body":"","internal":true}]},{"version":"0.9.6","date":"2026-07-18","merge":"e8d44db0ef69","bump":"patch","changes":[{"hash":null,"headline":"Run registered pipelines with node-qualified parameters, compute/resource controls, and strict CFS YAML overlays;","body":"expose registered manifests through `/bin`, contextual parameter help, and completion.","internal":false},{"hash":null,"headline":"Attach one plugin or pipeline after `pacs pull --new-feed`, preserving the valid Feed and root when attachment fails.","body":"","internal":false},{"hash":null,"headline":"Preserve shell-tokenized spaces, negative values, booleans, and `--name=value` across plugin and pipeline executable arguments.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.4 - @fnndsc/salsa@3.5.3","body":"","internal":true}]},{"version":"0.9.5","date":"2026-07-18","merge":"645026ee7e3b","bump":"patch","changes":[{"hash":null,"headline":"Add `pacs pull --new-feed \"TITLE\"` to create one named analysis feed from a completely retrieved PACS selection, with explicit feed/root IDs and all-or-nothing handling for partial or unresolved pulls.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.3 - @fnndsc/salsa@3.5.2","body":"","internal":true}]},{"version":"0.9.4","date":"2026-07-17","merge":"2ba2bc3d538d","bump":"patch","changes":[{"hash":null,"headline":"Add controllable syntax highlighting to `cat`, including Python and other popular source/configuration formats, while keeping ordinary pipes and redirects ANSI-free.","body":"","internal":false}]},{"version":"0.9.3","date":"2026-07-17","merge":"90c5b53729d7","bump":"patch","changes":[{"hash":null,"headline":"Carry cold, cached-refresh, and failed `/proc` lifecycle state through the CALYPSO prompt contract;","body":"reorder p10k segments and render distinct lifecycle clues for local and remote surfaces.","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.2","body":"","internal":true}]},{"version":"0.9.2","date":"2026-07-16","merge":"c79731f39938","bump":"patch","changes":[{"hash":"6f0833a","headline":"Release the coordinated ChELL stack with deterministic `/proc` topology progress, complete visible-feed indexing, safe warm-up query gating, remote one-shot command completion, and refreshed daemon documentation.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [6f0833a] - @fnndsc/cumin@3.8.1 - @fnndsc/salsa@3.5.1 - @fnndsc/chili@3.6.1","body":"","internal":true}]},{"version":"0.9.1","date":"2026-07-16","merge":"1896be7ff9ef","bump":"patch","changes":[{"hash":"71a6cd4","headline":"Route a pipeline executable's bare `--signalflow` flag to SignalFlow diagram output and provide contextual help for dynamic pipeline commands.","body":"Keep final-segment redirection on the originating surface, propagate remote pipe-segment failures back to the engine, and prevent remote command errors from terminating the interactive ChELL client. Daemon mode now reports shared startup cache warming and publishes its listening berth only after engine readiness. Consistently document `signalflow -` for stdin rendering.","internal":false}]},{"version":"0.9.0","date":"2026-07-15","merge":"942aabcdd399","bump":"minor","changes":[{"hash":"e630f79","headline":"Draw registered CUBE pipelines with `pipeline diagram <id|name>` or the `/bin` shorthand `<pipeline> --diagram`.","body":"Bare output uses the same shallow tree machinery as feed diagrams, `--withargs` appends stored non-null plugin defaults, and `--signalflow` emits the same SignalFlow YAML dialect as feeds. `feed diagram <specifier>` is now a shallow alias of `feed tree`; feed graph commands accept IDs, `feed_N`, exact or unambiguous title searches, and infer the feed from the current `feed_N` directory when omitted.","internal":false},{"hash":"e630f79","headline":"Classify CALYPSO as the assisted session host, rather than a user-facing surface, in the detailed stack information report.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [e630f79] - @fnndsc/cumin@3.8.0 - @fnndsc/salsa@3.5.0","body":"","internal":true}]},{"version":"0.8.0","date":"2026-07-14","merge":"107999d0bcd3","bump":"minor","changes":[{"hash":"8ac7ef9","headline":"Add `feed diagram --signalflow <feedId>` — emits a feed's DAG as a **SignalFlow YAML document to stdout**, composed with pipes rather than rendered in place:","body":"```\nfeed diagram --signalflow 1669 | signalflow - # ASCII\nfeed diagram --signalflow 1669 | signalflow - -o x.svg # SVG\nfeed diagram --signalflow 1669 > feed-1669.yaml # keep it\n```\n\nIt builds the graph cache-first, collapses isomorphic siblings into named `×N` chips, and\nencodes topological-join edges via SignalFlow's node-reuse mechanism. mise emits the\nrepresentation only — no renderer is invoked, discovered, or bundled — so there is nothing\nto install for the command itself; rendering is the user's own `signalflow`. `--signalflow`\nnames the dialect, leaving room for further emitters (`--json`, `--dot`, …).","internal":false}]},{"version":"0.7.0","date":"2026-07-14","merge":"177711eef683","bump":"minor","changes":[{"hash":"b8ae635","headline":"`feed tree` now collapses isomorphic sibling subtrees by default.","body":"Structurally-identical branches merge into one `×N` template node showing a proportional status bar, per-category counts (`97✓ 2⋯ 1✗`), and the ids of any non-done members (error first) so failures stay addressable. Collapsed groups use a double-line connector to signal multiplicity. Pass `--flat` to draw every node individually.","internal":false},{"hash":"ca63e8b","headline":"Add `feed tree <feedId>` — renders a feed's plugin-instance DAG as an annotated text tree.","body":"The anchor tree is drawn with box-drawing connectors; topological-join (`ts`) nodes are annotated inline with the extra sources they merge (`⋈ joins ...`). Supports `--focus <id>` to scope to a subtree and `--max-nodes <n>` to cap output (0 = all). The envelope carries a typed `feed.tree` FeedGraph model.","internal":false},{"hash":"01ab743","headline":"`feed tree` now builds the DAG **cache-first** from the warm ProcCache instead of re-crawling the feed on every call.","body":"It reuses already-loaded topology, fetches feed metadata only when missing or a placeholder, refreshes volatile status cheaply (one feed-scoped list call, active nodes only) when reusing a warm cache, and resolves join edges lazily. New salsa exports: `feedGraphData_ensure`, `feedMeta_ensure`, `feedInstances_ensureLoaded`, `feedStatus_refresh`.","internal":false},{"hash":null,"headline":"Updated dependencies [a1f6694]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [01ab743] - @fnndsc/cumin@3.7.0 - @fnndsc/salsa@3.4.0","body":"","internal":true}]},{"version":"0.6.1","date":"2026-07-14","merge":"5b197aba79a2","bump":"patch","changes":[{"hash":"0d358c5","headline":"/proc now caches settled job status.","body":"A finished plugin instance (`finishedSuccessfully`, `finishedWithError`, `cancelled`) never changes, so its status is kept permanently once observed. Consequences:\n\n- Listing a fully-finished feed under `/proc/jobs` is instant — no status calls.\n- Live status for active feeds is refreshed with a single feed-scoped list call\n (the list response already carries `status`) instead of one detail fetch per node.\n- Reading a settled instance's `status` returns the cached value without an API call.","internal":false},{"hash":null,"headline":"Updated dependencies [0d358c5] - @fnndsc/cumin@3.6.0 - @fnndsc/salsa@3.3.0","body":"","internal":true}]},{"version":"0.6.0","date":"2026-07-13","merge":"f0c19c50afad","bump":"minor","changes":[{"hash":"e5a30f7","headline":"Add `date` and `cal` builtins, in the spirit of their UNIX namesakes and fully self-contained (pure computation — no host binary, no subprocess).","body":"`date` prints the current date/time with the familiar default format, `-u` for UTC, and `+FORMAT` strftime-style format strings (it reports the time only, never sets the clock). `cal` prints a month (`cal`), a whole year (`cal <year>`), or a specific month (`cal <month> <year>`), with today highlighted. Both return their output in an envelope through the sink, so they work identically local, over a CALYPSO daemon, and in the standalone binary.","internal":false}]},{"version":"0.5.0","date":"2026-07-13","merge":"79729c9cf094","bump":"minor","changes":[{"hash":"c2087d0","headline":"Add a `fortune` builtin — the classic UNIX fortune cookie, as a shell builtin.","body":"It prints a random fortune and is fully self-contained: the content is bundled (vendored from the traditional fortune-mod datfiles, classic BSD `fortune` material), so it needs no host `fortune` binary and no datfiles on disk, and behaves identically in a local shell, over a CALYPSO daemon, and in the standalone binary. Output travels in an envelope through the sink like every other command. Regenerate the bundled set with `scripts/fortunes_generate.mjs`.","internal":false}]},{"version":"0.4.0","date":"2026-07-13","merge":"cb2432e85fb3","bump":"minor","changes":[{"hash":"880d37a","headline":"`chell --version` reported brasa's version in place of chell's: the version module moved into brasa during the engine split but still read its own `package.json` as \"chell\", so it printed brasa's number.","body":"It now resolves every package by name (reading brasa's own directly) and reports the full stack — chell, brasa, chili, salsa, cumin, calypso — with versions aligned in a column. A new `chell --info` flag prints a role-grouped table (surfaces / engine / layers) of each package, its full name, and version. The version report, the `--info` table, and the boot panel all draw from a single source of truth in brasa (`stackInfo_get`), and the standalone binary inlines every stack version at build time.","internal":false},{"hash":"e43f42a","headline":"Delegating an unknown chell command to chili no longer stalls or floods the terminal with context-init errors when the current directory is a pure-VFS path (`/proc`, `/net`, ...).","body":"chili now registers its file-group and plugin-context commands without resolving any ChRIS context — each controller is created lazily, only when a command's action runs — so an unrelated command (or a directory that is not a ChRIS folder) pays no network cost and produces no setup-time error wall. chili also exports `commandNames_get()`, a cheap network-free listing of its top-level commands. In brasa, the \"delegating to chili\" notice is now emitted on the live sink before chili runs, so it appears ahead of chili's output instead of after it; and a command chili does not know (a typo, a host program) is reported as `command not found` without delegating at all.","internal":false},{"hash":"512e14f","headline":"Fix `<command> --help` leaking to a daemon's terminal instead of reaching the surface.","body":"The `--help` flag path printed help through `console.log`, which on a CALYPSO daemon landed on the daemon's own terminal — and returned an empty envelope, so a remote surface saw nothing. Help now travels in an envelope through the sink like every other command output, so `--help` reaches the surface that asked for it and never prints on the daemon. This removes the last console-based path in the help flow (`help_show`).","internal":false},{"hash":null,"headline":"Updated dependencies [e43f42a] - @fnndsc/chili@3.6.0","body":"","internal":true}]},{"version":"0.3.0","date":"2026-07-13","merge":"96bb6d340604","bump":"minor","changes":[{"hash":"a0d3df5","headline":"The engine no longer intercepts the console anywhere.","body":"The pipe/redirect `output_capture` monkeypatch is deleted: pipes now capture through a `PipeCaptureSink` scoped over the (re-activated) `AsyncLocalStorage` sink scope, ANSI-stripping text writes and keeping binary writes (a raw `cat` of a DICOM file) byte-for-byte. `chiliCommand_run` drives chili through its `run_capture` seam and returns an envelope, so the pacs passthroughs and the unknown-command fallback are envelope-based. The remaining print-direct builtins are converted: `store`, `upload`, `download`, `connect`, `edit` return envelopes, while the streaming commands `pull`, `pipeline`, and `pacs` emit incremental output through the sink so it streams live to a terminal or daemon and is captured in a pipe.","internal":false},{"hash":null,"headline":"Updated dependencies [a0d3df5] - @fnndsc/chili@3.5.0","body":"","internal":true}]},{"version":"0.2.0","date":"2026-07-12","merge":"64fea26ab02f","bump":"minor","changes":[{"hash":"d69b086","headline":"Every printing builtin now returns a `CommandEnvelope` instead of writing to the console.","body":"`files`/`links`/`dirs`, `feed`, `plugin`, and `parametersofplugin` were the last holdouts; with them converted, the per-invocation console monkeypatch (`printingHandler_wrap`, which hijacked `console.log`/`console.error`/`process.stdout.write` to capture a builtin's output) is deleted, along with the `LiveEnvelopeOutputSink` marker that only served it.\n\nBehavioural notes: unknown subcommands of these resource commands now return a clear error envelope instead of spawning a chili subprocess, and `search` is handled natively via `list --search`. The unknown-_command_ fallback still delegates to chili, now over the same print-direct path as the other unconverted handlers.","internal":false},{"hash":null,"headline":"Updated dependencies [d69b086] - @fnndsc/chili@3.4.0","body":"","internal":true}]}]}
1
+ {"package":"@fnndsc/brasa","version":"0.33.2","built":"2026-10-07T16:38:31.678Z","releases":[{"version":"0.33.2","date":"2026-10-07","merge":"84821d12776f","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [bd0af06] - @fnndsc/orrery@0.5.0","body":"","internal":true}]},{"version":"0.33.1","date":"2026-10-07","merge":"455a27d24292","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [c5e033d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [71fe8a5]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [de04249] - @fnndsc/orrery@0.4.0","body":"","internal":true}]},{"version":"0.33.0","date":"2026-10-05","merge":"ecfe0e091342","bump":"minor","changes":[{"hash":"5a0eef5","headline":"The session can say what the installed releases changed.","body":"`notes` lists the newest release — one row per change, the package named — and `notes --since N|DATE`, `--long` and `--all` reach further; `help notes` is the same; `/usr/share/doc/NEWS` holds every release. The notes are read from the packages installed where the session runs, so they describe the code that is running.","internal":false},{"hash":null,"headline":"Updated dependencies [5a0eef5] - @fnndsc/menu@0.19.0 - @fnndsc/cumin@3.27.5","body":"","internal":true}]},{"version":"0.32.1","date":"2026-10-05","merge":"f194de9ca69f","bump":"patch","changes":[{"hash":"5b64328","headline":"Bare `help` has a \"Games and utilities\" group: fortune, cal, weather, motd, and a `help games` row saying how many more small tools and games the /usr/games shelf holds (the count read from the shelf).","body":"General keeps help, date, exit and `!`. And `sl` flies the Enterprise: the Constitution-class refit side view from the Star Trek ASCII Art blog (re-use granted with attribution, kept in the source).","internal":false}]},{"version":"0.32.0","date":"2026-10-05","merge":"ff1316b2d9fb","bump":"minor","changes":[{"hash":"6901a81","headline":"The games shelf (#892): `help games` lists `/usr/games` by category and `ls /usr/games` walks it;","body":"the text toys (cowsay with the ChRIS brain, cowthink, figlet, banner, lolcat, rev, tac, yes, seq, factor, primes, shuf, roll, rot13, morse, calc, units), the lab's own (wtf, chrisfetch, who, uptime, ping, file, xxd, strings, sha256sum, md5sum, qr, say), time and sky (pom, stardate, ddate, sunrise; the chimes timer, leave, stopwatch on the ambient channel), the showpieces (sl, cmatrix, rain, asciiquarium) and the games (tetris, snake on a surface with a canvas; quiz, hangman, 2048 by question anywhere). A pipe segment naming a builtin now runs in the kernel, so `fortune | cowsay` works in a browser; a segment the surface cannot run is refused in words.","internal":false}]},{"version":"0.31.5","date":"2026-10-04","merge":"8b4c28c80fc2","bump":"patch","changes":[{"hash":"73c0087","headline":"A PACS series that arrives is no longer shown as ERROR.","body":"The retrieve watch always asks CUBE before it gives up on a series (with no retries, the default, it never did), a series not seen to finish and not yet in CUBE is UNCONFIRMED with its reason rather than ERROR, and the watch waits a minute (not fifteen seconds) for a PACS slow to begin. Progress messages carry an optional `reason`, shown on the ARGUS badge and in pull's summary. In ARGUS a fresh answer finding a series home in CUBE replaces a pull that ended badly, and a pull waiting on its first file paces again instead of drawing a still empty bar. After twenty seconds of silence a pull checks CUBE and then asks whether to keep waiting (a surface that cannot be asked waits a minute). ARGUS runs the PACS pane's pull and query as the operator's own acts rather than instrument traffic, so they can ask.","internal":false},{"hash":null,"headline":"Updated dependencies [73c0087] - @fnndsc/salsa@3.21.1 - @fnndsc/menu@0.18.0 - @fnndsc/cumin@3.27.4","body":"","internal":true}]},{"version":"0.31.4","date":"2026-10-04","merge":"e57ca8f142f0","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [baec61d] - @fnndsc/orrery@0.3.0","body":"","internal":true}]},{"version":"0.31.3","date":"2026-10-03","merge":"607f07da0f3b","bump":"patch","changes":[{"hash":"ff80b59","headline":"A shared feed's `data` opens again.","body":"Listing a /proc link into a feed another user owns walked the owner's home asking for links, and that refusal, a side question, marked the whole `ls` an error though its listing came. chili's path walk no longer leaves its link probe's refusal on the error stack; it remembers the parent it could not read instead. When a listing then fails, `ls` says so after the reason (`ls: links in '/home/<owner>' could not be read; the path was walked as written`), because a link there, unseen, may be why the path was not found; when the listing succeeds nothing is said. ARGUS's files panel draws a listing the kernel sent even when the command is marked an error, and a refused press reads every line the kernel said, so the note reaches the bar too.","internal":false},{"hash":null,"headline":"Updated dependencies [ff80b59] - @fnndsc/chili@3.7.1","body":"","internal":true}]},{"version":"0.31.2","date":"2026-10-03","merge":"b737b6d5269e","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [51315ec]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [33e4f1d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fe4b6b4]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2d26368] - @fnndsc/menu@0.17.0 - @fnndsc/cumin@3.27.2","body":"","internal":true}]},{"version":"0.31.1","date":"2026-10-03","merge":"ff157a55d5ed","bump":"patch","changes":[{"hash":null,"headline":"Updated dependencies [685ed59] - @fnndsc/menu@0.16.0 - @fnndsc/cumin@3.27.1","body":"","internal":true}]},{"version":"0.31.0","date":"2026-10-03","merge":"30fbc2abbc84","bump":"minor","changes":[{"hash":"c1cf57e","headline":"An edit crosses the wire to a browser (#831, slice 2).","body":"The daemon's `edit` request now names the file being edited (`path`), and a surface may answer `opened`: it took the file into an editor that stays open, so the command saves nothing and says `(opened in the editor)`; each save made in that editor will run as its own command line. The argus client hands an edit to a host editor hook and answers at once that it opened, or refuses in words when it has none (the hook is installed by the editor pane, slice 3). `feed note edit N` is now `edit /proc/jobs/feed_N/note`: a terminal's editor and a browser's pane open the same text and save it the same way, and the title is set with `feed note N --title`.","internal":false},{"hash":"1c8b92d","headline":"A feed's name and sharing, whole, in the kernel (#853, slice 1).","body":"`/proc/jobs/feed_N/title` is writable: its first line renames the feed (`touch --withContents=`, `edit`), and the roster says so at once. `setfacl` speaks every entry CUBE has: `-m u:<user>:r`, `-m g:<group>:r`, `-m o::r` (public) and `-m o::-` (private again); `-x u:<user>` and `-x g:<group>` withdraw a grant (a feed that held none is said). `chmod o+r` / `o-r` is the other entry spelled as a mode. `getfacl` shows users, groups and `other::`. A feed is also named by `/proc/jobs/feed_N`. `feeds share` is retired and refuses by name; SHARE's question no longer says a grant is permanent; the access and tag verbs get a help category. cumin gains `feedAccess_read`, `feedShare_group`, `feedShare_revoke` and `feed_rename`.","internal":false},{"hash":"e20fb09","headline":"A feed's note is a file (#831, slice 1b): `/proc/jobs/feed_N/note` lists beside `status` and `title`, `cat` reads it and `touch --withContents` writes it whole (its title stands), and `edit /proc/jobs/feed_N/note` opens it in the surface's editor and saves it back through the projection — a projected file is never uploaded into CUBE's store.","body":"`feed note edit` now edits through the surface's editor seam (`localEdit`) instead of spawning `$EDITOR` itself, so a browser surface can offer its own editor; a surface without one refuses by name. Fixed on the way: an empty `# Title:` line no longer takes the note's first line as its title.","internal":false},{"hash":"ddbd551","headline":"A feed's tags are kernel-covered (#831, slice 1a): cumin's `feedTags_list`, `feedTag_add` (the user's tag of that name, made if missing;","body":"idempotent) and `feedTag_remove` (the tagging goes, the tag stays); brasa speaks them as an extended attribute — `getfattr feed_12` dumps them, `setfattr -n tag -v urgent feed_12` hangs one, `setfattr -x tag -v urgent feed_12` takes it off, `setfattr -x tag feed_12` takes every tag off. A feed is named by id, feed_N, a /feeds/ path or /proc/jobs/feed_N; another attribute or flag is refused by name.","internal":false},{"hash":"32e3919","headline":"`ls -l` shows a feed's tags (#831, slice 1c): a long listing that holds feeds (`feed_N` rows, as in `/proc/jobs` or `~/feeds`) prints each feed's tags as `#tags` after its title.","body":"The tags come from one map for the whole listing — the user's tags, then each tag's feeds — so a thousand feeds cost as many reads as the user has tags, never one per feed; the map is kept a minute and forgotten whenever a tag is added or removed here. A listing with no feed in it reads nothing.","internal":false},{"hash":"59399fe","headline":"Tags are a vocabulary of folders under `/proc/tags` (#831, slice 4a).","body":"`ls /proc/tags` lists your tags (an unworn one too); `ls /proc/tags/<tag>` lists the feeds wearing it as links to `/proc/jobs/feed_N`, and `cd` through one lands in the feed. `mkdir /proc/tags/<name>` makes a tag, `rmdir` deletes one no feed wears (\"Directory not empty\" otherwise), and `mv` renames one. `setfattr -n tag -v <name>` no longer makes a tag: naming one that does not exist is an error naming the cure (`mkdir /proc/tags/<name>`). `rm` under `/proc/tags` is refused and names the verb that does the job. New builtin `rmdir` (empty folders only, as on Linux, on the store and in projections); `mkdir` and `mv` route a projection's paths to it, and a projection that makes or renames nothing refuses by name (\"Read-only file system\", \"Invalid cross-device link\"). The old read-only `/tags` projection is retired. cumin gains `tags_index`, `tag_create`, `tag_delete`, `tag_rename` over one cached index of the user's tags and their feeds.","internal":false},{"hash":"51da853","headline":"ARGUS shows and changes a feed's tags (#831, slice 4b).","body":"The kernel's roster model (`feed.list`) carries each feed's tags from the cached tags index (one read per tag, never per feed). The runs roster marks a feed with its tags after its title, in one theme hue; a press on a mark filters the roster by it (`tag:`, a new filter-only term in the listing façade), and its × on the indicated row takes it off. A feed row offers NOTE (the editor on `/proc/jobs/feed_N/note`) and TAG. TAG asks a new `choose` question: your tags as pills (the worn ones dimmed), a field for a new name, DONE to end; each pick runs `setfattr`, a new name `mkdir /proc/tags/<name>` first, all as visible lines, and the question comes back with the tag held. A graph on stage reads its feed's note and tags when entered and shows ADD NOTE or EDIT NOTE, TAG and the marks under BACK, re-read after an editor save. A note is never read for the roster (CUBE's feed list says nothing about notes: ChRIS_ultron_backEnd#737).","internal":false},{"hash":"e057d20","headline":"ARGUS edits a file in a pane (#831, slice 3).","body":"The kernel's `edit <file>` (and `feed note edit N`) opens an EDIT pane beside the focused one: CodeMirror 6 as a guest field, loaded on demand and painted from the theme's tokens; SAVE and REVERT on the mode frame, lit only while the field differs from the file; the bar says DIRTY or SAVED; Ctrl-S saves and Esc gives the keyboard back. Every save is the line the operator could have typed, `touch --withContents='<text>' '<path>'`, echoed and run, so the transcript holds each save and a feed's note is written through its projection. A second `edit` of the same file finds its pane and keeps unsaved changes; a desktop card brings the pane back on its file. brasa fixes found on the way: the semicolon splitter honours a backslash escape (an escaped quote no longer ends the quoting, so a later `;` inside it no longer splits the line), and an empty value is a value (`--withContents ''` writes an empty file instead of touching the empty word as a path).","internal":false},{"hash":"2826f2b","headline":"A text file offers EDIT (#831, slice 3b).","body":"A file row whose extension is not on the binary list offers EDIT in ARGUS, a feed's note seen through `/proc` included; it runs `edit '<path>'` as a visible line and the kernel opens the editor pane, asking first for a file of a megabyte or more. The binary list moved into the contract package (`EDIT_BINARY_EXTENSIONS`, `path_isEditable`, `EDIT_CONFIRM_BYTES` in @fnndsc/menu), read by the kernel's `edit` for its refusal and by the surface for its offer, so the two cannot disagree. `feed note edit` help now says it edits in the surface's editor. New `docs/cube-feed-coverage.adoc`: each part of a CUBE feed, the kernel verb or path that reaches it, and the known gaps (rename, group permissions, revoking a grant, making a feed private again).","internal":false},{"hash":null,"headline":"Updated dependencies [2826f2b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c1cf57e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c8b92d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [e20fb09]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [ddbd551]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [32e3919]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [59399fe]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [51da853] - @fnndsc/menu@0.15.0 - @fnndsc/cumin@3.27.0 - @fnndsc/salsa@3.21.0 - @fnndsc/chili@3.7.0","body":"","internal":true}]},{"version":"0.30.0","date":"2026-10-01","merge":"0491d58506dd","bump":"minor","changes":[{"hash":"4864a34","headline":"`netstat`: the wire is a readout.","body":"The kernel keeps a ledger of every request it makes to CUBE (Node's diagnostics channels, nothing patched) — count since the session or the last reset, by endpoint family with time spent, the last few timed — and `netstat [-n <count>] [-r]` reads it as text and as the `net.stats` model.","internal":false},{"hash":"51f1ca9","headline":"`cd` into a virtual directory lists it through the listing façade — the cache the boot warmed and `ls` fills serves at once, a stale entry is refreshed behind the prompt — instead of fetching the whole of `/bin` from CUBE on every `cd`.","body":"","internal":false},{"hash":"c9c018b","headline":"`cd` reads the parent from the listing cache before asking CUBE: every `cd` listed the parent from CUBE (three requests) to learn whether the entry was a link, though the parent was the listing on screen.","body":"","internal":false},{"hash":"c371b47","headline":"A feed the roster gains after the topology sweep lands its jobs on its own: every roster sync (the watcher's delta, the full walk, a restored roster brought into service) queues the feeds it leaves without topology, and one detached walk at a time lands them behind the prompt — so a feed created after the sweep, or restored from a roster whose shard was never written, shows SIZE and TIME in the RUNS listing without anyone opening it.","body":"`proc stat` names the feeds still landing.","internal":false},{"hash":null,"headline":"Updated dependencies [fbb1579]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [576b948]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4864a34]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7fb4c21]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c371b47] - @fnndsc/cumin@3.26.0 - @fnndsc/menu@0.14.0 - @fnndsc/salsa@3.20.1","body":"","internal":true}]},{"version":"0.29.1","date":"2026-10-01","merge":"1b77ac182c25","bump":"patch","changes":[{"hash":"cbd71cc","headline":"A refused file read says why: the kernel names the status and reason (403 not yours to read, 404 no such file, 502 CUBE could not serve it), `/vfs` answers with them instead of 404 for everything, and an image pane that lost a slice reads the reason out.","body":"`porter --end <who>` ends one session so the next login boots on the current kernel.","internal":false}]},{"version":"0.29.0","date":"2026-09-26","merge":"a1a7cf9a2b2d","bump":"minor","changes":[{"hash":"fbd3f0a","headline":"The session lays the universe out itself: once the index is whole the daemon runs orrery's engines for every layout in a worker thread and keeps the places (`proc layout <name>` reports `laying` progress meanwhile).","body":"orrery is published; `layoutInput_build` builds the engines' input one way for browser and session, and the universe graph builders live in `@fnndsc/menu`. ARGUS waits on the session's progress instead of settling the same space, keeps places only for the view on stage (SHAPES apart), redraws a view already seen where it stood, and gives the session only default-physics places.","internal":false},{"hash":null,"headline":"Updated dependencies [fbd3f0a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fbd3f0a] - @fnndsc/orrery@0.2.0 - @fnndsc/menu@0.13.0 - @fnndsc/cumin@3.25.0 - @fnndsc/salsa@3.20.0","body":"","internal":true}]},{"version":"0.28.0","date":"2026-09-26","merge":"70016c84b4f0","bump":"minor","changes":[{"hash":"0f9ae74","headline":"The universe laid out once per identity: `proc layout <name>` answers where a surface last laid the universe out under a layout, and `proc layout put <name> <places>` keeps it beside the index checkpoint.","body":"ARGUS puts each settled layout and, in a browser that keeps none, draws from the session's copy at once, settling only feeds new since.","internal":false},{"hash":"d1da8ac","headline":"`proc universe` carries what each feed's data is (`data`: format, and for DICOM modality and series description) once the index has read it.","body":"The reader follows a copy job's links (to files and to folders), reads newest feeds first, four at a time, gives a feed that never answers 45 s before moving on, records a DICOM-named file whose header will not read as DICOM with the reason, and stamps each record with its reader's version so a better reader reads feeds again.","internal":false},{"hash":"d8e160c","headline":"`proc universe` says when each feed was made (`createdAt`, ISO 8601), the order a replay reveals the space in;","body":"a model from an older daemon reads it as empty.","internal":false},{"hash":null,"headline":"Updated dependencies [e7f82dd]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8602c78]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a8d0d99]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [f571a14]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d1da8ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [0f9ae74]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d8e160c] - @fnndsc/cumin@3.24.0 - @fnndsc/salsa@3.19.0 - @fnndsc/menu@0.12.0","body":"","internal":true}]},{"version":"0.27.0","date":"2026-09-24","merge":"43521c122e64","bump":"minor","changes":[{"hash":"53d43f6","headline":"`mkdir` follows POSIX: `-p`/`--parents` makes missing parents and accepts an existing folder;","body":"without it a missing parent is \"No such file or directory\" and an existing folder \"File exists\". `rm` says \"Is a directory\" for a directory without `-r`, reads `--recursive`/`--force`/`--interactive` whole, and stays silent on `-f` of a missing operand. `mkdir`, `rm`, `cp`, `mv` and `touch` refuse an option they do not have, by name, instead of skipping it or taking it for a path. argus ensures `~/gather` with `mkdir -p`.","internal":false}]},{"version":"0.26.0","date":"2026-09-23","merge":"4ce95fee1d4a","bump":"minor","changes":[{"hash":"7b89dcc","headline":"brasa: `pacs query --series` answers one row per series (each carrying its study's columns) on the screen and in the CSV, and alone shows the series table.","body":"`--table` shows the answer's rows aligned: the CSV's columns, for a screen, nothing cut. `--csv --table` is refused by name (two formats for one screen); `--csv-to <path> --table` writes the CSV and shows the table. A flag the command does not know is refused by name instead of being dropped in silence. `--anon` is in the help.","internal":false}]},{"version":"0.25.0","date":"2026-09-22","merge":"c1b0be0099d8","bump":"minor","changes":[{"hash":"dac2d96","headline":"brasa: `motd [surface]`, the greeting a session gives on arrival — who you are, how many feeds you hold, how many jobs have run and what share failed, what is running now, from the session's own index at no cost;","body":"\"so far\" while the index warms; ends with a fortune. A surface may name itself (`motd ARGUS`).","internal":false},{"hash":"c82f514","headline":"brasa: `proc universe` names each feed.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [247bf09]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [dac2d96]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50f8a4e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50f8a4e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c82f514]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c82f514] - @fnndsc/cumin@3.23.1 - @fnndsc/menu@0.11.0","body":"","internal":true}]},{"version":"0.24.0","date":"2026-09-21","merge":"576426874567","bump":"minor","changes":[{"hash":"e74ce1d","headline":"brasa: the pronouns a chain needs, and claims that look where a plugin files its output.","body":"Built while writing the first battery of manifests, and each one is a thing a real workflow could not say before:\n\n`${run.place}` — where the last run writes. A chain works on what the previous act produced, and only the kernel knows where CUBE put it; a manifest cannot spell a path that holds an instance id it never saw. With it, `cd ${run.place}` chains one plugin onto another's output.\n\n`expect path <p> count --deep` and a tag claim that looks beneath the path it was given. A plugin files its output in a tree — pfdicom writes under `share/incoming/<input tree>` — so a claim made at the node's own folder was false about a run that had worked perfectly. `--deep` is declared a boolean flag, because undeclared it took the comparison as its value and the claim lost its predicate.\n\nA dry run leaves a pronoun it cannot know as it was written, rather than refusing: nothing has run to give `${run}` a value, and refusing there made `--dry-run` useless for exactly the manifests that chain. `play` also reads a manifest from the engine's own disk when the ChRIS filesystem has no such file, which is how a repository's battery runs without uploading itself first — and it drains the CFS miss it went looking for, which was otherwise left behind and made a clean play exit non-zero.\n\nA claim now prints as it was MADE, narrowing included: a ✓ reading `count gt 0` for a claim about `*.nii*` says the wrong thing held, and a battery is read by people who were not there.","internal":false},{"hash":"1b2f7f7","headline":"brasa: `expect` — a claim the session can refuse.","body":"A workflow that only acts is a demonstration. `expect` is what makes one a test: it states a claim about the session and refuses, non-zero, when the claim does not hold. Because it is a verb rather than a harness, a workflow asserts wherever it is played — in CI, at a console, or in someone else's browser after they were handed the file.\n\n`expect gather size eq 14`, `expect feed 4599 status eq finishedSuccessfully`, `expect run 601152 status eq finishedSuccessfully --within 20m`, `expect path <p> count --matching '*.nii*' gt 0`, `expect path <p> tags PatientID eq anon-001`. Comparisons are `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `notContains` and `matches`.\n\nTime lives in this verb and nowhere else: `--within` keeps asking until the claim holds or the deadline passes, which is why no other verb needs a timeout and why the language has no sleep — a sleep asserts nothing and lies about how long the work took. A failed claim prints what it wanted, what it got, and how long it waited.\n\nTwo decisions worth naming. A subject that cannot answer — a feed this session has never indexed, a path holding no readable DICOM — is reported as NOT KNOWING rather than as a false claim, because those are different facts. And a DICOM tag that varies across a folder answers with every value it takes, so a claim about \"the\" value cannot pass by reading only the first file; that is what makes the tag check an honest proof that an anonymization ran.\n\nThird slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"3320487","headline":"brasa: the cohort is a kernel subject.","body":"`gather list|add|remove|clear|name` holds the set a session is working on, so building a cohort no longer requires a surface to press on. Membership is keyed by PATH: a PACS series still at the modality, a directory of uploads and a single file are all addressable, so one identity rule covers every kind, gathering the same thing twice merges rather than doubles, and `gather remove` takes the same operand whatever it is removing — or the index the operator just read back.\n\nThe cohort lives where the surface already kept it, `~/gather/current.json`, in the shape the surface already wrote, so the two read each other's cohorts with no migration: the kernel fills in what an older record left out (a member written before kinds existed is a series; one written before paths were identity carries its address in `seriesUID`), and preserves the fields it has no name for, because a surface knows things about a series that the kernel does not and a kernel write must not quietly forget them. What a gathered path IS is asked of the filesystem rather than guessed from its spelling, and the holding directory is made and its listing invalidated on every write — a cohort the next `ls` cannot see is the staleness a CSV export already taught once.\n\nThis is the first slice of the manifest work (`docs/manifest.adoc`): a workflow can now gather.","internal":false},{"hash":"a010cb0","headline":"brasa: `play` — a manifest runs, and plays.","body":"A manifest is a text file of the lines an operator could have typed. `play` reads it and feeds those lines to the session one at a time, through the same path a typed line takes — there is no second runtime, no driver and no selector language. Because every verb lowers to a line and every line's answer is a typed model the surface renders, a manifest played at a surface PLAYS: the PACS pane fills, the cohort's face counts up, a viewer opens. Played headlessly the same file prints the same envelopes and exits 0 or non-zero. Neither run knows which it is.\n\nA file says what it is (`@name`, `@description`) and what it needs (`@param NAME`, `@param NAME = default`). Everything it needs is settled BEFORE the first line runs: a manifest that gets half way and then asks has already changed the session. Lines may refer to what the session just did through a closed set of pronouns — `${gather}`, `${gather.first}`, `${gather.3}`, `${gather.size}`, `${feed}`, `${run}`, `${query}`, `${cwd}` — and a pronoun the session cannot answer refuses by name rather than expanding to nothing.\n\nA member of a cohort has two addresses and the difference matters: where it was gathered FROM, which `pull` takes, and where it LANDED, which `image` and a run take. `${gather.first}` is the first; `${gather.first.place}` is the second, and says so when a member is not in CUBE yet.\n\n`--pace` waits between lines, for watching a play at a surface. `--dry-run` expands everything and shows the lines without running them. `--step` is refused by name, because a keypress needs a terminal and a daemon has none. A refused line stops the play, naming the file and line, and the play exits non-zero — the second half of a workflow whose first half failed is noise.\n\nFourth slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"8a8dc25","headline":"A patient answers to a number: `@PAT001`.","body":"A PACS answer asked of two patients showed `1` beside each — the patient level had no kind, so it counted itself dim per group. Patients are now a kind of their own: `PAT001`, `PAT002` in their own sequence, lit on the pane, and `pull @PAT001` or `gather add @PAT001` hands a verb every series of every study of theirs, as the row's own GATHER does. A patient has no path of its own, so its address is minted from what names it (`patientAddress_of`, in the wire package, the same string on both sides); a patient the PACS answered nothing for has nothing to hand and wears no number.","internal":false},{"hash":"03e0f87","headline":"brasa: a row can be named by its number.","body":"An operator reads a listing and wants the third row. Saying which row meant repeating its address — for a PACS series, a line of text nobody types by hand — so acting on what you can see was a surface gesture, and a scripted session could not do it at all.\n\nNow a command that answers with rows records them as the session's last answer, and `@3` names one: `gather add @2,3,6`, `image @1`, `gather remove @15,16`. Lists and ranges read as they look (`@2-4`), and because the index is resolved by the one expander, every verb takes one without knowing anything about it.\n\nWhat gets numbered is the MODEL, not a rendering of it, so a console table and a graphical pane count the same rows — an index that meant different things on different surfaces would be worse than no index. What a row is worth is declared per listing (`fs.listing` and a PACS answer give paths; the cohort gives its members' paths), so a verb receives the operand it already takes.\n\nThe sigil is what keeps it honest: a directory called `2` is a path and `@2` never is, `'@2'` in quotes is text, and `user@host` is not an index because only a number at the start of a word is one. A line that acts on a number says which listing it counted (`from: ls ~/uploads · 7 rows`), and a number past the end refuses with how many rows there are rather than acting on the wrong one. An answer with no rows does not replace one that has them: an empty `ls` should not un-number the listing just read.\n\nNumbering lives as long as the session, so at a daemon it spans commands and at `chell -c` it spans the one line — `ls ~/uploads; gather add @1` is the one-shot form.","internal":false},{"hash":"1bdf876","headline":"A row wears the number it answers to.","body":"A row of the session's last answer can be named by the number beside it — `gather add @2,3,6`, `image @1` — so the number is now DRAWN beside it. The listing façade mints a leading index pill for any level that says how its rows are addressed, and no pane draws a number itself.\n\nWhat is numbered is the kernel's ANSWER rather than a rendering of it, so a console table and a graphical pane count the same rows. The lit number is found BY ADDRESS, not by counting down the screen, so a sorted or filtered listing keeps each row's own number instead of renumbering under the operator.\n\nEvery listing leads with the column and every row counts itself; only the rows the kernel reaches wear a LIT number. In a PACS answer that is the series, while the patient and study levels above count themselves dim — the distinction is the hue, never the digits, so a number nothing answers to cannot be mistaken for one that does. Numbers are zero-padded to the widest in their group, so a fourteen-series answer reads 01…14 and the column stays a column. The lit hue is the pane's FRAME hue (`--listing-index-hue`, set per pane), because the pill is chrome and not content: an orange pill beside the browser's orange kind glyph read as one blob and the number stopped being a number.\n\nThe surface learns which listing is numbered from a retained `numbered` message: brasa announces it as the answer changes, calypso relays it and replays it to a surface attaching late, and the addresses travel with it (capped, past which a long listing is numbered in the kernel and unnumbered on screen). The same shape regard already travels in.\n\nLaw: `a-row-wears-the-number-it-answers-to`.","internal":false},{"hash":"854ba2e","headline":"brasa: a verb that starts work waits for it.","body":"A run returned the moment CUBE accepted it, which reads as completion and is not: the next line of a script worked on output that did not exist yet, and the only way to sequence work was a sleep — a lie about time. Running a plugin from `/bin`, `plugin run` and `pipeline run` now wait until every node they scheduled reaches a status it does not leave, and report what happened: how long it took, and which node failed when one did (a failed run exits non-zero, so a script can be believed).\n\n`--detach` asks for the handle instead, which is what these verbs did before; `pull` gained `--detach` as the language's word for the `--nowait` it already had, and still answers to both. A cancellation — Esc at a surface, Ctrl-C at a console — DETACHES rather than kills: the run continues and its handle is printed, which is `--detach` asked for later. Progress goes out on the progress channel throughout, so a surface shows the work moving rather than a frozen line, and losing touch with CUBE is reported as not knowing rather than as failure, because a run whose status cannot be read has not failed.\n\nA pipeline schedules several nodes at once, so a wait is over the SET: it ends when all of them have settled, not when the first does.\n\nSecond slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"80ddce6","headline":"brasa: `record` — a session writes itself down as a manifest.","body":"The workflows worth sharing are the fiddly ones nobody will retype correctly. `record start <manifest>` … `record stop` captures the lines the session runs and writes them as a manifest someone else can play. It costs almost nothing because the lowering was already the law: a press on a surface and a line at a console arrive at the same place, so both are captured alike.\n\nA recording is a TRANSCRIPT and stays one. It never invents expectations — a file that guessed at assertions would be a fiction, and adding `expect` lines is the author's act, the one that turns a recording into a test. It does not film its own camera (`record` and `play` lines are not captured), and it does not capture the lines a played manifest runs, which are already written down in the file being played.\n\nWhat makes a recording personal is its identifiers, so the ones a PACS line wears on its face — `PatientID`, `AccessionNumber`, `PatientName` — are offered as parameters, written as defaults so the file still plays exactly as recorded. The same value gets one name wherever it appears and a second distinct value gets a name of its own, so a workflow over two patients does not collapse into one. A value stops at an underscore or a slash: a query's projection folder is `PatientID:1279049_qid:3125_owner`, and a looser match would have parameterized the query id along with the MRN.\n\nFifth slice of the manifest work (`docs/manifest.adoc`).","internal":false},{"hash":"5b520c7","headline":"An index says what it counts.","body":"`@2` said nothing about what it counted — in a PACS answer every study read \"1\", being the only study under its patient — so a bare number is no longer an index. A handle names its KIND and its place in that kind's sequence: `@SER3`, `@STD001`, `@FIL2,3,7`, `@DIR2-4`. One sequence per kind runs across the whole answer, so a handle is a name rather than a position and a sorted listing does not renumber it; zero padding is how the pill draws it, not something to type.\n\nWhat a row hands over differs by kind. A series, a file or a folder hands over its path; a STUDY hands over its series — what the surface's GATHER on a study hands over — so `gather add @STD001` and `pull @STD001` act on the whole study from a console exactly as a press does. A verb refuses a kind it does not take, by name and at expansion: `image @STD001` says \"image takes a series or a folder or a file; STD001 is a study\" instead of resolving to a path and failing three steps later for a reason that names nothing typed.\n\nOn the surface the pill draws the handle, lit in the pane's frame hue on the rows the session reaches, and a study row wears `STD001` where it read a meaningless \"1\". The `numbered` message carries each row's kind, place and address, so a pane finds its rows by address and draws the code the operator would type.","internal":false},{"hash":"ca3e39b","headline":"brasa: one expander, owned by the tokenizer.","body":"There were two, over one syntax. The dispatcher substituted the environment AFTER tokenizing, so a single-quoted `$HOME` expanded anyway and quoting could protect nothing. `play` substituted parameters and session pronouns BEFORE tokenizing, so a gathered path with a space had to be quoted by hand and an unresolved pronoun could fall through to the other expander. Two sources of truth that composed by accident.\n\nNow the tokenizer records references with the quoting they were written in, and one resolver answers them: the session's reserved pronouns (`gather`, `gather.first.place`, `feed`, `run`, `run.place`, `query`, `cwd`), then the played manifest's parameters, then the environment. `play` no longer substitutes anything; a manifest that declares a `@param` with a reserved name is refused when the file is read.\n\nThe shell's rules, kept, because the stack claims to wear them: a bare reference carrying several values becomes several operands, a double-quoted one stays a single operand, a single-quoted `$` is text, and an expanded value never globs afterwards. The command word expands too — a manifest names its plugin as a parameter so the same workflow runs where the builds differ.\n\nA reference nothing answers now REFUSES the line, naming it and where it looked, rather than expanding to nothing or standing as text: `rm -rf ${DIR}/scratch` with `DIR` unset is how a script deletes the wrong thing. A dry run is the one exception, having run nothing yet.\n\nBehaviour changes worth knowing: `'$HOME'` in single quotes no longer expands (it did, wrongly), and an unset reference refuses where it used to be left as literal text.","internal":false},{"hash":"7bba167","headline":"The door hands a token: a session can be started by a front that already logged the operator in.","body":"Today a session is started by the operator, at a terminal, with a password: `chell user@url -p … --daemon`. A login front on a shared host — the porter, the display manager the browser surface needs — exchanges the password for a CUBE token itself and holds the token, never the password. This is the seam that lets it start a session with what it holds.\n\n- cumin `connection_connectWithToken({ user, url, token })`: proves the token against the server BEFORE writing anything, so a refusal leaves the saved context alone; never exits the process; the refusal travels in the outcome, not on the error stack.\n- brasa `sessionConnect_withToken(user, url, token)`: the headless connect beside `sessionConnect_fromSaved`, setting the context the way a credentialed boot does and leaving the working directory as it was.\n- chell `--auth-token-stdin`: the token comes in on stdin, one line, never on argv where `ps` shows it to the host. Refuses by name: without a `<user>@<url>`, beside `--password`, or with no line on the stream. The boot row reads `Connect Connected to <url> (token)`.\n- chell `--daemon` off a TTY no longer spawns a console onto its pipe — a boot ends at a login only where there is a terminal to log in on; otherwise the daemon says so and keeps listening, as the standalone `calypso` binary already did.\n- menu `@fnndsc/menu/logo`: the mise brain and its frame renderer move from the kernel to the wire package, decoding with `atob` and touching no Node builtin, so a browser can draw the same brain a terminal boot does. brasa re-exports it; chell and calypso keep their import.\n\nExemplar `14_tokenLogin` starts a daemon this way against a live CUBE, off a TTY, in isolated directories, and proves the Connect row and a live berth.","internal":false},{"hash":"fc4eb7e","headline":"brasa: `weather [place] [-u|--units metric|imperial] [-d|--days N]` — the conditions now and a short forecast for a place, from Open-Meteo (no key, no account), geocoded by name;","body":"Boston when no place is given, metric unless `--units imperial`. A kernel command, so a TTY and ARGUS show the same report; the typed `sys.weather` model carries the numbers beneath it. Unknown flags refused by name; an unreachable service or an unknown place answers in one line.","internal":false},{"hash":"43b9440","headline":"The space of everything run here: the wait is drawn with what is known, and UNIVERSE is a face.","body":"While the job index warms, RUNS-02 had nothing to show but a refusal and a moving figure. Now the session reports each feed as the index reads it — its jobs collapsed by plugin per place in its pipeline (a fan of three hundred conversions is one node of 300), with a status on every node — on the prompt context (`procWarmup.landed`), and the pane draws them as they arrive: every feed its own small DAG, floating free like molecules in a solution, counts as weight and status as hue, feeds of one pipeline shape pulled together by an unseen anchor so the space settles into constellations. Every node is real. When the index is whole the roster takes over, as before.\n\n`proc universe` answers the same picture from the cache at any time, whole or warming, and says which; the dashboard's UNIVERSE tile opens it on the RUNS canvas. A feed's status words are now the DAG's own (`finishedWithError`, `started`, …) wherever cumin derives them from counts, so a feed on a surface wears the hue its jobs would.","internal":false},{"hash":"3c6a0d3","headline":"ARGUS and kernel: the gather gesture is reachable, and an exported table is visible where it landed.","body":"Three faults found by running the operator's own flow against a live PACS.\n\n**GATHER SHOWN was invisible.** It was put on the results mode frame, which is the closed spine at rest: measured live, 22px wide and `visibility: hidden`. It now stands beside EXPORT CSV on the command row, where the verbs that act on the whole answer already live.\n\n**GATHER was offered only once a series was home**, so on a fresh answer, where nothing is home, the gesture did not exist. A cohort is a set of targets and the feed it roots is made by a pull over its members, so a series is gatherable once it can be named: a PACS path, or the fact that it has landed. Query, filter, take what matched, fetch it as a set. PULL still means bring this one now.\n\n**The export was not broken; the listing was stale.** The CSV writer wrote straight to CUBE and never invalidated the folder's cached listing, which every other fs verb does — so `cat` returned the file while `ls` and every browser showed the folder without it. The write now invalidates the folder it wrote into, and the one that shows a folder it had to create. The WROTE readout also opens that folder when pressed.","internal":false},{"hash":null,"headline":"Updated dependencies [e74ce1d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8a8dc25]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1bdf876]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3320487]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a010cb0]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5b520c7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7bba167]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c169d7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [43b9440] - @fnndsc/salsa@3.18.2 - @fnndsc/menu@0.10.0 - @fnndsc/chili@3.6.7 - @fnndsc/cumin@3.23.0","body":"","internal":true}]},{"version":"0.23.2","date":"2026-09-17","merge":"9ed4e1e6d106","bump":"patch","changes":[{"hash":"48e112d","headline":"Kernel: a DICOM address is followed to the bytes behind it.","body":"A `pl-dircopy` of a PACS pull does not copy the files: its output folder is a link, and the DICOM under it is stored beneath `/SERVICES/PACS`. `dcm series` and `dcm tags` resolved the path they were given against the cwd and stopped there, so the logical name matched no file id and every slice of a pulled series was unreadable from the feed it was pulled into. The listing showed the files; nothing could open one. The same series read fine at its PACS address, and `cd` into the folder — which does follow links — made the very same command work on the very same file.\n\nBoth commands now resolve the address the rest of the file verbs already do: a projection through the provider that owns it, then the link walk. `file_read`, the door behind the browser's byte route, follows a node's data link the same way when the folder it lands on is itself a link. Resolution is best effort, so a path that resolves no further stands as it was and the command still refuses by its own name.","internal":false}]},{"version":"0.23.1","date":"2026-09-17","merge":"96133a6d86a6","bump":"patch","changes":[{"hash":"3f96d86","headline":"A daemon comes up where the operator left it.","body":"A credentialed boot used to write `/` into the working-directory context after connecting, so every restart began at the root whatever was stored; it no longer touches the directory, and with nothing stored the kernel's home default (`/home/<user>`, now exported as `homePath_of`) applies to the boot context too.","internal":false},{"hash":"6d8a595","headline":"`pipeline run` answers with the `run.scheduled` model a plugin run answers with (pipeline name, first instance, feed), so a surface lights the feed it landed in without reading the id out of the text.","body":"","internal":false},{"hash":"a4a38b1","headline":"A file under a `/proc` projection can be read.","body":"The proc provider resolved a node's `data` to the CFS file behind it and then handed that file back to the dispatcher, whose default provider is the host filesystem — which refused it; it now reads the target through `fileContent_get`, the door that knows both worlds. The engine's `file_read` (the daemon's `/vfs` route) resolves the path it is given, so a surface can read a file at the address the session uses for it.","internal":false},{"hash":"0d99ec5","headline":"`pull --new-feed` answers with a `feed.created` model beside its text: the feed it made, its pl-dircopy root instance, the owner, and the root's data folder, so a surface binds to the place rather than parsing the id and path out of the lines.","body":"","internal":false},{"hash":"480632d","headline":"A scheduled run answers with a typed model.","body":"Running a plugin from the shell now returns a `run.scheduled` envelope model (the executable, the instance, the feed it landed in, whether that feed is new, the output path) beside the lines it already printed, so a surface can point at what it started rather than parse the console.","internal":false},{"hash":"b4d9282","headline":"A first session begins in the identity's home directory.","body":"Before the context had stored a working directory the session answered `/`, which is a place nobody works in; it now answers `/home/<user>`, as a shell would. A stored directory is always honoured, the root included.","internal":false},{"hash":"b337285","headline":"`image` is a kernel command, but it was missing from the two places that enumerate commands for the operator: tab-completion (offered from the help registry) and the grouped `help` listing (grouped by a hardcoded category map that had no imaging category).","body":"It had a help entry, so `image --help` worked, but the entry carried no summary and belonged to no category, so it never appeared in `help`. Now `dcm` and `image` share an `Imaging` category in the listing, the `image` entry carries a one-line summary, and `image`/`dcm` take path-argument completion like the other path commands — so `ima⇥` completes to `image` and `image ~/up⇥` completes a path.","internal":false},{"hash":"ab127ea","headline":"`plugininstance list` answers with a `plugininstance.list` model beside its table: the listed instances as typed rows (id, plugin name and version, feed, owner, status, start), the fields the listing selected.","body":"A surface reads its run history rather than scraping it.","internal":false},{"hash":null,"headline":"Updated dependencies [a4a38b1] - @fnndsc/salsa@3.18.1","body":"","internal":true}]},{"version":"0.23.0","date":"2026-09-15","merge":"7717a54fa4b1","bump":"minor","changes":[{"hash":"9cf2ea2","headline":"`image <path>`'s text reflection now carries a thumbnail of the middle slice, so a TTY sharing the session shows the picture, not only the facts.","body":"The kernel reads the slice's pixels (`dicomSlice_gray` in salsa, uncompressed transfer syntaxes via dcmjs), window/levels to 8-bit with MONOCHROME1 inverted, and box-downsamples to a cell grid; brasa renders it as an ASCII ramp on any pipe or ANSI truecolour half-blocks (`▀`) on a colour terminal. Colour is a new declared surface capability: ARGUS declares it (its DOM console renders ANSI), chell reads its own terminal through chalk, a bare pipe gets the ramp. Compressed pixels say so in one line rather than pulling a codec into the kernel; an unreadable slice leaves the facts and no picture. The ASCII ramp is always the floor.","internal":false},{"hash":"d133ef6","headline":"`image <path>` is a kernel command, not a surface verb.","body":"A new brasa builtin resolves what a path is — a DICOM series folder, a study, a `.dcm`, or a NIfTI/MGZ volume — and emits a typed `image.view` intent (schema and `IMAGE_MODEL_KINDS` in `@fnndsc/menu`) that every surface renders in its own way: ARGUS opens a rendered pane from the intent, a TTY prints the reflection the command renders (for a series, the same facts `dcm series` shows). So the same `image ~/uploads/sag-anon` works from a browser and from a terminal sharing one CALYPSO session. The surface keeps only the subverbs that drive a pane already on the field (`image layout|slice|wl|colormap|save|tags|load|guard|ghost`), declared shared so `image <path>`, `image --help`, and `image` alone fall through to the session; help lives in the kernel's registry. Opening an image no longer drifts into a surface-only verb.","internal":false},{"hash":"c70e11e","headline":"A landed series says where it was filed, and the image pane answers the press at once.","body":"A retrieve is confirmed by count and filed by CUBE a beat later, so the pull's \"done\" never knew the folder and a surface offering IMAGE on the row waited for the next query. The progress message now carries an optional `path`, `pull` resolves each landed series' folder before its channel closes and says it on the same channel, and the ARGUS PACS row takes the folder from the landing: IMAGE appears the moment the pull says the series is home. In the same breath, `image <path>` puts the pane on stage before the kernel is asked, with `OPENING <name>` on the field and on the bar, so a header read that takes seconds is never seconds of nothing; what could not open is named on the field, and counted loads say their percentage beside the count.","internal":false},{"hash":null,"headline":"Updated dependencies [9cf2ea2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d133ef6]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c70e11e] - @fnndsc/salsa@3.18.0 - @fnndsc/menu@0.9.0 - @fnndsc/cumin@3.22.1","body":"","internal":true}]},{"version":"0.22.0","date":"2026-09-10","merge":"bbff653b165d","bump":"minor","changes":[{"hash":"163e4b1","headline":"`pacs query --anon` stands in for what an answer says about a person, for demonstrating against a live hospital PACS.","body":"Record numbers and accessions become salted tokens, a name becomes a stand-in name of the same DICOM shape, a study date falls back to the first of its month and a birth date to its year. The substitution happens on the decoded payload, so the model a surface renders, the console table and any CSV all say the same thing. The salt is made once per daemon and never written down, which makes tokens stable through one demo and meaningless after it: an unsalted digest of a seven-digit record number is a second's work to reverse. DICOM identifiers are left alone, since they are never shown and are what the held-state reconciliation matches on.","internal":false},{"hash":"aa923c6","headline":"A replayed PACS query no longer costs a round trip per series.","body":"Reconciling a stored answer against what CUBE already holds asked about every series in turn, two requests each, and held the whole answer back until the last one replied: a patient's history of 299 series took 20.6 seconds against a live CUBE, so a replay — whose entire purpose is not to trouble the wire — felt exactly like a fresh query. CUBE indexes a stored series by its study, so the reconciliation is now one page per study, with file counts fetched only for the series actually home. The same question costs 3.1 seconds. `seriesStorage_resolveMany` is the new bulk resolver; the per-series resolver stays for the paths that need it and for a study the answer cannot name.","internal":false},{"hash":"59aa79b","headline":"A run now follows itself until it settles, so the header's pulse tells the truth without a pane open on the feed.","body":"A new feed entered the cache with its jobs scheduled and nothing revisited them, so RUNNING stayed at zero through the operator's own run and SCHEDULED only ever climbed — one header read `SCHEDULED 90` with nothing running. Starting a run takes out a watch on that feed; the sampler already knew to stop when the feed settles. The watch is taken where a run is started, never inside the cache-writing helper, which would make recording a feed quietly begin visiting CUBE.","internal":false},{"hash":null,"headline":"Updated dependencies [163e4b1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aa923c6] - @fnndsc/salsa@3.17.0 - @fnndsc/cumin@3.22.0","body":"","internal":true}]},{"version":"0.21.0","date":"2026-09-10","merge":"b11d56847aad","bump":"minor","changes":[{"hash":"af2df66","headline":"New `dcm` builtin: `dcm series <folder>` renders a folder as a series and carries the `dicom.series` model;","body":"`dcm tags <file|folder> [--all] [--filter <text>]` renders a grouped tag listing, or a folder's constant and varying tags, and carries `dicom.tags`. Folder listings read every file up to 512 then sample evenly, and the text says how many were read of how many, and which were refused.","internal":false},{"hash":"7c793ad","headline":"`pacs query` fills each pulled series' `folderPath` from the storage state it already resolves.","body":"","internal":false},{"hash":"0eb202f","headline":"`dcm tags --filter` descends into sequence items, so a needle inside a referenced-image sequence keeps the sequence on screen.","body":"","internal":false},{"hash":"183bf02","headline":"`rm` treats an abandoned question as an answer, not a failure.","body":"A confirmation that comes back with no answer — the operator pressed Esc, or the surface lost its ability to ask — used to abort the per-file `-i` walk with `rm: cannot remove '<path>'`, an error over a file nothing had touched, and left every file behind it silently unoffered. It now skips that file by name, says why, stops asking, and says how many it left alone. The one-question `-I` path keeps everything as before and now states the reason too, so a surface that has quietly lost its voice is not reported as an operator who declined.","internal":false},{"hash":null,"headline":"Updated dependencies [7c793ad]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [af2df66]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [af2df66]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [b9ec3aa] - @fnndsc/menu@0.8.0 - @fnndsc/salsa@3.16.0 - @fnndsc/cumin@3.21.1","body":"","internal":true}]},{"version":"0.20.0","date":"2026-09-09","merge":"3a263f013a83","bump":"minor","changes":[{"hash":"3d11f59","headline":"Boot readout, second pass.","body":"A step that starts as `[PENDING]` (or falls to `[RETRY]`) is now rewritten in place when its outcome lands, the way a boot screen settles a row rather than repeating it beneath, so a settled step leaves one `[ OK ]`/`[FAIL]` row and no open pending row. The brain animation and the readout now share one stdout row counter, so neither miscounts the other's scrolling; a row that has scrolled off the screen, or an outcome taller than the pending row it replaces, appends as before. The daemon banner paints each package in three parts (its name bright, the rest of its backronym plain, the version dim in one column) and carries the build hash on its own CALYPSO row, so the redundant headline line is gone; a remote surface paints the daemon's stack the same way.","internal":false},{"hash":"9c320a0","headline":"The boot ends at a login.","body":"An interactive `chell --daemon` no longer rests on the console face: once the daemon is listening it puts an ordinary `chell --remote --attach` surface on its own terminal, as a child on the wire, so the boot log runs straight into a prompt. `exit` detaches and leaves the daemon running, Enter attaches again, Ctrl-C at the idle terminal stops the daemon, and whatever the daemon writes while a surface holds the terminal is held (calypso `consoleCage_start`/`consoleCage_stop`) and printed on detach. Every chell REPL, local or remote, now guards its idle prompt: a stray line (another surface's output, a late warm-up) lands on its own line with the prompt redrawn beneath it.\n\nThe boot readout is lifted with it. A warm-up step running behind the prompt printed its pending row twice and never reported an outcome; it now prints once as `[PENDING]` and settles on the daemon's readout as `[ OK ]` with what it cached or `[FAIL]` with why. Every count takes its noun in the right number (`1 PACS query`, `3 feeds`) through a new brasa `count_noun`. The daemon banner writes every package out in full (brasa `stackBanner_build`/`stackBanner_compose`), adds an ARGUS row when a web surface is served, and drops the welcome; the remote surface banners the daemon's stack the same way. A daemon bound on every interface advertises its fully qualified host name.","internal":false},{"hash":"d89e31b","headline":"A feed-list row now carries `jobsErrored` (the errored-or-cancelled count, from the proc cache's per-status counters), so a surface can fill an errored feed's progress bar to the work that actually succeeded — `jobsDone - jobsErrored` over `jobsTotal` — rather than run it full in the error hue.","body":"The RUNS roster uses it; a full red bar said nothing a red mark would not.","internal":false},{"hash":null,"headline":"Updated dependencies [711e1b3]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [612c052]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d89e31b] - @fnndsc/cumin@3.21.0 - @fnndsc/salsa@3.15.0 - @fnndsc/menu@0.7.0","body":"","internal":true}]},{"version":"0.19.0","date":"2026-09-09","merge":"4b93ef54c7d2","bump":"minor","changes":[{"hash":"49634f2","headline":"feat(brasa): the index snapshot the daemon heartbeats carries CUBE's pace","body":"","internal":false},{"hash":"2545ffa","headline":"feat(brasa): the index snapshot carries the lab's pulse — jobs running and waiting, and the feeds that errored — derived from the roster's counters","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [49634f2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [49634f2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2545ffa] - @fnndsc/cumin@3.20.0 - @fnndsc/menu@0.6.0","body":"","internal":true}]},{"version":"0.18.0","date":"2026-09-09","merge":"bb6d0d575ad0","bump":"minor","changes":[{"hash":"6cd5ab1","headline":"feat(brasa): `feed diagram`, `feed tree`, wire `feed.dag` and `proc refresh <feed>` answer at once with `feed.indexing` when the feed is cold;","body":"a feed watch holds the floor while its topology is indexing, never settles on an empty topology, and is kicked by the cache the moment the walk lands","internal":false},{"hash":"57ba039","headline":"feat(brasa): the prompt context names every feed walk in flight, not only the earliest","body":"","internal":false},{"hash":"98ab832","headline":"feat(brasa): `proc feeds` and `proc jobs list` answer from the cache at once and start the roster sync off the session's lane;","body":"the prompt context names the walk while it runs","internal":false},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [6cd5ab1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [57ba039]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [adf8cc1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [adf8cc1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [98ab832] - @fnndsc/cumin@3.19.0 - @fnndsc/menu@0.5.0 - @fnndsc/salsa@3.14.0","body":"","internal":true}]},{"version":"0.17.0","date":"2026-09-07","merge":"afa00444c12e","bump":"minor","changes":[{"hash":"a7f057c","headline":"feat: a value-taking flag given no value asks for it","body":"`pacs query … --csv-to` with nothing after it now means \"ask me where\". Before, such a flag was silently ignored: the operator asked for a table and got none, which is worse than either answering or refusing.\n\nNo sigil was needed. `?` was the obvious spelling and is already a glob in `string_checkHasWildcard`, so it would have expanded against the VFS; a flag that takes a value and is given none is unambiguous on its own, and the rule now generalises to every flag in the stack without another convention.\n\nThe ask carries what it wants — a `path`, with the session's own cwd as its anchor, a suggested basename, and `EXPORT HERE` as the word its committing control should read. The anchor is a fact rather than a guess: inventing a directory means creating one behind the operator's back. And it is raised only once there is something to write, since a question about a file that may never exist is asked too early.\n\nTwo rules ride with it, both in the daemon, both about who may interrupt an operator:\n\n- **A command marked `instrument` may never ask.** A pane's silent refresh or an ambient cycler raising a question is refused outright — the operator did not issue that command and cannot answer for it.\n- **One question at a time per surface.** A second is refused by name rather than queued, because a queued question is one whose command the operator has forgotten issuing.\n\nAn abandoned ask is not a failed query: the answer stands and only the writing does not happen.\n\n`prompt_current` now takes the whole request rather than a message and a flag, and the daemon relays `wants`, `path` and `commit` to the surface. `repl_confirm` and `repl_questionPath` join the kernel's ask helpers, so a caller states the kind once and every surface reads the same intent.","internal":false},{"hash":"da15f3b","headline":"feat(argus): the surface can be asked","body":"argus refused every question the session put to it — `the argus surface cannot answer prompts`, with `hiddenInput: false` as the stated reason. So `sudo` dead-ended in the web surface, a confirmation dead-ended, and no control could ask for a value the operator had not already typed.\n\nIt answers now, in the **console**: where the session speaks, and where the scrollback keeps what was asked, so an operator can look back at the question as well as the answer.\n\n- **`text`** takes an inline line.\n- **`secret`** masks the field and enters the transcript as dots — never its text, never its length — which is what lets `hiddenInput: true` be declared honestly.\n- **`confirm`** puts YES and NO capsules beside the question, because a control that reads as what it does beats a letter an operator has to know to type. Typing `y` still works for a hand already on the keys.\n- **`path`** is answered here too for now, with its suggestion offered so answering is a rename rather than a whole path typed out. The errand that walks a browser for it is the next slice; until then a location is answerable rather than refused.\n\nA question outranks the command line while it is open: an answer is never dispatched as a line, and a queued line never jumps ahead of it. **Esc abandons**, which is an answer of its own — the command is told, and reports what it did not do.\n\nTwo asks that never travelled were routed through the surface while here. `rm -i` built its own readline against `process.stdin`, so under a daemon its confirmation was put to whoever started the daemon rather than to the operator who typed the command — they would have waited forever for a question asked of somebody else's terminal. `upload`'s confirmation was untyped, and is now a yes/no like the download's.\n\nVerified live: `sudo` in argus asks for an administrator username, masks the password, leaks nothing to the transcript, and abandons cleanly. Law `a-question-is-answered-where-the-session-speaks`, smoke-enforced.","internal":false},{"hash":"349d7d2","headline":"feat: a missing destination is asked for, not refused with a usage line","body":"`mv foo` names what to move and never says where. Until now that was a usage line — telling an operator the shape of a command they had just typed correctly enough to be understood, instead of asking them the one thing they had not said. A required operand with no value is the same sentence as a value-taking flag with no value, and now gets the same answer: `mv` and `cp` ask.\n\nThe ask is the typed `path` ask that already exists, so neither verb knows which surface it is talking to: a terminal renders it as a line, and a surface that browses borrows a files pane for it. It opens where the source already lives, wants a directory when there is more than one source, and its committing control reads `MOVE HERE` or `COPY HERE`.\n\nIt offers no default name. The only name `mv` could propose is the source's own, and the first live run took Enter as exactly that and moved a file onto itself. The rule that came out of it holds for every path ask, and the terminal now implements it directly: the **anchor** says where to look, the **suggestion** says what to offer, and a verb with nothing to offer offers nothing — Enter on such a question answers nothing rather than answering with the question.\n\nAn abandoned ask moves nothing and says so (`mv: no destination given; nothing moved.`), rather than failing with a description of `mv`.","internal":false},{"hash":"02ef648","headline":"feat: a write is checked before it happens, and says what it did","body":"A chooser can hand a write a place that cannot be used, so `--csv-to` stopped trusting the path it is given:\n\n- **A provider path is refused by name.** `/net/pacs/x.csv` is somewhere to browse, not somewhere a file lands, and CUBE's own refusal talks about an upload, which says nothing about why.\n- **A missing folder is made, and said** — a directory appearing without a word is the silent side effect this replaces. argus's EXPORT CSV no longer runs `mkdir ~/audits` behind the operator's back; it lowers to `--csv-to` with no value, so the destination is asked for.\n- **An existing table is never overwritten in silence.** It takes `--force`, and the replacement is reported.\n- **The pane states the path** the kernel reported writing, rather than leaving it to the console alone.\n\nThree things the live run turned up that no mock would have shown:\n\n- **`files_listAll` answers null for an empty folder AND for one that is not there**, so existence can never be inferred from it. Doing so reported a folder \"made\" that already existed, and a file \"already there\" in a folder that did not exist at all. Existence is asked of `files_path_isDirectory` now.\n- **CUBE answers a re-upload over a path it already holds with a 500, thrown.** An unhandled throw in a builtin does not stop at the command: under a daemon it takes the process, and every surface attached to it. The write is wrapped — a store's bad day is a refusal, not an outage.\n- **Deletion is asynchronous**, so removing a file and writing in the same breath races the store and answers 500 again — with the operator's old file already gone. `--force` now removes, waits for the path to stop resolving, writes, and confirms by listing afterwards, because a write reported without confirmation was seen to leave nothing behind.\n\nBoth CUBE behaviours are recorded in `docs/CUBE-gaps.adoc`, with what the API could offer instead.","internal":false},{"hash":"116e8ba","headline":"feat(listing): progress is a trait of a row, and a row can carry verbs","body":"The operator's observation: progress is not a PACS quirk. A feed row should say how far its work has got without anyone opening it, and every level should report — a series its own pull, a study the sum of its series, a patient the sum of its studies.\n\n**The wire could not say it.** The `feed.list` model carried id, title, owner, status, created and two totals that need resident topology, so a roster genuinely could not know a feed's progress. CUBE's own job counters were already on the process cache, so the kernel now derives `jobsDone` and `jobsTotal` from them — settled meaning finished, errored or cancelled — and they travel with the feed row rather than waiting for topology. Kernel, wire, surface, in that order.\n\n**Progress aggregates by addition, not by average.** A study's progress is the sum of its series'. An average would let one finished series of a hundred files outweigh a stalled one of ten thousand.\n\n**A row with nothing scheduled still gets a track**, dimmed. The absence of a bar reads as \"no such thing\"; a dim track reads as \"nothing has happened yet\", which is the truth and the more useful statement.\n\n**Actions are not traits.** A trait says what a row is under some column; an action is a verb applied to it, and it sits outside the column grid because it answers to no cap. Capsules stop click propagation, so pressing one is not also activating the row.\n\n**Expansion has two modes**, declared rather than assumed: `replace` leaves the parent behind, `fold` keeps it on stage with the child inside. PACS exercises both in the next slice.\n\nThe runs roster gains NODES and PROGRESS, which widened its positional track list from seven columns to nine.\n\nOne smoke assertion changed, and deliberately: it counted seven cells per row as a literal. It now counts cells against the number of caps, which is the invariant that actually protects a positional grid and needs no edit when a column is added.","internal":false},{"hash":"ba1e5b4","headline":"feat: a PACS answer can be written as a table a spreadsheet reads","body":"`--csv` renders the answer; `--csv-to <cfs-path>` writes it into ChRIS storage; argus's EXPORT CSV lowers visibly to the second, as GATHER's SAVE already lowers, so the operator reads the command that ran.\n\nTwo flags rather than one with an optional value: `--csv PatientID:1234` cannot tell a destination from a query expression, and guessing wrong writes a file named after a patient.\n\n**Why a CFS destination exists.** A local terminal needs none — `--csv > audit.csv` writes the operator's own disk, because engine and operator share a filesystem. A detached surface does not: the engine runs on the daemon's host, so a redirect lands on somebody else's machine (#415, where `>` also turns out to bypass the `engineFilesystem` capability built to prevent exactly that). A cohort's MRNs and study descriptions stay inside ChRIS, and the file browser or `download` retrieves them.\n\n**The table's shape carries the level above's doctrine**: a row per study, and a row for every patient that owns none. A table built from studies alone would silently drop the misses, which are what an audit is usually asking about. `ANSWERED` carries an ISO timestamp rather than `3 MONTHS AGO` — a spreadsheet sorts dates; a phrase is for a human glance.\n\nEvery cell is quoted and embedded quotes doubled, the rule chili's `--csv` has always followed, now stated as a function rather than left inside a handler nothing else can call — so a study description like `MRI BRAIN, W/ AND W/O \"GAD\"` survives the trip.","internal":false},{"hash":"404f5e3","headline":"feat: a PACS question that names several patients becomes several questions","body":"A PACS will not match a list. `PatientID:4356325\\4433255` — the DICOM multi-value form — returns nothing from a real PACS, and the standard agrees: _List of UID Matching_ is defined only for attributes whose VR is `UI`, and `PatientID` is `LO`. So asking after two hundred MRNs is two hundred C-FINDs, and the fan-out is forced rather than chosen.\n\n**The inline comma list is the operator's own syntax, now accepted.** `query PatientID:1234,4532,6654` is three questions and one table; `query PatientID:1234,StudyDate:20240101` still means what it always meant. There is nothing to disambiguate — every genuine term carries a colon, so a bare segment can only be another value for the key before it. Several multi-valued keys fan out over their cross-product, and above 32 the command refuses by name rather than launching hundreds at a shared clinical system.\n\n**`--patients <list|@file>` carries a real cohort.** `@file` names a file in ChRIS storage, read through the session's own path and never the engine's host disk, so the flag behaves identically from a local shell, a remote shell and a browser — and a list on somebody's laptop reaches it through `upload`, the gated door. One MRN per line; blanks and `#` comments ignored.\n\n**Four questions in flight**, in one constant. Not operator-settable: nobody inside mise knows the right number for a given hospital, and a flag that can hurt a shared clinical system will eventually be set to 50.\n\n**A failure is not a miss.** A question that could not be asked is recorded `unasked` with its reason, never as zero studies. A server that timed out has told us nothing; a PACS that answered with nothing has told us something, and a clinician acts on the difference. The table says `FOUND 2 · NONE 1 · UNASKED 0`, and the model carries every row.\n\n**Replay applies per patient**, so a cohort's already-asked MRNs never leave the building — verified live against rows answered eight months ago.\n\nTwo things found live while proving it:\n\n- **CUBE refuses a second PACSQuery with a title it already holds for that server.** A fan-out under one title had every question after the first come back `You have already registered a PACS query with title=…`, which a less careful client would have rendered as \"no imaging\". Each question now carries its own title. Recorded in `docs/CUBE-gaps.adoc`.\n- **`chell -c` and `chell -f` had no replay at all.** A one-shot skips the boot warm-up — correct — but that left the replay index neither restored nor written, so a scripted cohort re-asked the PACS every single time, and the audit workflow the fan-out exists for was the one workflow replay never reached. A one-shot now primes the index from the checkpoint a previous run paid for, and flushes what it learned before exiting, since the debounced writer's timer never fires in a process that short.","internal":false},{"hash":"7181f7e","headline":"feat: a PACS question can be put to several servers at once","body":"`--pacsserver a,b` asks each named server and unions the answers into one listing. It is the same fan-out a cohort uses, discriminated by a different column: SERVER rides every study and every patient row, so two answers can be told apart, sorted and filtered.\n\n**A row names its server only when more than one could have answered.** On a single-server query the column would repeat one value down the page and tell an operator nothing.\n\n**There is no sweep-everything.** The CUBE this was designed against carries thirteen registered servers, several reading as one-off or per-person registrations; an `--all` would mean thousands of C-FINDs mostly into endpoints of unknown liveness. A fan-out is always something the operator named.\n\n**A server that could not be reached is `unasked`, not empty.** Verified live: naming a server that does not resolve leaves that row `—` with its reason while the server that did answer still reports what it found. A server that could not be reached has told us nothing about that patient; rendering it as a zero would say the opposite.\n\nThe reason on such a row is stripped of the error stack's debugging prefix where it becomes model data, since it is read in a terminal table and on a graphical surface alike.\n\nServers are keyed by their canonical identifier — what CUBE files a query under, and therefore what the replay index matches on, so a question already asked of one server replays while the same question to another is asked fresh.","internal":false},{"hash":"a245b5f","headline":"feat: choosing which PACS to ask is a strip you pick from, never a sweep","body":"SERVER became a column of the study listing, and the law that places a form's fields put its control there with no separate decision to make: the cell stands in SERVER's column, wearing the form's own label.\n\nIt is a **state readout you press** — `PACSDCM`, `PACSDCM +2` — that unfolds a strip of segments beneath the form, one per registered server, lit when included; the field's touch or Esc retracts it, the same retraction grammar the mode frames already use. Not a dropdown: LCARS has no popup layer and should not grow one, since a floating menu is exactly the window chrome this grammar rejects — and a strip of the pane's own width carries thirteen servers where a grid track could not carry three.\n\n**There is no ALL, and there will not be one.** Thirteen registrations of unknown liveness would mean thousands of C-FINDs mostly into the void. A fan-out is always something the operator named.\n\n**One chosen is a context; several is a query.** One lowers to a visible `pacs connect`, so the session moves and the linked terminal's prompt follows. Several lowers to `--pacsserver a,b` in the editable line and moves nothing. The prompt changing, or not, is the honest tell.\n\n**The list comes from the kernel, not from CUBE.** `pacs list` now carries a `pacs.servers` model beside the text it always printed — id, identifier, and which one the session is on. A surface asking CUBE itself would be reading a different CUBE from the one its commands run against. Liveness is deliberately absent: CUBE registers servers, it does not test them, and a field that looked like health would be a claim nobody checked.\n\nTwo things found live: the PACS pane's own commands had no way back to it — the DAG pane's already did — so `pacs list` answered into the void; and the document's retraction listener closed the strip with the very press that opened it.\n\nLaw `a-server-is-named-never-swept`, smoke-enforced.","internal":false},{"hash":"9e2a9dd","headline":"feat: a verb that acts on the place rides the frame — MKDIR and UPLOAD","body":"Two verbs act on the PLACE the field holds rather than on any row, so they ride the field's own frame beside HOME and BACK, and they appear on a browser's frame and nowhere else — there is no directory to make in a list of feeds.\n\n**MKDIR** asks for a name in the console and makes it where the field points, not where the session's cwd happens to be: a rooted browser is showing a place of its own, and a verb that acted on the session's place instead would make the folder somewhere the operator is not looking.\n\n**UPLOAD** is the verb this surface could not previously speak at all. `upload` reaches the daemon's disk, which a browser has never seen. So the operator's own picker chooses the file, and the bytes travel over the daemon's `/vfs` route — now answering `POST` as well as `GET` — where the **engine** writes them through the kernel. No surface talks to CUBE, the same attach token gates the write as gates the read, and the body is capped. The console keeps the account, because a gesture the surface performs itself still owes the transcript what it did.\n\nNew seam: `Engine.file_write(path, bytes)`, the twin of `file_read`. It writes through salsa's own create and **invalidates the listing it changed**, as every writing builtin does — without that the browser asks for the folder again and is served the folder as it was before the delivery, which reads as an upload that silently did nothing.","internal":false},{"hash":"5a339f0","headline":"feat: a plugin is data — the manual becomes a projection of the model","body":"A plugin's substance existed only as text. `cat /bin/<entry>` fetched the plugin, formatted a manual, and that manual was the whole of what any surface could have — a paragraph nothing downstream can act on: not a card, not a parameter list, not a form, not an export.\n\nThe kernel now builds the model first and renders the manual from it. `plugin info pl-dcm2niix-v2.0.0` answers with a `plugin.info` model carrying identity, the authoring facts, and every declared parameter; `cat /bin/pl-dcm2niix-v2.0.0` prints exactly the text it printed before, now as one projection of that model rather than a second independent scrape that can drift from it.\n\n**Parameters carry the flag as it is typed.** A plugin's own `flag` when it declares one, `--name` otherwise — a form built from the name alone would spell `--inputFile` where the plugin wants `-i`.\n\n**The parameter list is drained to exhaustion.** `getPluginParameters({ limit: 100 })` was one of the silent truncations catalogued in #401: a plugin declaring more lost the tail and said nothing about it. `pluginParameters_drain` walks to the end, and a live exemplar checks the model's count against the count CUBE itself reports — a client that both fetches and counts can agree with itself while being wrong.\n\ncumin gains `plugin_find` and `pluginParameters_drain` on the typed contract, so the `/bin` reader no longer reaches past it to the raw client.\n\nNo surface change: this is the wire fact a plugin's one-node graph will read.","internal":false},{"hash":"a8449bc","headline":"feat: the roster shares, and a feed can be removed","body":"`setfacl` grants to an identity and applies to a feed, so the feed roster is where sharing belongs — a browser row offers SHARE only because the path it holds names a feed.\n\nThe roster's rows learn the split the browser's already have: a click indicates, a double-click enters, and the action track is reserved on every row so a roster with verbs does not jump when one row starts speaking.\n\n- **SHARE** asks who, and the question itself says a grant cannot be taken back. The irreversibility is stated where the grant is made rather than discovered afterwards, and because the sentence lives in the kernel's ask, every surface says it.\n- Beside the verb, the row **reads back who already holds it** — a readout, not a verb: it says what the grant would be adding to.\n- **DELETE** lowers to `feed rm`, which is new: the kernel could delete a feed but no shell verb could. It asks first by default, naming the feed and what goes with it, and takes `-f` for a caller that has already asked its own question.\n\n`feed rm [<feed>] [-f]` joins the feed subcommands.","internal":false},{"hash":"8446459","headline":"feat: a row is indicated before it is acted on","body":"The browser's rows have their verbs. Two problems had to be solved before they could:\n\n**There was no indicate gesture.** A click entered a directory, opened a file, opened a `/bin` entry — nothing meant \"this one\" without also meaning \"go\". A click now indicates and a double-click activates: the split the DAG scene already teaches, and what every file manager does. Only a listing that hides its verbs learns it — a browser given no verbs (a node's overlay) keeps its single click, since asking for a second click while offering nothing for the first is a worse bargain than the one it replaced.\n\n**A row that grew when indicated would make the listing jump.** The action track is reserved on every row at the capsule's own height, declared once and read by the track and the capsules alike, so what changes when a row speaks is what the track holds and never the geometry around it.\n\nEach verb lowers to a command the operator can read in the transcript:\n\n- **DELETE** → `rm -i` (`rm -ri` for a directory), so the kernel raises the confirmation and one confirmation grammar serves every surface.\n- **MOVE** / **COPY** → a one-operand `mv` / `cp`, whose missing destination is the ask that opens the errand.\n- **DOWNLOAD** → the file, as before.\n- **SHARE FEED n** → on any row whose path holds a feed, naming the feed because CUBE grants a feed and never a file, with the grants that already exist read out beside it.\n\nA `/bin` row is offered nothing: an executable the catalogue lists is not a file in a store.\n\n`setfacl <feed>` with no entry now **asks who to share it with**. A path and no entry names what to share and not with whom, which is a question rather than a usage error — the same law that made `mv foo` ask. Abandoning it shares nothing and says so.","internal":false},{"hash":"17964a9","headline":"feat: SELECT is a mode, and a selection has verbs","body":"A mode describes how the field behaves, and this one changes what a click means. While SELECT is on a click gathers a row instead of indicating it, row verbs stand down — a row is not indicated then, it is selected — and the bar reads `SELECT · 2 SELECTED`, adding `· 1 SHOWN` when a filter hides some of what was gathered, since a verb acts on the selection and not on what is on screen.\n\nThe selection belongs to the **field**: it survives a filter (which is how a selection gets built in a folder of hundreds) and the same rows arriving again; it is cleared by navigation, because a selection that follows the operator elsewhere is one they can act on without seeing; and leaving the mode clears nothing. Esc leaves SELECT before it retreats anywhere — but never while a question is open, since abandoning a question is an answer and one press must not answer two things.\n\nIts verbs ride the frame, and each is ONE command the operator could have typed:\n\n- `rm -rI \"a\" \"b\"` — **new `-I`**: one question for the whole list, naming how many, rather than one per file. A refusal removes nothing at all; there is no half of a set.\n- `mv -t` / `cp -t` — **new `-t <dir>`**: names a target directory so every operand is a source, and given no value it asks, wanting a directory.\n- `setfacl` over the **distinct feeds** the selection touches, which asks who once for the set.\n\n## A defect this bought, and the guard for it\n\nThe live run found the surface lowering a two-file selection to `mv a b` — which the shell correctly reads as \"rename a onto b\", and which moved one file of the pair onto the other. Chasing it turned up something worse and older: **a move onto a path the store already holds leaves a row CUBE's own API cannot serve, and one such row makes every listing of that folder fail** — the poisoning first seen in #462, now reproducible in three commands.\n\n`mv` refuses that move by name instead of attempting it, which costs one listing call and keeps the folder readable, and a failed move now reports the reason the kernel gave rather than a bare \"Failed to move\".\n\nAlso: a browser re-lists after a verb that changed the folder it is showing (`fs.rm`, `fs.mv`, `fs.cp`) — `rm` reports what it removed, not where it removed it from, and a listing that keeps showing rows that are gone is a listing lying about the store.","internal":false},{"hash":"78d85ae","headline":"feat(argus): a location is asked for by borrowing a browser","body":"An ask is never a box. A `path` question opens the instrument that already shows that space: a **new** files pane beside the pane that asked — never an existing browser, since hijacking one loses the operator's place — anchored where the ask said, closing when the errand ends either way.\n\nIts controls ride a bar of its own across the top of the pane: the question as a caption, the composed path as an editable field, **MKDIR** for a folder that does not exist yet, and one verb that commits, reading the word the kernel sent (`EXPORT HERE`). The grill had put those on the mode frame; building it showed why they cannot live there — the frame is a narrow rail against the spine, right for a column of capsules and hopeless for a caption and a path, and it answers to what the _field_ holds, which an errand does not change.\n\nThree defects the live run turned up, each fixed here:\n\n- **The errand opened an empty browser, forever.** The daemon runs commands one at a time, so the listing that would answer the question queued behind the command that asked it: the answer waiting on the browsing, the browsing waiting on the answer. An instrument command now runs **beside** a command waiting on a question — the natural twin of the rule that an instrument may never ask. A pane's own read neither asks nor waits, and the daemon saves and restores the executing command rather than clearing it, so the outer command's output still finds its way home.\n- **The errand split beside the focused pane**, which can be one the current preset does not hold. A split beside a pane that is not in the tree fails silently, and an errand that never opens is a question asked of nobody. The host is now a leaf that is actually on stage.\n- **The anchor could be somewhere nothing can be written.** A session sitting in `/bin` or `/proc` is browsing a provider, not standing where a file lands, so the ask falls back to home rather than offering a destination that is refused the moment it is committed.\n\nLaw `an-ask-borrows-an-instrument`, smoke-enforced. Two consecutive full smoke runs, 103 checks.","internal":false},{"hash":"8312a79","headline":"feat(chell): a terminal answers the same typed question","body":"A terminal has one instrument — a line — so a location ask cannot borrow a browser the way a graphical surface does. It renders into the line instead: the composed default in brackets, and Enter takes the offer.\n\n```\nWhere should the table go? [/home/rudolphpienaar/pacs-2026-09-07.csv]\n```\n\nThe answer a terminal commits is then the same answer an errand would have committed, without either surface knowing the other exists — which is the point of putting the _kind_ on the wire rather than a rendering. A yes/no says which letters it takes and offers no default, because there is no safe guess. A prompt from a daemon that predates typed asks reads exactly as it always did.\n\nExemplar 12 closes the epic by driving the whole path through the kernel with a scripted surface: a value-taking flag given no value asks and the question says what it wants; the answer is what happens next and the file lands where it said; abandoning writes nothing and the command says so; and a session sitting in a provider path is not offered as a destination.\n\nIt caught the epic's last defect: **an abandoned ask was throwing the answer away with the writing.** The model crosses either way now — the operator waited for that answer, and only the writing of it did not happen.","internal":false},{"hash":"3df1c41","headline":"docs(calypso): say calypso where the component is meant, and make it typeable","body":"Every other part of the stack is a noun with a role — cumin, salsa, chili, brasa, menu, chell, argus. The session supervisor was the odd one out, described by a flag on another program, so the prose said \"the daemon\" for something that already has a name, a package, a binary and a doctrine document.\n\nHost control is _calypso's_ policy; berths are calypso's; the wire is calypso's. \"Grant calypso host access\" says whose policy changed. \"Grant the daemon\" does not.\n\nOperator-facing text now names it: the flag help, the not-running message, the already-running refusal, the listening banner, the attach errors, the several-sessions chooser, and the host-control sentences — `upload` refusing without the `files` tier, the HOST banner, and argus's HOST lamp tooltip.\n\nThe start hints used to read `chell --daemon <user>@<url>` while the prose said \"run calypso\". They now say `calypso <user>@<url>`, which is a real command, since calypso ships its own binary. The getting-started guide leads with what calypso _is_, then gives both ways to start it — standalone from a saved session, or through chell logging in fresh — because they genuinely differ and one does not replace the other.\n\n**Daemon survives where it is correct**: a mode name, an anchor, a make target, a background process. Code symbols are untouched — `daemon_launch` and its kin describe something that really is a daemon, and churning them buys nothing an operator sees. Command lines inside code blocks were left alone, so nothing became a command that does not exist.","internal":false},{"hash":"f5c16fe","headline":"fix: a listing says what it could not read, and cat reports instead of crashing","body":"**A listing that could not read part of itself says so.** `ls` asks a folder for its directories, its files and its links, and a refused sub-listing was being dropped for looking like an empty one — so the home root whose file listing CUBE refuses rendered its folders alone, as though that were everything. The provider now names what it could not read (`Cannot fully list <path>: could not read files (Internal server error)`) and the listing carries that reason with the entries it did get.\n\n**`files_listAll`'s `null` meant three things** — an empty folder, a folder that is not there, and a folder the server would not describe — and nothing above it could behave correctly on one word that means all three. `files_listOutcome` says which: `listing`, `empty`, `missing`, `refused`. `files_listAll` stays as the lossy wrapper, but a refusal now throws rather than passing for absence.\n\n**A file is deleted by its id**, not by finding it in a listing first. The folder whose listing the server refuses is exactly the one an operator needs to clear, and a delete that walks the parent cannot help there. `chrisIO.file_deleteById` is the new door; other asset kinds still resolve through the group.\n\n**`cat` reports an unreadable path instead of ending the session.** A read that threw — a path that cannot be resolved, a server that refuses — escaped as an unhandled rejection that dumped an axios request object, auth header included, and killed the process. Each path is now resolved and read inside its own guard: the failure is reported like any other unreadable file, the rest of the line still runs, and nothing is dumped.","internal":false},{"hash":"ec3ef9f","headline":"fix: a listing shows what is there — no silent page limits","body":"`ls /net/pacs/queries` answered with 100 of 1,817 stored queries and said nothing about the other 1,717. That was one symptom of a pattern: callers reaching a single-page list call with a literal limit and returning the page as though it were the collection.\n\n**The default is now the collection.** A caller that names no limit gets a walk to exhaustion, not the first twenty. A caller that names one gets exactly that page, marked `incomplete` with what it is showing of what exists — a bound the surface can state rather than a truncation nobody sees.\n\n**A walk that stops says where.** CUBE fails some collection queries past an offset (measured: `userfiles` answers to offset 800 and returns 500 at 1000). The walk keeps what it gathered and reports where it stopped, rather than handing back a prefix that reads as an ending. A walk whose _first_ page fails is still a failure, since a listing that never started is not a short listing.\n\n**Three callers fixed:**\n\n- `pacs status` searched the first 200 queries for a matching expression, so anything older answered \"not found\". It walks now — verified live against a log of 1,897, finding query 3.\n- `getfacl` read a feed's first 100 grants. A hundredth name is not a natural place for that answer to stop.\n- A `ts` node's join edges are read from its parameters, which were fetched one page deep. A node with more parameters than a page silently lost its edges — a graph drawn short.\n\n**And a gate**, `npm run lint:listings`, in CI: a literal `limit` above one reaching a single-page list call fails the build unless the line says `listing-bound:` and why. Two bounds are declared today; both are exact-path lookups, not collections.","internal":false},{"hash":"6234d55","headline":"test: a live exemplar proves a cohort answers for every patient asked","body":"Exemplar 11 drives the whole PACS plural path through `command_dispatchEnvelope` — the same entry chell uses — against a live CUBE and a real PACS.\n\nWhat it pins, in the order an operator would care:\n\n- **Every MRN asked has a row.** It counts rows, not hits: the answer an audit wants is usually the patients a hit-list would hide.\n- **Replay applies per patient.** The second ask of the same cohort is served from stored answers for the patient with imaging — same query id, provenance saying so — while the rows that found nothing are asked again, because an absence decays where a hit does not.\n- **A failure is not a miss.** Asked of a server CUBE does not register, the rows read `unasked` with a readable reason rather than zero studies. This is the property that needs a live PACS: a mock would only agree with whatever the code already does.\n- **The CSV round-trips.** `--csv-to` writes into CFS, `cat` reads it back, every row carries the same ten columns, and the misses are in the file.\n\nIt needs no new fixture: the designated test accession names a study, that study names a patient, and that patient is the MRN known to have imaging. Every PACSQuery the run creates is deleted and the file removed, so the CUBE ends as it began. Self-skips with exit 2 where no PACS fixture is configured, and joins the nightly e2e list.\n\nOne fix it drew out: the query model named its server with the raw `--pacsserver` argument, so a table asked with `--pacsserver 1` reported the server as `1`. The model now carries the canonical identifier CUBE files queries under, which is what `PACSDCM` means to an operator reading a CSV.","internal":false},{"hash":"17964a9","headline":"fix: no write goes to a path the store already holds","body":"A write onto an occupied path leaves a row CUBE's own API cannot serve, and one such row makes **every** listing of that folder fail — the damage first seen as \"the home root cannot list its files\". It is reproducible in three commands, and all three ordinary write routes did it:\n\n- `mv a b` where `b` exists — its own PUT of `upload_path`.\n- `cp a b` where `b` exists — the copy uploads to an occupied name, and reported _success_ while doing it.\n- `upload a.txt` where `a.txt` exists — and this is the one that made the original rows. CUBE renames a colliding upload (`a.txt` → `a_VlIxMSp.txt`); mise then PUT the wanted path back onto the file, to undo a rename it read as spurious. That PUT is the damage.\n\nEach route now asks first, which costs one listing call and keeps the folder readable:\n\n- `mv` and `cp` refuse by name — `Destination exists: <path> — mise cannot overwrite a file; remove it first` — and a failed move or copy now reports the kernel's own reason instead of a bare `Failed to move` / `Failed to copy`.\n- `upload` renames back only when the wanted path is genuinely free, which is the case that rule was written for (a path deleted and not yet committed). Otherwise it keeps the name CUBE gave the file and says so: `'<path>' already exists — the upload landed as '<other>' rather than overwriting it`. A probe that cannot answer counts the path as taken, since the cost of guessing wrong is the operator's folder.\n\nThe deliberate replace (`--csv-to --force`) is unaffected: it removes the file, waits for the path to stop resolving, then writes to a path that is free.\n\n`docs/CUBE-gaps.adoc` carries the reproduction, the mechanism and the client policy; upstream is ChRIS_ultron_backEnd#732.","internal":false},{"hash":null,"headline":"Updated dependencies [f5c16fe]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [116e8ba]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [ec3ef9f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7181f7e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d2a4315]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a245b5f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5a339f0]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [17964a9]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [62761a3]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [17964a9] - @fnndsc/salsa@3.13.0 - @fnndsc/cumin@3.18.0 - @fnndsc/menu@0.4.0 - @fnndsc/chili@3.6.6","body":"","internal":true}]},{"version":"0.16.1","date":"2026-09-05","merge":"0422da0acacf","bump":"patch","changes":[{"hash":"e3948f1","headline":"fix(release): publish `@fnndsc/menu`, without which nothing else installs","body":"`@fnndsc/menu` was marked private, so changesets versioned it and then skipped publishing it. cumin, brasa, chell and calypso all depend on it, so every published version since menu was extracted has been uninstallable from a clean registry:\n\n```\nnpm install @fnndsc/brasa\nnpm error 404 The requested resource '@fnndsc/menu@*' could not be found\n```\n\nIt worked in development only because the workspace resolves menu locally, and it went unnoticed because nothing ever installed these packages from npm alone.\n\nmenu is an ordinary library — the wire schemas and types three published packages import. Its manifest was already publish-ready and identical in shape to its siblings; only the flag stood in the way. It is dropped.\n\nThe dependency range is pinned to `^0.3.0` at the same time. `\"*\"` was an artefact of menu being private, since changesets does not rewrite ranges for packages it will not publish, and `\"*\"` on a published dependency means any future breaking change applies silently.\n\n`argus` stays private, and correctly so: it is a browser bundle served from calypso's web root, not a library anyone imports.","internal":false},{"hash":null,"headline":"Updated dependencies [e3948f1] - @fnndsc/menu@0.3.1 - @fnndsc/cumin@3.17.1","body":"","internal":true}]},{"version":"0.16.0","date":"2026-09-04","merge":"f25e3f4babc4","bump":"minor","changes":[{"hash":"5dc064e","headline":"feat(boot): warm-up leaves the gate, and a failure that leaves it is still heard","body":"Boot blocked on four prefetches in front of the prompt. Measured against a live CUBE, `/PUBLIC` costs 8.4 seconds and `/SHARED` 9.3 — roughly eighteen seconds an operator spent watching a prompt that was already theirs, buying freshness the stale-serve path delivers a moment later anyway, since the checkpoint restore has already put those listings in the cache.\n\nBoot now blocks only on `/bin`, which completion cannot work without: an empty completer reads as a broken prompt rather than a fast one. Groups, Feeds, Public and a newly added `/SHARED` step warm behind the prompt under a new `PENDING` boot status. They keep their bounded retry policy; a transient failure should be retried before it is announced.\n\n`/SHARED` had no step at all before. That is where another identity's work becomes visible, and with nothing to fail, a CUBE that stopped serving shared paths stayed silent until somebody went looking.\n\n**A deferred step leaves the boot failure gate**, so its failure can no longer stop a daemon binding — and a boot readout has scrolled away by the time it arrives. The failure is held until a later attempt succeeds and carried on the prompt context, so every surface says it: chell's prompt reads `[warm-up failed: groups]`, and argus names it on the JOBS readout in mars with the reason on hover. Named rather than counted, because \"Groups\" tells an operator which capability is degraded where \"1 warm-up failed\" only tells them to go looking.\n\nNothing reports a deferred completion. A warm that finishes and changes nothing is not news.\n\nCarries AEGIS law `deferred-warmup-failure-persists` with its smoke, which drives the surface's real prompt-context path rather than asserting a stub.","internal":false},{"hash":"eaf6c67","headline":"feat(acl): a feed's access list, in the shell's own verbs","body":"An exemplar needed to share a feed and mise had no wrapper, so it reached past the stack to CUBE's REST. That is backwards: a missing wrapper is the work. A capability living in one caller's private REST call is invisible to every other surface and untested by the kernel's suite.\n\nThe kernel gains `feed_share` and `feedShares_list`, over the client's own `addUserPermission` and `getUserPermissions` — which existed all along, so no raw REST was ever needed.\n\nIts shell face is `setfacl` and `getfacl`, not a `share` verb. Granting an identity access to a thing is an access control entry, and that is a verb a terminal already knows; \"share X with Y\" is a sentence, and reads as a natural-language assist rather than a shell:\n\n```\nsetfacl -m u:someone:r /home/me/feeds/feed_12\ngetfacl /home/me/feeds/feed_12\n # file: home/me/feeds/feed_12\n user::rw-\n user:someone:r--\n```\n\nA feed is named by id, by the `feed_N` a listing shows, or by any path holding one, so a path under `/SHARED` resolves by the same rule as one under a home folder. Group and other entries are refused rather than quietly read as users, an entry granting no read is refused because reading is what a CUBE share conveys, and `-x` is refused **by name** — CUBE models the grant but mise has no revocation to offer, and a shell that appears to strip an entry it cannot strip is worse than one that says so.\n\nThe grammar sits in its own dependency-free module, as `cat`'s does: the engine graph cannot be loaded under jest, so a grammar inside the builtin is a grammar nothing tests.","internal":false},{"hash":"73aa61a","headline":"The feed roster (`proc feeds`, the RUNS pane) reports each resident feed's total output size and wall span, derived from the cache with no wire;","body":"`cd` into a CFS link now follows it to its target, and a refusal names that target.","internal":false},{"hash":"75f1e5f","headline":"fix(boot): a step's label stops moving when it finishes","body":"A boot step is announced while it runs and again when it settles, and the two lines disagreed about where the label starts. The running line hardcoded a five-space indent against a comment asserting `[ OK ] ` was seven characters wide; the finished line pads its tag to the widest tag and adds a space. The label therefore jumped a column as each step resolved, and the plain non-interactive log was out by three rather than one.\n\nThe indent is now derived from the host's own tag width and from whether a spinner glyph precedes the text, since an animated line draws a frame and a space of its own and owes only the remainder while a plain log line owes the whole column. `chell` passes the width it actually renders with, so a longer status added later moves both lines together.\n\nThe arithmetic lives in its own import-free module, because the wider engine graph cannot be loaded under jest and an untestable alignment rule is how the first version came to be wrong.","internal":false},{"hash":"4f034b9","headline":"Host control: `chell --daemon --host-control[=shell,files,pipes]` lets the daemon declare capabilities of its own — `!` runs on the daemon host, pipe segments run there, `upload`/`download` reach its disk — off by default, refused on a non-loopback bind without `--expose-host-control`, and annunciated everywhere (attach ack `hostControl`, the daemon face, the prompt's HOST segment, a remote shell's banner).","body":"Without the `files` tier, `upload` under a daemon now refuses instead of reading the daemon host's disk.","internal":false},{"hash":"aa25502","headline":"The process cache records the compute resource each plugin instance ran on (from the CUBE list row), and the `feed.dag` model carries it per node (`mixed` for a group whose members ran on different resources), so a surface can hue a graph by where its work ran.","body":"","internal":false},{"hash":"f8d1b1c","headline":"Index movement is annunciated: a feed's first-visit topology load (`feed 812 indexing: 3400/20000 17%`) and roster arrivals (`+feed 812`, feeds created since or newly shared) reach the prompt context's `procWarmup` segment — `feed`, `arrived`, and `sweeping` so a renderer can tell a sweep from a load — and the chell prompt renders both.","body":"The process cache keeps the two registers (`feedLoad_progress/clear/get`, `arrivals_note/recent`); the salsa feed walk and roster syncs feed them.","internal":false},{"hash":null,"headline":"Updated dependencies [f73e6c2]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5dc064e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3afaa65]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [5b4b7db]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [eaf6c67]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [73aa61a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4f034b9]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aa25502]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [f8d1b1c]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [aaf0159]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [85c6813] - @fnndsc/cumin@3.17.0 - @fnndsc/menu@0.3.0 - @fnndsc/salsa@3.12.1","body":"","internal":true}]},{"version":"0.15.0","date":"2026-09-02","merge":"e4e14b714f31","bump":"minor","changes":[{"hash":"920e0ac","headline":"The `feed.dag` wire model is now the COLLAPSED projection: isomorphic sibling subtrees merge into one ×N node (the terminal tree's own collapse transform), so a massive fan-out crosses the wire and reaches a rendering surface as its shape, not its census.","body":"Each collapsed node keeps a real representative instance (id and data address), wears its worst member's status (an error anywhere in the group is visible on the node that stands for it), sums member metrics, and carries a tally with jump-to-error anomaly ids.","internal":false},{"hash":"c716624","headline":"Directory listings survive a restart.","body":"The listing cache is checkpointed (identity-keyed file under `~/.cache/chell/vfs/`, throttled writes) and restored at boot with each entry's original timestamp, so a restored listing is exactly as stale as it really is. Stale handling itself is fixed: the listing path used to serve any cached entry regardless of its TTL (a listing never refreshed until eviction or `ls -f`). Now a fresh entry serves as is; a stale one is served at once and revalidated behind itself when a host can carry the refresh (the daemon publishes the fresh `fs.listing` on the ambient bus, marked `fresh`), and is refetched in line at a plain console. `ls` models carry `fresh` per listing; `vfs.listing_get` exposes the listing with its freshness.","internal":false},{"hash":"97af423","headline":"Watches: a surface can keep a running feed live.","body":"New wire pair `watch` / `unwatch` (subject = `/proc/jobs/feed_N`, owned per surface, released on detach) and a `watched` report (`live` | `settled` | `stale`). While anyone watches a feed the engine samples it on an adaptive cadence (3 s while it changes, backing off to 30 s when quiet), and whenever a visit changes the cache it publishes the refreshed `feed.dag` model to every surface as a session-bus envelope from the `daemon` surface, off the scrollback. A feed that settles reports `settled` and the watch ends; a failed sample reports `stale` and keeps trying. `proc watch <feed>` / `proc unwatch <feed>` are the console forms (`proc watch` lists). The engine gains an ambient event bus for events it originates on its own. Feed visits within one second of each other now share one sync, and `feedVisit_sync` reports whether it succeeded.","internal":false},{"hash":"cece0dc","headline":"`/proc` freshness is now visit-driven.","body":"A revisit to `/proc/jobs/feed_N` (or a `feed diagram` re-render) is a delta, not a re-crawl: one feed-row fetch, and only when the job counters moved, a `min_end_date` walk of the nodes created or finished since the last visit plus an `active=true` sweep of the nodes still running. Nodes a dynamic pipeline spawns after the first load now appear on the next visit; previously they were invisible until `proc refresh`. Settled feeds are re-checked at most once per ten minutes, so work appended to a finished feed is still seen. A `/proc/jobs` visit (and `proc feeds` / `proc jobs list`) picks up feeds newer than the highest known id, and walks the whole index once the roster is older than ten minutes, so a feed shared later still appears.","internal":false},{"hash":null,"headline":"Updated dependencies [920e0ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [7a0f06e]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [c716624]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [cece0dc]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [97af423] - @fnndsc/cumin@3.16.0 - @fnndsc/salsa@3.11.0 - @fnndsc/menu@0.2.0","body":"","internal":true}]},{"version":"0.14.1","date":"2026-09-01","merge":"ee45cef0a1d0","bump":"patch","changes":[{"hash":"1bc0953","headline":"`ls` on a ChRIS link (`~/public`, `~/shared`) follows the link instead of rendering the link entry itself: the parent-cache leaf shortcut no longer captures links, so resolution falls through to the dispatcher's PathMapper.","body":"Native listings are also name-deduplicated (CUBE's links search can return the same row twice, observed on /PUBLIC).","internal":false},{"hash":null,"headline":"Updated dependencies [44ba77a]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1bc0953] - @fnndsc/cumin@3.15.1 - @fnndsc/salsa@3.10.1","body":"","internal":true}]},{"version":"0.14.0","date":"2026-09-01","merge":"77fa3b4e5149","bump":"minor","changes":[{"hash":"85791b3","headline":"`config write <cfs-path> <base64>` — the durable layer's write path.","body":"Per-user surface state (argus desktops, and the config-in-CFS family to\ncome) lives as files under `~/.config/...` in the user's CUBE home. Reads\nwere always `cat`; this adds the write: content arrives base64-encoded,\nstages through a host temp file, and rides the same upload\n(replace-in-place on an existing document) the edit flow uses. Documents\nare notes, not datasets: 256KB limit.","internal":false}]},{"version":"0.13.0","date":"2026-08-31","merge":"03f466be83ce","bump":"minor","changes":[{"hash":"0adb4f2","headline":"The daemon terminal gets a resting state: the console face.","body":"The boot-phase brain animation repainted by cursor arithmetic against a\nscrolling buffer — counting rows printed beneath it and jumping up — so a\nretry warning landing mid-frame interleaved with the art, and the pulse had\nto kill itself the moment output scrolled the logo away: the brain went\nsilent exactly when the daemon came alive.\n\nBoot is unchanged and text-first: the brain prints, pulses while\ncredentials are checked, and every address and token scrolls into ordinary\nscrollback. What is new is what happens when boot completes on a TTY: the\ndaemon switches to the alternate screen buffer — the same screen vim and\nhtop own — where the brain pulses forever at fixed coordinates over an\nidentity panel (identity, wire, ARGUS URL, token, berth, attach line,\nuptime, attached surfaces, index counts), with the last few log lines caged\nin a dim strip beneath. The pulse is honest: idle is a slow shimmer, an\nexecuting command quickens it, a surface attaching flares it.\n\nEsc or `q` drops to the normal buffer, restored byte-perfect with the boot\nlog intact and the lines captured while the face was up flushed beneath it;\nany key returns. Ctrl-C stops the daemon from either mode. Off a TTY\n(systemd, nohup) nothing changes: sequential logging, as before.\n\nThe brain art and its frame renderer moved from chell to brasa\n(`logo_frameRender`, `logo_linesRender`, and the new `logoRows_count` /\n`logoColumns_count`), so any surface can draw it; chell keeps only its\nboot-terminal animation host. `daemon_launch` now returns the launched\ndaemon's addresses and handle, which both launch paths (`chell --daemon`\nand the standalone `calypso` binary) feed to the face.","internal":false},{"hash":"31c2a50","headline":"`pipeline diagram` and `feed diagram` now carry their graphs as typed envelope models: `pipeline.diagram` (the authored topology) and `feed.dag` (the live instance graph with statuses and each node's `/proc` data address).","body":"The rendered trees are unchanged; a graphical surface reads the model where a terminal reads the text — one command, two projections.","internal":false},{"hash":"9ed68cd","headline":"`download` no longer writes to the daemon host's disk.","body":"File delivery is now a surface capability, like prompting, piping and editing already were.\n\nThe builtin resolved its destination with `path.resolve()` inside the engine,\nso the bytes landed on whatever machine hosted the engine. For a local shell\nthat is right — the engine is in-process and the operator's disk is the\nengine's disk. Under a daemon it put files on a machine nobody attending the\nsession was sitting at, and from a browser the question \"download to where?\"\nhad no answer at all.\n\n`Surface` gains `fileDeliver`, and `SurfaceCapabilities` gains `fileDelivery`\nalongside `engineFilesystem` — which says whether a path the engine resolves is\na path this surface's operator can open. Only an in-process local shell claims\nit. When it is false, `download` hands the file to the surface, which puts it\nsomewhere its operator can actually reach: the client's disk for\n`chell --remote`, the download manager for `argus`.\n\nOnly the request crosses the wire. Each surface fetches the bytes itself\nthrough the daemon's existing token-gated `/vfs` route, so a DICOM series is\nnot base64'd across a channel meant for session state — the intent travels\nthrough the vocabulary and the bytes travel through the byte route.\n\nThe local path is unchanged: a local `chell` still uses the existing transfer\nmachinery, with its globs, directory walks and progress reporting.\n\nA directory has no bytes to hand over, and what to do about that depends on\nwhat the surface has — a third capability, `localFilesystem`, declared in the\nattach handshake and answered by the surface rather than by the daemon. A shell\nowns a filesystem wherever it runs, so `chell --remote` receives the tree file\nby file and gets the folder it asked for. A browser owns no directory and can\ntake files only one at a time, so several hundred DICOM instances would be\nseveral hundred saves; for that surface alone a directory is archived into a\nsingle CUBE file first, through\nthe registered `zip v20240311` pipeline — `pl-dircopy` into a zip plugin, the\nsame mechanism the ChRIS web UI has used for years, but living in `brasa` where\nevery surface reaches one implementation instead of each client re-deriving the\nsequence. `CHRIS_ARCHIVE_PIPELINE` names a different pipeline where a\ndeployment registered one. When it is absent, the failure says which pipeline\nis missing and that it can be registered from the store.\n\nThe archive run announces itself rather than creating a feed silently, because\nit is a workaround for CUBE having no directory-archive route: issue #233.\n\n`upload` has the mirror problem — it reads from the engine host's disk — and is\nnot addressed here, because the browser direction needs a file picker. See\nissue #232.","internal":false},{"hash":"e58bf58","headline":"The wire contract moves out of `calypso` into a package of its own, `@fnndsc/menu`.","body":"A surface author should depend on the contract, not on the daemon that happens\nto serve it. Until now the contract was a subpath of `@fnndsc/calypso`, so a\nthird-party surface took a dependency on the session host to learn the shape of\na result. `menu` imports nothing from the stack and sits below `cumin`, so both\nthe engine that produces envelopes and the browser that renders them can load\nit.\n\nTwo vocabularies the contract narrows to moved with it, because they were\ndeclared above the thing that describes them: the structured-progress values\n(previously `@fnndsc/brasa/progress`) and the prompt-facing process-index state\n(previously `@fnndsc/cumin/proc-prompt`). Both are re-exported from their old\nhomes, so existing importers are unaffected; `@fnndsc/cumin/proc-prompt` as a\nsubpath is gone, and its types are available from `@fnndsc/cumin` directly.\n\n`CommandEnvelope` is now inferred from the schema that validates it. The engine\nand the wire previously carried separate declarations of the same shape, tied\ntogether by a compile-time assertion that one stayed assignable to the other; a\nsingle inferred type makes that drift impossible rather than detected.\n`@fnndsc/cumin` re-exports the name, so nothing that imports it changes.\n\n`@fnndsc/calypso/protocol` no longer exists as a subpath. Import\n`@fnndsc/menu`. The names remain re-exported from `@fnndsc/calypso` itself for\nnow, since most of the stack has always reached them there.\n\nThis is the scaffold for envelope Phase-2 — a typed result model for every\ncommand — recorded in `docs/menu.adoc`. `menu` itself is unpublished until that\nwork lands, so thirty payload shapes can settle without forcing a release each\ntime.","internal":false},{"hash":"e062dbf","headline":"Two more commands speak in models: `proc feeds` (now listing the whole cache-resident roster when unqueried) carries a `feed.list` model, and `pacs query` carries a `pacs.query` model — studies and series with their instance UIDs and pullable VFS paths.","body":"Terminal renderings are unchanged.","internal":false},{"hash":"28b9a9f","headline":"Regard: the session learns what the operator is indicating.","body":"The wire contract gains one message, `regard`, travelling both directions\nunder one shape: a surface reports the addressable thing the operator most\nrecently indicated (a file clicked in a browser, a DAG node selected), and\nthe daemon retains it as session truth — last write wins — mirrors it into\nthe engine, and rebroadcasts it to every attached surface. A late attacher\nreceives the retained value with its ack, so spawn-then-see workflows start\nseeing immediately.\n\nThe value is an address in the namespace plus the model kind it was\nindicated through, never view-space coordinates: what has no address is view\nstate and stays surface-side. The brasa session retains the value behind\n`regard_get`/`regard_set`, so engine-side consumers can answer \"what is the\noperator regarding\" without any surface geometry crossing the seam. Design\nrecord: apps/argus/docs/aegis.adoc.","internal":false},{"hash":"8842ab4","headline":"Indeterminate progress now crosses the daemon wire as typed facts rather than terminal escapes.","body":"The spinner used to write `\\r\\x1b[K<frame>` and cursor hide/show to the status\nchannel twelve times a second, so every attached surface received terminal\nchoreography whether or not it was a terminal — a web surface had to emulate a\ncharacter grid to recover the meaning, and got it subtly wrong. It now announces\n`operation: 'task'`, `kind: 'inspection'`, `phase: 'working'` with a label, and\ncloses with `phase: 'complete'`. Each surface draws waiting in its own idiom.\n\nOnly state changes cross the wire: frames and elapsed counters are the\nrenderer's, so a spin of any length costs two events instead of dozens per\nsecond. `chell` gained the elapsed counter its spinner used to bake into the\nlabel, and `argus` gained a full progress renderer — indeterminate work spins,\ncounted work fills a bar — which also surfaces the download progress it had\nbeen silently discarding.\n\nThe `operation` and `phase` enums gained `task` and `working`. Every enum on\nthe progress message now degrades on an unknown value instead of failing the\nparse and dropping the message whole: `operation` to `task`, `phase` to\n`working`, `status` to `unknown`, `kind` and `unit` to absent. That makes good\nthe contract's promise that change within a major is additive — for future\nadditions, since the fallback lives in the build doing the reading.\n\nThe spinner keeps its call signature, so its callers are unchanged. Its\n`showTiming` and `clearLine` arguments are now ignored: both are rendering\ndecisions. It also no longer inspects `process.stdout.isTTY` before announcing,\nwhich had suppressed progress inside the daemon, where the engine's own stdout\nis never a terminal but the attached surface may well be able to draw.","internal":false},{"hash":"1d600ac","headline":"An archive that cannot run no longer leaves a feed behind, and says the right reason for not running.","body":"Live testing found both. `download <dir>` from a browser reported that the\n`zip v20240311` pipeline was not registered, directly below a line explaining\nthat one of its nodes was not registered on the target compute environment. The\npipeline was present; the advice to fetch it from the store was wrong. Every\n`pipeline_run` failure had been collapsed into the one message.\n\n`pipeline_readiness` in salsa answers whether a pipeline exists and could be\nprepared, distinguishing _unregistered_ from _registered but unpreparable_ —\ndifferent problems calling for different actions.\n\nPreparing a pipeline needs no previous instance, so readiness is now checked\n_before_ the feed is created. The failed run above had already created a feed\nthat nothing then used, leaving litter in a feed list and a copy in the compute\ngraph asserting an analysis that produced nothing.\n\nA run that fails after its feed exists now removes it, and names the feed when\nremoval fails. A run that merely exceeds its time limit is left alone, since it\nmay yet finish and deleting would remove the feed from under a running job.","internal":false},{"hash":"1c195a7","headline":"Execution metrics ride the warmup for free: node differentiation lands.","body":"Every CUBE plugin-instance list row already carries `start_date`,\n`end_date`, and `size` — the same rows warmup and status refresh page\nthrough. They are now typed on the contract, captured into the proc cache\n(`ProcInstance.startedAt/finishedAt/outputBytes`, merged defined-only so a\nrefresh never erases what warmup saw), persisted by the checkpoint, and\nprojected by `feed diagram` onto each node's `metrics` (wall-clock\n`computeSeconds`, `dataBytes`). Zero new CUBE calls at any point.\n\nThe molecule rendering scales by them (a SCALE pill flips between wall\ntime and output bytes, re-projecting the remembered model locally), and\ntimestamp-true pulse replay becomes possible.","internal":false},{"hash":"87f7c59","headline":"The `pacs.query` model now says what CUBE already holds: each series carries a `pulled` flag and file count (one bounded sweep of single-attempt lookups), and studies carry their own VFS path so a surface can pull a whole study.","body":"","internal":false},{"hash":"3125517","headline":"A dropped retrieve watch is no longer reported as a failed pull.","body":"Pulling a 22-series study reported `0/22 series complete` with every series\nmarked `ERROR` — and the CUBE path report printed immediately below it listed\nfour of those same series with real paths and file counts. The retrieves were\nfine; the watch had died.\n\nOne websocket failure marked every in-flight series `error`, because\n`RetrieveStatus` had no value meaning _the client stopped watching and does not\nknow the outcome_. The code knew the difference — a comment in `pull` says a\nwatch failure \"is usually cosmetic, the PACS keeps pushing and CUBE keeps\nregistering after detach\" — but nothing downstream acted on it.\n\nA lost watch now marks its series `unconfirmed`. The confirmation loop, which\nalready asks CUBE about series whose confirmation went missing, now asks about\nthese too: a series CUBE reports as stored is `pulled`, with its file count and\npath, whatever the watch managed to see.\n\nWhat remains unconfirmed is reported as unknown rather than lost — `? … [WATCH","internal":false},{"hash":"b39b584","headline":"A dropped retrieve watch reconnects instead of giving up.","body":"The LONK socket is only how a client watches a retrieve; the retrieve itself\nruns on the server and is unaffected by the socket dying. Losing the view was\nnonetheless the end of the watch — which is why a 22-series study failed where\na 2-series one did not: a longer retrieve gives the socket more chances to\ndrop.\n\nA dropped socket is now reopened, up to three times, and re-subscribed to the\nseries still in flight. Nothing is re-fired: those retrieves were never lost.\nThe reconnection is announced, because a silent one during a long pull is\nindistinguishable from a stall.\n\nOnly when reconnection is exhausted does the watch stop, and it still records\nthe remaining series as `unconfirmed` rather than failed.","internal":false},{"hash":null,"headline":"Updated dependencies [1d600ac]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [a78b5ee]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [1c195a7]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [e58bf58]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [56ade16]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2bb9c29]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [3125517]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [2c38d8b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [b39b584] - @fnndsc/salsa@3.10.0 - @fnndsc/cumin@3.15.0","body":"","internal":true}]},{"version":"0.12.0","date":"2026-08-26","merge":"6d1e218fbad2","bump":"minor","changes":[{"hash":"0ad04f2","headline":"The engine can now hand raw file bytes to a hosting daemon: brasa's `BrasaEngine` gains an optional `file_read(filePath)` that resolves a ChRIS VFS path to a `Buffer` through chili's binary cat.","body":"Calypso's daemon exposes it as a token-gated `/vfs?path=&token=` HTTP route with extension-derived content types, letting a web surface render images and other binary content that a text transcript cannot carry.","internal":false}]},{"version":"0.11.1","date":"2026-08-25","merge":"752c986dcf84","bump":"patch","changes":[{"hash":"b0b478b","headline":"PACS payload and path-grammar consolidation.","body":"Cumin gains `dicomPayload`, the one home for DICOM query-payload interpretation (tag unwrapping including the DICOM-JSON `{Value: [...]}` form, study and series array location, UID lookups), replacing four diverged private copies. Salsa's `pacsHelpers` becomes the single authority for the PACS folder grammar, adding `folderUID_get`, `queryLabel_extractFromFolder`, and `queryFolderName_build`; the query path a surface builds now always matches the name the listing shows, including the title fallback the old builder lacked. Brasa's `pacsUtils` and `query` builtins consume the shared helpers; `pacs_tagValueExtract` remains as a compatibility alias.","internal":false},{"hash":null,"headline":"Updated dependencies [b0b478b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [bb4e06a] - @fnndsc/cumin@3.12.0 - @fnndsc/salsa@3.8.0","body":"","internal":true}]},{"version":"0.11.0","date":"2026-08-24","merge":"fbb9d089711b","bump":"minor","changes":[{"hash":"69b0617","headline":"Typed chell API, third tranche: `cp` and `mv` join the facade with their existing `fs.cp`/`fs.mv` models.","body":"Fixing what their first typed run surfaced: file rename was silently broken against current CUBEs (the PUT field drifted from `path` to `upload_path`); cumin now sends the current field and falls back for older servers.","internal":false},{"hash":"7ed8e1c","headline":"Typed chell API, second tranche: `ls` and `cat`.","body":"`ls` gains the `fs.listing` model (entries per target as data), `cat` joins with its existing `fs.cat` outcomes and content in the rendered channel, and both enter the same cores the parsed builtins enter. Programmatic `cat` never injects syntax highlighting.","internal":false},{"hash":"7dc3bca","headline":"The typed chell API, first tranche.","body":"`chellApi_create()` returns the shell's vocabulary as function calls: `pwd`, `cd`, `mkdir`, `touch` and `rm` take typed options, enter the same per-command cores the parsed builtins enter, and return envelopes whose model slot is typed per command (`TypedEnvelope<K>` over the new `FsModelMap`). No command line is assembled or parsed anywhere on the path.","internal":false},{"hash":null,"headline":"Updated dependencies [14f8ee8]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [4da2673]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [69b0617] - @fnndsc/cumin@3.11.0 - @fnndsc/salsa@3.7.0 - @fnndsc/chili@3.6.5","body":"","internal":true}]},{"version":"0.10.1","date":"2026-08-24","merge":"1bc0f7746067","bump":"patch","changes":[{"hash":"5285bd5","headline":"Cast burndown to the adapter floor.","body":"The wire contract gains the pipelines surface (`pipeline_get` with piping items and plugin metadata handles, `pipelineSourceFilesPage_get`); salsa's pipeline modules, feed joins, and brasa/chili call sites migrate onto typed accessors and honest converters. The repository's `as unknown as` count is now 2, both inside the licensed adapter seam, and the CI ratchet holds it there. chell progress bars also draw on the renderer's configured stream instead of assuming stdout.","internal":false},{"hash":null,"headline":"Updated dependencies [5285bd5]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [919648f] - @fnndsc/cumin@3.10.0 - @fnndsc/salsa@3.6.1 - @fnndsc/chili@3.6.4","body":"","internal":true}]},{"version":"0.10.0","date":"2026-08-21","merge":"364c31576aa7","bump":"minor","changes":[{"hash":"22db63f","headline":"Make PACS pulls observable and recoverable.","body":"A new `pacs status <expression | path | queryId>` reports one line per series: a fill bar of files registered in CUBE against the PACS-reported instance count, the derived state, and the in-CUBE `/SERVICES` folder for every landed series; an expression reuses the caller's most recent matching query instead of minting a new record per check. `pull` is now idempotent: series already fully registered in CUBE are skipped before firing, so re-running the same pull fetches exactly the missing series. Retrieve firing gets three attempts with backoff and is bounded to four concurrent creations, replacing the all-at-once stampede that overloaded CUBE and silently lost retrieves. A series whose firing still fails is reported as `FAILED TO FIRE, will not arrive; re-run pull`, distinct from watch-side failures, which now point at `pacs status` since their transfers usually complete server-side anyway. Cumin's status report gains the per-series storage folder (`seriesStorage_resolve`). The failure modes and operator playbook are documented in docs/pacs-pull-recovery.adoc.","internal":false},{"hash":"590a943","headline":"Converge the two PACS retrieve watchers onto one engine.","body":"pull's battle-tested LONK machinery (bounded, retried firing; stall/timeout/no-activity detection; storage confirmation; refire loop) moves into salsa as the presentation-free `retrieve/watch` module; the `pull` builtin becomes a thin consumer rendering engine events onto its sink, and the PACS VFS provider's `cp` drops its 5-second polling watcher for the same engine, gaining firing retry, LONK push, and the idempotency skip. The convergence surfaced and fixed two latent defects that had made PACS `cp` unusable on real paths: the source parser assumed paths without the `/queries/` segment and rejected every real listing path, and the file downloader only consulted the userfiles collection, so PACS files under `/SERVICES` read as \"not found\"; it now falls back to the PACS files collection. Supporting dedup: cumin gains `retry_untilValue` (the one bounded-backoff loop) and `seriesStorage_resolve` gains bounded re-probing, replacing the per-package storage-resolver copies; the `_qid:` parser is now shared from salsa.","internal":false},{"hash":"b2f5ad3","headline":"Greet every surface with the stack's identity.","body":"Builds now record the short git hash they were produced from (`dist/buildinfo.json`, written by the new `scripts/buildinfo.mjs` build step), and brasa exposes it through `buildHash_get` alongside `welcomeLine_build`/`welcomeLine_compose`, which render a banner of the form `ChELL Executes Layered Logic, v 5.3.0 (886f09). Welcome.` An interactive chell session prints the banner and a short fortune at boot, the calypso daemon announces its banner plus an aligned version line for every stack layer (chell, brasa, chili, salsa, cumin, calypso) when it starts listening, and the attach handshake gains an optional `stack` field carrying all six versions and the build hash, so a remote surface banners the daemon's full reported stack rather than its local install's. `fortune_random` is exported for reuse, with an optional line-count bound so banners favour short cookies.","internal":false},{"hash":"2e60785","headline":"Cache the `/etc/group` projection per CUBE connection for five minutes, invalidate it after ChELL membership changes, and show semantic inspection progress while an uncached projection is resolving.","body":"","internal":false},{"hash":"66bc932","headline":"Add ChELL group membership commands and include current CUBE usernames in the live `/etc/group` projection.","body":"","internal":false},{"hash":"8ec8a4b","headline":"Add explicit, command-scoped CUBE elevation with `sudo <command>`.","body":"The active surface collects an administrator identity and hidden password; a temporary CUBE client runs only the nested command, then the normal session is restored. Group membership and plugin registration now suggest a copyable `sudo` rerun after an authorization failure, and plugin registration no longer owns a separate interactive admin prompt.","internal":false},{"hash":"d0ac04f","headline":"Preserve failure causes and tighten cache contracts.","body":"Errors that were replaced by generic messages now carry their real cause: controller and handler creation failures name the underlying error instead of \"no ChRIS context\", `cat` shows the actual fetch failure, docker command failures surface stderr detail, context-set errors actually print, settings load/save failures are announced instead of silently using defaults or losing changes, unreadable berth and discovery files are named instead of reading as \"no daemon\", token-file read errors are distinguished from the logged-out state, and resource deletes report why they failed. Every remaining deliberate error absorption now carries a dated adjudication comment. Cache contracts: the PACS provider's decoded-query cache caches settled results only and is size-bounded; the object-context factory gains an eviction hook (`objContext_evict`) for deleted plugin or feed ids; `mv` and `cp` invalidate the whole affected listing subtree; and a new PACS query invalidates the cached `/net/pacs/queries` listing so it appears in the next `ls` immediately.","internal":false},{"hash":"886f09c","headline":"Fix stale directory state in long-lived daemons.","body":"Folder object contexts are no longer cached in the cumin context factory: folder paths are not stable identities in CUBE (deleting and re-uploading a directory assigns a new folder id), so a group bound at first touch kept serving the dead folder for the life of the process, which in a calypso daemon meant a freshly uploaded directory listed as permanently empty and `ls -f` could not recover it. Plugin and feed contexts keep their stable-id cache. The listing cache gains `cache_invalidateTree`, and `rm` and directory uploads now invalidate the whole affected subtree, so a delete-and-re-upload cycle cannot serve still-fresh nested listings from the old tree. The salsa native provider also distinguishes a missing folder from an empty one: when every sub-listing comes back empty it probes the parent, and a nonexistent path reports \"No such file or directory\" instead of rendering as an empty directory.","internal":false},{"hash":"50e9bb1","headline":"Make CUBE group membership commands name-first and batch-capable.","body":"`group members`, `group inspect`, `group adduser`, and `group removeuser` now accept an exact group name or a numeric ID; add and remove accept multiple usernames, report every result, and make already-satisfied membership changes successful no-ops.","internal":false},{"hash":"d0ac04f","headline":"BEHAVIOR CHANGE: failures no longer read as empty results.","body":"Commands that previously printed nothing and exited 0 when a fetch failed now report the error and exit non-zero, following the codebase's `Result<T>` pattern throughout. Affected surfaces: listing a PACS study or series that does not exist errors instead of showing an empty directory; `store list`/`store search` and `compute list` error when the store or CUBE is unreachable instead of showing no entries; plugin registration no longer defaults to the `host` compute resource when the compute fetch failed; `rm` on a feed fails when running jobs could not be cancelled instead of claiming success; a recursive scan that cannot list a subtree reports the exclusion instead of silently omitting it; batch job-status gaps and disconnected status fetches push visible warnings; the native VFS distinguishes \"folder absent\" from \"could not verify\" so probe failures no longer masquerade as missing directories; `cp` fails when path resolution fails rather than proceeding with a guessed path; join-edge resolution retries after a transient failure instead of permanently recording no joins; and the remote client reports invalid daemon messages instead of silently dropping them. Exit codes are now truthful end to end: `ls` reports an error status when any listing fails instead of aggregating failures into success, and `chell -c` derives its exit code from the command's envelopes, so a failed one-shot command exits non-zero even when the builtin did not set an exit code itself.","internal":false},{"hash":"ac69b3e","headline":"Run ChELL shell escapes on the originating surface, including remote clients, instead of exposing the CALYPSO daemon host process and filesystem.","body":"","internal":false},{"hash":"0e92d4d","headline":"Persist and reconcile daemon `/proc` checkpoints, fix wildcard listing of virtual executables, keep remote admin prompts on their originating surface, add a Unix-style `id` builtin for the current CUBE UID/GID projection and group memberships, and make versioned-plugin help, parameters, and README output compose correctly through terminals, pipes, and redirects.","body":"","internal":false},{"hash":"fa81126","headline":"Retain failed `/proc` topology sweep state and add `proc retry` to continue at the failed page without repeating already successful pagination work.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [2e60785]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [66bc932]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [8ec8a4b]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d0ac04f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [886f09c]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [50e9bb1]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [d0ac04f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [22db63f]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [0e92d4d]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [590a943]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [fa81126] - @fnndsc/salsa@3.6.0 - @fnndsc/cumin@3.9.0 - @fnndsc/chili@3.6.2","body":"","internal":true}]},{"version":"0.9.9","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Keep `cat /bin/<pipeline>` immediate with a cache-only executable summary;","body":"move complete registered invocation YAML and delayed inspection progress to `pipeline manifest <specifier>` and `<pipeline> --manifest`.","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/salsa@3.5.5","body":"","internal":true}]},{"version":"0.9.8","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Emit delayed semantic progress when an exact `/bin` Pipeline manifest read takes longer than 300 milliseconds, while leaving fast cache hits silent.","body":"","internal":false}]},{"version":"0.9.7","date":"2026-07-18","merge":"534d57150256","bump":"patch","changes":[{"hash":null,"headline":"Resolve exact `/bin` Pipeline reads without global Pipeline enumeration or per-node hosted-plugin metadata requests, cache repeat reads, and render Pipeline topology in linear time.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/salsa@3.5.4","body":"","internal":true}]},{"version":"0.9.6","date":"2026-07-18","merge":"e8d44db0ef69","bump":"patch","changes":[{"hash":null,"headline":"Run registered pipelines with node-qualified parameters, compute/resource controls, and strict CFS YAML overlays;","body":"expose registered manifests through `/bin`, contextual parameter help, and completion.","internal":false},{"hash":null,"headline":"Attach one plugin or pipeline after `pacs pull --new-feed`, preserving the valid Feed and root when attachment fails.","body":"","internal":false},{"hash":null,"headline":"Preserve shell-tokenized spaces, negative values, booleans, and `--name=value` across plugin and pipeline executable arguments.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.4 - @fnndsc/salsa@3.5.3","body":"","internal":true}]},{"version":"0.9.5","date":"2026-07-18","merge":"645026ee7e3b","bump":"patch","changes":[{"hash":null,"headline":"Add `pacs pull --new-feed \"TITLE\"` to create one named analysis feed from a completely retrieved PACS selection, with explicit feed/root IDs and all-or-nothing handling for partial or unresolved pulls.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.3 - @fnndsc/salsa@3.5.2","body":"","internal":true}]},{"version":"0.9.4","date":"2026-07-17","merge":"2ba2bc3d538d","bump":"patch","changes":[{"hash":null,"headline":"Add controllable syntax highlighting to `cat`, including Python and other popular source/configuration formats, while keeping ordinary pipes and redirects ANSI-free.","body":"","internal":false}]},{"version":"0.9.3","date":"2026-07-17","merge":"90c5b53729d7","bump":"patch","changes":[{"hash":null,"headline":"Carry cold, cached-refresh, and failed `/proc` lifecycle state through the CALYPSO prompt contract;","body":"reorder p10k segments and render distinct lifecycle clues for local and remote surfaces.","internal":false},{"hash":null,"headline":"Updated dependencies - @fnndsc/cumin@3.8.2","body":"","internal":true}]},{"version":"0.9.2","date":"2026-07-16","merge":"c79731f39938","bump":"patch","changes":[{"hash":"6f0833a","headline":"Release the coordinated ChELL stack with deterministic `/proc` topology progress, complete visible-feed indexing, safe warm-up query gating, remote one-shot command completion, and refreshed daemon documentation.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [6f0833a] - @fnndsc/cumin@3.8.1 - @fnndsc/salsa@3.5.1 - @fnndsc/chili@3.6.1","body":"","internal":true}]},{"version":"0.9.1","date":"2026-07-16","merge":"1896be7ff9ef","bump":"patch","changes":[{"hash":"71a6cd4","headline":"Route a pipeline executable's bare `--signalflow` flag to SignalFlow diagram output and provide contextual help for dynamic pipeline commands.","body":"Keep final-segment redirection on the originating surface, propagate remote pipe-segment failures back to the engine, and prevent remote command errors from terminating the interactive ChELL client. Daemon mode now reports shared startup cache warming and publishes its listening berth only after engine readiness. Consistently document `signalflow -` for stdin rendering.","internal":false}]},{"version":"0.9.0","date":"2026-07-15","merge":"942aabcdd399","bump":"minor","changes":[{"hash":"e630f79","headline":"Draw registered CUBE pipelines with `pipeline diagram <id|name>` or the `/bin` shorthand `<pipeline> --diagram`.","body":"Bare output uses the same shallow tree machinery as feed diagrams, `--withargs` appends stored non-null plugin defaults, and `--signalflow` emits the same SignalFlow YAML dialect as feeds. `feed diagram <specifier>` is now a shallow alias of `feed tree`; feed graph commands accept IDs, `feed_N`, exact or unambiguous title searches, and infer the feed from the current `feed_N` directory when omitted.","internal":false},{"hash":"e630f79","headline":"Classify CALYPSO as the assisted session host, rather than a user-facing surface, in the detailed stack information report.","body":"","internal":false},{"hash":null,"headline":"Updated dependencies [e630f79] - @fnndsc/cumin@3.8.0 - @fnndsc/salsa@3.5.0","body":"","internal":true}]},{"version":"0.8.0","date":"2026-07-14","merge":"107999d0bcd3","bump":"minor","changes":[{"hash":"8ac7ef9","headline":"Add `feed diagram --signalflow <feedId>` — emits a feed's DAG as a **SignalFlow YAML document to stdout**, composed with pipes rather than rendered in place:","body":"```\nfeed diagram --signalflow 1669 | signalflow - # ASCII\nfeed diagram --signalflow 1669 | signalflow - -o x.svg # SVG\nfeed diagram --signalflow 1669 > feed-1669.yaml # keep it\n```\n\nIt builds the graph cache-first, collapses isomorphic siblings into named `×N` chips, and\nencodes topological-join edges via SignalFlow's node-reuse mechanism. mise emits the\nrepresentation only — no renderer is invoked, discovered, or bundled — so there is nothing\nto install for the command itself; rendering is the user's own `signalflow`. `--signalflow`\nnames the dialect, leaving room for further emitters (`--json`, `--dot`, …).","internal":false}]},{"version":"0.7.0","date":"2026-07-14","merge":"177711eef683","bump":"minor","changes":[{"hash":"b8ae635","headline":"`feed tree` now collapses isomorphic sibling subtrees by default.","body":"Structurally-identical branches merge into one `×N` template node showing a proportional status bar, per-category counts (`97✓ 2⋯ 1✗`), and the ids of any non-done members (error first) so failures stay addressable. Collapsed groups use a double-line connector to signal multiplicity. Pass `--flat` to draw every node individually.","internal":false},{"hash":"ca63e8b","headline":"Add `feed tree <feedId>` — renders a feed's plugin-instance DAG as an annotated text tree.","body":"The anchor tree is drawn with box-drawing connectors; topological-join (`ts`) nodes are annotated inline with the extra sources they merge (`⋈ joins ...`). Supports `--focus <id>` to scope to a subtree and `--max-nodes <n>` to cap output (0 = all). The envelope carries a typed `feed.tree` FeedGraph model.","internal":false},{"hash":"01ab743","headline":"`feed tree` now builds the DAG **cache-first** from the warm ProcCache instead of re-crawling the feed on every call.","body":"It reuses already-loaded topology, fetches feed metadata only when missing or a placeholder, refreshes volatile status cheaply (one feed-scoped list call, active nodes only) when reusing a warm cache, and resolves join edges lazily. New salsa exports: `feedGraphData_ensure`, `feedMeta_ensure`, `feedInstances_ensureLoaded`, `feedStatus_refresh`.","internal":false},{"hash":null,"headline":"Updated dependencies [a1f6694]","body":"","internal":true},{"hash":null,"headline":"Updated dependencies [01ab743] - @fnndsc/cumin@3.7.0 - @fnndsc/salsa@3.4.0","body":"","internal":true}]},{"version":"0.6.1","date":"2026-07-14","merge":"5b197aba79a2","bump":"patch","changes":[{"hash":"0d358c5","headline":"/proc now caches settled job status.","body":"A finished plugin instance (`finishedSuccessfully`, `finishedWithError`, `cancelled`) never changes, so its status is kept permanently once observed. Consequences:\n\n- Listing a fully-finished feed under `/proc/jobs` is instant — no status calls.\n- Live status for active feeds is refreshed with a single feed-scoped list call\n (the list response already carries `status`) instead of one detail fetch per node.\n- Reading a settled instance's `status` returns the cached value without an API call.","internal":false},{"hash":null,"headline":"Updated dependencies [0d358c5] - @fnndsc/cumin@3.6.0 - @fnndsc/salsa@3.3.0","body":"","internal":true}]},{"version":"0.6.0","date":"2026-07-13","merge":"f0c19c50afad","bump":"minor","changes":[{"hash":"e5a30f7","headline":"Add `date` and `cal` builtins, in the spirit of their UNIX namesakes and fully self-contained (pure computation — no host binary, no subprocess).","body":"`date` prints the current date/time with the familiar default format, `-u` for UTC, and `+FORMAT` strftime-style format strings (it reports the time only, never sets the clock). `cal` prints a month (`cal`), a whole year (`cal <year>`), or a specific month (`cal <month> <year>`), with today highlighted. Both return their output in an envelope through the sink, so they work identically local, over a CALYPSO daemon, and in the standalone binary.","internal":false}]},{"version":"0.5.0","date":"2026-07-13","merge":"79729c9cf094","bump":"minor","changes":[{"hash":"c2087d0","headline":"Add a `fortune` builtin — the classic UNIX fortune cookie, as a shell builtin.","body":"It prints a random fortune and is fully self-contained: the content is bundled (vendored from the traditional fortune-mod datfiles, classic BSD `fortune` material), so it needs no host `fortune` binary and no datfiles on disk, and behaves identically in a local shell, over a CALYPSO daemon, and in the standalone binary. Output travels in an envelope through the sink like every other command. Regenerate the bundled set with `scripts/fortunes_generate.mjs`.","internal":false}]},{"version":"0.4.0","date":"2026-07-13","merge":"cb2432e85fb3","bump":"minor","changes":[{"hash":"880d37a","headline":"`chell --version` reported brasa's version in place of chell's: the version module moved into brasa during the engine split but still read its own `package.json` as \"chell\", so it printed brasa's number.","body":"It now resolves every package by name (reading brasa's own directly) and reports the full stack — chell, brasa, chili, salsa, cumin, calypso — with versions aligned in a column. A new `chell --info` flag prints a role-grouped table (surfaces / engine / layers) of each package, its full name, and version. The version report, the `--info` table, and the boot panel all draw from a single source of truth in brasa (`stackInfo_get`), and the standalone binary inlines every stack version at build time.","internal":false},{"hash":"e43f42a","headline":"Delegating an unknown chell command to chili no longer stalls or floods the terminal with context-init errors when the current directory is a pure-VFS path (`/proc`, `/net`, ...).","body":"chili now registers its file-group and plugin-context commands without resolving any ChRIS context — each controller is created lazily, only when a command's action runs — so an unrelated command (or a directory that is not a ChRIS folder) pays no network cost and produces no setup-time error wall. chili also exports `commandNames_get()`, a cheap network-free listing of its top-level commands. In brasa, the \"delegating to chili\" notice is now emitted on the live sink before chili runs, so it appears ahead of chili's output instead of after it; and a command chili does not know (a typo, a host program) is reported as `command not found` without delegating at all.","internal":false},{"hash":"512e14f","headline":"Fix `<command> --help` leaking to a daemon's terminal instead of reaching the surface.","body":"The `--help` flag path printed help through `console.log`, which on a CALYPSO daemon landed on the daemon's own terminal — and returned an empty envelope, so a remote surface saw nothing. Help now travels in an envelope through the sink like every other command output, so `--help` reaches the surface that asked for it and never prints on the daemon. This removes the last console-based path in the help flow (`help_show`).","internal":false},{"hash":null,"headline":"Updated dependencies [e43f42a] - @fnndsc/chili@3.6.0","body":"","internal":true}]},{"version":"0.3.0","date":"2026-07-13","merge":"96bb6d340604","bump":"minor","changes":[{"hash":"a0d3df5","headline":"The engine no longer intercepts the console anywhere.","body":"The pipe/redirect `output_capture` monkeypatch is deleted: pipes now capture through a `PipeCaptureSink` scoped over the (re-activated) `AsyncLocalStorage` sink scope, ANSI-stripping text writes and keeping binary writes (a raw `cat` of a DICOM file) byte-for-byte. `chiliCommand_run` drives chili through its `run_capture` seam and returns an envelope, so the pacs passthroughs and the unknown-command fallback are envelope-based. The remaining print-direct builtins are converted: `store`, `upload`, `download`, `connect`, `edit` return envelopes, while the streaming commands `pull`, `pipeline`, and `pacs` emit incremental output through the sink so it streams live to a terminal or daemon and is captured in a pipe.","internal":false},{"hash":null,"headline":"Updated dependencies [a0d3df5] - @fnndsc/chili@3.5.0","body":"","internal":true}]},{"version":"0.2.0","date":"2026-07-12","merge":"64fea26ab02f","bump":"minor","changes":[{"hash":"d69b086","headline":"Every printing builtin now returns a `CommandEnvelope` instead of writing to the console.","body":"`files`/`links`/`dirs`, `feed`, `plugin`, and `parametersofplugin` were the last holdouts; with them converted, the per-invocation console monkeypatch (`printingHandler_wrap`, which hijacked `console.log`/`console.error`/`process.stdout.write` to capture a builtin's output) is deleted, along with the `LiveEnvelopeOutputSink` marker that only served it.\n\nBehavioural notes: unknown subcommands of these resource commands now return a clear error envelope instead of spawning a chili subprocess, and `search` is handled natively via `list --search`. The unknown-_command_ fallback still delegates to chili, now over the same print-direct path as the other unconverted handlers.","internal":false},{"hash":null,"headline":"Updated dependencies [d69b086] - @fnndsc/chili@3.4.0","body":"","internal":true}]}]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnndsc/brasa",
3
- "version": "0.33.1",
3
+ "version": "0.33.2",
4
4
  "description": "BRASA Runs Abstracted Shell Actions - the hostable ChRIS shell engine (kernel)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,7 +52,7 @@
52
52
  "minimatch": "^10.1.1",
53
53
  "ws": "^8.21.0",
54
54
  "@fnndsc/menu": "^0.19.0",
55
- "@fnndsc/orrery": "^0.4.0"
55
+ "@fnndsc/orrery": "^0.5.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/jest": "^30.0.0",