diff --git a/.claude/.revcon-manifest.json b/.claude/.revcon-manifest.json deleted file mode 100644 index ddc2e21..0000000 --- a/.claude/.revcon-manifest.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "mode": "copy", - "editor": "claude", - "profiles": ["revealfleet"], - "files": { - "rules/code-over-docs.md": { - "source": "profiles/revealfleet/claude/rules/code-over-docs.md", - "sha256": "fcc9af90a0fc31a1c04202891099a5e6e0f13502649011fd7a9cfe59bf6b5500" - }, - "rules/control-layer.md": { - "source": "profiles/revealfleet/claude/rules/control-layer.md", - "sha256": "289388f1038de4f372f5eeea5a61ee583fbea93042d01801b6c90b01d878c9f9" - }, - "rules/disposition-actions.md": { - "source": "profiles/revealfleet/claude/rules/disposition-actions.md", - "sha256": "db2e1964f359e0df046788ee7b828168efd8ea27c20384edc99fe5647404a603" - }, - "rules/durable-solutions.md": { - "source": "profiles/revealfleet/claude/rules/durable-solutions.md", - "sha256": "1d9ab978b70bf67f68b65c4b9e06f1065730d1b212fb51d71c42bffc26de1e63" - }, - "rules/exhaustive.md": { - "source": "profiles/revealfleet/claude/rules/exhaustive.md", - "sha256": "53779eb567f634c7108e5fc12ea2d2fcbd4c3b1eeaef8bf48ba3d67b202b21a8" - }, - "rules/extend-before-create.md": { - "source": "profiles/revealfleet/claude/rules/extend-before-create.md", - "sha256": "eda319933321d2d69db659dfd6d441640d25d69b8c51b2ae026ca0974e0f430c" - }, - "rules/git.md": { - "source": "profiles/revealfleet/claude/rules/git.md", - "sha256": "feb72f9ac185ab69c27b38653f2631b9f5bec908af80c6131b87d109c36564c0" - }, - "rules/model-allocation.md": { - "source": "profiles/revealfleet/claude/rules/model-allocation.md", - "sha256": "d71bb65d9315523e29b927ae47c7a3e9e35e763c7c30602d04c3043f92413c9b" - }, - "rules/no-em-dashes.md": { - "source": "profiles/revealfleet/claude/rules/no-em-dashes.md", - "sha256": "18b1cfe5218fd816837c13a2e6cd08786befad19a292f1fa85874a3ad0b5f22e" - }, - "rules/opensrc.md": { - "source": "profiles/revealfleet/claude/rules/opensrc.md", - "sha256": "bdb095fac109fbb5616dde222e0cfbc3e1d52ebf616609f3e865717773a2fb73" - }, - "rules/planning.md": { - "source": "profiles/revealfleet/claude/rules/planning.md", - "sha256": "315062ea332839617615e4c5fda5e1d50fde69090e55af6895ed343e18027e7d" - }, - "rules/secrets.md": { - "source": "profiles/revealfleet/claude/rules/secrets.md", - "sha256": "79d09010deb767f582cbd0a28f12d6bf3a36cc44e2f4260a7fbaddd709318d68" - }, - "rules/temp-scripts.md": { - "source": "profiles/revealfleet/claude/rules/temp-scripts.md", - "sha256": "66a574826b46352275606d88a9854a0e63c39433a97a55c75f50ccc0f4862a15" - }, - "rules/token-economy.md": { - "source": "profiles/revealfleet/claude/rules/token-economy.md", - "sha256": "b30a830e9dd333b05085bc5ce3994c477ac66390aabe4cf41741a64ccd12ef57" - } - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed40e25..8825b23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: - # GAP-372: materialized .claude rules must match .revcon-manifest.json + # GAP-372: first-party .revealui content must match its RevCon manifest. copy-lockstep: name: Copy lockstep runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 7a64828..d90d647 100644 --- a/.gitignore +++ b/.gitignore @@ -46,12 +46,12 @@ coverage/ # opensrc context (fetched on demand) opensrc/ -# Local Claude/agent state ignored; materialized rules/agents/skills tracked (GAP-372 copy-mode). -.claude/* -!.claude/rules/ -!.claude/agents/ -!.claude/skills/ -!.claude/.revcon-manifest.json +# First-party RevealUI policy content is tracked; local state and vendor adapters are not. +.revealui/* +!.revealui/content/ +!.revealui/.revcon-manifest.json +!.revealui/README.md +.claude/ .agents/ # Pinned attribution tool installs and caches (maintained NOTICE generator) diff --git a/.revealui/.revcon-manifest.json b/.revealui/.revcon-manifest.json new file mode 100644 index 0000000..6c5ff32 --- /dev/null +++ b/.revealui/.revcon-manifest.json @@ -0,0 +1,65 @@ +{ + "editor": "revealui", + "files": { + "content/rules/code-over-docs.md": { + "sha256": "fcc9af90a0fc31a1c04202891099a5e6e0f13502649011fd7a9cfe59bf6b5500", + "source": "profiles/revealfleet/revealui/rules/code-over-docs.md" + }, + "content/rules/control-layer.md": { + "sha256": "289388f1038de4f372f5eeea5a61ee583fbea93042d01801b6c90b01d878c9f9", + "source": "profiles/revealfleet/revealui/rules/control-layer.md" + }, + "content/rules/disposition-actions.md": { + "sha256": "db2e1964f359e0df046788ee7b828168efd8ea27c20384edc99fe5647404a603", + "source": "profiles/revealfleet/revealui/rules/disposition-actions.md" + }, + "content/rules/durable-solutions.md": { + "sha256": "1d9ab978b70bf67f68b65c4b9e06f1065730d1b212fb51d71c42bffc26de1e63", + "source": "profiles/revealfleet/revealui/rules/durable-solutions.md" + }, + "content/rules/exhaustive.md": { + "sha256": "53779eb567f634c7108e5fc12ea2d2fcbd4c3b1eeaef8bf48ba3d67b202b21a8", + "source": "profiles/revealfleet/revealui/rules/exhaustive.md" + }, + "content/rules/extend-before-create.md": { + "sha256": "eda319933321d2d69db659dfd6d441640d25d69b8c51b2ae026ca0974e0f430c", + "source": "profiles/revealfleet/revealui/rules/extend-before-create.md" + }, + "content/rules/git.md": { + "sha256": "68d729dbb87d13b3c9f4ead1b4ff6a3d76aac076a1a28136284c612f1c89e6f2", + "source": "profiles/revealfleet/revealui/rules/git.md" + }, + "content/rules/model-allocation.md": { + "sha256": "d71bb65d9315523e29b927ae47c7a3e9e35e763c7c30602d04c3043f92413c9b", + "source": "profiles/revealfleet/revealui/rules/model-allocation.md" + }, + "content/rules/no-em-dashes.md": { + "sha256": "18b1cfe5218fd816837c13a2e6cd08786befad19a292f1fa85874a3ad0b5f22e", + "source": "profiles/revealfleet/revealui/rules/no-em-dashes.md" + }, + "content/rules/opensrc.md": { + "sha256": "bdb095fac109fbb5616dde222e0cfbc3e1d52ebf616609f3e865717773a2fb73", + "source": "profiles/revealfleet/revealui/rules/opensrc.md" + }, + "content/rules/planning.md": { + "sha256": "315062ea332839617615e4c5fda5e1d50fde69090e55af6895ed343e18027e7d", + "source": "profiles/revealfleet/revealui/rules/planning.md" + }, + "content/rules/secrets.md": { + "sha256": "79d09010deb767f582cbd0a28f12d6bf3a36cc44e2f4260a7fbaddd709318d68", + "source": "profiles/revealfleet/revealui/rules/secrets.md" + }, + "content/rules/temp-scripts.md": { + "sha256": "66a574826b46352275606d88a9854a0e63c39433a97a55c75f50ccc0f4862a15", + "source": "profiles/revealfleet/revealui/rules/temp-scripts.md" + }, + "content/rules/token-economy.md": { + "sha256": "b30a830e9dd333b05085bc5ce3994c477ac66390aabe4cf41741a64ccd12ef57", + "source": "profiles/revealfleet/revealui/rules/token-economy.md" + } + }, + "mode": "copy", + "profiles": [ + "revealfleet" + ] +} diff --git a/.revealui/README.md b/.revealui/README.md new file mode 100644 index 0000000..4899f60 --- /dev/null +++ b/.revealui/README.md @@ -0,0 +1,78 @@ +# RevDev — Native Developer Tools + +This is RevDev's first-party project guide. Managed fleet rules live in +`.revealui/content/rules/` and are materialized from the RevealUI profile by +RevCon. Vendor adapters, when configured, consume this first-party content. + +Native developer experience for RevealUI. One product, two interfaces. + +## Products + +| App | Stack | Purpose | +|-----|-------|---------| +| Studio | Tauri 2 + React 19 | Desktop AI editor + agent coordination dashboard | +| Console | Go (Bubble Tea) | SSH payment + licensing TUI (tiers, checkout, license, email/OTP) + agent-terminal proxy | + +## Architecture + +``` +RevDev +├── apps/studio/ # Tauri desktop app (Studio UI) +├── apps/console/ # Go TUI (RevDev Console — SSH ops cockpit) +├── packages/bridge/ # Thin adapter layer between daemon + apps +├── packages/daemon/ # Harness daemon (agent coordination, PTY, tools) +├── packages/protocol/ # JSON-RPC types shared across all apps +└── packages/theme/ # Console theme tokens +``` + +## Naming Convention + +Monorepo-wide split on `package.json` `name` fields: +- **Apps** (`apps/*`): unscoped — `"studio"`, `"console"`. Never `@scope/`. +- **Packages** (`packages/*`): scoped — `"@revdev/daemon"`, `"@revdev/protocol"`. + +Rationale: apps are deploy targets, not consumable libraries; the `@scope/` prefix is reserved for things that get published. + +## Relationship to RevealUI + +RevDev **consumes** RevealUI packages — it does not contain them: +- `@revealui/contracts` — shared Zod schemas (npm dependency) +- `@revealui/security` — shared security primitives incl. input sanitization (npm dependency; studio terminal uses `sanitizeTerminalLine`) +- `@revealui/harnesses` — AI harness adapters (npm dependency, Pro) +- RevealUI API — HTTP/WebSocket (Console talks directly to API) + +The harness daemon is the brain; Studio is its UI. Console is a separate SSH surface talking to the RevealUI API, not the daemon. + +## Git Identity +Follow [the maintained fleet Git rule](content/rules/git.md) for author and +signing identity. Keep the default in the global Git configuration; do not add +per-repository identity overrides. + +## Stack +- **Studio**: Rust (Tauri 2 backend) + TypeScript/React 19 (frontend) +- **Console**: Go 1.23+ (Bubble Tea TUI framework) +- **Daemon**: TypeScript (Node.js, extracted from @revealui/harnesses) +- **Protocol**: TypeScript (JSON-RPC 2.0 type definitions) + +## Commands + +```bash +# Studio +pnpm --filter studio tauri:dev # Start Studio in dev mode +pnpm --filter studio tauri build # Build desktop binary +pnpm typecheck:studio # Typecheck Studio frontend + +# Console (no root go.mod; module lives under apps/console/ per CI ci.yml:78-79) +cd apps/console && go run . # Run Console +cd apps/console && go build -o ../../rvui . # Build Console binary + +# Workspace +pnpm -r --filter=!studio build # Build all packages (skip Tauri) +pnpm test # Run tests +``` + +## CI Pipelines + +- `studio-release.yml` — Tauri build + code signing + notarization + auto-update (macOS, Windows, Linux) +- `console-release.yml` — Go cross-compile + checksums + GitHub Release +- `ci.yml` — Lint + test for both apps diff --git a/.claude/rules/code-over-docs.md b/.revealui/content/rules/code-over-docs.md similarity index 100% rename from .claude/rules/code-over-docs.md rename to .revealui/content/rules/code-over-docs.md diff --git a/.claude/rules/control-layer.md b/.revealui/content/rules/control-layer.md similarity index 100% rename from .claude/rules/control-layer.md rename to .revealui/content/rules/control-layer.md diff --git a/.claude/rules/disposition-actions.md b/.revealui/content/rules/disposition-actions.md similarity index 100% rename from .claude/rules/disposition-actions.md rename to .revealui/content/rules/disposition-actions.md diff --git a/.claude/rules/durable-solutions.md b/.revealui/content/rules/durable-solutions.md similarity index 100% rename from .claude/rules/durable-solutions.md rename to .revealui/content/rules/durable-solutions.md diff --git a/.claude/rules/exhaustive.md b/.revealui/content/rules/exhaustive.md similarity index 100% rename from .claude/rules/exhaustive.md rename to .revealui/content/rules/exhaustive.md diff --git a/.claude/rules/extend-before-create.md b/.revealui/content/rules/extend-before-create.md similarity index 100% rename from .claude/rules/extend-before-create.md rename to .revealui/content/rules/extend-before-create.md diff --git a/.claude/rules/git.md b/.revealui/content/rules/git.md similarity index 88% rename from .claude/rules/git.md rename to .revealui/content/rules/git.md index 65bafe2..2a2ac0a 100644 --- a/.claude/rules/git.md +++ b/.revealui/content/rules/git.md @@ -64,4 +64,5 @@ Grok worktrees: use `rfg … --worktree=…` (injects `--ref test`) or pass ## Identity - Professional repos (RevealUIStudio): display name `RevealUI Studio` plus the **signing account's** verified GitHub noreply address (shape `id+login@users.noreply.github.com` on the account that holds the SSH signing key). Do not paste a personal login into public rule copies; resolve the real address from the machine git config / fleet hardline when committing. -- Amended 2026-07-10: never "restore" founder@revealui.com. It belongs to the org account, so SSH-signed commits carrying it render Unverified and required_signatures rulesets silently block the merge. Full rationale in the fleet-level Studio git hardline (not re-copied here). +- Accepted additional author identity (owner 2026-09-30): `founder@revealui.com` also belongs to the project owner. Preserve this author identity when re-signing commits; do not replace it merely because the configured signing identity differs. +- Keep the current signing identity as the default. For signed commits, the committer email must be verified on the GitHub account holding the signing key. An unsigned commit needs a signature regardless of its author email; check GitHub's verification reason before diagnosing an identity mismatch. diff --git a/.claude/rules/model-allocation.md b/.revealui/content/rules/model-allocation.md similarity index 100% rename from .claude/rules/model-allocation.md rename to .revealui/content/rules/model-allocation.md diff --git a/.claude/rules/no-em-dashes.md b/.revealui/content/rules/no-em-dashes.md similarity index 100% rename from .claude/rules/no-em-dashes.md rename to .revealui/content/rules/no-em-dashes.md diff --git a/.claude/rules/opensrc.md b/.revealui/content/rules/opensrc.md similarity index 100% rename from .claude/rules/opensrc.md rename to .revealui/content/rules/opensrc.md diff --git a/.claude/rules/planning.md b/.revealui/content/rules/planning.md similarity index 100% rename from .claude/rules/planning.md rename to .revealui/content/rules/planning.md diff --git a/.claude/rules/secrets.md b/.revealui/content/rules/secrets.md similarity index 100% rename from .claude/rules/secrets.md rename to .revealui/content/rules/secrets.md diff --git a/.claude/rules/temp-scripts.md b/.revealui/content/rules/temp-scripts.md similarity index 100% rename from .claude/rules/temp-scripts.md rename to .revealui/content/rules/temp-scripts.md diff --git a/.claude/rules/token-economy.md b/.revealui/content/rules/token-economy.md similarity index 100% rename from .claude/rules/token-economy.md rename to .revealui/content/rules/token-economy.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9f849dc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +# RevDev agent instructions + +Start with [.revealui/README.md](.revealui/README.md) and the maintained rules +in `.revealui/content/rules/`. RevCon materializes and verifies those rules; +change their owning profile rather than editing copies in this repository. +Vendor integrations may project from `.revealui` when configured. diff --git a/CLAUDE.md b/CLAUDE.md index f70e96a..67b034c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,74 +1,5 @@ -# RevDev — Native Developer Tools +# RevDev Claude adapter -Native developer experience for RevealUI. One product, two interfaces. - -## Products - -| App | Stack | Purpose | -|-----|-------|---------| -| Studio | Tauri 2 + React 19 | Desktop AI editor + agent coordination dashboard | -| Console | Go (Bubble Tea) | SSH payment + licensing TUI (tiers, checkout, license, email/OTP) + agent-terminal proxy | - -## Architecture - -``` -RevDev -├── apps/studio/ # Tauri desktop app (Studio UI) -├── apps/console/ # Go TUI (RevDev Console — SSH ops cockpit) -├── packages/bridge/ # Thin adapter layer between daemon + apps -├── packages/daemon/ # Harness daemon (agent coordination, PTY, tools) -├── packages/protocol/ # JSON-RPC types shared across all apps -└── packages/theme/ # Console theme tokens -``` - -## Naming Convention - -Monorepo-wide split on `package.json` `name` fields: -- **Apps** (`apps/*`): unscoped — `"studio"`, `"console"`. Never `@scope/`. -- **Packages** (`packages/*`): scoped — `"@revdev/daemon"`, `"@revdev/protocol"`. - -Rationale: apps are deploy targets, not consumable libraries; the `@scope/` prefix is reserved for things that get published. - -## Relationship to RevealUI - -RevDev **consumes** RevealUI packages — it does not contain them: -- `@revealui/contracts` — shared Zod schemas (npm dependency) -- `@revealui/security` — shared security primitives incl. input sanitization (npm dependency; studio terminal uses `sanitizeTerminalLine`) -- `@revealui/harnesses` — AI harness adapters (npm dependency, Pro) -- RevealUI API — HTTP/WebSocket (Console talks directly to API) - -The harness daemon is the brain; Studio is its UI. Console is a separate SSH surface talking to the RevealUI API, not the daemon. - -## Git Identity -Commit as the fleet's verified GitHub noreply identity, configured once in the global `~/.gitconfig` (never override it per-repo; the literal address lives there, not in docs, and this repo's leak scanner enforces that). - -> Amended 2026-07-10. The prior founder@revealui.com address is retired: commits carrying it render Unverified under required signatures. Do not restore it, and do not re-add local user.email overrides. - -## Stack -- **Studio**: Rust (Tauri 2 backend) + TypeScript/React 19 (frontend) -- **Console**: Go 1.23+ (Bubble Tea TUI framework) -- **Daemon**: TypeScript (Node.js, extracted from @revealui/harnesses) -- **Protocol**: TypeScript (JSON-RPC 2.0 type definitions) - -## Commands - -```bash -# Studio -pnpm --filter studio tauri:dev # Start Studio in dev mode -pnpm --filter studio tauri build # Build desktop binary -pnpm typecheck:studio # Typecheck Studio frontend - -# Console (no root go.mod; module lives under apps/console/ per CI ci.yml:78-79) -cd apps/console && go run . # Run Console -cd apps/console && go build -o ../../rvui . # Build Console binary - -# Workspace -pnpm -r --filter=!studio build # Build all packages (skip Tauri) -pnpm test # Run tests -``` - -## CI Pipelines - -- `studio-release.yml` — Tauri build + code signing + notarization + auto-update (macOS, Windows, Linux) -- `console-release.yml` — Go cross-compile + checksums + GitHub Release -- `ci.yml` — Lint + test for both apps +Read [the RevealUI project guide](.revealui/README.md) and the first-party +rules under `.revealui/content/rules/` before using this adapter. The +`.revealui` tree owns the policy content. diff --git a/apps/studio/README.md b/apps/studio/README.md index 2b416d7..82cc021 100644 --- a/apps/studio/README.md +++ b/apps/studio/README.md @@ -93,7 +93,7 @@ Key Rust crates: `russh` 0.60 (SSH client), `portable-pty` (PTY sessions), `git2 ## Related - [Architecture Guide](../../docs/ARCHITECTURE.md) -- [Distribution Guide](../../.claude/rules/distribution.md) +- [Studio release workflow](../../.github/workflows/studio-release.yml) ## License diff --git a/biome.json b/biome.json index 3e05907..09b7f4e 100644 --- a/biome.json +++ b/biome.json @@ -15,7 +15,8 @@ "!**/src-tauri/target", "!**/src-tauri/icons", "!**/bindings", - "!**/pnpm-lock.yaml" + "!**/pnpm-lock.yaml", + "!.revealui/.revcon-manifest.json" ] }, "formatter": { diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index ba14eaa..161f892 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -217,4 +217,4 @@ To check manually: use the "Check for Updates" command in Studio settings. - [Troubleshooting](./TROUBLESHOOTING.md) — Common issues and fixes - [API Reference](./API_REFERENCE.md) — All 36 daemon RPC methods -- [Architecture](../CLAUDE.md) — How Studio, Daemon, and Bridge connect +- [Architecture](../.revealui/README.md) — How Studio, Daemon, and Bridge connect diff --git a/docs/MASTER_PLAN.md b/docs/MASTER_PLAN.md index 6bdf350..6b0a546 100644 --- a/docs/MASTER_PLAN.md +++ b/docs/MASTER_PLAN.md @@ -36,4 +36,4 @@ Current verified state, workstreams (launch readiness, identity rollout, cross-m - [`docs/PLAN.md`](./PLAN.md) — **the plan** (single source) - [`docs/MASTER_SPEC.md`](./MASTER_SPEC.md) → [`docs/SPEC.md`](./SPEC.md) — the spec - [`docs/INDEX.md`](./INDEX.md) — documentation index -- [`README.md`](../README.md) — overview + architecture · [`CLAUDE.md`](../CLAUDE.md) — agent context +- [`README.md`](../README.md) — overview + architecture · [`.revealui/README.md`](../.revealui/README.md) — agent context diff --git a/docs/MASTER_SPEC.md b/docs/MASTER_SPEC.md index 425778c..e5ae2cb 100644 --- a/docs/MASTER_SPEC.md +++ b/docs/MASTER_SPEC.md @@ -26,4 +26,4 @@ Native developer tools for RevealUI. **One product, two interfaces, one daemon.* - [`docs/MASTER_PLAN.md`](./MASTER_PLAN.md) → [`docs/PLAN.md`](./PLAN.md) — the plan - [`docs/API_REFERENCE.md`](./API_REFERENCE.md) — JSON-RPC API reference - [`docs/INDEX.md`](./INDEX.md) — documentation index -- [`CLAUDE.md`](../CLAUDE.md) — agent context · [`README.md`](../README.md) — overview +- [`.revealui/README.md`](../.revealui/README.md) — agent context · [`README.md`](../README.md) — overview diff --git a/docs/SPEC.md b/docs/SPEC.md index dd243ac..fa6aa69 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -26,7 +26,7 @@ Native developer tools for RevealUI. **One product, two interfaces, one daemon.* ``` revdev/ ├── README.md -├── CLAUDE.md # agent context +├── .revealui/README.md # agent context ├── NOTICE.md # third-party attributions ├── package.json # pnpm workspace root ├── pnpm-workspace.yaml @@ -290,4 +290,4 @@ Pre-1.0 per the fleet versioning convention. Per-package SemVer (`@revdev/daemon - [`MASTER_PLAN.md`](./MASTER_PLAN.md) / [`MASTER_SPEC.md`](./MASTER_SPEC.md) — stable entry points - [`API_REFERENCE.md`](./API_REFERENCE.md) — JSON-RPC API reference - [`GETTING_STARTED.md`](./GETTING_STARTED.md) · [`KEY_GENERATION.md`](./KEY_GENERATION.md) · [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md) -- [`CLAUDE.md`](../CLAUDE.md) — agent context · [`README.md`](../README.md) — overview +- [`.revealui/README.md`](../.revealui/README.md) — agent context · [`README.md`](../README.md) — overview diff --git a/scripts/verify-copy-lockstep.sh b/scripts/verify-copy-lockstep.sh index caed28e..7cb5e44 100644 --- a/scripts/verify-copy-lockstep.sh +++ b/scripts/verify-copy-lockstep.sh @@ -3,13 +3,14 @@ # # GAP-372 design (shared gate, not per-repo ports of revealui rules-lockstep.ts): # One script, many consumers. Each fleet repo that materializes -# .claude/{rules,agents,skills} via `link.sh --mode copy` runs this gate in +# .revealui/content/{rules,commands,agents,skills} via `link.sh --mode copy` +# runs this gate in # CI so hand-edits and stray tracked files fail the build. # # Self-consistency only (no sibling revcon profile checkout required): -# 1. .claude/.revcon-manifest.json present, mode=copy +# 1. .revealui/.revcon-manifest.json present, mode=copy # 2. Every manifest entry exists as a real file (not a symlink) with matching sha256 -# 3. Every git-tracked file under .claude/{rules,agents,skills} appears in the manifest +# 3. Every git-tracked native content file appears in the manifest # # Profile-source freshness (stale vs current profiles) remains an operator # concern via status.sh (needs the revcon checkout). CI only needs the @@ -17,23 +18,22 @@ # # Usage: # bash scripts/verify-copy-lockstep.sh --target /path/to/repo -# bash scripts/verify-copy-lockstep.sh --target . --dot .claude +# bash scripts/verify-copy-lockstep.sh --target . --dot .claude # optional adapter # # Exit: 0 ok · 1 drift / missing manifest · 2 usage error set -euo pipefail TARGET="" -DOT=".claude" -MATERIALIZED_SUBDIRS=(rules agents skills) +DOT=".revealui" usage() { cat <<'EOF' -Usage: verify-copy-lockstep.sh --target DIR [--dot .claude] +Usage: verify-copy-lockstep.sh --target DIR [--dot .revealui|.claude] Options: --target DIR Repo root that owns the materialized editor dir (required) - --dot NAME Dot-dir under target (default: .claude) + --dot NAME Materialized root (default: .revealui) -h, --help Show this help EOF } @@ -53,6 +53,12 @@ if [[ -z "$TARGET" ]]; then exit 2 fi +case "$DOT" in + .revealui) MATERIALIZED_SUBDIRS=(content/rules content/commands content/agents content/skills) ;; + .claude) MATERIALIZED_SUBDIRS=(rules agents skills) ;; + *) echo "error: unsupported materialized root: $DOT" >&2; exit 2 ;; +esac + if [[ ! -d "$TARGET" ]]; then echo "error: target is not a directory: $TARGET" >&2 exit 2 @@ -64,7 +70,7 @@ MANIFEST="$TARGET/$DOT/.revcon-manifest.json" if [[ ! -f "$MANIFEST" ]]; then echo "✗ missing $DOT/.revcon-manifest.json" >&2 echo " Materialize with revcon:" >&2 - echo " bash ~/revealfleet/revcon/link.sh --target $TARGET --profile revealfleet --editor claude --mode copy" >&2 + echo " bash ~/revealfleet/revcon/link.sh --target $TARGET --profile revealfleet --editor ${DOT#.} --mode copy" >&2 exit 1 fi @@ -76,20 +82,28 @@ fi # Parse and validate the entire manifest before checking any paths. A parser # failure inside process substitution would otherwise leave the outer loop's # exit status at zero and allow a malformed manifest to pass as an empty one. -if ! manifest_rows="$(jq -s -r ' +if ! manifest_rows="$(jq -s -r --arg dot "$DOT" ' if length == 1 and (.[0] | type == "object") then .[0] as $m | if ($m.mode == "copy") + and ($m.editor == $dot[1:]) and ($m.profiles | type == "array") and all($m.profiles[]; type == "string") and ($m.files | type == "object") + and (if $dot == ".revealui" then ($m.files | length > 0) else true end) and all($m.files | to_entries[]; - ((.key | startswith("rules/") or startswith("agents/") or startswith("skills/")) + ((if $dot == ".revealui" then + (.key | startswith("content/rules/") or startswith("content/commands/") + or startswith("content/agents/") or startswith("content/skills/")) + else + (.key | startswith("rules/") or startswith("agents/") or startswith("skills/")) + end) and (.key | split("/") | all(.[]; length > 0 and . != "." and . != "..")) and (.key | test("[[:cntrl:]]") | not) and (.value | type == "object") and (.value.source | type == "string") and (.value.source | length > 0) + and (if $dot == ".revealui" then (.value.source | contains("/revealui/")) else true end) and (.value.source | test("[[:cntrl:]]") | not) and (.value.sha256 | type == "string") and (.value.sha256 | test("^[0-9a-f]{64}$")))) @@ -153,6 +167,9 @@ pathspecs=() for sub in "${MATERIALIZED_SUBDIRS[@]}"; do pathspecs+=("$DOT/$sub") done +if [[ "$DOT" == ".revealui" ]]; then + pathspecs+=(".claude") +fi tracked_file_list="$(mktemp)" trap 'rm -f -- "$tracked_file_list"' EXIT if ! git -C "$TARGET" ls-files -z -- "${pathspecs[@]}" > "$tracked_file_list"; then @@ -160,6 +177,11 @@ if ! git -C "$TARGET" ls-files -z -- "${pathspecs[@]}" > "$tracked_file_list"; t exit 2 fi while IFS= read -r -d '' tracked; do + if [[ "$DOT" == ".revealui" && "$tracked" == .claude/* ]]; then + echo " $tracked — tracked vendor content; materialize first-party content in .revealui." >&2 + problems=$((problems + 1)) + continue + fi if [[ -z "${manifest_paths[$tracked]+x}" ]]; then echo " $tracked — tracked but not in the manifest (hand-added?)." >&2 echo " Add it to the revcon profile and re-run link.sh --mode copy, or untrack it." >&2 @@ -171,7 +193,7 @@ profiles="$(jq -r '.profiles | join(", ")' "$MANIFEST" 2>/dev/null || echo "?")" if (( problems > 0 )); then echo "✗ copy-lockstep: $problems violation(s) ($count manifest entr(y/ies), profiles: $profiles)" >&2 - echo " Re-apply: bash ~/revealfleet/revcon/link.sh --target $TARGET --editor claude --mode copy --profile …" >&2 + echo " Re-apply: bash ~/revealfleet/revcon/link.sh --target $TARGET --editor ${DOT#.} --mode copy --profile …" >&2 exit 1 fi diff --git a/scripts/verify-copy-lockstep.test.mjs b/scripts/verify-copy-lockstep.test.mjs index 8339895..b00bc13 100644 --- a/scripts/verify-copy-lockstep.test.mjs +++ b/scripts/verify-copy-lockstep.test.mjs @@ -15,15 +15,104 @@ beforeEach(() => { mkdirSync(join(root, '.claude'), { recursive: true }); writeFileSync( join(root, '.claude', '.revcon-manifest.json'), - JSON.stringify({ mode: 'copy', profiles: [], files: {} }), + JSON.stringify({ mode: 'copy', editor: 'claude', profiles: [], files: {} }), ); }); afterEach(() => rmSync(root, { recursive: true, force: true })); function check(env = process.env) { - return spawnSync('bash', [script, '--target', root], { encoding: 'utf8', env }); + return spawnSync('bash', [script, '--target', root, '--dot', '.claude'], { + encoding: 'utf8', + env, + }); +} + +function checkNative() { + return spawnSync('bash', [script, '--target', root], { encoding: 'utf8' }); } +function writeNativeManifest(files) { + mkdirSync(join(root, '.revealui', 'content', 'rules'), { recursive: true }); + writeFileSync( + join(root, '.revealui', '.revcon-manifest.json'), + JSON.stringify({ mode: 'copy', editor: 'revealui', profiles: ['revealfleet'], files }), + ); +} + +describe('first-party RevealUI admission', () => { + it('requires a native manifest even when Claude adapter content exists', () => { + expect(spawnSync(systemGit, ['init', '-q', root]).status).toBe(0); + const result = checkNative(); + expect(result.status).toBe(1); + expect(result.stderr).toContain('missing .revealui/.revcon-manifest.json'); + }); + + it('rejects an empty native policy inventory', () => { + expect(spawnSync(systemGit, ['init', '-q', root]).status).toBe(0); + writeNativeManifest({}); + const result = checkNative(); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + '.revealui/.revcon-manifest.json is not one valid copy manifest', + ); + }); + + it('accepts native content without a Claude projection and rejects edited bytes', () => { + expect(spawnSync(systemGit, ['init', '-q', root]).status).toBe(0); + rmSync(join(root, '.claude'), { recursive: true, force: true }); + const rel = 'content/rules/durable-solutions.md'; + const content = 'canonical rule'; + const file = join(root, '.revealui', rel); + writeNativeManifest({ + [rel]: { + source: 'profiles/revealfleet/revealui/rules/durable-solutions.md', + sha256: createHash('sha256').update(content).digest('hex'), + }, + }); + writeFileSync(file, content); + expect(spawnSync(systemGit, ['-C', root, 'add', '-f', `.revealui/${rel}`]).status).toBe(0); + expect(checkNative().status).toBe(0); + + writeFileSync(file, 'edited vendor-independent rule'); + const result = checkNative(); + expect(result.status).toBe(1); + expect(result.stderr).toContain('content differs from the manifest'); + }); + + it('rejects Claude source provenance in the native manifest', () => { + expect(spawnSync(systemGit, ['init', '-q', root]).status).toBe(0); + writeNativeManifest({ + 'content/rules/legacy.md': { + source: 'profiles/revealfleet/claude/rules/legacy.md', + sha256: 'a'.repeat(64), + }, + }); + const result = checkNative(); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + '.revealui/.revcon-manifest.json is not one valid copy manifest', + ); + }); + + it('rejects tracked vendor content while retaining native ownership', () => { + expect(spawnSync(systemGit, ['init', '-q', root]).status).toBe(0); + const content = 'native policy'; + const nativeRel = 'content/rules/native.md'; + writeNativeManifest({ + [nativeRel]: { + source: 'profiles/revealfleet/revealui/rules/native.md', + sha256: createHash('sha256').update(content).digest('hex'), + }, + }); + writeFileSync(join(root, '.revealui', nativeRel), content); + writeFileSync(join(root, '.claude', 'rules.md'), 'old vendor copy'); + expect(spawnSync(systemGit, ['-C', root, 'add', '.claude/rules.md']).status).toBe(0); + const result = checkNative(); + expect(result.status).toBe(1); + expect(result.stderr).toContain('tracked vendor content'); + }); +}); + describe('copy lockstep tracked-file inventory', () => { it('fails when the target has no Git inventory', () => { const result = check(); @@ -58,16 +147,20 @@ describe('copy lockstep tracked-file inventory', () => { describe('copy lockstep manifest parsing', () => { it.each([ - ['array file entry', '{"mode":"copy","profiles":[],"files":{"rules/example.md":[]}}'], + [ + 'array file entry', + '{"mode":"copy","editor":"claude","profiles":[],"files":{"rules/example.md":[]}}', + ], [ 'missing digest', - '{"mode":"copy","profiles":[],"files":{"rules/example.md":{"source":"profile"}}}', + '{"mode":"copy","editor":"claude","profiles":[],"files":{"rules/example.md":{"source":"profile"}}}', ], - ['missing profiles', '{"mode":"copy","files":{}}'], + ['missing profiles', '{"mode":"copy","editor":"claude","files":{}}'], [ 'control character in key', JSON.stringify({ mode: 'copy', + editor: 'claude', profiles: [], files: { 'rules/bad\nname.md': { source: 'profile', sha256: 'a'.repeat(64) } }, }), @@ -76,6 +169,7 @@ describe('copy lockstep manifest parsing', () => { 'traversal key', JSON.stringify({ mode: 'copy', + editor: 'claude', profiles: [], files: { 'rules/../escape.md': { source: 'profile', sha256: 'a'.repeat(64) } }, }), @@ -83,7 +177,7 @@ describe('copy lockstep manifest parsing', () => { ['empty document', ''], [ 'multiple documents', - '{"mode":"copy","profiles":[],"files":{}}\n{"mode":"copy","profiles":[],"files":{}}', + '{"mode":"copy","editor":"claude","profiles":[],"files":{}}\n{"mode":"copy","editor":"claude","profiles":[],"files":{}}', ], ])('rejects %s before inventory', (_label, manifest) => { writeFileSync(join(root, '.claude', '.revcon-manifest.json'), manifest); @@ -105,6 +199,7 @@ describe('copy lockstep manifest parsing', () => { join(root, '.claude', '.revcon-manifest.json'), JSON.stringify({ mode: 'copy', + editor: 'claude', profiles: ['synthetic'], files: { [rel]: { source, sha256: createHash('sha256').update(content).digest('hex') } }, }),