Makes §7 ownership usable end to end from the CLI.
- manifest: a `[project]` block (slug + optional name), independent of the
[app]/[group] XOR; threaded onto every apply from the repo. --dir reads it
from the tree ROOT's picloud.toml (a [project] elsewhere is ignored with a
note).
- client: apply_node/apply_tree carry project + takeover; the 409 branch now
surfaces the server message verbatim (covers StateMoved AND OwnershipConflict,
both self-contained). New groups_list_with_owner captures the owner slug.
- pic apply --takeover flag; pic groups ls gains an `owner` column (the owning
project's slug, or — when unclaimed).
- server: /api/v1/admin/groups now returns each group flattened with its owner
slug (via list_with_owner) — the shared Group deserialize ignores the extra
field, so pic groups tree / the dashboard are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread `sealed` from the manifest through to the DB column:
- manifest: `sealed` on ManifestRoute + the four event-kind trigger specs
(Kv/Docs/Files/Pubsub), flowing to Bundle via serde.
- persist: NewRoute.sealed + insert_route_tx; insert_trigger_tx `sealed`
param; the reconcile passes br.sealed / bt.sealed().
- validate_bundle_for rejects `sealed` on an app owner (meaningless — an
app route/trigger is never inherited).
- diff: `sealed` joins the route Update comparison and the trigger identity
(both bundle + current sides), so toggling it re-applies rather than NoOp.
`sealed` lives on shared Route + manager-core Trigger (both pure DTOs) so
the diff can see the current value; RouteRow/TriggerRow default it so
RETURNING clauses that omit it still hydrate. No runtime read effect yet —
the two suppression gates land in M3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The declarative half — a `[suppress]` block persists as markers; no runtime
effect yet (S3 consumes them).
- manifest: `[suppress]` table on an app with `triggers = [...]` (handler
script names) + `routes = [...]` (paths), `deny_unknown_fields`; rejected
on a `[group]` (a group just wouldn't declare the template).
- suppression_repo.rs: app-keyed `list_for_app` / `insert` / `delete_tx`
over `(app_id, target_kind, reference)`, mirroring extension_point_repo
minus the owner polymorphism.
- apply_service: the extension-point marker-reconcile pattern —
`Bundle.suppress_triggers/_routes`, `Plan.suppressions`,
`CurrentState.suppressions`, `load_current(App)` load, `diff_suppressions`
(key `"{kind}:{reference}"`, split on the first `:` so a route param path
survives), create + prune reconcile blocks, `validate_bundle_for` group
reject, `ApplyReport` counters.
- CLI: `build_bundle` carries the two vecs; `pic plan` + apply report gain a
suppressions row (DTOs + display).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the manifest `extension_points` from a top-level key to an `[app]`/
`[group]` node key (`ManifestApp`/`ManifestGroup` + a `Manifest::extension_points()`
accessor). A bare top-level array placed after the node header was silently
re-nested by TOML into that table and dropped; as a node key it parses
unambiguously wherever it sits. build_bundle/pull/init updated.
- Journeys (live server + Postgres): a group default body resolves for an
inheriting app; the app provides its own module and OVERRIDES the EP (the
inverse of the Phase 4b sealed import); `extension-points ls` shows the
provider; a body-less EP with no provider fails `pic plan`.
- Re-bless the schema snapshot (new `extension_points` table).
- Docs: §5.5 marked complete (extension points ✅) + CLAUDE.md current-focus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread extension-point markers through the manifest and the apply engine —
a name-only resource modeled on `secrets` (shape) with `vars`-style
create/prune write behavior.
- manifest: top-level `extension_points = ["theme", …]` (allowed on app and
group; overlay stays base-only via deny_unknown_fields); `build_bundle`
emits the names.
- apply_service: `Bundle.extension_points`, `CurrentState.extension_point_names`,
`Plan.extension_points` (+ is_noop), `diff_extension_points`
(declared→Create / live-undeclared→Delete / else NoOp), `load_current`
loads them, `reconcile_node_tx` inserts on Create + deletes on prune,
`state_token_with_names` folds in `ep|<name>`, `validate_bundle` rejects
duplicates + reserved names, `ApplyReport` gains created/deleted counts.
- CLI client + plan/apply rendering: `extension_points` in PlanDto/NodePlanDto/
ApplyReportDto, an `extension_point` row group in `pic plan`, a count in the
apply summary.
pull sets it empty for now (wired to the read endpoint in C4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5 C2. A `picloud.toml` can now declare a `[group]` node (instead of
`[app]`): the group's own scripts + `[vars]` (+ secret names). `pic plan` /
`pic apply` reconcile it via the C1 group endpoints — the same plan/apply/
bound-token flow as an app, routed by node kind.
* `Manifest` is now app-XOR-group: `app`/`group` are both optional with an
exactly-one check in `parse`, plus a group-node guard that rejects
`[[routes]]`/`[[triggers]]` (those bind to an app). Accessors `slug()` /
`is_group()` replace the hardcoded `manifest.app.slug`.
* `client`: `plan_node`/`apply_node` take a `NodeKind { App | Group }` that
selects the `apps` vs `groups` API path; the old `plan`/`apply` wrappers
are gone (callers pass the kind).
* `pic plan`/`apply` detect the node kind from the manifest and label output
`group`/`app`; `pic config --effective` cleanly rejects a group manifest
(effective config is an app's inherited view).
Live-validated: a `[group]` manifest with a script + var → `pic plan` (script
+ var creates) → `pic apply` (group-owned) → idempotent re-plan noop →
`pic scripts ls --group` shows it. Manifest unit test for group parse +
app-only-block rejection; init/pull/plan/apply/overlay journeys all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `[vars]` block to `picloud.toml` (key → TOML value), reconciled by
`pic plan`/`pic apply` alongside scripts/routes/triggers/secrets. Unlike
`[secrets]` (name-only), var values live inline since they aren't secret.
The overlay (`picloud.<env>.toml`) may carry `[vars]`; overlay keys win
per key (the env-specific value of an env-agnostic default).
`build_bundle` serializes the vars to JSON for the wire (TOML round-trips
via serde); `pic plan` shows a `var` row group and `pic apply` reports
`vars +c ~u -d`. The `PlanDto`/`ApplyReportDto` gain the matching fields.
`pic pull` carries an empty `[vars]` for now (export lands next). Adds a
round-trip + overlay-precedence unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase-1 `enabled` three-state lifecycle (§4.3), data-model half. Triggers
already carried `enabled`; this adds it to scripts and routes and threads
it through the declarative project tool. Runtime honoring (disabled route
404 / script non-invocable / dispatcher fire-time re-check) is the next
commit — this change only stores and reconciles the flag.
- Migration 0045: `enabled BOOLEAN NOT NULL DEFAULT TRUE` on scripts +
routes. Default true ⇒ no behavior change on migrate.
- `Script`/`Route` (shared) gain `enabled` (serde default true via the new
`picloud_shared::default_true`); repos' SELECT/INSERT/UPDATE SQL, row
structs, `NewScript`/`NewRoute`/`ScriptPatch` all carry it.
- Apply diff treats `enabled` as a declarative field (omitted ⇒ active):
`script_update_reason` + `diff_routes` detect a toggle as an Update, and
the create/update/insert paths persist it. The bound-plan `state_token`
re-includes script/route `enabled` (removed in the earlier review fix
precisely because the diff didn't key on it yet — now it does).
- CLI manifest model + `build_bundle` + `pull` round-trip `enabled`
(serialized only when false; omitted ⇒ active). `pic init` scaffold and
the interactive script/route create paths default active.
Tested: manager-core lib 365 (incl. enabled diff + token sensitivity) +
cli bins 31 (incl. manifest skip-serialize) green; clippy -D warnings
clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remediation after an independent review of the two feature commits.
pic init:
- Fix the headline `pic init --dir <new-dir>` flow: slug derivation used
`canonicalize()`, which fails on a not-yet-created directory → empty
slug → bail. Derive from the path's own final component first, falling
back to canonicalize only for `.`. Add a test that exercised the gap
(the existing tests all passed an explicit slug).
- `ensure_gitignored` no longer overwrites an existing-but-unreadable
`.gitignore` (only a genuinely-absent file is treated as empty).
Bound-plan staleness:
- Token trigger coverage now mirrors the diff exactly via
`current_trigger_identity`: dead-letter triggers (which the diff
ignores and the manifest can't represent) and the currently-inert
`enabled` flag are no longer hashed, so an out-of-band dead-letter
change can't force a spurious `--force`. Add a test.
- Correct two overclaiming comments: the check shares the diff's
pool-read apply-vs-interactive-write window (it is not tx-scoped), and
the token deliberately mirrors the diff's inputs.
- `.picloud/` self-ignores via a `*` `.gitignore` written alongside the
plan token, so projects created with `pic pull`/`plan` (not just
`init`) keep it out of git.
- `clear_plan` is now app-scoped: it won't discard a token recorded for a
different app sharing the directory.
Tested: manager-core lib 364 + cli bins 31 + 14 project-tool journeys
green; clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Offline project scaffolding — the on-ramp to pull/plan/apply. Writes a
`picloud.toml` describing a minimal *working* app (a `hello` endpoint
bound to GET /hello) plus commented examples for the other blocks,
`scripts/hello.rhai`, and a `.gitignore` that pre-ignores the project
tool's `.picloud/` link-state directory (the home for the bound-plan
state token, landing next). Refuses to overwrite an existing manifest
without `--force`; derives the app slug from the directory name when not
given (validated against the canonical slug rule).
The active manifest is rendered through the same `to_toml` model the rest
of the tool uses, so a freshly-init'd project is guaranteed valid and
immediately `pic plan`-able. Offline — no server contact, so the journey
tests need no DATABASE_URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>