Two read-only operator surfaces existed server-side but the CLI was app-only:
- `pic dead-letters ls --group <slug>` — mirrors the app listing onto a group's
shared-queue dead-letters (§11.6 D3, `GET /groups/{id}/dead-letters`), via the
existing `require_one_owner`/`OwnerRef` dispatch. List-only (show/replay/
resolve stay app-only, matching the server's operator surface); a group DL row
shows its `collection` and carries no trigger, so it renders its own columns.
- `pic docs ls/get --group <slug>` — a new `Docs` command (`cmds/docs.rs`)
wrapping `GET /groups/{id}/docs[/{collection}/{id}]`, mirroring `pic kv`.
Group-only: per-app docs have no admin read route yet (unlike kv/files), so
there is no `--app` variant — noted for a later pass.
Read-only by design (writes go through the SDK). New client methods +
`GroupDocsListDto`/`GroupDeadLetterDto`. Pinned by a new
`operator_reads_shared_docs_and_group_dead_letters_via_cli` journey
(152/152 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `pic kv ls --group` / `pic kv get --group` (mutually exclusive with --app)
hit the M4 admin API; client group_kv_list/group_kv_get.
- collections journey: a script writes a shared KV value, the operator lists +
fetches it via the admin API with no script.
- docs: §11.6 + CLAUDE.md record M3 quotas + M4 read-only admin API.
Completes M4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- collections.rs: shared_files_collection_is_read_write_across_the_subtree
— a group declares kv+docs+files in one string-or-table manifest; authed
app A creates a blob via files::shared_collection("assets").create(...),
app B lists + gets the SAME bytes back across the subtree, a
sibling-subtree app gets CollectionNotShared, collections ls shows all
three kinds. Live-verified the bytes land under
files/groups/<group_id>/assets/<id[0:2]>/<id>.
- .gitignore: ignore /crates/picloud-cli/data (the CLI journey harness
spawns the server from that dir; the files journey is the first CLI test
to write blobs).
- docs: design §11.6 (KV+DOCS+FILES shipped; topics/queue still deferred),
sdk-shape (files::shared_collection), CLAUDE.md current-focus.
Full journey suite 117/117; schema blessed (55 migrations); workspace
clippy -D clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end docs journey: a group declares a kv AND a docs collection via the
string-or-table manifest; an authenticated app A docs::shared_collection(
"articles").create(#{...}), app B finds it back across the subtree (exercising
the shared find DSL); a sibling-subtree app gets CollectionNotShared;
collections ls shows both kinds; re-apply NoOp. Full journey suite 116/116.
Docs: groups-and-project-tool §11.6 (KV+docs slices shipped; files/topics/queue
deferred), sdk-shape.md (docs::shared_collection + string-or-table), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review findings:
- F2 (defense-in-depth): validate_bundle_for now rejects `collections` on an
app node — the inverse of the existing group route/trigger guard. The CLI
already prevents it (ManifestApp has no field + deny_unknown_fields), but a
hand-rolled wire Bundle POSTed to /apps/{id}/apply would otherwise insert an
inert app_id-owned group_collections row that no resolver reads.
- F1 (coverage): a journey for nearest-ancestor-group-wins, the CoW-shadowing
guarantee the resolver's `ORDER BY depth LIMIT 1` provides. A parent and a
child group both declare `catalog`; an app under the child writes, and a
second app directly under the parent reads its (separate, empty) store —
proving the deep write stayed in the nearest (child) store. Previously only
the FakeResolver and flat sibling groups were exercised, so the ordering was
untested.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finish the §11.6 KV slice: declarative authoring, read-only inspection, the
runtime journey, and docs — plus a forced SDK-name fix.
- SDK name: `shared` is a Rhai RESERVED keyword, so `kv::shared(...)` is a parse
error. Renamed the handle constructor to `kv::shared_collection(...)`
(keeps the user's "shared" intent; unambiguous). Updated everywhere.
- CLI manifest: `collections = [...]` on `[group]` only (ManifestApp has no such
field + deny_unknown_fields → an app manifest carrying it is a hard error);
Manifest::collections() accessor; build_bundle emits it.
- plan/apply: a `collection` row group in `pic plan`; created/deleted counts in
the apply summary; collections threaded through PlanDto/NodePlanDto/
ApplyReportDto.
- read-only `pic collections ls --group` → GET /admin/groups/{id}/collections
(viewer+), backed by ApplyService::collection_report + CollectionInfo.
- journey: a group declares `catalog`; app A writes, app B (same subtree) reads
it back; a sibling-subtree app gets CollectionNotShared; `collections ls` +
re-apply NoOp. Full suite 114/114.
- docs: groups-and-project-tool §11.6 (KV-only MVP shipped + deferrals),
sdk-shape.md (the shared-collection handle + trust model), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>