@rizom/ops 0.2.0-alpha.242 → 0.2.0-alpha.244

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.
@@ -0,0 +1,11 @@
1
+ import { type CohortConfig, type PilotConfig } from "./schema";
2
+ /** Offline-only conversion. Active desired-state loading never calls this. */
3
+ export declare function migrateLegacyPilotConfig(input: unknown): PilotConfig;
4
+ /** Offline-only cohort conversion. */
5
+ export declare function migrateLegacyCohortConfig(input: unknown): CohortConfig;
6
+ /** Offline-only YAML rewrite that retains comments while changing the schema. */
7
+ export declare function migrateLegacyPilotYaml(input: string): string;
8
+ /** Offline-only cohort rewrite that retains comments while changing selection. */
9
+ export declare function migrateLegacyCohortYaml(input: string): string;
10
+ export declare function renderPilotConfig(config: PilotConfig): string;
11
+ export declare function renderCohortConfig(config: CohortConfig): string;
@@ -1,4 +1,4 @@
1
- import { type PilotConfig, type PilotPreset } from "./schema";
1
+ import { type PilotConfig, type CanonicalBundleId } from "./schema";
2
2
  export type ExternalStatus = "unknown" | "ready" | "failed";
3
3
  export type SnapshotStatus = "present" | "missing";
4
4
  export interface ObservedUserStatus {
@@ -11,7 +11,9 @@ export interface ResolvedCohort {
11
11
  id: string;
12
12
  members: string[];
13
13
  brainVersionOverride?: string;
14
- presetOverride?: PilotPreset;
14
+ bundlesOverride?: CanonicalBundleId[];
15
+ addOverride?: string[];
16
+ removeOverride?: string[];
15
17
  aiApiKeyOverride?: string;
16
18
  gitSyncTokenOverride?: string;
17
19
  }
@@ -74,13 +76,13 @@ export interface ResolvedUserIdentity {
74
76
  handle: string;
75
77
  cohort: string;
76
78
  brainVersion: string;
77
- model: "rover";
78
- preset: PilotPreset;
79
+ bundles: CanonicalBundleId[];
80
+ add: string[];
81
+ remove: string[];
79
82
  domain: string;
80
83
  cloudflareZoneId?: string | undefined;
81
84
  contentRepo: string;
82
85
  profileKind?: string | undefined;
83
- addOverride?: string[];
84
86
  siteOverride?: ResolvedSiteOverride;
85
87
  discordEnabled: boolean;
86
88
  discordAnchorUserId?: string;
@@ -0,0 +1,9 @@
1
+ export interface ReconcileDryRunResult {
2
+ firstPassChangedFiles: string[];
3
+ secondPassChangedFiles: string[];
4
+ }
5
+ /**
6
+ * Reconcile an isolated copy twice with external content-repo access disabled.
7
+ * The source repository is never written.
8
+ */
9
+ export declare function dryRunReconcileAll(rootDir: string): Promise<ReconcileDryRunResult>;
package/dist/schema.d.ts CHANGED
@@ -1,26 +1,46 @@
1
1
  import { z } from "@brains/utils/zod";
2
- export declare const presetSchema: z.ZodEnum<{
3
- core: "core";
4
- default: "default";
5
- pro: "pro";
6
- }>;
7
2
  export declare const exactVersionSchema: z.ZodString;
8
3
  export declare const handleSchema: z.ZodString;
9
4
  export declare const secretNameSchema: z.ZodString;
10
5
  export declare const agePublicKeySchema: z.ZodString;
11
- export declare const pilotSchema: z.ZodObject<{
12
- schemaVersion: z.ZodLiteral<1>;
13
- brainVersion: z.ZodString;
14
- model: z.ZodLiteral<"rover">;
15
- githubOrg: z.ZodString;
16
- contentRepoPrefix: z.ZodString;
17
- domainSuffix: z.ZodString;
18
- preset: typeof presetSchema;
19
- aiApiKey: z.ZodString;
20
- gitSyncToken: z.ZodString;
21
- contentRepoAdminToken: z.ZodString;
22
- agePublicKey: z.ZodString;
6
+ export declare const canonicalBundleIdSchema: z.ZodEnum<{
7
+ core: "core";
8
+ site: "site";
9
+ publishing: "publishing";
10
+ team: "team";
23
11
  }>;
12
+ export type CanonicalBundleId = z.output<typeof canonicalBundleIdSchema>;
13
+ export interface PilotConfig {
14
+ brainVersion: string;
15
+ bundles: CanonicalBundleId[];
16
+ add?: string[] | undefined;
17
+ remove?: string[] | undefined;
18
+ githubOrg: string;
19
+ contentRepoPrefix: string;
20
+ domainSuffix: string;
21
+ aiApiKey: string;
22
+ gitSyncToken: string;
23
+ contentRepoAdminToken: string;
24
+ agePublicKey: string;
25
+ }
26
+ export interface SiteOverrideConfig {
27
+ package: string;
28
+ version: string;
29
+ theme?: string | undefined;
30
+ themeVersion?: string | undefined;
31
+ }
32
+ export interface CohortConfig {
33
+ members: string[];
34
+ brainVersionOverride?: string | undefined;
35
+ bundlesOverride?: CanonicalBundleId[] | undefined;
36
+ addOverride?: string[] | undefined;
37
+ removeOverride?: string[] | undefined;
38
+ aiApiKeyOverride?: string | undefined;
39
+ gitSyncTokenOverride?: string | undefined;
40
+ }
41
+ /** Sole active desired-state contract for the canonical brain. */
42
+ export declare const pilotSchema: z.ZodType<PilotConfig>;
43
+ export declare const cohortSchema: z.ZodType<CohortConfig>;
24
44
  declare const anchorProfileSocialLinkSchema: z.ZodObject<{
25
45
  platform: z.ZodEnum<{
26
46
  github: "github";
@@ -70,12 +90,7 @@ declare const atprotoSchema: z.ZodObject<{
70
90
  lexiconAuthority: z.ZodOptional<z.ZodBoolean>;
71
91
  jetstream: z.ZodOptional<typeof atprotoJetstreamSchema>;
72
92
  }>;
73
- declare const siteOverrideSchema: z.ZodObject<{
74
- package: z.ZodString;
75
- version: z.ZodOptional<typeof exactVersionSchema>;
76
- theme: z.ZodOptional<z.ZodString>;
77
- themeVersion: z.ZodOptional<typeof exactVersionSchema>;
78
- }>;
93
+ export declare const siteOverrideSchema: z.ZodType<SiteOverrideConfig>;
79
94
  declare const playbooksSchema: z.ZodObject<{
80
95
  onboarding: z.ZodOptional<z.ZodBoolean>;
81
96
  }>;
@@ -92,24 +107,15 @@ export declare const userSchema: z.ZodObject<{
92
107
  contentRepoOverride: z.ZodOptional<z.ZodString>;
93
108
  profileKind: z.ZodOptional<z.ZodString>;
94
109
  addOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
110
+ removeOverride: z.ZodOptional<z.ZodArray<z.ZodString>>;
95
111
  siteOverride: z.ZodOptional<typeof siteOverrideSchema>;
96
112
  setup: z.ZodOptional<typeof setupDeliverySchema>;
97
113
  atproto: z.ZodOptional<typeof atprotoSchema>;
98
114
  playbooks: z.ZodOptional<typeof playbooksSchema>;
99
115
  anchorProfile: z.ZodOptional<typeof anchorProfileSchema>;
100
116
  }>;
101
- export declare const cohortSchema: z.ZodObject<{
102
- members: z.ZodArray<z.ZodString>;
103
- brainVersionOverride: z.ZodOptional<z.ZodString>;
104
- presetOverride: z.ZodOptional<typeof presetSchema>;
105
- aiApiKeyOverride: z.ZodOptional<z.ZodString>;
106
- gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
107
- }>;
108
- export type PilotConfig = z.output<typeof pilotSchema>;
109
117
  export type PilotConfigInput = z.input<typeof pilotSchema>;
110
118
  export type UserConfig = z.output<typeof userSchema>;
111
119
  export type UserConfigInput = z.input<typeof userSchema>;
112
- export type CohortConfig = z.output<typeof cohortSchema>;
113
120
  export type CohortConfigInput = z.input<typeof cohortSchema>;
114
- export type PilotPreset = z.output<typeof presetSchema>;
115
121
  export {};
@@ -2,6 +2,8 @@ import { z } from "@brains/utils/zod";
2
2
  declare const encryptedUserSecretsSchema: z.ZodObject<{
3
3
  gitSyncToken: z.ZodOptional<z.ZodString>;
4
4
  discordBotToken: z.ZodOptional<z.ZodString>;
5
+ discordPublicKey: z.ZodOptional<z.ZodString>;
6
+ discordApplicationId: z.ZodOptional<z.ZodString>;
5
7
  aiApiKey: z.ZodOptional<z.ZodString>;
6
8
  atprotoAppPassword: z.ZodOptional<z.ZodString>;
7
9
  certificatePem: z.ZodOptional<z.ZodString>;
@@ -0,0 +1,20 @@
1
+ import { type SiteOverrideConfig } from "./schema";
2
+ export interface StagedCrossover {
3
+ outputDir: string;
4
+ changedFiles: string[];
5
+ }
6
+ interface ReviewedSitePinManifest {
7
+ sites: Record<string, SiteOverrideConfig>;
8
+ }
9
+ export type ReviewedSitePins = ReviewedSitePinManifest["sites"];
10
+ export interface StageLegacyCrossoverOptions {
11
+ sitePins?: ReviewedSitePins | undefined;
12
+ }
13
+ export declare function parseReviewedSitePins(input: string): ReviewedSitePins;
14
+ export declare function loadReviewedSitePins(filePath: string): Promise<ReviewedSitePins>;
15
+ /**
16
+ * Build a complete review copy without mutating the source repository.
17
+ * This is offline migration tooling; active desired-state loading is canonical-only.
18
+ */
19
+ export declare function stageLegacyCrossover(sourceDir: string, outputDir: string, options?: StageLegacyCrossoverOptions): Promise<StagedCrossover>;
20
+ export {};
@@ -10,7 +10,7 @@ export interface FailedCheck {
10
10
  }
11
11
  export interface VerifyPilotUserResult {
12
12
  handle: string;
13
- preset: ResolvedUser["preset"];
13
+ bundles: ResolvedUser["bundles"];
14
14
  domain: string;
15
15
  contentRepo: string;
16
16
  checks: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/ops",
3
- "version": "0.2.0-alpha.242",
3
+ "version": "0.2.0-alpha.244",
4
4
  "description": "Operator CLI for managing private brain fleet registry repos",
5
5
  "keywords": [
6
6
  "brains",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "bugs": "https://github.com/rizom-ai/brains/issues",
19
19
  "dependencies": {
20
- "age-encryption": "^0.3.0"
20
+ "age-encryption": "^0.3.0",
21
+ "yaml": "^2.9.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@brains/deploy-support": "workspace:*",
@@ -25,6 +25,13 @@ CONTENT_REPO_ADMIN_TOKEN=
25
25
  # @sensitive
26
26
  DISCORD_BOT_TOKEN=
27
27
 
28
+ # Stored with the per-user Discord credentials and injected at deploy time.
29
+ # @sensitive
30
+ DISCORD_PUBLIC_KEY=
31
+
32
+ # @sensitive
33
+ DISCORD_APPLICATION_ID=
34
+
28
35
  # AT Protocol publishing/discovery (optional, per-user)
29
36
  # Comes from the decrypted users/<handle>.secrets.yaml.age file when configured.
30
37
  # @sensitive
@@ -1,6 +1,6 @@
1
- # rover-pilot
1
+ # private brain pilot
2
2
 
3
- Private desired-state repo for the rover pilot.
3
+ Private desired-state repository for the hosted brain pilot. The generated repository/service name may remain `rover-pilot` for deployment identity compatibility.
4
4
 
5
5
  This is a single operator-owned repo. Pilot users do not get their own brain repos.
6
6
  Per-user deploy config lives under `users/<handle>/`, while content stays in per-user content repos.
@@ -31,7 +31,7 @@ The repo also checks in its deploy contract:
31
31
  `.env.schema` is the single source of truth for required and sensitive deploy vars.
32
32
  Use separate GitHub tokens: `CONTENT_REPO_ADMIN_TOKEN` for operator-side content repo creation/checks, and `GIT_SYNC_TOKEN` for runtime directory-sync git access.
33
33
  The default pilot image tag is `brain-${brainVersion}` end to end. A user with `siteOverride` gets an isolated `brain-${brainVersion}-sites-${packageHash}` image instead.
34
- When the effective brain version (`pilot.yaml.brainVersion`, or a cohort override) changes and you push, CI rebuilds the required default/site tags, refreshes generated user env files, and redeploys affected users. An omitted `siteOverride.version` follows that effective version; an explicit exact version remains pinned.
34
+ When the effective brain version (`pilot.yaml.brainVersion`, or a cohort override) changes and you push, CI rebuilds the required default/site tags, refreshes generated user env files, and redeploys affected users. Every external site and theme package uses its own required exact version pin; ops never infers either version from the brain or from another package.
35
35
  When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
36
36
 
37
37
  ## Commands
@@ -46,3 +46,6 @@ When a push changes only deploy contract files, CI prints `No affected user conf
46
46
  - `brains-ops secrets:encrypt <repo> <handle>`
47
47
  - `brains-ops reconcile-cohort <repo> <cohort>`
48
48
  - `brains-ops reconcile-all <repo>`
49
+ - `brains-ops reconcile-all <repo> --dry-run` (isolated, no external content-repository access)
50
+
51
+ Use `docs/canonical-crossover-record.md` to record exact forward and rollback artifact pins before an approved crossover window.
@@ -25,6 +25,8 @@ writeSecretGitHubEnv(
25
25
  secrets["cmsContentRepoPat"] ?? secrets["gitSyncToken"],
26
26
  );
27
27
  writeSecretGitHubEnv("DISCORD_BOT_TOKEN", secrets["discordBotToken"]);
28
+ writeSecretGitHubEnv("DISCORD_PUBLIC_KEY", secrets["discordPublicKey"]);
29
+ writeSecretGitHubEnv("DISCORD_APPLICATION_ID", secrets["discordApplicationId"]);
28
30
  // Per-user AT Protocol publishing credential (optional; from the user's
29
31
  // encrypted secrets file). The scaffold wires this so a pilot can publish its
30
32
  // brain's agent card to its PDS; a deployment that doesn't publish simply
@@ -14,6 +14,17 @@ for (const key of requiredKeys) {
14
14
  }
15
15
  }
16
16
 
17
+ const discordKeys = [
18
+ "DISCORD_BOT_TOKEN",
19
+ "DISCORD_PUBLIC_KEY",
20
+ "DISCORD_APPLICATION_ID",
21
+ ];
22
+ if (discordKeys.some((key) => process.env[key])) {
23
+ for (const key of discordKeys) {
24
+ if (!process.env[key]) missing.push(key);
25
+ }
26
+ }
27
+
17
28
  if (missing.length > 0) {
18
29
  throw new Error(`Missing required secrets: ${missing.join(", ")}`);
19
30
  }
@@ -0,0 +1,103 @@
1
+ # Canonical Crossover Approval Record
2
+
3
+ > Draft evidence record only. Completing this file does not authorize a merge, publish, reconcile, or deployment.
4
+
5
+ Do not include secret values, private keys, access tokens, or decrypted user configuration.
6
+
7
+ ## Authorization and freeze
8
+
9
+ - Operator:
10
+ - Approval reference:
11
+ - Approved window:
12
+ - Freeze start:
13
+ - Build workflow disabled and idle:
14
+ - Reconcile workflow disabled and idle:
15
+ - Deploy workflow disabled and idle:
16
+
17
+ ## Reviewed inputs
18
+
19
+ - `brains` crossover commit:
20
+ - Private-pilot source commit used for staging:
21
+ - Canonical review commit:
22
+ - Secret-free review diff SHA-256:
23
+ - Identity-review evidence SHA-256:
24
+ - Reviewed hosted-site pin manifest SHA-256:
25
+ - Source and review worktrees clean:
26
+
27
+ ## Hosted site and theme pins
28
+
29
+ List every hosted site. External site and theme package versions must be exact and must
30
+ match the staged user desired state; do not infer them from the brain or from each other.
31
+
32
+ | Handle | Site package | Exact site version | Theme package | Exact theme version | Package/image evidence |
33
+ | ------ | ------------ | ------------------ | ------------- | ------------------- | ---------------------- |
34
+ | | | | | | |
35
+
36
+ ## Forward artifact pins
37
+
38
+ | Artifact | Exact version | Registry integrity or digest | Verified installable |
39
+ | ---------------------- | ------------- | ---------------------------- | -------------------- |
40
+ | `@rizom/brain` | | | |
41
+ | `@rizom/ops` | | | |
42
+ | private-pilot lockfile | n/a | | |
43
+
44
+ Record every image. Tags alone are not immutable evidence.
45
+
46
+ | Cohort/handle | Config commit | Image repository | Image tag | Image digest |
47
+ | ------------- | ------------- | ---------------- | --------- | ------------ |
48
+ | | | | | |
49
+
50
+ ## Rollback pair
51
+
52
+ - Prior private-pilot commit:
53
+ - Prior `@rizom/ops` version and registry integrity:
54
+
55
+ | Cohort/handle | Prior config commit | Prior image repository | Prior image tag | Prior image digest |
56
+ | ------------- | ------------------- | ---------------------- | --------------- | ------------------ |
57
+ | | | | | |
58
+
59
+ ## Identity review
60
+
61
+ - Repository and image names unchanged or explicitly approved:
62
+ - GitHub organization and content repository identities unchanged:
63
+ - Server, domain, Cloudflare zone, and ATProto identities unchanged:
64
+ - Secret selector names and encrypted secret artifacts unchanged:
65
+ - Per-user runtime-version and site-package tag inputs reviewed:
66
+ - No plaintext source secrets copied into the review artifact:
67
+ - No removed model, preset, or old-format schema discriminator remains:
68
+ - Every external hosted site and theme package has an exact reviewed pin:
69
+
70
+ ## Offline convergence
71
+
72
+ Command:
73
+
74
+ ```sh
75
+ bunx brains-ops reconcile-all <canonical-review-copy> --dry-run
76
+ ```
77
+
78
+ - First-pass changed files:
79
+ - Second-pass changed files (must be zero):
80
+ - Review copy unchanged:
81
+ - External content-repository access blocked:
82
+
83
+ ## Pre-window validation
84
+
85
+ - Package tests, typecheck, lint, build, and packed-consumer startup:
86
+ - Architecture and dependency boundaries:
87
+ - Environment-schema and workspace checks:
88
+ - Crossover migration and comment preservation:
89
+ - Canary-first order and health checks reviewed:
90
+ - Paired rollback reviewed:
91
+
92
+ ## Execution record
93
+
94
+ Complete only inside the explicitly approved maintenance window.
95
+
96
+ - Unified packages published and verified:
97
+ - Canonical desired state committed while automation remained frozen:
98
+ - Image digest set matched this record:
99
+ - First reconcile reviewed:
100
+ - Per-instance health, MCP authorization, identity, content, and site checks:
101
+ - Second reconcile produced zero drift and no deploy work:
102
+ - Freeze lifted:
103
+ - One-week soak start:
@@ -23,18 +23,14 @@
23
23
  10. Run `bunx brains-ops secrets:encrypt <repo> <handle>`.
24
24
  11. Commit and push `users/<handle>.secrets.yaml.age`.
25
25
  12. Run `bunx brains-ops onboard <repo> <handle>`.
26
- 13. Verify the deployed Rover contract:
27
- - all presets:
28
- - `https://<handle>.rizom.ai/health` returns `200`
29
- - `https://<handle>.rizom.ai/chat` loads the web chat and accepts passkey sign-in
30
- - `https://<handle>.rizom.ai/` loads the dashboard (or site surface on `default` preset)
31
- - `https://<handle>.rizom.ai/cms` loads the CMS/login surface
32
- - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns the expected auth failure
33
- - content repo exists and runtime sync is healthy
34
- - background jobs are not repeatedly failing, except for expected missing optional integrations
35
- - for `presetOverride: default` users:
36
- - initial site build completes
37
- 14. For fleet upgrades, edit `pilot.yaml.brainVersion` and push once; CI rebuilds the required default/site image tags, refreshes generated user env files, and redeploys affected users. Site overrides follow the user's effective brain version unless they declare an explicit exact `version` pin.
26
+ 13. Verify the deployed canonical contract:
27
+ - `https://<handle>.rizom.ai/health` returns `200`
28
+ - `https://<handle>.rizom.ai/chat` loads the web chat and accepts passkey sign-in
29
+ - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns the expected auth failure
30
+ - content repo exists and runtime sync is healthy
31
+ - background jobs are not repeatedly failing, except for expected missing optional integrations
32
+ - when `site` is selected, the browser/CMS surfaces load and the initial app-managed site build completes
33
+ 14. For fleet upgrades, edit `pilot.yaml.brainVersion` and push once; CI rebuilds the required default/site image tags, refreshes generated user env files, and redeploys affected users. Every external site and theme package keeps its own required exact version pin and never follows the brain version implicitly.
38
34
  15. Confirm the user received the setup email, registered their passkey, and can sign in to web chat at `https://<handle>.rizom.ai/chat`. That completes the default onboarding; everything below is per-cohort extras.
39
35
  16. Hand over the browser surfaces:
40
36
  - Chat (primary): `https://<handle>.rizom.ai/chat`
@@ -30,14 +30,42 @@ When `pilot.yaml.brainVersion` changes and you push:
30
30
  3. deploy runs for handles whose generated config changed
31
31
  4. generated file commits happen once in a final aggregation step after the deploy matrix finishes
32
32
 
33
- An omitted `siteOverride.version` follows the user's effective brain version, so a
34
- cohort or pilot version bump advances its site and theme packages automatically.
35
- Set an exact `siteOverride.version` only when that user needs a deliberate pin.
33
+ Every external site and theme package has its own exact version pin. A cohort or
34
+ pilot brain-version bump never changes those package versions implicitly; update each
35
+ pin deliberately from reviewed package and image evidence.
36
36
 
37
37
  When a push changes only deploy contract files and no generated `users/<handle>/.env` or `users/<handle>/brain.yaml` files, the deploy workflow exits through its explicit no-op path and prints `No affected user configs; skipping deploy.`
38
38
 
39
39
  They are scaffolded from `@rizom/ops`, then versioned in this repo like any other deploy contract.
40
40
 
41
+ ## Canonical contract crossover maintenance window
42
+
43
+ Do not run this procedure without explicit operator approval. The canonical desired state, canonical `@rizom/ops`, and unified runtime image form one contract and must move or roll back together. Complete `docs/canonical-crossover-record.md` as the approval evidence without adding secret values.
44
+
45
+ Before the window, record and review:
46
+
47
+ - the prior pilot commit and exact `@rizom/ops` version;
48
+ - every prior runtime image tag and immutable digest;
49
+ - the reviewed canonical pilot commit;
50
+ - the exact unified `@rizom/brain` and `@rizom/ops` versions;
51
+ - every unified image tag and immutable digest;
52
+ - canary-first rollout order, followed by the remaining cohorts;
53
+ - expected `/health` version, unauthenticated MCP response, site marker, and content repository identity for each posture.
54
+
55
+ Run `bunx brains-ops reconcile-all <canonical-review-copy> --dry-run` against the isolated review copy. The command blocks external content-repository access, leaves the review copy untouched, and must report second-pass zero drift.
56
+
57
+ During the approved window:
58
+
59
+ 1. Freeze unrelated merges and releases. Wait for active Build, Reconcile, and Deploy runs to finish, then disable all three pilot workflows with `gh workflow disable build.yml`, `gh workflow disable reconcile.yml`, and `gh workflow disable deploy.yml`.
60
+ 2. Publish and verify the reviewed unified runtime and matching ops artifacts. Do not update pilot desired state until the exact versions are installable and the expected images can be built.
61
+ 3. Apply the reviewed canonical pilot revision while automation remains disabled. Confirm repository names, server/domain identity, content repositories, secret selectors, image names, and tag identity against the review diff.
62
+ 4. Enable only Build, run it for the canonical desired-state revision, and record every resulting image digest. Stop if the observed digest set differs from the cutover record.
63
+ 5. Enable only Reconcile, run it once, and review its generated config commit. No generated file may combine canonical config with a retired image version.
64
+ 6. Enable Deploy and deploy one handle at a time in the approved order. After each deploy, run `bunx brains-ops verify-user . <handle>` and complete the manual identity, content-sync, and app-managed site checks.
65
+ 7. Run Reconcile a second time. Require no generated diff and no deploy work before re-enabling normal automation and lifting the merge/release freeze.
66
+
67
+ If any gate fails, disable all three workflows again. Restore the prior pilot desired-state and dependency revision, reconcile with the prior ops version, and redeploy the prior image tag/digest as one rollback pair. Verify the prior `/health` version and identity/content/site checks before re-enabling automation. Never restore only config or only an image.
68
+
41
69
  ## Stale deploy lock recovery
42
70
 
43
71
  Kamal intentionally leaves its remote deploy lock in place when a deployment is cancelled or interrupted. Confirm that no deployment for the user is still active before releasing the lock, then use the deploy workflow's explicit recovery input:
@@ -77,7 +105,7 @@ When `@rizom/ops` changes the scaffolded deploy contract:
77
105
  3. review the resulting changes to `.env.schema`, `deploy/scripts/`, and workflows in git
78
106
  4. commit the updated deploy artifacts together
79
107
 
80
- ## Rover verification notes
108
+ ## Canonical verification notes
81
109
 
82
110
  Use the verification script after deploy:
83
111
 
@@ -85,35 +113,31 @@ Use the verification script after deploy:
85
113
  bunx brains-ops verify-user . <handle>
86
114
  ```
87
115
 
88
- It checks every Rover preset:
116
+ For every bundle posture it checks:
89
117
 
90
- - `https://<handle>.rizom.ai/health` should return `200`
91
- - unauthenticated `POST https://<handle>.rizom.ai/mcp` should return the expected auth failure
92
- - background jobs should not be repeatedly failing, except for expected missing optional integrations
118
+ - `https://<handle>.rizom.ai/health` returns `200`;
119
+ - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns the expected auth failure;
120
+ - background jobs are not repeatedly failing, except for missing optional integrations.
93
121
 
94
- Additional `rover:core` note:
95
-
96
- - Rover core is MCP-only; a bare `GET /` may return `401`, which does not indicate a bad deploy.
97
-
98
- For `preset: default`, the script also checks:
99
-
100
- - `https://<handle>.rizom.ai/` loads the browser/site surface
101
- - `https://<handle>.rizom.ai/cms` loads the CMS/login surface
122
+ A `core`-only instance is MCP-only; a bare `GET /` may return `401` without indicating a bad deploy. When `site` is selected, verification also checks the browser and CMS/login surfaces.
102
123
 
103
124
  Manual checks that remain:
104
125
 
105
- - initial site build is correct for the expected content/theme
106
- - content repo exists and runtime sync is healthy beyond the basic `/health` response
107
- - passkey setup/handoff is completed from the setup email
126
+ - initial app-managed site output is correct for the expected content/theme;
127
+ - content repository identity and runtime sync are healthy;
128
+ - passkey setup/handoff is completed from the setup email.
108
129
 
109
- ## One-user `rover:default` baseline canary
130
+ ## One-user canonical site canary
110
131
 
111
132
  Run this before adding custom site/theme packages or rolling a larger browser/CMS-first cohort.
112
133
 
113
- 1. Create or choose a canary cohort with the default preset:
134
+ 1. Create or choose a canary cohort with explicit bundles:
114
135
 
115
136
  ```yaml
116
- presetOverride: default
137
+ bundles:
138
+ - core
139
+ - site
140
+ - publishing
117
141
  ```
118
142
 
119
143
  2. Add exactly one canary user to that cohort.
@@ -131,11 +155,7 @@ Run this before adding custom site/theme packages or rolling a larger browser/CM
131
155
  7. Ask the user to complete passkey setup from the setup email.
132
156
  8. Continue to visual customization only after the canary is healthy.
133
157
 
134
- Rollback:
135
-
136
- - move the canary back to a core cohort, or remove `presetOverride: default` from the cohort
137
- - reconcile generated outputs
138
- - rebuild/redeploy the affected user
158
+ Rollback must restore the prior desired-state revision and prior runtime image together. Never pair canonical config with the retired image, or retired config with the canonical image.
139
159
 
140
160
  ## Hosted site and theme package contract
141
161
 
@@ -144,7 +164,7 @@ Start with the public [site mockup migration guide](https://github.com/rizom-ai/
144
164
  - A site package must default-export a valid `SitePackage` and use documented public APIs such as `@rizom/brain/site`.
145
165
  - A theme package must default-export its CSS as a string. Hosted custom themes currently use the `@rizom/*` scope so the fleet image installs them with the site package; `@brains/*` themes are bundled with `@rizom/brain`.
146
166
  - Site and custom theme packages must be public npm packages that install without registry credentials.
147
- - Site and theme packages publish on their own release cadences, independent of `@rizom/brain` and of each other. Hosted configuration resolves package names plus pinned versions into exact npm refs.
167
+ - Site, theme, and brain packages publish independently. Hosted configuration requires exact site and external-theme version pins and never derives one package version from another.
148
168
  - Keep site structure and theme CSS in separate packages. Do not put private content or secrets in either package.
149
169
 
150
170
  Configure a user in `users/<handle>.yaml`:
@@ -152,22 +172,20 @@ Configure a user in `users/<handle>.yaml`:
152
172
  ```yaml
153
173
  siteOverride:
154
174
  package: "@rizom/site-example"
175
+ version: <exact-site-version>
155
176
  theme: "@rizom/theme-example"
156
- themeVersion: <exact-theme-version> # required for @rizom/* themes
157
- # version: <exact-site-version> # optional deliberate pin
177
+ themeVersion: <exact-theme-version>
158
178
  ```
159
179
 
160
- When `version` is omitted, it defaults to the user's effective brain version
161
- (cohort override first, then `pilot.yaml.brainVersion`). A `@rizom/*` theme
162
- always requires an explicit `themeVersion` — themes and sites version
163
- independently, so the theme's version is never inferred. A site override
164
- produces an isolated per-instance image; it never changes the fleet's shared
165
- default image.
180
+ Missing external package versions fail desired-state validation. A site override
181
+ produces an isolated per-instance image; it never changes the fleet's shared default
182
+ image. Bundled `@brains/*` themes omit `themeVersion` because they are not installed as
183
+ separate packages.
166
184
 
167
185
  ### Custom-package canary and rollback
168
186
 
169
187
  1. Confirm the exact site/theme versions are public-installable without npm credentials.
170
- 2. Apply the package names to one healthy `rover:default` canary.
188
+ 2. Apply the exact package names and versions to one healthy canonical site canary.
171
189
  3. Reconcile the canary, push the generated output, and let build/deploy create its site image.
172
190
  4. Run `bunx brains-ops verify-user . <handle>`.
173
191
  5. Manually verify the site, theme, CMS, content sync, and passkey sign-in before adding more users.
@@ -246,31 +264,32 @@ Notes:
246
264
  - The ATProto app password is secret and belongs only in the encrypted per-user secret payload.
247
265
  - For smoke deployments, pin only the smoke cohort/user to the released brain version that contains ATProto support.
248
266
 
249
- ## Discord bot token checklist
267
+ ## Discord application credential checklist
250
268
 
251
269
  Use this when enabling Discord for a pilot user.
252
270
 
253
271
  1. Pick the user handle (for example `smoke`).
254
272
  2. Open the Discord Developer Portal.
255
- 3. Create a **new application** for that user's rover.
273
+ 3. Create a **new application** for that user's brain.
256
274
  4. Add a **Bot** to the application.
257
- 5. Copy the bot token.
258
- 6. Put that value in `.env` or `.env.local` in this repo as `DISCORD_BOT_TOKEN=...` while onboarding that user.
275
+ 5. Copy the bot token, application public key, and application ID.
276
+ 6. Put those values in `.env` or `.env.local` while onboarding that user:
277
+ - `DISCORD_BOT_TOKEN=...`
278
+ - `DISCORD_PUBLIC_KEY=...`
279
+ - `DISCORD_APPLICATION_ID=...`
259
280
  7. Keep `discord.enabled: true` in `users/<handle>.yaml` unless you explicitly want to disable the primary pilot interface.
260
- 8. Encrypt the current per-user secret payload:
281
+ 8. Encrypt the current per-user credential payload:
261
282
  - `bunx brains-ops secrets:encrypt . <handle>`
262
283
  9. Reconcile/deploy the user or cohort:
263
-
264
- - `bunx brains-ops onboard . <handle>`
265
- - or `bunx brains-ops reconcile-cohort . <cohort>`
266
-
267
- 11. In the Discord Developer Portal, generate an install URL and invite the bot to the right server.
268
- 12. Send a test message in Discord and confirm the rover responds.
284
+ - `bunx brains-ops onboard . <handle>`
285
+ - or `bunx brains-ops reconcile-cohort . <cohort>`
286
+ 10. In the Discord Developer Portal, generate an install URL and invite the bot to the right server.
287
+ 11. Send a test message in Discord and confirm the brain responds.
269
288
 
270
289
  Notes:
271
290
 
272
- - Use **one bot token per user/rover**.
273
- - Do not reuse the same Discord bot token across multiple pilot users.
291
+ - Use **one Discord application credential set per user/brain**.
292
+ - Do not reuse the same Discord application across multiple pilot users.
274
293
  - Discord is the default pilot interface moving forward.
275
294
  - The encrypted `users/<handle>.secrets.yaml.age` file is the durable checked-in deploy input; your local env is only the operator staging source.
276
295
  - Direct MCP client access should use OAuth/passkey-capable clients where possible.