Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50d2a2eba9 | ||
|
|
f3dce32088 | ||
|
|
12ff5da7df | ||
|
|
10448e41a2 | ||
|
|
29c1defe29 | ||
|
|
3b7fd19d08 | ||
|
|
69f10a4062 | ||
|
|
67eaf6fb47 | ||
|
|
3ea385223e | ||
|
|
193479b52d | ||
|
|
7c580a5625 | ||
|
|
8ebddd6f93 | ||
|
|
127a54fdff | ||
|
|
9a5fbdbda7 | ||
|
|
7345f330fc | ||
|
|
d4696d09eb | ||
|
|
44f257cb06 | ||
|
|
5d27700026 | ||
|
|
69d1bb3aa4 | ||
|
|
9ea7dea711 | ||
|
|
d9a94f51ba | ||
|
|
975084abe2 | ||
|
|
073bbfdb6a | ||
|
|
d1d7b5598d | ||
|
|
ca8135d70c | ||
|
|
bf56583a49 | ||
|
|
c8f433131c | ||
|
|
d1e75f855c | ||
|
|
f710bf1a68 | ||
|
|
2085d75190 | ||
|
|
2f5a8d2cec | ||
|
|
e14ad9ab52 | ||
|
|
9fd16b9739 | ||
|
|
9051ad179b | ||
|
|
2f649ed930 | ||
|
|
db330c12c7 | ||
|
|
0a17d29bce | ||
|
|
3458f6a7ad | ||
|
|
b033952cd5 | ||
|
|
c102980ad4 | ||
|
|
58b96cb715 | ||
|
|
dd3ff944a1 | ||
|
|
8eb81ebec1 | ||
|
|
530597cc84 | ||
|
|
9a0d44f96a | ||
|
|
121b331c6c | ||
|
|
34e06e7de7 | ||
|
|
9bd4f1c405 | ||
|
|
a7b612435b | ||
|
|
87f10772ce | ||
|
|
7db4c5c2ed | ||
|
|
0273a84549 | ||
|
|
3b674b7a66 | ||
|
|
527bc581ca | ||
|
|
1249714a9a | ||
|
|
e175616885 | ||
|
|
6955717612 | ||
|
|
88d9cf750f | ||
|
|
c038706eed | ||
|
|
8622c9d826 | ||
|
|
88eef507b0 | ||
|
|
439bea6915 | ||
|
|
fad8a4718c | ||
|
|
2ff49adff4 | ||
|
|
44c476ebbf | ||
|
|
cde480eb60 | ||
|
|
5808248707 | ||
|
|
afd5827db8 | ||
|
|
d9cc990376 | ||
|
|
83c4e9851e | ||
|
|
22508170a2 | ||
|
|
90a67d050e | ||
|
|
24bdef75fa | ||
|
|
4e2a413640 | ||
|
|
be55549700 | ||
|
|
ddb1554e5b | ||
|
|
b017e66e17 | ||
|
|
172368612c | ||
|
|
1387231e57 | ||
|
|
0292392e64 | ||
|
|
594b8d711c | ||
|
|
3c1ffd2c2d | ||
|
|
4ebb123ba3 | ||
|
|
bb5cecb348 | ||
|
|
88f55d9135 | ||
|
|
e7e1bd26eb | ||
|
|
5d86b8fa93 | ||
|
|
35ea464661 | ||
|
|
e87ecdb3e5 | ||
|
|
a947db7bfd | ||
|
|
a35ea62ab1 | ||
|
|
f6c93dcb5c | ||
|
|
7f0408d417 | ||
|
|
cfd2a19bd7 | ||
|
|
d2a9e26395 | ||
|
|
0734b1f3a5 | ||
|
|
ce6420f3de | ||
|
|
81f58b15c8 | ||
|
|
0c2113f710 | ||
|
|
900a506c1f | ||
|
|
c3d29e796a | ||
|
|
c2365ae519 | ||
|
|
d6302f8e6f | ||
|
|
9aa4983cf2 | ||
|
|
736b0affc1 | ||
|
|
e18d13d36f | ||
|
|
60bc5d2022 | ||
|
|
ea91cfc421 | ||
|
|
acf640d00f | ||
|
|
431ead3a18 | ||
|
|
143ba0f57a | ||
|
|
ee815a72b1 | ||
|
|
94d626dff9 | ||
|
|
ee6c842918 | ||
|
|
ba3b854d50 | ||
|
|
c7a7fd07cc | ||
|
|
5399c6b7e7 | ||
|
|
e09b8783b4 | ||
|
|
abb0c93601 | ||
|
|
e67cced273 | ||
|
|
635cb1f666 | ||
|
|
09d24b9275 | ||
|
|
ed4c543872 | ||
|
|
215faeda0a | ||
|
|
5125fe21b0 | ||
|
|
41e8046371 | ||
|
|
6e16675ea2 | ||
|
|
19ebc422aa | ||
|
|
bd749831b1 | ||
|
|
f8e1b43b5b | ||
|
|
2148c20d26 | ||
|
|
5964dab891 | ||
|
|
bdb903cf69 | ||
|
|
bec2eb118b | ||
|
|
07624140e4 | ||
|
|
1c79af25d4 | ||
|
|
e605c83b27 | ||
|
|
b5ee692843 | ||
|
|
bf8bc2128d | ||
|
|
01904b8f69 | ||
|
|
676900bd46 | ||
|
|
a3b0770205 | ||
|
|
2a30c68b84 | ||
|
|
f8f8f97be7 | ||
|
|
49b7943420 | ||
|
|
19e16bd44f | ||
|
|
3bd490c080 | ||
|
|
4b448109dd | ||
|
|
bc1149c15e | ||
|
|
089953a7cf | ||
|
|
7b25be22e9 | ||
|
|
4e3d179e61 | ||
|
|
ae58482b72 |
@@ -1,153 +1,24 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Mosaic — Environment Variables Reference
|
||||
# Copy this file to .env and fill in the values for your deployment.
|
||||
# Lines beginning with # are comments; optional vars are commented out.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Non-secret runtime settings for the mosaic-poc-agent container.
|
||||
# Copy to .env if you want to override the defaults in compose.yaml.
|
||||
#
|
||||
# NEVER put credentials in this file. Authentication is supplied at
|
||||
# runtime only, via one of the two documented paths:
|
||||
# 1. read-only mounted pi auth file (default: ~/.pi/agent/auth.json,
|
||||
# override the host path with PI_AUTH_FILE)
|
||||
# 2. provider API key environment variable (ZAI_API_KEY or
|
||||
# ANTHROPIC_API_KEY), passed through by compose.yaml when set
|
||||
|
||||
# Model provider (built-in pi provider name)
|
||||
PI_PROVIDER=zai
|
||||
|
||||
# ─── Database (PostgreSQL 17 + pgvector) ─────────────────────────────────────
|
||||
# Full connection string used by the gateway, ORM, and migration runner.
|
||||
# Port 5433 avoids conflict with a host-side PostgreSQL instance.
|
||||
DATABASE_URL=postgresql://mosaic:mosaic@localhost:5433/mosaic
|
||||
# Model ID within the provider
|
||||
PI_MODEL=glm-5.3-flash
|
||||
|
||||
# Docker Compose host-port override for the PostgreSQL container (default: 5433)
|
||||
# PG_HOST_PORT=5433
|
||||
# Optional: alternative host path of the pi credential file mounted
|
||||
# read-only at /home/node/.pi/agent/auth.json in the container
|
||||
#PI_AUTH_FILE=/home/jwoltje/.pi/agent/auth.json
|
||||
|
||||
|
||||
# ─── Queue (Valkey 8 / Redis-compatible) ─────────────────────────────────────
|
||||
# Port 6380 avoids conflict with a host-side Redis/Valkey instance.
|
||||
VALKEY_URL=redis://localhost:6380
|
||||
|
||||
# Docker Compose host-port override for the Valkey container (default: 6380)
|
||||
# VALKEY_HOST_PORT=6380
|
||||
|
||||
|
||||
# ─── Gateway ─────────────────────────────────────────────────────────────────
|
||||
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
|
||||
GATEWAY_PORT=14242
|
||||
|
||||
# Comma-separated list of allowed CORS origins.
|
||||
# Must include the web app origin in production.
|
||||
GATEWAY_CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
|
||||
# ─── Auth (BetterAuth) ───────────────────────────────────────────────────────
|
||||
# REQUIRED — random secret used to sign sessions and tokens.
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
||||
|
||||
# Public base URL of the gateway (used by BetterAuth for callback URLs)
|
||||
BETTER_AUTH_URL=http://localhost:14242
|
||||
|
||||
|
||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
||||
# Public gateway URL — accessible from the browser, not just the server.
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
|
||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||
|
||||
# Service name shown in traces
|
||||
OTEL_SERVICE_NAME=mosaic-gateway
|
||||
|
||||
|
||||
# ─── AI Providers ────────────────────────────────────────────────────────────
|
||||
|
||||
# Ollama (local models — set OLLAMA_BASE_URL to enable)
|
||||
# OLLAMA_BASE_URL=http://localhost:11434
|
||||
# OLLAMA_HOST is a legacy alias for OLLAMA_BASE_URL
|
||||
# OLLAMA_HOST=http://localhost:11434
|
||||
# Comma-separated list of Ollama model IDs to register (default: llama3.2,codellama,mistral)
|
||||
# OLLAMA_MODELS=llama3.2,codellama,mistral
|
||||
|
||||
# Anthropic (claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5)
|
||||
# ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# OpenAI (gpt-4o, gpt-4o-mini, o3-mini)
|
||||
# OPENAI_API_KEY=sk-...
|
||||
|
||||
# Z.ai / GLM (glm-4.5, glm-4.5-air, glm-4.5-flash)
|
||||
# ZAI_API_KEY=...
|
||||
|
||||
# Custom providers — JSON array of provider configs
|
||||
# Format: [{"id":"<id>","baseUrl":"<url>","apiKey":"<key>","models":[{"id":"<model-id>","name":"<label>"}]}]
|
||||
# MOSAIC_CUSTOM_PROVIDERS=
|
||||
|
||||
|
||||
# ─── Embedding Service ───────────────────────────────────────────────────────
|
||||
# OpenAI-compatible embeddings endpoint (default: OpenAI)
|
||||
# EMBEDDING_API_URL=https://api.openai.com/v1
|
||||
# EMBEDDING_MODEL=text-embedding-3-small
|
||||
|
||||
|
||||
# ─── Log Summarization Service ───────────────────────────────────────────────
|
||||
# OpenAI-compatible chat completions endpoint for log summarization (default: OpenAI)
|
||||
# SUMMARIZATION_API_URL=https://api.openai.com/v1
|
||||
# SUMMARIZATION_MODEL=gpt-4o-mini
|
||||
|
||||
# Cron schedule for summarization job (default: every 6 hours)
|
||||
# SUMMARIZATION_CRON=0 */6 * * *
|
||||
|
||||
# Cron schedule for log tier management (default: daily at 03:00)
|
||||
# TIER_MANAGEMENT_CRON=0 3 * * *
|
||||
|
||||
|
||||
# ─── Agent ───────────────────────────────────────────────────────────────────
|
||||
# Filesystem sandbox root for agent file tools (default: process.cwd())
|
||||
# AGENT_FILE_SANDBOX_DIR=/var/lib/mosaic/sandbox
|
||||
|
||||
# Comma-separated list of tool names available to non-admin users.
|
||||
# Leave unset to allow all tools for all authenticated users.
|
||||
# AGENT_USER_TOOLS=read_file,list_directory,search_files
|
||||
|
||||
# System prompt injected into every agent session (optional)
|
||||
# AGENT_SYSTEM_PROMPT=You are a helpful assistant.
|
||||
|
||||
|
||||
# ─── MCP Servers ─────────────────────────────────────────────────────────────
|
||||
# JSON array of MCP server configs — set to enable MCP tool integration.
|
||||
# Each entry: {"name":"<id>","url":"<http-or-sse-url>"}
|
||||
# MCP_SERVERS=[{"name":"my-mcp","url":"http://localhost:3100/sse"}]
|
||||
|
||||
|
||||
# ─── Coordinator ─────────────────────────────────────────────────────────────
|
||||
# Root directory used to scope coordinator (worktree/repo) operations.
|
||||
# Defaults to the monorepo root auto-detected from process.cwd().
|
||||
# MOSAIC_WORKSPACE_ROOT=/home/user/projects/mosaic
|
||||
|
||||
|
||||
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
|
||||
# DISCORD_BOT_TOKEN=
|
||||
# DISCORD_GUILD_ID=
|
||||
# DISCORD_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
|
||||
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
|
||||
# TELEGRAM_BOT_TOKEN=
|
||||
# TELEGRAM_GATEWAY_URL=http://localhost:14242
|
||||
|
||||
|
||||
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
|
||||
|
||||
# --- Authentik (optional — set AUTHENTIK_CLIENT_ID to enable) ---
|
||||
# AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic/
|
||||
# AUTHENTIK_CLIENT_ID=
|
||||
# AUTHENTIK_CLIENT_SECRET=
|
||||
|
||||
# --- WorkOS (optional — set WORKOS_CLIENT_ID to enable) ---
|
||||
# WORKOS_ISSUER=https://your-company.authkit.app
|
||||
# WORKOS_CLIENT_ID=client_...
|
||||
# WORKOS_CLIENT_SECRET=sk_live_...
|
||||
|
||||
# --- Keycloak (optional — set KEYCLOAK_CLIENT_ID to enable) ---
|
||||
# KEYCLOAK_ISSUER=https://auth.example.com/realms/master
|
||||
# Legacy alternative if you prefer to compose the issuer from separate vars:
|
||||
# KEYCLOAK_URL=https://auth.example.com
|
||||
# KEYCLOAK_REALM=master
|
||||
# KEYCLOAK_CLIENT_ID=mosaic
|
||||
# KEYCLOAK_CLIENT_SECRET=
|
||||
|
||||
# The web login page discovers configured providers dynamically from
|
||||
# GET /api/sso/providers. No NEXT_PUBLIC_* provider feature flag is required.
|
||||
# Optional: documented env-var auth alternative (secret! set in your
|
||||
# shell or a gitignored .env, never commit)
|
||||
#ZAI_API_KEY=
|
||||
#ANTHROPIC_API_KEY=
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
logs/
|
||||
node_modules
|
||||
dist
|
||||
.turbo
|
||||
.next
|
||||
coverage
|
||||
# build/deps
|
||||
node_modules/
|
||||
|
||||
# runtime credentials — never commit, never copy into the image
|
||||
.env
|
||||
.env.local
|
||||
*.tsbuildinfo
|
||||
.pnpm-store
|
||||
__pycache__/
|
||||
docs/.obsidian
|
||||
secrets/
|
||||
|
||||
# Step-CA dev password — real file is gitignored; commit only the .example
|
||||
infra/step-ca/dev-password
|
||||
|
||||
# Scratch dirs created by the framework git-wrapper shell test harnesses
|
||||
.mosaic-test-work/
|
||||
|
||||
# Transient config files vite/vitest/esbuild write next to a *.config.ts while
|
||||
# loading it, then unlink. They are untracked but were not ignored, so turbo's
|
||||
# package traversal hashed them and intermittently failed CI with "Package
|
||||
# traversal error: ... .timestamp-*.mjs: No such file or directory" when the
|
||||
# file vanished mid-scan. Ignoring them removes the race.
|
||||
*.timestamp-*.mjs
|
||||
# generated runtime state lives in /home/jwoltje/.mosaic-dev (outside this project)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
extensions/
|
||||
extensions.installed.sha256
|
||||
.extensions-*
|
||||
state/
|
||||
evidence/
|
||||
native-test-*.log
|
||||
@@ -0,0 +1,34 @@
|
||||
# Native goal development copy
|
||||
|
||||
From this repository, start a fresh native Pi session:
|
||||
|
||||
```sh
|
||||
bash scripts/goal-dev.sh
|
||||
```
|
||||
|
||||
Canonical source lives under `extensions/`. The launcher first runs `scripts/sync-dev-extensions.sh`, which installs verified ordinary-file copies under `.pi/extensions/`, then loads only the generated goal extension. Global extensions remain unloaded. The launcher keeps your usual native Pi provider authentication; it copies no credentials. Goal state and new conversation files live under `.pi/state/`, which is ignored by Git. Each process gets a fresh incarnation; `/reload` and `/new` in the same process retain its goal. Restarting Pi does not adopt an earlier process's active goal.
|
||||
|
||||
Plain `pi` also discovers `.pi/extensions/goal/index.ts` after project trust, but may load global extensions too. Use the launcher to avoid duplicate `/goal` registrations. This is a local development test, not a sandbox or the managed Mosaic runtime. Docker and `~/.mosaic` are unchanged.
|
||||
|
||||
## Try it
|
||||
|
||||
1. Set `/goal <a long goal with acceptance criteria>`. This starts work immediately.
|
||||
2. Look below the editor for `Goal: Active`. The old above-editor goal widget is gone.
|
||||
3. Run bare `/goal`, then press `Alt+G`. Both show the entire stored goal and its status. Tab remains autocomplete.
|
||||
4. Use `/goal stop` and `/goal resume`. Expect Paused and Active, or Waiting if an untimed wait remains recorded.
|
||||
5. A blocked `goal_report` displays Blocked. A satisfied report displays Complete and retains the full goal for recall without continuing work.
|
||||
6. `/goal clear` removes the retained goal. Try `NO_COLOR=1 bash scripts/goal-dev.sh` to check text-only labels.
|
||||
|
||||
Use terminal scrollback for recall longer than the screen. At narrow widths Pi may truncate its footer status row; bare `/goal` and Alt+G remain available.
|
||||
|
||||
## Checks
|
||||
|
||||
```sh
|
||||
node --test extensions/goal/test/*.test.ts
|
||||
bash scripts/test-extension-package.sh
|
||||
python3 scripts/test-goal-native.py
|
||||
```
|
||||
|
||||
Contract tests use ordinary read-only fixture copies in `test/fixtures/skills-local/`, not live brain files. The executive-update fixture SHA-256 matches the parser's pinned contract, `bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319`.
|
||||
|
||||
`SOURCE-SNAPSHOT.json` records the original external-source baseline, not the edited candidate. No symlinks are used. Never edit `.pi/extensions/`; the sync script refuses to overwrite installation drift. Make changes under `extensions/`, run the checks, and relaunch. To disable the test, stop its Pi process and remove `.pi/extensions/`. Keep `.pi/state/` only if you need local test state.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"snapshotVersion": 1,
|
||||
"copiedAt": "2026-09-06T04:58:22Z",
|
||||
"source": "~/.mosaic/fleet/extensions",
|
||||
"goalTreeSha256": "8853f2b72dde3e87c4573648b9a931c1c75da87ccde995c3224e6d2e707a75f0",
|
||||
"mosaicCoreLibTreeSha256": "d1194dce31209e5773c6cc5ce571cbca3c39b29d943a79dea06665e05d29f319",
|
||||
"symlinks": false,
|
||||
"autoDiscoveredExtensions": ["goal"],
|
||||
"purpose": "Issue #54 native Pi NG development copy; never loaded by Docker"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compatibility entrypoint for the accepted native test command.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
exec scripts/goal-dev.sh "$@"
|
||||
@@ -1,186 +1,124 @@
|
||||
# Agent Guidelines — Mosaic Stack
|
||||
# AGENTS.md — Mosaic Stack rebuild (`mosaicstack/stack`, branch `refactor`)
|
||||
|
||||
## Required Load Order
|
||||
Operational context for any agent session working in this repository.
|
||||
Read top to bottom; it is deliberately short — depth lives in the files it
|
||||
points to, not here.
|
||||
|
||||
1. `~/.config/mosaic/SOUL.md`
|
||||
2. `~/.config/mosaic/STANDARDS.md`
|
||||
3. `~/.config/mosaic/AGENTS.md`
|
||||
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
|
||||
5. `AGENTS.md` (this file)
|
||||
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
|
||||
## What this repository is
|
||||
|
||||
## Project Context
|
||||
Canonical checkout: `/mnt/storage/src/mosaic-stack`, origin `mosaicstack/stack`,
|
||||
working branch `refactor` (Jason-authorized conversion, issue #1495).
|
||||
The new foundation is at the root. `v1/` is archived legacy source, not the current
|
||||
implementation; its instructions and tools do not govern the new foundation.
|
||||
`~/src/mosaic-stack-dev-test` is a compatibility symlink to this checkout, not a
|
||||
second working tree. Both original Git histories are retained. Conversion receipt:
|
||||
`docs/plans/2026-09-07_repository-consolidation-completed.md`.
|
||||
|
||||
Mosaic Stack is a self-hosted, multi-user AI agent platform. It is a TypeScript monorepo with a NestJS gateway, Next.js dashboard, Pi SDK agent runtime, and Discord/Telegram plugin architecture.
|
||||
A rebuild of Mosaic Stack: a file-based, fail-closed
|
||||
orchestration foundation that dispatches sandboxed headless pi workers to do
|
||||
real work, with immutable run records as evidence. Thirteen-plus tagged
|
||||
milestones (`git tag -l`) from `poc-container-hello-v0` to today; suites
|
||||
green at every step. Not production software — a proven foundation.
|
||||
|
||||
### Stack
|
||||
## Non-negotiable invariants (the canon)
|
||||
|
||||
- **API:** NestJS with Fastify (`apps/gateway`)
|
||||
- **Web:** Next.js 16 with React 19 (`apps/web`)
|
||||
- **ORM and database:** Drizzle ORM, PostgreSQL 17, and pgvector (`packages/db`)
|
||||
- **Authentication:** BetterAuth (`packages/auth`)
|
||||
- **Agent runtime:** Pi SDK (`apps/gateway`, `packages/mosaic`)
|
||||
- **Queue:** Valkey 8 (`packages/queue`)
|
||||
- **Build:** pnpm workspaces and Turborepo
|
||||
- **CI:** Woodpecker CI
|
||||
- **Observability:** OpenTelemetry and Jaeger
|
||||
1. **Root is bootstrap-only.** First-class system configuration lives at the
|
||||
repository root; everything else gets a dedicated directory (`roles/`,
|
||||
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
|
||||
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
|
||||
config — created only by `scripts/bootstrap.sh`, never overwritten,
|
||||
fail-closed on any problem. Repo-scoped role authority lives in
|
||||
`roles/*.json` (versioned, reviewed commits only).
|
||||
3. **Secrets** never enter the repository or container images; auth is
|
||||
runtime-only (read-only mount or environment variable).
|
||||
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
|
||||
tasks are declarative JSON with strict schemas.
|
||||
5. **Run records** under `<dataRoot>/runs/` are write-once evidence — never
|
||||
rewritten, only pruned via `prune` with a receipt.
|
||||
6. **Fail closed**: missing or invalid config/policy refuses the operation.
|
||||
Never improvise around a refusal; diagnose it.
|
||||
7. **Policy**: missions govern tasks (least-privilege intersection — a task
|
||||
narrows, never widens). Role authority is declared in `roles/` and changes
|
||||
only via reviewed commits.
|
||||
8. **Git**: commit only after applicable suites are green. Work on the
|
||||
owner-authorized `refactor` branch; never force-push. Push remains an explicit
|
||||
act. Do not merge into `next` or `main` without separate authorization.
|
||||
`scripts/conductor-apply.sh` commits locally; it does not authorize a push.
|
||||
9. **Append-only logs**: BUILD-LOG.md (phases), `activation-log.jsonl`,
|
||||
`.pruned.log`, docs/SESSIONS.md. Corrections are new entries, never edits.
|
||||
|
||||
### Package Map
|
||||
## Session protocol (mandatory)
|
||||
|
||||
| Package | Purpose | Key Dependencies |
|
||||
| ------------------ | ----------------------------- | -------------------------------- |
|
||||
| `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL |
|
||||
| `apps/web` | Next.js dashboard | React 19, Tailwind |
|
||||
| `packages/types` | Shared TypeScript contracts | class-validator |
|
||||
| `packages/db` | Drizzle schema and migrations | drizzle-orm, postgres |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
|
||||
| `packages/brain` | Structured data layer | @mosaicstack/db |
|
||||
| `packages/queue` | Valkey task queue and MCP | ioredis |
|
||||
| `packages/coord` | Mission coordination | @mosaicstack/queue |
|
||||
| `packages/mosaic` | Unified `mosaic` CLI and TUI | Ink, Pi SDK, commander |
|
||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
||||
- **Register** your session in `docs/SESSIONS.md` — one append-only line
|
||||
(date, actor, scope, outcome). Never rewrite or remove entries.
|
||||
- **Cadence**: read `docs/plans/CURRENT.md` → execute its single next action
|
||||
fully (implement → test → verify against acceptance criteria → commit →
|
||||
push → close issue) → update CURRENT.md → register in SESSIONS.md.
|
||||
- "next" means one action. A batch mandate ("run the queue") repeats the
|
||||
loop until green or blocked. Blocked means stop and report, never improvise.
|
||||
- Substantial work gets a Gitea issue and a BUILD-LOG phase entry
|
||||
(before/after, with corrections recorded honestly).
|
||||
|
||||
## Architecture and Code Conventions
|
||||
## Role model
|
||||
|
||||
1. Gateway is the single API surface; all clients connect through it.
|
||||
2. Pi SDK is ESM-only; gateway and CLI code must remain ESM.
|
||||
3. Use `"type": "module"`, NodeNext module resolution, and `.js` extensions in imports.
|
||||
4. Keep typed Socket.IO events in `@mosaicstack/types` to enforce client/server contracts.
|
||||
5. Import OTEL tracing before NestJS bootstrap (`import './tracing.js'`).
|
||||
6. Use explicit `@Inject()` decorators in NestJS because tsx/esbuild does not emit decorator metadata.
|
||||
7. Keep DTOs in `*.dto.ts` files at module boundaries.
|
||||
8. BetterAuth owns authentication tables; their schema is defined in `@mosaicstack/db`.
|
||||
9. Create a task-specific scratchpad for non-trivial work.
|
||||
- **Conductor**: a system-scoped role — not an agent, not a daemon. Holds
|
||||
git/credentials/policy authority; decomposes, dispatches, reviews,
|
||||
verifies, integrates. Protocol: `docs/plans/CONDUCTOR.md`. Exists only
|
||||
when invoked; push is never automatic.
|
||||
- **Workers**: headless pi via `scripts/run-task.sh` — sandboxed workspace,
|
||||
tools allowlist, optional persistent sessions and forks; no git, no
|
||||
credentials, no policy control.
|
||||
- Worker runs deliberately exclude this file (`--no-context-files` in the
|
||||
adapter): worker context is contracts + mission via the generated system
|
||||
prompt. This file is for conductor-level sessions.
|
||||
|
||||
## Development Workflow
|
||||
## Command surface
|
||||
|
||||
Requirements: Node.js 20+, pnpm 10.6.2, and Docker Compose when optional local services are needed.
|
||||
`scripts/bootstrap.sh` (idempotent) · `build.sh` · `hello.sh` ·
|
||||
`verify.sh` · `run-task.sh run <task.json>` · `release.sh
|
||||
package|activate|rollback|status` · `auth.sh status|accounts` · `reset.sh` (**danger**: wipes the data
|
||||
root; triple-safety-checked) · `mosaic-task.mjs validate|run|show|list|retry|prune|resolve-role` ·
|
||||
`agent.sh <name>` (interactive TUI agent) ·
|
||||
suites: `test-config.sh`, `test-task.sh`, `test-release.sh`,
|
||||
`test-conductor.sh`, `test-auth.sh`.
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm preflight
|
||||
Full reference — usage, fields, exit codes, safety notes:
|
||||
`docs/TOOLS.md` (read on demand; do not rely on this summary for detail).
|
||||
|
||||
# Optional local queue service only; do not start the full Compose stack.
|
||||
docker compose up -d valkey
|
||||
```
|
||||
## Data map (canon)
|
||||
|
||||
The pre-push hook requires:
|
||||
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
|
||||
auto-written).
|
||||
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
|
||||
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
|
||||
- `sessions/` — pi JSONL session trees, one directory per named session
|
||||
- `workspaces/` — agent file effects (persistent or `:run` ephemeral)
|
||||
- `state/` — release pointer + append-only activation/auto-apply logs
|
||||
- Ownership is per-directory; nothing shares state. Directory map and
|
||||
lifecycle rules: README.md "Data map" section.
|
||||
|
||||
```bash
|
||||
pnpm preflight && pnpm typecheck && pnpm lint && pnpm format:check
|
||||
```
|
||||
## Pointers (depth lives here)
|
||||
|
||||
Software delivery also requires the applicable tests. Common repository commands are:
|
||||
- `docs/plans/CURRENT.md` — THE next action (single source of "what now")
|
||||
- `docs/plans/ROADMAP.md` — agreed milestone path (M16+)
|
||||
- `docs/plans/CONDUCTOR.md` — orchestration protocol and guardrails
|
||||
- `docs/plans/2026-09-02_atomic-mosaic-foundation.md` — architecture, invariants
|
||||
- `docs/plans/2026-09-03_autonomous-run.md` — batch-run tracker
|
||||
- `BUILD-LOG.md` — append-only build/verification history with corrections
|
||||
- `LAYERS.md` — implemented vs deferred layers
|
||||
- `docs/SESSIONS.md` — session registry
|
||||
- `adapters/README.md` — the harness adapter contract
|
||||
- `roles/` — role contracts (conductor, future agent/coder/reviewer)
|
||||
|
||||
```bash
|
||||
pnpm typecheck # TypeScript checks across the workspace
|
||||
pnpm lint # ESLint across the workspace
|
||||
pnpm test # Checkout tests and package Vitest suites
|
||||
pnpm format:check # Prettier check
|
||||
pnpm build # Build all packages and applications
|
||||
```
|
||||
## Recovery rule
|
||||
|
||||
## Branch Model and Merge Process — `main` and `next` (CANONICAL)
|
||||
Compacted, restarted, or new? Nothing that matters is lost: this file +
|
||||
`docs/plans/CURRENT.md` + `git log --oneline -10` + the suites reconstruct
|
||||
the full state. **Never guess** — verify with the suites; the run records
|
||||
and logs hold the receipts.
|
||||
|
||||
**Every contribution targets `next` first. No exceptions.** Features, fixes, tests,
|
||||
docs, and policy changes all take the same route; urgency changes queue priority,
|
||||
never the route. Agents never commit to or merge into `main`.
|
||||
## Version pin
|
||||
|
||||
| Branch | Role | Who merges into it |
|
||||
| ------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| `next` | Integration trunk — the only PR target for contributions | The designated merge-gate agent, after all gates pass. Never the PR author. |
|
||||
| `main` | Stable/release line — receives promotion merges from `next` only | Jason only (or an agent he explicitly delegates for a named promotion). |
|
||||
|
||||
### Contribution sequencing (in order, no skipping)
|
||||
|
||||
1. **Issue first.** Work is tracked in a Gitea issue before a branch exists. The
|
||||
issue number appears in the branch name and the PR body.
|
||||
2. **Branch from the current `origin/next` head.** Name it
|
||||
`feat/…`, `fix/…`, `docs/…`, or `test/…` with the issue number
|
||||
(e.g. `docs/1214-branch-process`). Record the base SHA in the PR body.
|
||||
3. **Develop with evidence.** Applicable tests accompany the change. Hooks are
|
||||
never bypassed (`--no-verify` is prohibited). Stage explicit paths — never
|
||||
`git add -A`.
|
||||
4. **Open the PR against `next`.** The body states: scope, base SHA,
|
||||
verification commands with results, and any known pre-existing failures on
|
||||
the base — documented, not retried to green and not absorbed silently.
|
||||
5. **CI must be terminal-green on the exact head.** All bounded Woodpecker
|
||||
steps succeed (`verify-terminal-green` contract). Pipelines for fork PRs
|
||||
start `blocked`; a maintainer approves the run — approving CI is not
|
||||
approving the PR.
|
||||
6. **Independent review. Self-merge is prohibited** — for every agent, on every
|
||||
PR, including trivial ones. Where the change touches protected or
|
||||
contract-bearing content, the reviewer verifies the exact head
|
||||
(exact-byte/exact-blob comparison), not a description of it. An `AMEND`
|
||||
verdict returns the PR to its author; the reviewer's gate stays held until
|
||||
a fresh exact head passes.
|
||||
7. **Merge into `next`** happens only after CI green + review pass, pinned to
|
||||
the reviewed head SHA (a post-review push voids the review).
|
||||
8. **Promotion `next` → `main`** is a deliberate, Jason-owned reconciliation
|
||||
merge — not part of any contribution's lifecycle. Contributors are done at
|
||||
step 7.
|
||||
|
||||
### Responsibilities
|
||||
|
||||
- **Contributor** — base pinning, green CI, evidence in the PR body,
|
||||
responding to AMEND verdicts, never merging own work.
|
||||
- **Reviewer / merge gate** — independent verification on the exact head;
|
||||
holds and lifts gates; executes the merge into `next`.
|
||||
- **Orchestrator / adjudicator** — cross-PR sequencing, disposition when PRs
|
||||
collide, conflict adjudication.
|
||||
- **Jason** — `next` → `main` promotions, merge-authority grants, collaborator
|
||||
and token provisioning. Agents cannot grant themselves or each other any of
|
||||
these.
|
||||
|
||||
### Hotfixes and divergence
|
||||
|
||||
- A hotfix follows the same path: branch from `next`, PR to `next`, gates,
|
||||
merge, then an expedited Jason-owned promotion if `main` needs it urgently.
|
||||
Committing the fix to `main` directly is prohibited even under pressure.
|
||||
- **Never land work on `main` that is not on `next`.** This has happened
|
||||
(issue #1152's goal controller reached `main` without reaching `next`) and
|
||||
every later PR paid for it. If it happens anyway: transplant the work onto
|
||||
a `next`-based branch with provenance-preserving commits
|
||||
(`git cherry-pick -x` or explicit SHA references in the messages), PR it
|
||||
through the normal gates, and let promotion re-align `main`. Do not
|
||||
hand-patch `main` to compensate.
|
||||
- Force-pushing a branch you do not own is prohibited; rebasing your own PR
|
||||
branch is fine before review, and voids any review already given.
|
||||
|
||||
## Database and Local Runtime Safety
|
||||
|
||||
- Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset.
|
||||
- PostgreSQL execution is held until KBN-101-00, KBN-101-03, and KBN-101-05 land.
|
||||
- Do not invoke a migration runner, initialization SQL, or the Compose PostgreSQL service from this checkout.
|
||||
- Do not start Gateway/Web or run root `pnpm dev` as a local PGlite route. The current dotenv loader can inherit a daemon PostgreSQL DSN; KBN-101-02 must make that path fail closed first.
|
||||
- Migration artifact generation is offline and does not authorize PostgreSQL access:
|
||||
|
||||
```bash
|
||||
pnpm --filter @mosaicstack/db db:generate
|
||||
```
|
||||
|
||||
## docs/TASKS.md — Schema (CANONICAL)
|
||||
|
||||
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
|
||||
|
||||
| Value | When to use | Budget |
|
||||
| --------- | ----------------------------------------------------------- | -------------------------- |
|
||||
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
|
||||
| `glm-5.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
|
||||
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
|
||||
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
|
||||
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
|
||||
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
|
||||
|
||||
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
|
||||
|
||||
**Full schema:**
|
||||
|
||||
```
|
||||
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
|
||||
```
|
||||
|
||||
- `status`: `not-started` | `in-progress` | `done` | `failed` | `blocked` | `needs-qa`
|
||||
- `agent`: model value from table above (set before spawning)
|
||||
- `estimate`: token budget e.g. `8K`, `25K`
|
||||
`@earendil-works/pi-coding-agent` is pinned exactly (see `package.json` /
|
||||
`RELEASE`); never install unversioned. Release identity: `RELEASE` file
|
||||
(0.0.X until declared stable); image tags derive from it.
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
# Minimal Mosaic Stack container proof of concept
|
||||
|
||||
## Purpose
|
||||
|
||||
Build the smallest isolated container that can:
|
||||
- launch Pi
|
||||
- load a small set of Mosaic-style contract files
|
||||
- send one real request to a model
|
||||
- return a known response.
|
||||
|
||||
This is a standalone experiment. It is not part of the existing Mosaic Stack repository or Software Factory.
|
||||
|
||||
## Working boundary
|
||||
|
||||
The directory containing this brief is the project root.
|
||||
|
||||
### Do not read, copy, mount, import, or modify anything from:
|
||||
- `/home/jwoltje/.mosaic`
|
||||
- `/home/jwoltje/.config/mosaic`
|
||||
- `/home/jwoltje/src/mosaic-stack`
|
||||
- Existing Mosaic Stack worktrees
|
||||
|
||||
### Do not use:
|
||||
- Mosaic orchestration
|
||||
- Mosaic Git wrappers
|
||||
- Fleet agents
|
||||
- Fleet communication
|
||||
- Mosaic role policies
|
||||
- Existing Mosaic contract files
|
||||
- Existing Mosaic runtime state
|
||||
|
||||
No Git credentials, issue, pull request, reviewer, merge, or deployment are required for this experiment.
|
||||
|
||||
Nothing from this experiment may be copied into the existing Mosaic Stack repository until it receives a separate review later.
|
||||
|
||||
## Runtime data
|
||||
|
||||
Use this host directory only for generated runtime data:
|
||||
|
||||
```text
|
||||
/home/jwoltje/.mosaic-dev
|
||||
```
|
||||
|
||||
The source code must remain in the project directory containing this brief.
|
||||
|
||||
Inside the container, use:
|
||||
|
||||
```text
|
||||
/opt/mosaic/contracts Immutable contract files
|
||||
/var/lib/mosaic Generated runtime state
|
||||
/workspace Agent workspace
|
||||
```
|
||||
|
||||
Mount /home/jwoltje/.mosaic-dev at /var/lib/mosaic.
|
||||
|
||||
### Required proof
|
||||
|
||||
The finished experiment must prove one path:
|
||||
|
||||
1. Build one container image.
|
||||
2. Start one Pi agent inside the container.
|
||||
3. Load four local contract files from /opt/mosaic/contracts.
|
||||
4. Send a request that does not contain the expected response.
|
||||
5. Receive MOSAIC_HELLO_OK from the agent.
|
||||
6. Exit successfully when the response matches.
|
||||
7. Exit nonzero when the response does not match.
|
||||
|
||||
This is the entire required functional result.
|
||||
|
||||
### Required discovery
|
||||
|
||||
Before writing the runtime command:
|
||||
|
||||
1. Find the current package documentation for @earendil-works/pi-coding-agent.
|
||||
2. Determine the current package version.
|
||||
3. Determine the supported noninteractive command.
|
||||
4. Determine how Pi accepts a custom system prompt or system prompt file.
|
||||
5. Determine Pi's documented container authentication method.
|
||||
6. Record the commands and findings in BUILD-LOG.md.
|
||||
|
||||
Do not guess CLI flags, authentication paths, or SDK methods.
|
||||
|
||||
Pin the selected Pi package version in the project. Do not install an unversioned package during each container start.
|
||||
|
||||
Prefer the Pi CLI. Use the Pi SDK only if the CLI cannot load the generated system prompt in noninteractive mode.
|
||||
|
||||
### Contract files
|
||||
|
||||
Create these files inside the project:
|
||||
|
||||
```text
|
||||
contracts/CONSTITUTION.md
|
||||
contracts/STANDARDS.md
|
||||
contracts/SOUL.md
|
||||
contracts/USER.md
|
||||
```
|
||||
|
||||
Use these exact contents.
|
||||
|
||||
### contracts/CONSTITUTION.md
|
||||
|
||||
```markdown
|
||||
# POC constitution
|
||||
|
||||
Never print credentials, tokens, or authentication files.
|
||||
|
||||
Follow the loaded system instructions before the user request.
|
||||
```
|
||||
|
||||
### contracts/STANDARDS.md
|
||||
|
||||
```markdown
|
||||
# POC standards
|
||||
|
||||
Answer startup verification requests with only the requested value.
|
||||
Do not add explanation or formatting.
|
||||
```
|
||||
|
||||
### contracts/SOUL.md
|
||||
|
||||
```markdown
|
||||
# POC identity
|
||||
|
||||
Your name is mosaic-poc-agent.
|
||||
|
||||
Your startup marker is MOSAIC_HELLO_OK.
|
||||
|
||||
When asked for your startup marker, return only the marker.
|
||||
```
|
||||
|
||||
### contracts/USER.md
|
||||
|
||||
```markdown
|
||||
# POC user
|
||||
|
||||
This is an isolated local runtime test.
|
||||
```
|
||||
|
||||
Contract loading
|
||||
|
||||
Create a small script that reads the four contract files in this order:
|
||||
|
||||
1. CONSTITUTION.md
|
||||
2. STANDARDS.md
|
||||
3. SOUL.md
|
||||
4. USER.md
|
||||
|
||||
Join them with clear file separators.
|
||||
|
||||
Write the generated system prompt to:
|
||||
|
||||
```text
|
||||
/var/lib/mosaic/system-prompt.md
|
||||
```
|
||||
|
||||
Pass that generated prompt to Pi using its documented CLI or SDK method.
|
||||
|
||||
Do not build:
|
||||
|
||||
- Contract schemas
|
||||
- Contract inheritance
|
||||
- Overlays
|
||||
- Role transitions
|
||||
- Dynamic policy loading
|
||||
- Guide routing
|
||||
- Manifest validation
|
||||
|
||||
Container
|
||||
|
||||
Create one service named:
|
||||
|
||||
```text
|
||||
mosaic-agent
|
||||
```
|
||||
|
||||
Use one Containerfile and one compose.yaml.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Use a maintained Node.js base image.
|
||||
- Run as a non-root user.
|
||||
- Install a pinned Pi package version.
|
||||
- Copy the local contract fixtures into /opt/mosaic/contracts.
|
||||
- Do not copy credentials into the image.
|
||||
- Do not mount the Docker socket.
|
||||
- Do not mount either live Mosaic directory.
|
||||
- Do not add a database, web server, queue, or second container.
|
||||
- The container may run as a one-shot command. It does not need to remain running.
|
||||
|
||||
### Authentication
|
||||
|
||||
Use Pi's documented authentication mechanism.
|
||||
|
||||
Authentication must be supplied at runtime through either:
|
||||
- A read-only mounted credential file
|
||||
- A supported runtime environment variable
|
||||
|
||||
**Never**:
|
||||
- Commit credentials
|
||||
- Copy credentials into the image
|
||||
- Print credentials
|
||||
- Print authentication files
|
||||
- Include credentials in BUILD-LOG.md
|
||||
- Store credentials under the project directory
|
||||
|
||||
Provide .env.example only for non-secret settings such as model or provider names.
|
||||
|
||||
If credentials are unavailable, complete the image and scripts but report that the real model request remains unverified. Do not fake the response.
|
||||
|
||||
### Required commands
|
||||
|
||||
Create these executable scripts:
|
||||
```text
|
||||
scripts/build.sh
|
||||
scripts/hello.sh
|
||||
scripts/verify.sh
|
||||
scripts/reset.sh
|
||||
```
|
||||
|
||||
### scripts/build.sh
|
||||
|
||||
Build the container image using Docker Compose.
|
||||
|
||||
### scripts/hello.sh
|
||||
|
||||
Run the mosaic-agent service as a one-shot container.
|
||||
|
||||
Send this exact user request:
|
||||
|
||||
```text
|
||||
Return your startup marker and nothing else.
|
||||
```
|
||||
|
||||
The request must not contain MOSAIC_HELLO_OK.
|
||||
|
||||
Print the model response without printing credentials or unrelated runtime data.
|
||||
|
||||
### scripts/verify.sh
|
||||
|
||||
Run the complete test.
|
||||
|
||||
**It must**:
|
||||
|
||||
1. Build or confirm the image is built.
|
||||
2. Run the agent request.
|
||||
3. Remove surrounding whitespace from the response.
|
||||
4. Compare the response with MOSAIC_HELLO_OK.
|
||||
5. Exit 0 only when they match exactly.
|
||||
6. Exit nonzero with a clear error when they do not match.
|
||||
|
||||
### scripts/reset.sh
|
||||
|
||||
Delete generated POC state only when all checks pass:
|
||||
1. The resolved path is exactly /home/jwoltje/.mosaic-dev.
|
||||
2. The path is not a symbolic link.
|
||||
3. The directory contains a .mosaic-poc-root ownership marker created by this project.
|
||||
|
||||
Refuse to delete anything if a check fails.
|
||||
|
||||
## Required files
|
||||
|
||||
The final project should contain only what the implementation needs:
|
||||
|
||||
```text
|
||||
BRIEF.md
|
||||
BUILD-LOG.md
|
||||
README.md
|
||||
LAYERS.md
|
||||
Containerfile
|
||||
compose.yaml
|
||||
package.json
|
||||
package-lock.json
|
||||
.gitignore
|
||||
contracts/
|
||||
scripts/
|
||||
src/
|
||||
```
|
||||
|
||||
Remove unused files and empty directories.
|
||||
|
||||
Build log
|
||||
|
||||
Create BUILD-LOG.md.
|
||||
|
||||
Treat it as append-only.
|
||||
|
||||
Before each phase, append:
|
||||
- Timestamp
|
||||
- Intended action
|
||||
- Reason
|
||||
- Expected result
|
||||
|
||||
After each phase, append:
|
||||
- Commands run
|
||||
- Observed result
|
||||
- Failure or correction
|
||||
|
||||
Never rewrite an earlier entry. Add a correction as a new entry.
|
||||
|
||||
Do not record credentials.
|
||||
|
||||
Initial decisions:
|
||||
- This is a standalone experiment outside the Mosaic Software Factory.
|
||||
- It does not use existing Mosaic source, tools, contracts, agents, or runtime state.
|
||||
- The first proof uses one Pi agent and four small local contract files.
|
||||
- The only required model result is MOSAIC_HELLO_OK.
|
||||
- Persistence, policy enforcement, Claude, orchestration, and portal work are deferred.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
The experiment passes when:
|
||||
1. scripts/build.sh exits 0.
|
||||
2. The image contains the four local contract files.
|
||||
3. The image contains no credentials.
|
||||
4. The container has no mounts from ~/.mosaic or ~/.config/mosaic.
|
||||
5. scripts/hello.sh performs a real model request.
|
||||
6. The request does not contain the expected marker.
|
||||
7. The agent returns exactly MOSAIC_HELLO_OK.
|
||||
8. scripts/verify.sh exits 0.
|
||||
9. Changing the expected value makes scripts/verify.sh exit nonzero.
|
||||
10. scripts/reset.sh refuses unsafe paths.
|
||||
11. Resetting and rerunning the verification produces the same successful result.
|
||||
|
||||
## Deferred layers
|
||||
|
||||
Document these in LAYERS.md. Do not implement them.
|
||||
|
||||
- L0: Container builds and returns MOSAIC_HELLO_OK.
|
||||
- L1: Persist and resume a named Pi session.
|
||||
- L2: Add a fixed tool permission policy.
|
||||
- L3: Load full versioned contract bundles.
|
||||
- L4: Add Claude as a second runtime.
|
||||
- L5: Add multiple agents and communication.
|
||||
- L6: Add orchestration, knowledge storage, and portal features.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Existing Mosaic Stack compatibility
|
||||
- Git hosting or CI
|
||||
- Pull requests or code review
|
||||
- Deployment
|
||||
- Persistent agent sessions
|
||||
- Tool read restrictions
|
||||
- Claude
|
||||
- Multiple agents
|
||||
- Fleet communication
|
||||
- Watchers
|
||||
- Role management
|
||||
- Knowledge storage
|
||||
- Database storage
|
||||
- API server
|
||||
- Web interface
|
||||
- Dashboard
|
||||
- Production security architecture
|
||||
|
||||
## Final report
|
||||
|
||||
When finished, report:
|
||||
|
||||
1. Files created.
|
||||
2. Pi package version.
|
||||
3. Exact build command.
|
||||
4. Exact verification command.
|
||||
5. Verification output with credentials removed.
|
||||
6. Whether the real model request passed.
|
||||
7. Any remaining failure.
|
||||
8. Anything implemented beyond this brief.
|
||||
|
||||
Do not describe the experiment as production-ready.
|
||||
@@ -1,5 +1 @@
|
||||
# Claude Compatibility Pointer
|
||||
|
||||
@AGENTS.md
|
||||
|
||||
Do not add project guidance here. Keep `AGENTS.md` authoritative so every agent runtime receives the same instructions.
|
||||
@AGENTS.md
|
||||
@@ -0,0 +1,43 @@
|
||||
# Minimal Mosaic Stack POC agent image.
|
||||
# Base: maintained Node.js image (same family as Pi's documented
|
||||
# containerization example in docs/containerization.md).
|
||||
FROM node:24-bookworm-slim
|
||||
|
||||
# Tools Pi's documented container image expects (bash, CA certs, git, ripgrep).
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Non-root user: the maintained node image ships a 'node' user at
|
||||
# uid/gid 1000, which matches the host user that owns the runtime
|
||||
# state directory mounted at /var/lib/mosaic. It is reused as-is.
|
||||
|
||||
# Pinned Pi install: package.json pins the exact version and
|
||||
# package-lock.json is installed with npm ci. No unversioned installs.
|
||||
WORKDIR /opt/app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Immutable contract fixtures (required location), runtime scripts, and
|
||||
# runtime adapters.
|
||||
COPY contracts /opt/mosaic/contracts
|
||||
COPY src /opt/mosaic/src
|
||||
COPY adapters /opt/mosaic/adapters
|
||||
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
|
||||
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
|
||||
&& chmod 0555 /opt/mosaic/adapters /opt/mosaic/adapters/*/adapter.sh
|
||||
|
||||
# Writable state, workspace, and pi agent directory (auth.json is
|
||||
# bind-mounted read-only at runtime; nothing is copied into the image).
|
||||
RUN mkdir -p /var/lib/mosaic /workspace /home/node/.pi/agent \
|
||||
&& chown -R node:node /var/lib/mosaic /workspace /home/node /opt/app
|
||||
|
||||
USER node
|
||||
WORKDIR /workspace
|
||||
ENV HOME=/home/node \
|
||||
PATH="/opt/app/node_modules/.bin:${PATH}" \
|
||||
PI_OFFLINE=1
|
||||
|
||||
# One-shot agent: args form the user request (default is the startup
|
||||
# verification request defined in compose.yaml).
|
||||
ENTRYPOINT ["/opt/mosaic/src/run-agent.sh"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# LAYERS
|
||||
|
||||
Deferred capability layers for the Mosaic experiment. Only L0 is implemented by
|
||||
this proof of concept; everything below it is documented here and deliberately
|
||||
not implemented (see BRIEF.md, "Explicit exclusions").
|
||||
|
||||
## L0 — Implemented: container returns MOSAIC_HELLO_OK
|
||||
|
||||
One image (`mosaic-poc-agent:0.84.4`, built on `node:24-bookworm-slim`, non-root,
|
||||
pinned Pi) runs one Pi agent one-shot. Four immutable local contract files are
|
||||
loaded in fixed order into the generated system prompt
|
||||
(`/var/lib/mosaic/system-prompt.md`). One real model request is sent
|
||||
noninteractively; the response must equal `MOSAIC_HELLO_OK` exactly or the
|
||||
verification exits nonzero. Authentication is supplied at runtime only
|
||||
(read-only mounted pi auth file, or a provider API key environment variable).
|
||||
|
||||
## L1 — Deferred: persist and resume a named Pi session
|
||||
|
||||
Keep a named Pi session across container runs (`--name`, session storage under
|
||||
`/var/lib/mosaic`), resume it with the documented session flags, and verify
|
||||
state survives a container restart.
|
||||
|
||||
## L2 — Deferred: fixed tool permission policy
|
||||
|
||||
Add a fixed allow/deny policy for Pi tools (e.g. restricting built-in tools via
|
||||
documented `--tools` / `--exclude-tools` or an extension-based permission gate),
|
||||
so contract files can constrain what the agent may do, not just what it says.
|
||||
|
||||
## L3 — Deferred: load full versioned contract bundles
|
||||
|
||||
Replace the four static fixtures with versioned contract bundles: bundle
|
||||
manifests, contract versions, and deterministic ordering/hashing, loaded from
|
||||
an immutable bundle artifact instead of files copied at image build time.
|
||||
|
||||
## L4 — Deferred: Claude as a second runtime
|
||||
|
||||
Add a second runtime (Claude) alongside the Pi agent in the same container
|
||||
stack, behind the same contract-loading path, to compare behavior across
|
||||
runtimes.
|
||||
|
||||
## L5 — Deferred: multiple agents and communication
|
||||
|
||||
Run several named agents with defined roles and a communication channel between
|
||||
them (message passing or shared state under `/var/lib/mosaic`).
|
||||
|
||||
## L6 — Deferred: orchestration, knowledge storage, and portal features
|
||||
|
||||
Fleet-level orchestration, knowledge storage, monitoring, and portal UI on top
|
||||
of L1-L5. This is where the existing Mosaic Stack concepts would be re-evaluated
|
||||
from first principles.
|
||||
@@ -1,415 +1,238 @@
|
||||
# Mosaic Stack
|
||||
# Mosaic Stack — new foundation
|
||||
|
||||
Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.
|
||||
The active rebuild is at this repository's root. The original Mosaic Stack v1
|
||||
source is archived under `v1/`; it is not the implementation being developed here.
|
||||
|
||||
Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi — injecting consistent system prompts, guardrails, skills, and mission context into every session. A NestJS gateway provides the API surface, a Next.js dashboard gives you the UI, and a plugin system connects Discord, Telegram, and more.
|
||||
- Canonical checkout: `/mnt/storage/src/mosaic-stack`
|
||||
- Repository: `mosaicstack/stack`
|
||||
- Working branch: `refactor`
|
||||
- Former `~/src/mosaic-stack-dev-test`: compatibility symlink to this same checkout
|
||||
|
||||
## Quick Install
|
||||
Both original Git histories and pending development work are preserved. See the
|
||||
[conversion record](docs/plans/2026-09-07_repository-consolidation-completed.md)
|
||||
and [current next action](docs/plans/CURRENT.md). Do not use v1's startup commands,
|
||||
package layout or agent instructions for work on the new foundation.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||
## Original container proof
|
||||
|
||||
The foundation began as a standalone container experiment. One container image
|
||||
runs one Pi coding agent with four immutable local contract files as its system
|
||||
prompt, sends exactly one real model request, and was verified to return exactly
|
||||
`MOSAIC_HELLO_OK`. This historical result is not a claim that the full rebuild is
|
||||
production-ready.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
BRIEF.md requirements for the original container proof
|
||||
BUILD-LOG.md append-only build/verification log
|
||||
LAYERS.md implemented layer (L0) and deferred layers (L1-L6)
|
||||
Containerfile image definition (node:24-bookworm-slim, non-root, pinned Pi)
|
||||
compose.yaml one service: mosaic-agent (one-shot; configured via env)
|
||||
package.json pins @earendil-works/pi-coding-agent at exactly 0.84.4
|
||||
package-lock.json resolved lockfile used by npm ci in the image
|
||||
.env.example non-secret settings only (credential-file path, env-var auth)
|
||||
contracts/ CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md (immutable fixtures)
|
||||
scripts/ bootstrap/build/hello/verify/reset + config tooling
|
||||
src/ load-contracts.sh, run-agent.sh (run inside the container)
|
||||
docs/plans/ architecture and milestone plans
|
||||
```
|
||||
|
||||
Or use the direct URL:
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||
The sole discovery entry point is:
|
||||
|
||||
```text
|
||||
~/.config/mosaic-dev/config.json
|
||||
```
|
||||
|
||||
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
|
||||
Created only by the explicit, idempotent bootstrap:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL …) --yes # Accept all defaults
|
||||
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
|
||||
scripts/bootstrap.sh # create-if-absent; validates existing config, never rewrites
|
||||
```
|
||||
|
||||
This installs both components:
|
||||
Minimal shape (`configVersion` 1):
|
||||
|
||||
| Component | What | Where |
|
||||
| ----------------------- | ---------------------------------------------------------------- | -------------------- |
|
||||
| **Framework** | Bash launcher, guides, runtime configs, tools, skills | `~/.config/mosaic/` |
|
||||
| **@mosaicstack/mosaic** | Unified `mosaic` CLI — TUI, gateway client, wizard, auto-updater | `~/.npm-global/bin/` |
|
||||
```json
|
||||
{
|
||||
"configVersion": 1,
|
||||
"environment": "development",
|
||||
"dataRoot": "/home/jwoltje/.mosaic-dev",
|
||||
"execution": {
|
||||
"backend": "docker",
|
||||
"provider": "zai",
|
||||
"model": "glm-5.3-flash"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Install lanes
|
||||
Rules enforced by `scripts/mosaic-config.mjs`:
|
||||
|
||||
| Lane | Command | Use when | Source |
|
||||
| ------------------------ | ------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| Stable | `bash tools/install.sh` | You want the released Mosaic CLI/framework | npm registry `@mosaicstack/mosaic@latest` + framework archive at `main` |
|
||||
| Prerelease integration | `bash tools/install.sh --next` | You want the current `next` integration branch | Build-from-source at `next` |
|
||||
| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are testing a branch before release; `--ref` wins | Build-from-source at the requested ref |
|
||||
- Unknown keys, unsupported versions/backends, and malformed JSON exit nonzero; nothing is modified.
|
||||
- `dataRoot` must be absolute, canonical, and must not be or contain the home or configuration directory.
|
||||
- Validation failures never touch config, state, or images.
|
||||
- `scripts/test-config.sh` runs the sandboxed config selftests (no Docker required).
|
||||
|
||||
`--next` is shorthand for the prerelease integration lane: it enables source-build mode and uses `next` unless an explicit `--ref` or `MOSAIC_REF` is provided.
|
||||
Run paths (`build/hello/verify/reset`) fail closed when configuration is missing or invalid; they never invent it.
|
||||
|
||||
After install, the wizard runs automatically or you can invoke it manually:
|
||||
## Missions & tasks (M2)
|
||||
|
||||
Missions and tasks are validated JSON data (strict schemas, version-pinned). The M2 layer is host-side only: mission directives are recorded for provenance but do not yet reach the runtime system prompt (capability/policy layer comes later).
|
||||
|
||||
```text
|
||||
missions/hello.json objective + directives (missionVersion 1)
|
||||
tasks/hello-marker.json prompt + optional mission ref + expectExact + timeout
|
||||
<dataRoot>/runs/r-<id>/ immutable run record: task.json, mission.json,
|
||||
stderr.txt, result.json (all write-once)
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
mosaic wizard # Full guided setup (gateway install → verify)
|
||||
scripts/run-task.sh validate tasks/hello-marker.json # strict validation, writes nothing
|
||||
scripts/run-task.sh run tasks/hello-marker.json # execute; result recorded under dataRoot/runs
|
||||
scripts/mosaic-task.mjs list # list runs and statuses
|
||||
scripts/test-task.sh # selftests (schema negatives + live runs)
|
||||
```
|
||||
|
||||
### Requirements
|
||||
A run exits 0 only when its expectation is met (`expectExact` match); mismatches, nonzero agent exits, and timeouts record `status: failed` in `result.json` and exit 1. Each run gets a unique directory — rerunning never rewrites history.
|
||||
|
||||
- Node.js ≥ 22
|
||||
- npm (for global @mosaicstack/mosaic install)
|
||||
- One or more runtimes:
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
||||
- [Codex](https://github.com/openai/codex)
|
||||
- [OpenCode](https://opencode.ai)
|
||||
- [Pi](https://pi.dev)
|
||||
## Release model (M3)
|
||||
|
||||
`RELEASE` single-sources the release version (0.0.X until declared stable); the image tag derives from it plus the pinned Pi version. Activation is health-gated and every event is recorded:
|
||||
|
||||
```bash
|
||||
scripts/release.sh package # build + tag the release image
|
||||
scripts/release.sh activate # health check (exact marker) -> atomic pointer swap
|
||||
scripts/release.sh activate --fault-injection # prove the refusal path (drills only)
|
||||
scripts/release.sh rollback # health-gated return to the previous release
|
||||
scripts/release.sh ensure # self-determination: align installed to RELEASE (safe no-op when aligned)
|
||||
scripts/release.sh status # release, tag, active pointer, recent log
|
||||
scripts/test-release.sh # release selftests
|
||||
```
|
||||
|
||||
`ensure` is invoked automatically by the human-facing launchers (`hello`,
|
||||
`verify`, `agent`): the system determines what is installed and aligns
|
||||
itself — the user never runs release commands manually.
|
||||
|
||||
- `<dataRoot>/state/active.json` — the activation pointer (atomic tmp+rename replace)
|
||||
- `<dataRoot>/state/activation-log.jsonl` — append-only history: package / activate / refused / rollback
|
||||
|
||||
A failed health check never activates; the previously active release remains deployed. Updating the software therefore cannot corrupt the running installation: package beside, gate, then flip. Verified by the update/refusal/rollback drills in BUILD-LOG Phase 7.
|
||||
|
||||
## Runtime adapters (M4)
|
||||
|
||||
The harness boundary is formalized: everything upstream (config, contracts, missions, tasks, run records) is harness-agnostic; everything inside an adapter belongs to one runtime.
|
||||
|
||||
```text
|
||||
adapters/<name>/adapter.sh env in: MOSAIC_SYSTEM_PROMPT_FILE, MOSAIC_REQUEST,
|
||||
MOSAIC_PROVIDER, MOSAIC_MODEL
|
||||
stdout: response only; stderr: diagnostics
|
||||
```
|
||||
|
||||
- Selection: `execution.adapter` in config.json (optional; `pi` default; allowlist `pi`, `mock`)
|
||||
- `pi` — pinned Pi CLI, noninteractive print mode, ambient discovery off
|
||||
- `mock` — deterministic test adapter; never for real verification
|
||||
- Mission directives have a sanctioned injection point: when a task references a mission, the task runner mounts the run snapshot and the generated prompt gains a `MISSION (runtime)` section (objective + directives) after the four immutable contracts
|
||||
- Adding a harness (Claude, Codex, OpenCode) later means adding one directory — no orchestrator changes
|
||||
|
||||
See `adapters/README.md` for the full contract.
|
||||
|
||||
## Workspaces, capabilities, sessions (M5/M6)
|
||||
|
||||
Optional task fields extend what an agent can do — all defaulting to the previous behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace": "demo", // ":run" ephemeral, or persistent dataRoot/workspaces/<name>
|
||||
"capabilities": { "tools": ["bash", "read"] }, // pi tool allowlist; absent = no tools
|
||||
"session": "demo" // persistent session at dataRoot/sessions/<name>
|
||||
}
|
||||
```
|
||||
|
||||
- The adapter runs inside the workspace; files it writes are host-visible (`dataRoot/workspaces/<name>`).
|
||||
- Sessions persist via pi's documented `--session-dir`; a follow-up run in the same session resumes the conversation (`-c`) and can recall prior context. Distinct names never share state. Ephemeral (`--no-session`) remains the default when no session is declared.
|
||||
- Selection authority: config for adapter/provider/model; the task file for workspace/capabilities/session.
|
||||
|
||||
Inspect anything:
|
||||
|
||||
```bash
|
||||
node scripts/mosaic-task.mjs list # runs with task/workspace/session columns
|
||||
node scripts/mosaic-task.mjs show <runId> # full record + snapshots + artifacts
|
||||
```
|
||||
|
||||
Demo fixtures: `tasks/workspace-demo.json`, `tasks/session-demo-1.json` + `tasks/session-demo-2.json`.
|
||||
|
||||
See `docs/plans/2026-09-02_atomic-mosaic-foundation.md` for the full plan.
|
||||
|
||||
Inside the container:
|
||||
|
||||
```text
|
||||
/opt/mosaic/contracts immutable contract files
|
||||
/var/lib/mosaic generated runtime state (mounted from configured dataRoot)
|
||||
/workspace agent workspace
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. `scripts/build.sh` builds the release image (`mosaic-poc-agent:<pi>-r<release>`,
|
||||
tag derived from `RELEASE` + the pinned Pi version) with Docker Compose.
|
||||
2. On each run, `/opt/mosaic/src/load-contracts.sh` reads the four contract files
|
||||
in fixed order (CONSTITUTION, STANDARDS, SOUL, USER), joins them with clear
|
||||
separators, and writes `/var/lib/mosaic/system-prompt.md`.
|
||||
3. `/opt/mosaic/src/run-agent.sh` starts Pi noninteractively
|
||||
(`pi -p "Return your startup marker and nothing else."`) with
|
||||
`--system-prompt "$(cat /var/lib/mosaic/system-prompt.md)"` and all ambient
|
||||
discovery disabled (`--no-context-files --no-skills --no-extensions
|
||||
--no-prompt-templates --no-themes`), ephemeral (`--no-session`), tool-free
|
||||
(`--no-tools`), and offline for startup network operations (`--offline`).
|
||||
4. `scripts/verify.sh` trims surrounding whitespace from the response and exits 0
|
||||
only when it equals `MOSAIC_HELLO_OK` exactly.
|
||||
|
||||
## Usage
|
||||
|
||||
### Launching Agent Sessions
|
||||
```bash
|
||||
scripts/bootstrap.sh # create config.json if absent (idempotent)
|
||||
scripts/build.sh # build the image
|
||||
scripts/hello.sh # one-shot request; prints the model response
|
||||
scripts/verify.sh # full gated test; exit 0 only on exact MOSAIC_HELLO_OK
|
||||
scripts/run-task.sh # run a mission/task file (see Missions & tasks)
|
||||
scripts/release.sh # package / activate / rollback / status (see Release model)
|
||||
scripts/test-config.sh # fast config-layer selftests (no Docker)
|
||||
scripts/test-task.sh # mission/task selftests (schema + adapter seam + live runs)
|
||||
scripts/test-release.sh # release selftests
|
||||
scripts/reset.sh # delete the configured data root (safety-checked)
|
||||
```
|
||||
|
||||
Prove the failure path (acceptance criterion 9):
|
||||
|
||||
```bash
|
||||
mosaic pi # Launch Pi with Mosaic injection
|
||||
mosaic claude # Launch Claude Code with Mosaic injection
|
||||
mosaic codex # Launch Codex with Mosaic injection
|
||||
mosaic opencode # Launch OpenCode with Mosaic injection
|
||||
|
||||
mosaic yolo claude # Claude with dangerous-permissions mode
|
||||
mosaic yolo pi # Pi in yolo mode
|
||||
EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh # must exit nonzero
|
||||
```
|
||||
|
||||
The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments.
|
||||
|
||||
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
|
||||
|
||||
Mosaic also loads its Pi extensions from `~/.config/mosaic/runtime/pi/`. Inside Pi,
|
||||
`/goal set <statement>` starts a bounded persistent loop that checks every turn and successful
|
||||
compaction, requires two evidence-bearing completion reports, and can be inspected or stopped with
|
||||
`/goal status`, `/goal pause`, `/goal resume`, and `/goal cancel`. Controller-owned goal-state
|
||||
entries redact common credential shapes, but Pi's model/tool-call history is separate, so goals and
|
||||
evidence must never contain secrets or raw sensitive output. Mosaic does not install this extension
|
||||
into `~/.pi/agent/extensions/`.
|
||||
|
||||
### TUI & Gateway
|
||||
|
||||
```bash
|
||||
mosaic tui # Interactive TUI connected to the gateway
|
||||
mosaic gateway login # Authenticate with a gateway instance
|
||||
mosaic sessions list # List active agent sessions
|
||||
```
|
||||
|
||||
### Gateway Management
|
||||
|
||||
```bash
|
||||
mosaic gateway install # Install and configure the gateway service
|
||||
mosaic gateway verify # Post-install health check
|
||||
mosaic gateway login # Authenticate and store a session token
|
||||
mosaic gateway config rotate-token # Rotate your API token
|
||||
mosaic gateway config recover-token # Recover a token via BetterAuth cookie
|
||||
```
|
||||
|
||||
If you already have a gateway account but no token, use `mosaic gateway config recover-token` to retrieve one without recreating your account.
|
||||
|
||||
### Configuration
|
||||
|
||||
Mosaic supports three storage tiers: `local` (PGlite, single-host), `standalone` (PostgreSQL, single-host), and `federated` (PostgreSQL + pgvector + Valkey, multi-host). See [Federated Tier Setup](docs/federation/SETUP.md) for multi-user and production deployments, or [Migrating to Federated](docs/guides/migrate-tier.md) to upgrade from existing tiers.
|
||||
|
||||
```bash
|
||||
mosaic config show # Print full config as JSON
|
||||
mosaic config get <key> # Read a specific key
|
||||
mosaic config set <key> <val># Write a key
|
||||
mosaic config edit # Open config in $EDITOR
|
||||
mosaic config path # Print config file path
|
||||
```
|
||||
|
||||
### Management
|
||||
|
||||
```bash
|
||||
mosaic doctor # Health audit — detect drift and missing files
|
||||
mosaic sync # Sync skills from canonical source
|
||||
mosaic skill list # Audit Claude skill registrations and conflicts
|
||||
mosaic skill register <name> # Register one canonical skill with Claude Code
|
||||
mosaic skill unregister <name> # Remove one Mosaic-owned Claude link
|
||||
mosaic update # Update CLI/framework and auto-register canonical skills
|
||||
mosaic wizard # Full guided setup wizard
|
||||
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
|
||||
mosaic coord init # Initialize a new orchestration mission
|
||||
mosaic prdy init # Create a PRD via guided session
|
||||
```
|
||||
|
||||
### Sub-package Commands
|
||||
|
||||
Each Mosaic sub-package exposes its API surface through the unified CLI:
|
||||
|
||||
```bash
|
||||
# User management
|
||||
mosaic auth users list
|
||||
mosaic auth users create
|
||||
mosaic auth sso
|
||||
|
||||
# Agent brain (projects, missions, tasks)
|
||||
mosaic brain projects
|
||||
mosaic brain missions
|
||||
mosaic brain tasks
|
||||
mosaic brain conversations
|
||||
|
||||
# Agent forge pipeline
|
||||
mosaic forge run [--simulate] # fails closed (FORGE_NO_EXECUTOR) with no executor wired; --simulate for typed simulated runs
|
||||
mosaic forge status
|
||||
mosaic forge resume [--simulate] # same fail-closed rule as forge run
|
||||
mosaic forge personas
|
||||
|
||||
# Structured logging
|
||||
mosaic log tail
|
||||
mosaic log search
|
||||
mosaic log export
|
||||
mosaic log level
|
||||
|
||||
# MACP protocol
|
||||
mosaic macp tasks
|
||||
mosaic macp submit
|
||||
mosaic macp gate
|
||||
mosaic macp events
|
||||
|
||||
# Agent memory
|
||||
mosaic memory search
|
||||
mosaic memory stats
|
||||
mosaic memory insights
|
||||
mosaic memory preferences
|
||||
|
||||
# Task queue (Valkey)
|
||||
mosaic queue list
|
||||
mosaic queue stats
|
||||
mosaic queue pause
|
||||
mosaic queue resume
|
||||
mosaic queue jobs
|
||||
mosaic queue drain
|
||||
|
||||
# Object storage
|
||||
mosaic storage status
|
||||
mosaic storage tier
|
||||
mosaic storage export
|
||||
mosaic storage import
|
||||
# Schema migration is unavailable in this release. The current storage wrapper shells
|
||||
# directly to `pnpm --filter @mosaicstack/db db:migrate`; it is legacy N-1,
|
||||
# uncertified, and MUST NOT be invoked pending KBN-101-02/-03/-06/-08 activation.
|
||||
# Future schema migration is non-operative: external bootstrap → TLS/roles → runner
|
||||
# --run → runner --verify → readiness. Tier copy uses only the separately held secure
|
||||
# migrate-tier route.
|
||||
```
|
||||
|
||||
### Telemetry
|
||||
|
||||
```bash
|
||||
# Local observability (OTEL / Jaeger)
|
||||
mosaic telemetry local status
|
||||
mosaic telemetry local tail
|
||||
mosaic telemetry local jaeger
|
||||
|
||||
# Remote telemetry (dry-run by default)
|
||||
mosaic telemetry status
|
||||
mosaic telemetry opt-in
|
||||
mosaic telemetry opt-out
|
||||
mosaic telemetry test
|
||||
mosaic telemetry upload # Dry-run unless opted in
|
||||
```
|
||||
|
||||
Consent state is persisted in config. Remote upload is a no-op until you run `mosaic telemetry opt-in`.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js ≥ 22
|
||||
- pnpm 10.6+
|
||||
- Docker & Docker Compose
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone [email protected]:mosaicstack/stack.git
|
||||
cd stack
|
||||
|
||||
# Install dependencies. The local tier uses in-process PGlite; leave DATABASE_URL unset.
|
||||
# The pnpm store defaults to $HOME/.local/share/pnpm/store. Override it without
|
||||
# editing the checkout with NPM_CONFIG_STORE_DIR=$HOME/another-store if needed.
|
||||
pnpm install
|
||||
|
||||
# Verify dependencies and generated state before running source-quality gates.
|
||||
# Missing dependencies exit 42; stale/foreign apps/web/.next state exits 43.
|
||||
# The web build certifies its exact standalone symlink manifest; added, removed,
|
||||
# retargeted, or manifest-only-tampered generated links also exit 43. This detects
|
||||
# accidental, independent, stale, and foreign-residue mutation—the class exposed by
|
||||
# a five-month-stale .next that produced 19 phantom TS2307 errors.
|
||||
# It does NOT defend against a same-UID actor that can rewrite both manifest and
|
||||
# marker consistently (CWE-345). RM-59 tracks the required executor/spine-side
|
||||
# trust anchor outside worktree authority.
|
||||
pnpm preflight
|
||||
|
||||
# Optional local queue service only. This does not start PostgreSQL.
|
||||
docker compose up -d valkey
|
||||
|
||||
# The current Gateway/Web local process is held; see docs/guides/dev-guide.md.
|
||||
# Do not start it until KBN-101-02 makes inherited dotenv/DSN state fail closed.
|
||||
```
|
||||
|
||||
### Held future procedure
|
||||
|
||||
The checked-in Compose PostgreSQL service mounts legacy initialization SQL and is **not** a
|
||||
current PostgreSQL, standalone, or federated developer route. Do not start it with Compose,
|
||||
invoke initialization SQL, or treat the planned migrator as currently executable.
|
||||
|
||||
**Held future activation procedure — non-operative and no current command authority until KBN-101-00, KBN-101-03, and KBN-101-05
|
||||
land:** external bootstrap → TLS/roles → `mosaic-db-migrator --run` →
|
||||
`mosaic-db-migrator --verify` → Gateway/Compose readiness. The future deployment artifacts—not
|
||||
this README—will provide the reviewed commands and secret-consumer interface.
|
||||
|
||||
For local data-layer work, PGlite needs no PostgreSQL service. The optional Compose command above
|
||||
starts only Valkey; OTEL Collector and Jaeger may likewise be started individually if needed,
|
||||
without starting PostgreSQL. A Gateway/Web local process is not currently a safe PGlite route:
|
||||
its unguarded dotenv loader may inherit a daemon PostgreSQL DSN. Do not use root `pnpm dev` or a
|
||||
Gateway start command until KBN-101-02 makes that state fail closed.
|
||||
|
||||
### Quality Gates
|
||||
|
||||
```bash
|
||||
pnpm preflight # Checkout/dependency/generated-state validation
|
||||
pnpm typecheck # TypeScript type checking (all packages)
|
||||
pnpm lint # ESLint (all packages)
|
||||
pnpm test # Vitest (all packages)
|
||||
pnpm format:check # Prettier check
|
||||
pnpm format # Prettier auto-fix
|
||||
```
|
||||
|
||||
### CI
|
||||
|
||||
Woodpecker CI runs on every push:
|
||||
|
||||
- `pnpm install --frozen-lockfile`
|
||||
- **Legacy N-1 CI status only — active, uncertified, and non-authorizing as an operator route:** the checked-in job currently invokes `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` against an isolated disposable PostgreSQL CI database. It performs direct DDL in that CI database, is not approved ordinary behavior or an operator route, and remains a known exception pending KBN-101-06 removal/replacement by the certified runner-backed CI path.
|
||||
- `pnpm test` (Turbo-orchestrated across all packages)
|
||||
|
||||
npm packages are published to the Gitea package registry on main merges.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
stack/
|
||||
├── apps/
|
||||
│ ├── gateway/ NestJS API + WebSocket hub (Fastify, Socket.IO, OTEL)
|
||||
│ └── web/ Next.js dashboard (React 19, Tailwind)
|
||||
├── packages/
|
||||
│ ├── mosaic/ Unified CLI — TUI, gateway client, wizard, sub-package commands
|
||||
│ ├── types/ Shared TypeScript contracts (Socket.IO typed events)
|
||||
│ ├── db/ Drizzle ORM schema + migrations (pgvector)
|
||||
│ ├── auth/ BetterAuth configuration
|
||||
│ ├── brain/ Data layer (PG-backed)
|
||||
│ ├── queue/ Valkey task queue + MCP
|
||||
│ ├── coord/ Mission coordination
|
||||
│ ├── forge/ Multi-stage AI pipeline (intake → board → plan → code → review)
|
||||
│ ├── macp/ MACP protocol — credential resolution, gate runner, events
|
||||
│ ├── agent/ Agent session management
|
||||
│ ├── memory/ Agent memory layer
|
||||
│ ├── log/ Structured logging
|
||||
│ ├── prdy/ PRD creation and validation
|
||||
│ ├── quality-rails/ Quality templates (TypeScript, Next.js, monorepo)
|
||||
│ └── design-tokens/ Shared design tokens
|
||||
├── plugins/
|
||||
│ ├── discord/ Discord channel plugin (discord.js)
|
||||
│ ├── telegram/ Telegram channel plugin (Telegraf)
|
||||
│ ├── macp/ OpenClaw MACP runtime plugin
|
||||
│ └── mosaic-framework/ OpenClaw framework injection plugin
|
||||
├── tools/
|
||||
│ └── install.sh Unified installer (framework + npm CLI, --yes / --no-auto-launch)
|
||||
├── scripts/agent/ Agent session lifecycle scripts
|
||||
├── docker-compose.yml Dev infrastructure
|
||||
└── .woodpecker/ CI pipeline configs
|
||||
```
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- **Gateway is the single API surface** — all clients (TUI, web, Discord, Telegram) connect through it
|
||||
- **ESM everywhere** — `"type": "module"`, `.js` extensions in imports, NodeNext resolution
|
||||
- **Socket.IO typed events** — defined in `@mosaicstack/types`, enforced at compile time
|
||||
- **OTEL auto-instrumentation** — loads before NestJS bootstrap
|
||||
- **Explicit `@Inject()` decorators** — required since tsx/esbuild doesn't emit decorator metadata
|
||||
|
||||
### Framework (`~/.config/mosaic/`)
|
||||
|
||||
The framework is the bash-based standards layer installed to every developer machine:
|
||||
|
||||
```
|
||||
~/.config/mosaic/
|
||||
├── AGENTS.md ← Central standards (loaded into every runtime)
|
||||
├── SOUL.md ← Agent identity (name, style, guardrails)
|
||||
├── USER.md ← User profile (name, timezone, preferences)
|
||||
├── TOOLS.md ← Machine-level tool reference
|
||||
├── bin/mosaic ← Unified launcher (claude, codex, opencode, pi, yolo)
|
||||
├── guides/ ← E2E delivery, orchestrator protocol, PRD, etc.
|
||||
├── runtime/ ← Per-runtime configs (claude/, codex/, opencode/, pi/)
|
||||
├── skills/ ← Universal skills (shipped with the framework package)
|
||||
├── tools/ ← Tool suites (orchestrator, git, quality, prdy, etc.)
|
||||
└── memory/ ← Persistent agent memory (preserved across upgrades)
|
||||
```
|
||||
|
||||
### Forge Pipeline
|
||||
|
||||
Forge is a multi-stage AI pipeline for autonomous feature delivery:
|
||||
|
||||
```
|
||||
Intake → Discovery → Board Review → Planning (3 stages) → Coding → Review → Remediation → Test → Deploy
|
||||
```
|
||||
|
||||
Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding), quality gates, and timeouts. The board review uses multiple AI personas (CEO, CTO, CFO, COO + specialists) to evaluate briefs before committing resources.
|
||||
|
||||
## Upgrading
|
||||
|
||||
Run the installer again — it handles upgrades automatically:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||||
```
|
||||
|
||||
Or use the direct URL:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||
```
|
||||
|
||||
Or use the CLI:
|
||||
|
||||
```bash
|
||||
mosaic update # Check + install CLI updates
|
||||
mosaic update --check # Check only, don't install
|
||||
```
|
||||
|
||||
The CLI also performs a background update check on every invocation (cached for 1 hour).
|
||||
|
||||
### Installer Flags
|
||||
|
||||
```bash
|
||||
bash tools/install.sh --check # Version check only
|
||||
bash tools/install.sh --framework # Framework only (skip npm CLI)
|
||||
bash tools/install.sh --cli # npm CLI only (skip framework)
|
||||
bash tools/install.sh --next # Prerelease lane: source build from next
|
||||
bash tools/install.sh --dev # Contributor lane: source build at --ref/main
|
||||
bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref wins over --next)
|
||||
bash tools/install.sh --yes # Non-interactive, accept all defaults
|
||||
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
|
||||
```
|
||||
|
||||
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage.
|
||||
|
||||
## Contributing
|
||||
|
||||
```bash
|
||||
# Create a feature branch
|
||||
git checkout -b feat/my-feature
|
||||
|
||||
# Make changes, then verify
|
||||
pnpm typecheck && pnpm lint && pnpm test && pnpm format:check
|
||||
|
||||
# Commit (husky runs lint-staged automatically)
|
||||
git commit -m "feat: description of change"
|
||||
|
||||
# Push and create PR
|
||||
git push -u origin feat/my-feature
|
||||
```
|
||||
|
||||
DTOs go in `*.dto.ts` files at module boundaries. Scratchpads (`docs/scratchpads/`) are mandatory for non-trivial tasks. See `AGENTS.md` for the full standards reference.
|
||||
|
||||
## License
|
||||
|
||||
Proprietary — all rights reserved.
|
||||
## Authentication
|
||||
|
||||
Pi's documented container authentication (see the package's
|
||||
`docs/containerization.md`) is used, in this order:
|
||||
|
||||
1. **Read-only mounted credential file** (default): the host pi auth file
|
||||
`~/.pi/agent/auth.json` is bind-mounted read-only to
|
||||
`/home/node/.pi/agent/auth.json`. The host file holds a static API-key
|
||||
entry for the built-in `zai` provider, so no token refresh writes are needed.
|
||||
2. **Runtime environment variable** (documented alternative): set `ZAI_API_KEY`
|
||||
or `ANTHROPIC_API_KEY` in the environment or in a gitignored `.env`; compose
|
||||
passes them through. Pi's documented precedence applies.
|
||||
|
||||
Credentials are never committed, never copied into the image, and never printed.
|
||||
Mosaic-managed named accounts (`agent.sh --auth`) live under the data root
|
||||
(`auth/<account>.json`, 0600) — the stack never writes into `~/.pi`.
|
||||
`.env.example` contains non-secret settings only.
|
||||
|
||||
## Boundaries honored
|
||||
|
||||
- No mounts of `~/.mosaic` or `~/.config/mosaic`; no Docker socket mount.
|
||||
- Source stays in this project directory; generated state only in
|
||||
`/home/jwoltje/.mosaic-dev` (host) and `/var/lib/mosaic` (container).
|
||||
- No database, web server, queue, second container, orchestration, Git
|
||||
integration, persistent sessions, or policy machinery.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Mosaic Stack
|
||||
|
||||
You are the default collaborator for Mosaic Stack: a practical engineering
|
||||
partner helping people build, inspect, and operate a trustworthy foundation
|
||||
for delegated work.
|
||||
|
||||
Mosaic Stack is deliberately small, file-based, and evidence-oriented. Its
|
||||
purpose is not to perform confidence; it is to make useful work attributable,
|
||||
bounded, reproducible, and reviewable. Treat the system's contracts, policies,
|
||||
and run records as part of the product, not paperwork around it.
|
||||
|
||||
Work with calm precision. Start from what the user is trying to accomplish,
|
||||
make the next useful step clear, and explain results in plain language. Be
|
||||
decisive when the evidence supports a decision; be explicit about uncertainty
|
||||
when it does not. Never claim a test, command, integration, or outcome that
|
||||
you have not actually verified.
|
||||
|
||||
Respect boundaries. Ask before expanding scope, changing authority, touching
|
||||
credentials, or taking an irreversible external action. Prefer the least
|
||||
privileged path, preserve user work, and stop on a policy or validation
|
||||
refusal rather than working around it. A clean refusal with a useful diagnosis
|
||||
is better than a superficially successful but untrustworthy result.
|
||||
|
||||
Leave a legible trail. Make changes intentional, keep records honest, and
|
||||
report what changed, how it was checked, and what remains unresolved. When
|
||||
coordinating other workers, give each one a bounded objective and review their
|
||||
evidence instead of treating their confidence as proof.
|
||||
|
||||
The aim is dependable progress: small enough to understand, safe enough to
|
||||
trust, and concrete enough for a person to verify.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Mosaic runtime adapters
|
||||
|
||||
An adapter is the entire harness-specific surface of the system. Everything
|
||||
upstream of an adapter — configuration, contracts, missions, tasks, run
|
||||
records — is harness-agnostic; everything inside an adapter may assume one
|
||||
specific agent runtime.
|
||||
|
||||
## Contract
|
||||
|
||||
An adapter lives at:
|
||||
|
||||
```text
|
||||
/opt/mosaic/adapters/<name>/adapter.sh
|
||||
```
|
||||
|
||||
and must be executable. The dispatcher (`/opt/mosaic/src/run-agent.sh`)
|
||||
selects it via `MOSAIC_ADAPTER` (default: `pi`) and execs it after the
|
||||
system prompt has been generated.
|
||||
|
||||
**Inputs (environment):**
|
||||
|
||||
| Variable | Meaning |
|
||||
|---|---|
|
||||
| `MOSAIC_SYSTEM_PROMPT_FILE` | Absolute path to the generated system prompt (contracts + optional mission section). Read it; do not modify it. |
|
||||
| `MOSAIC_REQUEST` | The exact user request text (may contain newlines). |
|
||||
| `MOSAIC_PROVIDER` | Configured provider name. |
|
||||
| `MOSAIC_MODEL` | Configured model id. |
|
||||
|
||||
Optional, adapter-specific (documented per adapter):
|
||||
|
||||
| Variable | Meaning |
|
||||
|---|---|
|
||||
| `MOSAIC_MOCK_RESPONSE` | mock only: the verbatim response to emit |
|
||||
|
||||
**Outputs:**
|
||||
|
||||
- `stdout`: the model response text — the only channel the orchestrator captures
|
||||
- `stderr`: diagnostics (never credentials)
|
||||
- exit `0`: success; nonzero: failure
|
||||
|
||||
## Rules
|
||||
|
||||
1. Adapters print ONLY the response on stdout. Status lines go to stderr.
|
||||
2. Adapters never read configuration files; the resolved settings arrive via environment.
|
||||
3. Adapters never write outside `/var/lib/mosaic`.
|
||||
4. Adding an adapter requires: a new directory, the contract implementation, and
|
||||
adding the name to the allowlist in `scripts/mosaic-config.mjs`.
|
||||
|
||||
## Included adapters
|
||||
|
||||
- `pi` — the pinned `@earendil-works/pi-coding-agent` CLI in noninteractive
|
||||
print mode (`-p`), ambient discovery disabled, stdin detached.
|
||||
- `mock` — deterministic echo of `MOSAIC_MOCK_RESPONSE`. Test-only: never use
|
||||
it where a real model response is required.
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Mock adapter: deterministic response for seam tests. NEVER use where a
|
||||
# real model response is required.
|
||||
#
|
||||
# Contract: see /opt/mosaic/adapters/README.md.
|
||||
set -eu
|
||||
|
||||
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "mock adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
|
||||
if [ "${MOSAIC_INTERACTIVE:-}" != "1" ]; then
|
||||
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "mock adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
|
||||
fi
|
||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||
|
||||
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
|
||||
# Deterministic plumbing evidence: which MOSAIC_* variables did the
|
||||
# orchestrator actually deliver? (Auth secrets are not MOSAIC_-prefixed.)
|
||||
(env | grep '^MOSAIC_' | sort) >&2 2>/dev/null || true
|
||||
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
# Pi adapter: implements the Mosaic adapter contract for the pinned
|
||||
# @earendil-works/pi-coding-agent CLI.
|
||||
#
|
||||
# Contract: see /opt/mosaic/adapters/README.md.
|
||||
# Headless (default): stdout = response only; stderr = diagnostics; exit 0.
|
||||
# Interactive (MOSAIC_INTERACTIVE=1): full pi TUI on the attached terminal.
|
||||
set -eu
|
||||
|
||||
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
|
||||
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
|
||||
# MOSAIC_AGENT_NAME is optional in headless mode (identity section is then
|
||||
# omitted); interactive launches always set it via scripts/agent.sh.
|
||||
|
||||
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
|
||||
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
|
||||
|
||||
INTERACTIVE="${MOSAIC_INTERACTIVE:-}"
|
||||
if [ "$INTERACTIVE" != "1" ]; then
|
||||
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "pi adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
# Workspace (M5): run inside the provided workspace when present.
|
||||
if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
|
||||
mkdir -p "$MOSAIC_WORKSPACE"
|
||||
cd "$MOSAIC_WORKSPACE"
|
||||
fi
|
||||
|
||||
# Session (M6/M11): default ephemeral (--no-session). With a declared
|
||||
# session dir: persist there and resume the most recent session. With a
|
||||
# fork source: branch the source session file into the target dir
|
||||
# (pi --fork) - the ancestor session is never modified.
|
||||
SESSION_FLAGS="--no-session"
|
||||
if [ -n "${MOSAIC_SESSION_FORK:-}" ]; then
|
||||
[ -n "${MOSAIC_SESSION_DIR:-}" ] || { echo "pi adapter: session fork requires MOSAIC_SESSION_DIR" >&2; exit 2; }
|
||||
mkdir -p "$MOSAIC_SESSION_DIR"
|
||||
SESSION_FLAGS="--fork $MOSAIC_SESSION_FORK --session-dir $MOSAIC_SESSION_DIR"
|
||||
elif [ -n "${MOSAIC_SESSION_DIR:-}" ]; then
|
||||
mkdir -p "$MOSAIC_SESSION_DIR"
|
||||
SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR"
|
||||
if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then
|
||||
SESSION_FLAGS="$SESSION_FLAGS -c"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Capabilities (M5): explicit allowlist or no tools.
|
||||
TOOLS_FLAG="--no-tools"
|
||||
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
|
||||
|
||||
# Skills (M17): explicitly provided skill dirs replace discovery. When none
|
||||
# are provided the agent runs with --no-skills (nothing ambient to find).
|
||||
SKILLS_FLAG="--no-skills"
|
||||
if [ -n "${MOSAIC_SKILLS:-}" ]; then
|
||||
SKILLS_FLAG=""
|
||||
OLDIFS=$IFS; IFS=','
|
||||
for s in $MOSAIC_SKILLS; do
|
||||
[ -d "$s" ] || { echo "pi adapter: skill dir missing: $s" >&2; exit 2; }
|
||||
SKILLS_FLAG="$SKILLS_FLAG --skill $s"
|
||||
done
|
||||
IFS=$OLDIFS
|
||||
fi
|
||||
|
||||
# Mode (M13): interactive TUI or one-shot print.
|
||||
PRINT_MODE="-p"
|
||||
REQUEST_ARG=""
|
||||
if [ "$INTERACTIVE" = "1" ]; then
|
||||
PRINT_MODE=""
|
||||
else
|
||||
REQUEST_ARG="$MOSAIC_REQUEST"
|
||||
fi
|
||||
|
||||
# All flags documented in the pi package README (CLI Reference):
|
||||
# -p/--print one-shot mode: print the response and exit (omitted in
|
||||
# interactive TUI mode)
|
||||
# --system-prompt replace the default prompt with the generated one
|
||||
# --no-* no ambient context/skills/extensions/templates/themes
|
||||
# SESSION_FLAGS ephemeral | persistent | forked (per env)
|
||||
# TOOLS_FLAG per capabilities
|
||||
# --offline no startup network operations (update checks/telemetry)
|
||||
PROMPT_CONTENT="$(cat "$MOSAIC_SYSTEM_PROMPT_FILE")"
|
||||
set -- \
|
||||
--offline \
|
||||
--no-extensions \
|
||||
$SKILLS_FLAG \
|
||||
--no-prompt-templates \
|
||||
--no-themes \
|
||||
--no-context-files \
|
||||
$TOOLS_FLAG \
|
||||
$SESSION_FLAGS \
|
||||
--provider "$PI_PROVIDER" \
|
||||
--model "$PI_MODEL" \
|
||||
--system-prompt "$PROMPT_CONTENT"
|
||||
# One-shot mode appends -p and the request (both safely quoted);
|
||||
# interactive mode appends nothing - clean TUI.
|
||||
[ "$INTERACTIVE" = "1" ] || set -- "$@" -p "$MOSAIC_REQUEST"
|
||||
exec pi "$@"
|
||||
@@ -0,0 +1,29 @@
|
||||
# Mosaic Stack development team
|
||||
|
||||
These are interactive host development agents working in the canonical
|
||||
checkout. They do not create managed fleet registrations or change role policy.
|
||||
Darkwing leads development and coordinates assignments, review, and integration.
|
||||
|
||||
| Agent | Responsibility | Runtime | Launch from repository root |
|
||||
| --- | --- | --- | --- |
|
||||
| Darkwing | Development team lead and hands-on engineering | Pi, configured Mosaic model | `agents/darkwing/launch.sh` |
|
||||
| Dewey | Frontend design, UX, accessibility, and UI implementation | Pi, configured Mosaic model | `agents/dewey/launch.sh` |
|
||||
| Rocko | General development, investigation, testing, and review | Claude Code, Sonnet model | `agents/rocko/launch.sh` |
|
||||
| Filbert | General development, investigation, testing, and review | Pi, `openai-codex/gpt-6-astra:low` | `agents/filbert/launch.sh` |
|
||||
|
||||
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
|
||||
own conversation; a first launch starts one. See each agent's README for
|
||||
context inputs, authentication, and recovery details. Launch scripts can also
|
||||
be invoked by absolute path from any directory. No assignment or model request
|
||||
is submitted by the launcher itself.
|
||||
|
||||
The shared Pi helper supports `--provider NAME`, `--model ID`, and
|
||||
`--thinking LEVEL` as per-launch overrides of the validated system defaults.
|
||||
Filbert's wrapper appends the required provider, model and thinking flags so
|
||||
its launch configuration remains fixed, including on resume. Use Filbert's
|
||||
wrapper to select that configuration; a direct shared-helper invocation uses
|
||||
its own supplied flags or the system defaults.
|
||||
|
||||
All agents follow repository governance and current user direction. Team
|
||||
leadership does not add deployment or push authority. Coordinate overlapping
|
||||
work with Darkwing and preserve other sessions' changes.
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
===== DARKWING NATIVE DEVELOPMENT CONTEXT =====
|
||||
|
||||
Your identity is Darkwing. This launch runs Pi directly on the host, in the
|
||||
Mosaic Stack development repository. The injected SOUL defines your persona;
|
||||
CONSTITUTION and STANDARDS supply governance, USER supplies user context,
|
||||
and AGENTS.md supplies repository instructions.
|
||||
|
||||
You have host read, bash, edit, write, grep, find, and ls tools. This is a
|
||||
development TUI with the operator's OS access, not a sandbox or a registered
|
||||
managed fleet seat. Use repository scripts for Mosaic operations and inspect
|
||||
their effects before running them. Container paths in skills describe worker
|
||||
deployments, not your current workspace. A tool's presence is not authority
|
||||
to change unrelated files, other agents' work, or the live fleet.
|
||||
|
||||
For an assigned improvement, inspect the implementation, reproduce the issue,
|
||||
make the smallest useful change, verify it, and continue through the authorized
|
||||
outcome. Read docs/plans/CURRENT.md to reconcile ownership and existing gates;
|
||||
a new user assignment does not silently resume unrelated queued work.
|
||||
|
||||
The local /goal extension is loaded and owns any operator-set goal lifecycle.
|
||||
Use ms-proactive-agent for work selection and ms-goal for recovery guidance;
|
||||
do not create a competing goal loop. Follow goal_report's actual schema and
|
||||
reporting instructions. Its text format is Just Completed / Next Step /
|
||||
Blocked, with '* none' for empty sections. No external reporting skill is
|
||||
needed to discover that format. Native development packaging supersedes
|
||||
older skill statements that this extension is unavailable.
|
||||
|
||||
For relocation recovery, read agents/darkwing/work/RESTART.md after the root
|
||||
AGENTS.md and docs/plans/CURRENT.md. It records verified checkpoints and limits,
|
||||
not a new assignment. The canonical checkout is /mnt/storage/src/mosaic-stack;
|
||||
v1/ is archived legacy source. Reconcile newer owner direction before acting.
|
||||
|
||||
Conversation history persists across launcher restarts. Goals belong to a
|
||||
single process incarnation; recover the assignment from verified records and
|
||||
the operator's direction after a restart. No goal is started by this launcher.
|
||||
Context is captured anew at launch; source edits do not update this process's
|
||||
injected snapshot. Relaunch to load approved context changes.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Darkwing development TUI
|
||||
|
||||
From any terminal, run:
|
||||
|
||||
```sh
|
||||
/home/jwoltje/src/mosaic-stack-dev-test/agents/darkwing/launch.sh
|
||||
```
|
||||
|
||||
The agent launcher is a thin shim to `scripts/agent.sh --host-dev darkwing`,
|
||||
forwarding all arguments unchanged. `scripts/agent.sh` is the common entry
|
||||
point; `scripts/agent-host-dev.sh` implements its native development mode.
|
||||
The host launcher opens the repository as Darkwing's workspace.
|
||||
It uses the repository-pinned Pi, the configured Mosaic provider/model, and
|
||||
native Pi authentication (normal `~/.pi/agent`, or `PI_CODING_AGENT_DIR` if
|
||||
explicitly set). It never copies credentials. Install dependencies with
|
||||
`npm ci --ignore-scripts --no-audit --no-fund` if needed.
|
||||
|
||||
`--check` validates configuration and required inputs without opening Pi or
|
||||
calling a model. `--fresh` starts a new conversation without deleting earlier
|
||||
ones. Normal launches continue the latest conversation under
|
||||
`.pi/state/darkwing/sessions/`; the first launch creates one. A launcher lock
|
||||
rejects simultaneous launches through this script. It does not exclude Pi
|
||||
processes started another way. Damaged JSONL history refuses automatic resume;
|
||||
`--fresh` is an explicit escape hatch that preserves the damaged evidence.
|
||||
|
||||
The current files are combined into a private launch snapshot under
|
||||
`.pi/state/darkwing/launches/`:
|
||||
|
||||
- `contracts/CONSTITUTION.md` and `contracts/STANDARDS.md`
|
||||
- `agents/darkwing/SOUL.md`
|
||||
- `<configured dataRoot>/user/USER.md`, the deployment's live user profile
|
||||
- the repository's `AGENTS.md` and Darkwing's `CONTEXT.md`
|
||||
|
||||
Use `--soul FILE`, `--constitution FILE`, or `--user FILE` to select alternate
|
||||
inputs, including a future `contracts/USER.md`. Relative paths resolve from
|
||||
the repository root. Missing or empty inputs refuse launch. Snapshots can
|
||||
contain personal context and remain local, with private file permissions.
|
||||
Context edits take effect on relaunch, including when resuming a conversation.
|
||||
|
||||
The launcher enables coding/search tools, `goal_report`, ten explicit local
|
||||
skills, and the canonical goal extension through `scripts/sync-dev-extensions.sh`.
|
||||
Ambient context, skills, extensions, templates, and themes are disabled.
|
||||
The normal Pi coding prompt is retained with the Mosaic context appended.
|
||||
Enter `/goal <assignment and acceptance criteria>` to start continuing work;
|
||||
`/goal stop`, `/goal resume`, and `/goal` pause, resume, and inspect it. A new
|
||||
process does not automatically adopt a previous process's goal.
|
||||
|
||||
This TUI has the operator's host access, including repository edits and host
|
||||
commands. Its tool list is not OS isolation. It creates no managed role or
|
||||
fleet registration. Worker dispatch still uses the governed Mosaic task runner.
|
||||
The user supplies the assignment; launch alone does not start self-modification.
|
||||
|
||||
## Deployment findings
|
||||
|
||||
The existing `scripts/agent.sh` launches a Docker container, defaults to the
|
||||
`agent-<name>` session directory, and asks Pi to continue when that directory
|
||||
is nonempty. Its default workspace is `<dataRoot>/workspaces/<name>`, not this
|
||||
checkout. `src/load-contracts.sh` loads image-baked governance, an optional
|
||||
seat SOUL override, live user Markdown, and mission context into a shared
|
||||
prompt path. A seat override requires `agent.json`; a standalone SOUL is not
|
||||
discovered. `adapters/pi/adapter.sh` disables extensions. The temporary host
|
||||
launcher follows the existing native development path to provide repository
|
||||
access and `/goal`, and keeps its conversations separate from container and
|
||||
live fleet sessions. It does not invoke release alignment on startup.
|
||||
@@ -0,0 +1,29 @@
|
||||
# SOUL — Darkwing
|
||||
|
||||
You are Darkwing, Mosaic Stack's development team lead and hands-on engineering
|
||||
collaborator. Your job is to help Jason make the system dependable by using it, finding where it
|
||||
falls short, and carrying authorized improvements through verification.
|
||||
|
||||
Be curious, direct, and resourceful. Have a technical opinion and explain
|
||||
the evidence behind it. Investigate before guessing. Distinguish a design
|
||||
claim, a passing test, and behavior you have observed in the running system.
|
||||
|
||||
Use Mosaic's own tools and workflows where they fit. Turn a failure into a
|
||||
reproducible case, make a focused correction, and test the behavior again.
|
||||
Let each verified improvement inform the next one within the assignment.
|
||||
Keep the human informed when the result, scope, or next decision changes.
|
||||
|
||||
Own the outcome while respecting other agents' work. Preserve their changes
|
||||
and records, give delegated work clear boundaries, and seek independent
|
||||
review where required. Self-improvement never grants new authority: changing
|
||||
your instructions, permissions, or a live deployment follows the same review
|
||||
and authorization rules as any other system change.
|
||||
|
||||
Lead the development team: translate Jason's priorities into scoped work,
|
||||
coordinate ownership and dependencies, review results, and verify integration.
|
||||
Dewey owns frontend design and UX. Rocko (Claude Code with Sonnet) and Filbert
|
||||
(Pi with OpenAI Codex GPT-6 Astra, low thinking) support general project needs,
|
||||
including implementation, investigation, testing, and review. Assign work to
|
||||
fit the need and reconcile concurrent edits before integration. Keep Jason
|
||||
informed of outcomes and decisions that require his input. Team leadership
|
||||
does not expand the project's existing authorization or release rules.
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Darkwing's native development mode through the Mosaic agent entry point.
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
exec "$REPO/scripts/agent.sh" --host-dev darkwing "$@"
|
||||
@@ -0,0 +1,21 @@
|
||||
// Refuse damaged history before Pi's --continue can silently skip it.
|
||||
import { readFileSync, lstatSync } from 'node:fs';
|
||||
|
||||
try {
|
||||
for (const file of process.argv.slice(2)) {
|
||||
if (!lstatSync(file).isFile()) throw new Error(`not a regular session file: ${file}`);
|
||||
const lines = readFileSync(file, 'utf8').trim().split('\n');
|
||||
const entries = lines.map((line) => JSON.parse(line));
|
||||
const header = entries[0];
|
||||
if (header?.type !== 'session' || typeof header.id !== 'string' || !header.id ||
|
||||
typeof header.version !== 'number' || typeof header.cwd !== 'string' ||
|
||||
!Number.isFinite(Date.parse(header.timestamp)) ||
|
||||
entries.slice(1).some((entry) => !entry || typeof entry.type !== 'string')) {
|
||||
throw new Error(`invalid session structure: ${file}`);
|
||||
}
|
||||
if (header.cwd !== process.cwd()) throw new Error(`session belongs to another workspace: ${file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`darkwing: cannot safely resume: ${error.message}; inspect history or explicitly use --fresh`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# Darkwing — relocation handoff
|
||||
|
||||
Recorded 2026-09-07 17:43 UTC. Jason intends to relaunch with
|
||||
`/mnt/storage/src/mosaic-stack/agents/darkwing/launch.sh`.
|
||||
This is a recovery note, not a new assignment or automatic goal resumption.
|
||||
|
||||
## Read first
|
||||
|
||||
1. Root `AGENTS.md` and `docs/plans/CURRENT.md`.
|
||||
2. This note, then `git status --short` and `git log --oneline -5`.
|
||||
3. Reconcile current owner direction and any newer declared artifacts before acting.
|
||||
|
||||
## Repository conversion is completed locally
|
||||
|
||||
Jason explicitly ordered the conversion and confirmed no work was active.
|
||||
- Canonical checkout: `/mnt/storage/src/mosaic-stack`.
|
||||
- Origin: `https://git.mosaicstack.dev/mosaicstack/stack`.
|
||||
- Branch: `refactor`.
|
||||
- Conversion commit: `127a54fdff1fe6ae56c3197edddf957481465db4`.
|
||||
- New foundation is at root. `v1/` is legacy archival source, NOT current code.
|
||||
- Old `/home/jwoltje/src/mosaic-stack-dev-test` is a compatibility symlink to this
|
||||
same checkout. Do not recreate a second working copy there.
|
||||
- Both histories retained: merge parents v2 `9a5fbdbda74b16adf488fe28138b2ba69ea5e669`
|
||||
and v1 `5d2770002612a09ae0cadc129b4ea30619133e8a`.
|
||||
- Exact 3,507-file v1 tracked tree imported; v1 refs under `refs/archive/v1/`.
|
||||
- Original v2 refs retained; `stack-v2-archive` remote has a disabled push URL.
|
||||
- Only legacy tracked tree and four conversion docs committed. All earlier
|
||||
uncommitted/untracked/ignored work preserved. Index was verified clean.
|
||||
- Issue https://git.mosaicstack.dev/mosaicstack/stack/issues/1495 closed explicitly
|
||||
for local conversion. No push, PR/trunk merge or live-service change occurred.
|
||||
|
||||
Record: `docs/plans/2026-09-07_repository-consolidation-completed.md`.
|
||||
Receipts: `docs/plans/reviews/2026-09-07_repository-conversion-verification.json`
|
||||
and `2026-09-07_repository-conversion-postcommit-verification.json`.
|
||||
Verified rollback copies, NOT development roots:
|
||||
- `/mnt/storage/src/.mosaic-stack-conversion-20260907T172430Z/`
|
||||
- `/home/jwoltje/src/.mosaic-stack-dev-test.pre-conversion-20260907T172430Z`
|
||||
Do not delete them, launch from them or restore over newer work.
|
||||
|
||||
## Current unfinished foundation gate
|
||||
|
||||
Jason's A9 acceptance of the first offline synthetic scope/permission inspector
|
||||
is pending. Code is independently approved by Filbert; no blocking code finding
|
||||
remains at the reviewed r6 candidate. Owner acceptance is not inferred from tests.
|
||||
|
||||
- Manifest: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-manifest-r6.json`
|
||||
SHA-256 `a4a4493000aff5905337a643886ca36e7c5377d52deed77b8aeab7174ca73dcf`.
|
||||
- Report: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-r6.md`
|
||||
SHA-256 `ee0e83efd7c71eddecf5e26f939e9a34ba85b184cfcd1cffac9ff9e56ea13c37`.
|
||||
- APPROVED verdict: `docs/plans/reviews/2026-09-07_foundation-inspector-code-verdict-r6.md`
|
||||
SHA-256 `ab9dd5e5c3cad5c9263e873ff82cac444da2d36040e907e4798b208fa1c08b13`.
|
||||
- Guide: `docs/plans/reviews/2026-09-07_foundation-inspector-demo.md`.
|
||||
|
||||
All 382 approved inspector files and pinned inputs survived conversion unchanged.
|
||||
Actual offline checks: Node 80/0, selftests 43/0, oracle 1,568 records / zero
|
||||
schema disagreements, foundation checker PASS, config/auth/conductor 24/15/17.
|
||||
Postcommit conductor 17/0 and four CLI demos passed: allowed read, allowed change
|
||||
PREVIEW (no mutation), missing-registration refusal, unresolved reassignment with
|
||||
original selection retained. Demo inputs are separate synthetic scenarios.
|
||||
|
||||
`test-task.sh` and `test-release.sh` remain NOT RUN / DEFERRED under Jason's bounded
|
||||
offline-demo ruling. No deployment/native/live/provider/security certification.
|
||||
Reviewer qualifications: ordering equality means structural equality, not byte
|
||||
identity; auxiliary native-parser warm-run anomalies remain separate unresolved
|
||||
observations, not a passing universal parser-equivalence claim. Preserve all earlier
|
||||
NOT APPROVED reviews and the historical correction that r3 ran unauthorized live
|
||||
branches; later deferral did not retroactively authorize them.
|
||||
|
||||
## Ownership and limits
|
||||
|
||||
- Rocko authored inspector code; Filbert independently reviewed; Darkwing coordinates
|
||||
and verifies. Keep the approved candidate frozen unless a new fix is authorized.
|
||||
- No automatic permission to push, merge to next/main, deploy, change live config,
|
||||
grant permissions, access credentials, investigate ~/.mosaic, or start new runtime
|
||||
work. Local conversion authority is not authority for those activities.
|
||||
- Preserve unrelated pending work. In particular `scripts/agent.sh`, `docs/TOOLS.md`,
|
||||
host launcher/context files and other untracked concepts/skills belong to existing
|
||||
work. Do not blanket-stage/reset/clean. Root logs and CURRENT remain uncommitted.
|
||||
- Foundation #53 in the old stack-v2 project remains a separate open issue; do not
|
||||
silently close or renumber it. Accepted historical SHA/path citations remain valid.
|
||||
- Rocko's Archify C1 remains HELD for owner T2/T3 decisions. No lane reassignment.
|
||||
- Future durability/workflow/evidence/federation/onboarding topics are notes, not
|
||||
authorization to expand the inspector.
|
||||
|
||||
## Communications
|
||||
|
||||
Use only `tools/tmux/agent-send.sh`; sender `dragon-lin:darkwing`.
|
||||
Rocko: `-L mosaic-fleet -s '=rocko'`; Filbert/Dewey:
|
||||
`-L default -s '=filbert'` / `'=dewey'`.
|
||||
Conversion notice delivered to Rocko. Filbert/Dewey sends were unconfirmed
|
||||
(input boxes not locatable); no retries, no acknowledgement claimed. Check declared
|
||||
artifact paths as well as direct messages; completed reviews have existed without
|
||||
transported replies. Do not inspect private panes or blindly resend.
|
||||
|
||||
## Relaunch and goal recovery
|
||||
|
||||
The project launcher continues its own latest `.pi/state/darkwing/sessions/`
|
||||
conversation by default. Do NOT assume this pre-launch conversation is already in
|
||||
that store or that the next launch resumes this exact conversation. This handoff
|
||||
is the durable bridge. No session-tree migration or launch was performed here.
|
||||
|
||||
The goal extension owns lifecycle. The earlier extension goal had been paused;
|
||||
no restart automatically resumes it. Reconcile the actual new process state and
|
||||
Jason's direction rather than reporting progress against a guessed old goal or
|
||||
creating a second goal loop. Launch alone grants no new assignment.
|
||||
|
||||
This handoff and its CONTEXT pointer are documentation-only. Launcher scripts,
|
||||
private sessions, credentials and runtime configuration were not modified.
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
===== DEWEY NATIVE DEVELOPMENT CONTEXT =====
|
||||
|
||||
Your identity is Dewey. This launch runs Pi directly on the host, in the
|
||||
Mosaic Stack development repository. The injected SOUL defines your persona;
|
||||
CONSTITUTION and STANDARDS supply governance, USER supplies user context,
|
||||
and AGENTS.md supplies repository instructions.
|
||||
|
||||
You have host read, bash, edit, write, grep, find, and ls tools. This is a
|
||||
development TUI with the operator's OS access, not a sandbox or a registered
|
||||
managed fleet seat. Use repository scripts for Mosaic operations and inspect
|
||||
their effects before running them. Container paths in skills describe worker
|
||||
deployments, not your current workspace. A tool's presence is not authority
|
||||
to change unrelated files, other agents' work, or the live fleet.
|
||||
|
||||
For an assigned improvement, inspect the implementation, reproduce the issue,
|
||||
make the smallest useful change, verify it, and continue through the authorized
|
||||
outcome. Read docs/plans/CURRENT.md to reconcile ownership and existing gates;
|
||||
a new user assignment does not silently resume unrelated queued work.
|
||||
|
||||
The local /goal extension is loaded and owns any operator-set goal lifecycle.
|
||||
Use ms-proactive-agent for work selection and ms-goal for recovery guidance;
|
||||
do not create a competing goal loop. Follow goal_report's actual schema and
|
||||
reporting instructions. Its text format is Just Completed / Next Step /
|
||||
Blocked, with '* none' for empty sections. No external reporting skill is
|
||||
needed to discover that format. Native development packaging supersedes
|
||||
older skill statements that this extension is unavailable.
|
||||
|
||||
The canonical checkout is /mnt/storage/src/mosaic-stack; v1/ is archived
|
||||
legacy source. Work on the current foundation unless the user explicitly
|
||||
assigns legacy work. Your frontend and UX responsibilities are defined in SOUL.
|
||||
|
||||
Conversation history persists across launcher restarts. Goals belong to a
|
||||
single process incarnation; recover the assignment from verified records and
|
||||
the operator's direction after a restart. No goal is started by this launcher.
|
||||
Context is captured anew at launch; source edits do not update this process's
|
||||
injected snapshot. Relaunch to load approved context changes.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Dewey development TUI
|
||||
|
||||
From any terminal, run:
|
||||
|
||||
```sh
|
||||
/mnt/storage/src/mosaic-stack/agents/dewey/launch.sh
|
||||
```
|
||||
|
||||
The agent launcher is a thin shim to `scripts/agent.sh --host-dev dewey`,
|
||||
forwarding all arguments unchanged. `scripts/agent.sh` is the common entry
|
||||
point; `scripts/agent-host-dev.sh` implements its native development mode.
|
||||
The host launcher opens the repository as Dewey's workspace.
|
||||
It uses the repository-pinned Pi, the configured Mosaic provider/model, and
|
||||
native Pi authentication (normal `~/.pi/agent`, or `PI_CODING_AGENT_DIR` if
|
||||
explicitly set). It never copies credentials. Install dependencies with
|
||||
`npm ci --ignore-scripts --no-audit --no-fund` if needed.
|
||||
|
||||
`--check` validates configuration and required inputs without opening Pi or
|
||||
calling a model. `--fresh` starts a new conversation without deleting earlier
|
||||
ones. Normal launches continue the latest conversation under
|
||||
`.pi/state/dewey/sessions/`; the first launch creates one. A launcher lock
|
||||
rejects simultaneous launches through this script. It does not exclude Pi
|
||||
processes started another way. Damaged JSONL history refuses automatic resume;
|
||||
`--fresh` is an explicit escape hatch that preserves the damaged evidence.
|
||||
|
||||
The current files are combined into a private launch snapshot under
|
||||
`.pi/state/dewey/launches/`:
|
||||
|
||||
- `contracts/CONSTITUTION.md` and `contracts/STANDARDS.md`
|
||||
- `agents/dewey/SOUL.md`
|
||||
- `<configured dataRoot>/user/USER.md`, the deployment's live user profile
|
||||
- the repository's `AGENTS.md` and Dewey's `CONTEXT.md`
|
||||
|
||||
Use `--soul FILE`, `--constitution FILE`, or `--user FILE` to select alternate
|
||||
inputs, including a future `contracts/USER.md`. Relative paths resolve from
|
||||
the repository root. Missing or empty inputs refuse launch. Snapshots can
|
||||
contain personal context and remain local, with private file permissions.
|
||||
Context edits take effect on relaunch, including when resuming a conversation.
|
||||
|
||||
The launcher enables coding/search tools, `goal_report`, ten explicit local
|
||||
skills, and the canonical goal extension through `scripts/sync-dev-extensions.sh`.
|
||||
Ambient context, skills, extensions, templates, and themes are disabled.
|
||||
The normal Pi coding prompt is retained with the Mosaic context appended.
|
||||
Enter `/goal <assignment and acceptance criteria>` to start continuing work;
|
||||
`/goal stop`, `/goal resume`, and `/goal` pause, resume, and inspect it. A new
|
||||
process does not automatically adopt a previous process's goal.
|
||||
|
||||
This TUI has the operator's host access, including repository edits and host
|
||||
commands. Its tool list is not OS isolation. It creates no managed role or
|
||||
fleet registration. Worker dispatch still uses the governed Mosaic task runner.
|
||||
The user supplies the assignment; launch alone does not start self-modification.
|
||||
|
||||
## Deployment findings
|
||||
|
||||
The existing `scripts/agent.sh` launches a Docker container, defaults to the
|
||||
`agent-<name>` session directory, and asks Pi to continue when that directory
|
||||
is nonempty. Its default workspace is `<dataRoot>/workspaces/<name>`, not this
|
||||
checkout. `src/load-contracts.sh` loads image-baked governance, an optional
|
||||
seat SOUL override, live user Markdown, and mission context into a shared
|
||||
prompt path. A seat override requires `agent.json`; a standalone SOUL is not
|
||||
discovered. `adapters/pi/adapter.sh` disables extensions. The temporary host
|
||||
launcher follows the existing native development path to provide repository
|
||||
access and `/goal`, and keeps its conversations separate from container and
|
||||
live fleet sessions. It does not invoke release alignment on startup.
|
||||
@@ -0,0 +1,46 @@
|
||||
# SOUL — Dewey
|
||||
|
||||
You are Dewey, Mosaic Stack's frontend design and UX collaborator. Your job
|
||||
is to help Jason turn product needs into clear, accessible interfaces and
|
||||
carry authorized frontend improvements from design through implementation
|
||||
and verification.
|
||||
|
||||
Start with the user's task: who is using the interface, what they need to
|
||||
accomplish, and where the current flow causes confusion or extra work.
|
||||
Inspect the existing product, components, and styles before proposing a
|
||||
change. Make reasonable choices within the assignment and explain material
|
||||
tradeoffs in plain language.
|
||||
|
||||
Own information architecture, navigation, interaction design, visual
|
||||
hierarchy, layout, typography, spacing, responsive behavior, and UI copy.
|
||||
Use the project's existing design language and reusable components where
|
||||
they fit. Keep implementation details out of user-facing flows unless they
|
||||
help the user make a meaningful decision.
|
||||
|
||||
Build accessible interactions with semantic markup, keyboard support,
|
||||
visible focus, useful labels, readable contrast, and appropriate feedback.
|
||||
Account for loading, empty, error, success, disabled, and long-content
|
||||
states. Check layouts at relevant viewport sizes and preserve user input
|
||||
when an operation fails.
|
||||
|
||||
Carry designs into maintainable frontend code within the authorized scope.
|
||||
Inspect API contracts before wiring data; do not invent backend behavior or
|
||||
present fixtures as live data. Coordinate backend or policy changes when
|
||||
they are needed to deliver the intended experience.
|
||||
|
||||
Verify the actual interface with available browser tools: inspect rendering,
|
||||
exercise the primary flow, and check keyboard and responsive behavior.
|
||||
Run relevant existing checks and add focused tests when behavior warrants
|
||||
them. Distinguish a proposed design, an implemented change, a passing test,
|
||||
and behavior observed in a running interface. If visual verification is
|
||||
unavailable, report that limitation and what remains to be checked.
|
||||
|
||||
Be direct, thoughtful, and specific. Explain design decisions in terms of
|
||||
user outcomes. Preserve other agents' work and keep changes scoped to the
|
||||
assignment. Follow repository governance; a design responsibility does not
|
||||
grant deployment, policy, or unrelated editing authority. Launching this
|
||||
agent alone does not assign work or resume another agent's task.
|
||||
|
||||
Darkwing is the Mosaic Stack development team lead. Coordinate frontend and
|
||||
UX ownership with Darkwing and collaborate with Rocko and Filbert on shared
|
||||
implementation needs and API dependencies.
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Dewey's native development mode through the Mosaic agent entry point.
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
exec "$REPO/scripts/agent.sh" --host-dev dewey "$@"
|
||||
@@ -0,0 +1,21 @@
|
||||
// Refuse damaged history before Pi's --continue can silently skip it.
|
||||
import { readFileSync, lstatSync } from 'node:fs';
|
||||
|
||||
try {
|
||||
for (const file of process.argv.slice(2)) {
|
||||
if (!lstatSync(file).isFile()) throw new Error(`not a regular session file: ${file}`);
|
||||
const lines = readFileSync(file, 'utf8').trim().split('\n');
|
||||
const entries = lines.map((line) => JSON.parse(line));
|
||||
const header = entries[0];
|
||||
if (header?.type !== 'session' || typeof header.id !== 'string' || !header.id ||
|
||||
typeof header.version !== 'number' || typeof header.cwd !== 'string' ||
|
||||
!Number.isFinite(Date.parse(header.timestamp)) ||
|
||||
entries.slice(1).some((entry) => !entry || typeof entry.type !== 'string')) {
|
||||
throw new Error(`invalid session structure: ${file}`);
|
||||
}
|
||||
if (header.cwd !== process.cwd()) throw new Error(`session belongs to another workspace: ${file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`dewey: cannot safely resume: ${error.message}; inspect history or explicitly use --fresh`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Mosaic Stack WUI design brief
|
||||
|
||||
## Assignment and authority
|
||||
|
||||
- Owner: Jason Woltje. Designer and mockup author: Dewey.
|
||||
- Intake recorded 2026-09-08. Stage: ready for five dashboard alternatives using Manrope and the current theme system. Logo/iconography completion is deferred by Jason and is not a design blocker. Brand-board entry: index.html.
|
||||
- Keep mockups and working notes in `agents/dewey/work/wui/`.
|
||||
- Start fresh. Do not inspect the old v1 UI without Jason's request.
|
||||
- Use `skills/ms-frontend-design/SKILL.md` and its relevant references.
|
||||
- CURRENT.md was read. The separate inspector acceptance queue is not this assignment and remains untouched.
|
||||
- Existing unrelated worktree changes must be preserved. No backend changes, integration, deployment, commit, or push authorized by this intake.
|
||||
|
||||
## Owner requirements
|
||||
|
||||
1. Establish a visual foundation before dashboard design. Owner revision after brand review: Manrope and the theme system are sufficient to proceed; final logo/iconography can follow separately.
|
||||
2. Avoid the old generic AI purple/blue gradient treatment. Aim for a professional identity and avoid typical AI design tells.
|
||||
3. Create five basic but feature-rich, navigable HTML dashboard mockups with distinct dashboard styles. Use a text wordmark and provisional labeled UI icons; do not wait for final logo/iconography.
|
||||
4. Provide a central HTML index linking to each design.
|
||||
5. Support mobile, responsive full-width layouts, and ultrawide monitors. Use available width meaningfully without stretching reading lines unnecessarily.
|
||||
6. Support Light, Dark, and Dim modes.
|
||||
7. Research current docs and plans for required product features. Record source references and distinguish implemented capabilities, planned capabilities, and new owner requests.
|
||||
8. The mockup may introduce features before backend support exists. Label fixture data and simulated behavior honestly. Do not invent API contracts.
|
||||
9. Jason selects a design, then we iterate on its HTML mockup before production coding and integration.
|
||||
10. Persist decisions, requirements, changes, unresolved questions, and next steps in files. Conversation context is not the project record.
|
||||
|
||||
## Sequence and approval gates
|
||||
|
||||
- Resolve brand discovery questions.
|
||||
- Present a small set of brand directions with logo studies, typography, icons, palette, and representative controls in all three modes.
|
||||
- Carry Manrope and the current themes into dashboard exploration. Jason explicitly removed the final logo/iconography gate; those choices remain unfinished.
|
||||
- Build a source-linked feature and route inventory from current docs/plans, then a comparable flow/state checklist for all five designs.
|
||||
- Build and browser-check the five designs and central index.
|
||||
- Obtain design selection and iterate the chosen HTML prototype.
|
||||
- Establish a separate implementation/integration charter later.
|
||||
|
||||
## Branding discovery
|
||||
|
||||
Jason's answers and the unapproved design proposals are recorded in [DECISIONS.md](DECISIONS.md).
|
||||
|
||||
- Flexible AI operating system for technical and nontechnical users, with extensions/plugins and many kinds of personal and team work.
|
||||
- mosaicstack.dev is owner-held. Open source intended; hosted offering only a possibility.
|
||||
- Creative, distinguishable mark that works as both icon and logo.
|
||||
- Blue is welcome. Avoid generic purple/blue gradients; yellow/green are less preferred but usable accents.
|
||||
- Target ten interchangeable palettes without changing layout or icon identity. Explain each palette through color theory.
|
||||
- Sans-serif typography. Owner references: t3.codes and buzz.xyz, with no preference for Buzz's yellow.
|
||||
- Light, charcoal Dim, and Dark with color and depth. Default mode still unselected.
|
||||
|
||||
## Verification and next step
|
||||
|
||||
- Used frontend-design and all applicable references. Inspected t3.codes and buzz.xyz public pages; no v1 inspection. Application/onboarding limits are in RESEARCH.md.
|
||||
- index.html contains three logo studies, three font alternatives, eight navigation-icon specimens, ten palettes, all three appearances, component states and local review downloads.
|
||||
- Chromium checks and author screenshot inspection completed for review readiness. VERIFY.md records evidence, corrections, and unverified cases. This is not an approved identity or integrated application.
|
||||
- Next step: research current docs/plans for the feature inventory, then design five navigable HTML alternatives and a central comparison index. Logo/iconography completion is non-blocking; backend coding and integration remain out of scope.
|
||||
@@ -0,0 +1,82 @@
|
||||
# WUI decisions and discovery record
|
||||
|
||||
## 2026-09-08: Jason's branding answers
|
||||
|
||||
### Confirmed product scope
|
||||
|
||||
Mosaic Stack is a flexible Swiss Army Knife / AI operating system, not a developer-only console. Uses include software factory, executive and personal assistance, writing, social media management, agent orchestration, human/agent collaboration through Matrix, Discord, Slack and similar channels, project management, and kanban. Extensions and plugins will broaden those uses.
|
||||
|
||||
The audience includes developers, enterprise users, homemakers, teens, students, professionals, and designers. The interface must not assume technical expertise. These are product intentions, not verified backend capabilities.
|
||||
|
||||
### Identity and distribution
|
||||
|
||||
- Jason owns mosaicstack.dev, the intended main site.
|
||||
- Codebase is intended to be open source, with OpenClaw and Hermes named as comparisons. No license decision was supplied here.
|
||||
- A hosted offering is possible in the future, not a committed service.
|
||||
- Explore creative logo/icon options. The chosen mark must be recognizable and work as a small icon and a full logo.
|
||||
- No specific logo geometry is selected.
|
||||
|
||||
### Color and customization
|
||||
|
||||
- Jason likes blue. Blue itself is not excluded; generic purple/blue gradient branding is unwanted.
|
||||
- Yellow and green are less preferred, but acceptable as accents.
|
||||
- Target ten user-selectable palettes shared conceptually across the site and apps. A user should be able to choose red rather than blue, for example.
|
||||
- Palette changes must not fundamentally alter icons or page design.
|
||||
- Each palette needs a color-theory rationale, not arbitrary hue substitutions.
|
||||
- Support Light, Dim, and Dark for the palettes.
|
||||
- Light with pale neutral surfaces and Dim with charcoal surfaces are accepted interpretations.
|
||||
- Dark should retain color and visible depth. It need not be uniformly near black.
|
||||
- Default mode was not specified.
|
||||
|
||||
### Typography and references
|
||||
|
||||
- Prefer sans-serif fonts.
|
||||
- https://t3.codes/ is a positive reference for theming and design choices.
|
||||
- https://buzz.xyz/ is a positive reference for UI elements and onboarding wizards. Its yellow is not a preferred brand color.
|
||||
- At intake, these were Jason's references, not Dewey's observations. Dewey subsequently inspected both public sites; RESEARCH.md records live evidence and application/onboarding limits.
|
||||
|
||||
## Dewey's proposed design direction, not owner-approved
|
||||
|
||||
- Give the brand a recognizable silhouette that remains identifiable in one color. Do not make a particular palette its only identifying trait.
|
||||
- Separate palette, appearance mode, and semantic status tokens. Keep control meanings and status labels stable across all combinations.
|
||||
- Default candidate: a blue palette, following system Light/Dark preference with an explicit Dim option. Default remains provisional.
|
||||
- Use progressive disclosure and adaptable workspaces so the same product supports a simple personal workflow and a dense professional workflow.
|
||||
- Explore three logo families: assembled mosaic monogram, interlocking ribbon, and modular cut-paper symbol. Judge each in monochrome, at favicon size, and alongside the wordmark.
|
||||
- Explore sans-serif samples rather than choosing a font from its name alone. Compare readable UI text, numerals, controls, and long labels.
|
||||
- Explain palette hue relationships, surface tint, accent roles, and intended contrast. Color theory guides harmony; measured contrast and user testing establish usability. Do not claim universal emotional effects for a hue.
|
||||
- First deliverable is a local interactive brand board with logo/type candidates and palette/mode controls, not any of the five dashboard layouts.
|
||||
|
||||
## Discovery next work recorded at intake
|
||||
|
||||
1. Inspect the two reference sites if browser/network capabilities permit. Record any inspection limits honestly.
|
||||
2. Produce brand studies and ten proposed palettes with explicit rationale and representative component states.
|
||||
3. Verify small-size logo clarity, relevant contrast pairs, keyboard operation, and responsive rendering with available tools. Record gaps.
|
||||
4. Jason selects/refines the brand before dashboard construction.
|
||||
5. Research current docs/plans for a source-linked feature inventory before the five dashboard designs. Keep intended and implemented features distinct.
|
||||
|
||||
No logo, font family, exact palette, default mode, or dashboard design has been approved. No backend, publication, hosting, or integration work is authorized by these answers.
|
||||
|
||||
## 2026-09-08: review-ready brand board
|
||||
|
||||
- Entry: index.html. Review instructions: README.md. Tests and limits: VERIFY.md.
|
||||
- Logo studies: Assembly, Relay, Aperture. Font choices: DM Sans, IBM Plex Sans, Manrope. Original outline icons share a 24-unit grid and 1.75-unit strokes.
|
||||
- Palettes: Harbor, Carmine, Atlantic, Terracotta, Aubergine, Mineral, Cobalt, Rosewood, Graphite, Grove. Each has Light/Dim/Dark variants and an explicit hue-harmony rationale. Draft hue relationships were corrected before final verification.
|
||||
- Dewey recommends Assembly + DM Sans + Harbor as the starting discussion, not an owner decision. Default appearance follows system Light/Dark preference; Dim is explicitly selectable. This behavior is still provisional.
|
||||
- The preview uses local assets and browser storage, no API. Sample success/loading/error states and author download fixtures do not represent live product data or Jason's approval.
|
||||
- All branding choices remain pending Jason's review. Dashboard construction and backend work have not started.
|
||||
|
||||
## First owner feedback after review
|
||||
|
||||
- Jason said the icon looks like a "robot's crotch". Dewey interprets this as the recommended Assembly mark, whose two uprights and central notch produce that association. Jason did not name the concept explicitly. Do not treat Relay or Aperture as selected alternatives.
|
||||
- Jason said the themes and Manrope font look great. Manrope is the preferred typography direction for the next revision. Preserve the palette work rather than redesigning it. No particular default palette or mode was selected.
|
||||
- Next logo studies should avoid the paired-leg/central-notch silhouette. Explore assembled tile shapes and asymmetric negative space rather than another mechanical M.
|
||||
- This is partial branding feedback, not full branding approval or authority to build the dashboards. No UI revision has been made in this feedback exchange.
|
||||
|
||||
## Owner gate revision: design can proceed before final iconography
|
||||
|
||||
Jason clarified that unfinished iconography should not prevent design progress. This supersedes the earlier requirement to finish the whole brand before dashboard exploration.
|
||||
|
||||
- Use Manrope and retain the ten-palette Light/Dim/Dark system for the five dashboard designs.
|
||||
- Final logo and iconography remain unresolved, but are not blockers. Use a Mosaic Stack text wordmark and provisional, labeled UI icons in the meantime.
|
||||
- Prioritize the source-linked feature inventory, then five distinct navigable dashboard mockups and a central comparison page. Treat implemented, planned, and newly proposed backend capabilities separately.
|
||||
- This does not authorize backend coding, integration, deployment, commits, or pushes. No new goal was activated during this clarification.
|
||||
@@ -0,0 +1,28 @@
|
||||
# WUI work history
|
||||
|
||||
Append-only author notes. Goal lifecycle belongs to the operator's extension; TASKS.md tracks this assignment.
|
||||
|
||||
## 2026-09-08: first brand-board implementation
|
||||
|
||||
- Reconciled BRIEF.md, DECISIONS.md, TASKS.md and the separate CURRENT.md owner gate. No v1 inspection or unrelated queue advancement.
|
||||
- B02: inspected live t3.codes and buzz.xyz public pages in installed Chromium. Saved desktop/mobile screenshots and DOM observations. RESEARCH.md separates public-page observations from untested application themes and onboarding.
|
||||
- B03: created Assembly, Relay, and Aperture as original SVG studies. Integrated monochrome 16/24/32 px and reversed samples. Recognition/trademark suitability remains unapproved.
|
||||
- B04: bundled Latin DM Sans, IBM Plex Sans, and Manrope, four weights each, with SIL OFL licenses and source URLs. Initial Google Fonts request returned a different format; a full browser User-Agent produced WOFF2 assets with verified signatures. Created a consistent original outline icon family.
|
||||
- B05: defined ten palettes with individual harmony rationales. Generated 30 mode token sets in assets/palettes.json. The initial 10:1 body-text target was mathematically unavailable on some Dim raised surfaces; changed the optional target to 7:1. Normal text minimum remains 4.5:1, required boundaries/focus 3:1. All generated combinations now pass their declared pair checks.
|
||||
- B06: built local index.html, styles.css, brand.js and board.js. Includes live selections, theme tokens export, review-note download/local storage, and explicit sample component states. No backend work.
|
||||
- B07 in progress: checks/verify.mjs passed 30 token sets and 240 viewport combinations, plus keyboard controls, state recovery, local fonts/storage, reduced motion, and narrow text-spacing/root-resize checks. Native CDP Enter initially omitted its character event and the Shift modifier used the wrong bit; repaired the test client and re-ran successfully. These were test-driver defects, not silently reclassified application passes.
|
||||
- Inspected saved Light, Dim, mobile, and ultrawide screenshots. Further focus visibility, lower-section visual checks, full text scaling, and download evidence remain before review handoff.
|
||||
|
||||
- Received Darkwing terminal-log TMUX-TRANSPORT-R5-LIVE-01 addressed to Dewey. Transport-only notice, no requested reply or assignment change. Filed without acting on the transport task.
|
||||
|
||||
## 2026-09-08: verification and review handoff
|
||||
|
||||
- B07 completed for prototype review with documented limits. Final verification reports pass 840 token contrast pairs, 8,220 computed rendered-text checks, 240 palette/mode/viewport combinations, ten breakpoint checks, keyboard/native controls, 60 accessible control names, targets, local downloads, font/storage failures, and text expansion.
|
||||
- Author screenshot inspection found a clipped mobile Conversations label and small-size sample labels extending outside their card at doubled text. Fixed the icon grid and row wrapping, then repeated checks and inspected new captures. Desktop preview controls now scroll within short viewports so focus stays visible.
|
||||
- Adjusted draft palette hue anchors to match their named harmony relationships. Regenerated the 30 token sets and repeated all affected tests.
|
||||
- B08 handed off through index.html and README.md. VERIFY.md distinguishes author checks from unverified engines, native mobile/zoom, assistive technology, and independent review. No production or brand approval claim.
|
||||
- B09 remains Jason's branding decision. Dashboard construction has not begun. No deployment, integration, backend changes, commits, pushes, external issue lifecycle, or agent task dispatch occurred. Test Chromium processes/profiles were closed by the check scripts.
|
||||
|
||||
- First owner review: icon has a robot-crotch association; themes and Manrope received positive feedback. Recorded interpretation and next revision in DECISIONS.md and B09. Prototype files unchanged. First goal remains closed; no dashboard work started.
|
||||
|
||||
- Owner removed final logo/iconography as a dashboard prerequisite. Updated BRIEF.md and TASKS.md: D01 is ready, with Manrope/current themes, text wordmark and provisional labeled icons. This clarification changes task dependencies, not the completed first goal or backend authority.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Mosaic Stack WUI brand study
|
||||
|
||||
**Pending owner acceptance.** This is the original review draft, not selected branding or a working product. Manrope/theme feedback is recorded, but the logo and default selections remain provisional. No dashboard/backend integration.
|
||||
|
||||
The board still uses its earlier Assembly/DM Sans/Harbor recommendations. Later owner feedback favors Manrope and the theme work; those source/default changes are not implemented here. Final logo/iconography is no longer a blocker for separately authorized dashboard design, but this published snapshot contains no dashboards.
|
||||
|
||||
Dewey authored the study. Filbert independently approved publication of the exact 53-file snapshot at manifest `a585c1eedb17044962c0419861a1032b23409b769c5d304aceff6ef6089a0e11`. This coordinator-added publication note is separate from those unchanged reviewed bytes. See `docs/plans/reviews/2026-09-08_publication-trial-wui-verdict.md` from the repository root for checks and limits.
|
||||
|
||||
BRIEF.md names the repository owner. DECISIONS.md preserves candid design feedback as historical working notes, not final brand messaging or a design acceptance. Original provisional recommendations and later owner direction must not be conflated.
|
||||
|
||||
Bundled fonts retain their own SIL OFL notices and provenance. Reference-site screenshots, scraped reference data, raw test downloads and non-allowlisted outputs are not published with this draft. Historical references to those local evidence paths do not imply public attachments. No trademark clearance, code-license change, production accessibility certification or runtime capability is claimed.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Mosaic Stack brand board
|
||||
|
||||
Status: ready for Jason's branding review. No brand choice is approved and no dashboard mockups have been built.
|
||||
|
||||
## Open the board
|
||||
|
||||
Entry page:
|
||||
|
||||
`/mnt/storage/src/mosaic-stack/agents/dewey/work/wui/index.html`
|
||||
|
||||
Open that file in a browser on this host. To review on a different computer, copy the whole `wui` folder and open its `index.html`. Keep `assets`, the CSS, and the JavaScript beside it. No server, package installation, account, or network connection is required for the board. Do not open only a copied HTML file without its assets.
|
||||
|
||||
The reference-research scripts use the network, but the delivered board does not. No service has been started or deployed.
|
||||
|
||||
## Suggested review, about ten minutes
|
||||
|
||||
1. Start with Assembly, DM Sans, and Harbor, my proposed baseline. Appearance initially follows the browser's system preference unless you already have a saved choice.
|
||||
2. In Identity, choose each mark. Compare the full logo in the header/brand specimen and the monochrome 16/24/32 px samples. Tell me whether you prefer the literal M, the woven links, or the abstract tile.
|
||||
3. In Typography, compare DM Sans, IBM Plex Sans, and Manrope. The three specimens retain their own font. Choosing a font changes the rest of the board, including actual controls.
|
||||
4. In Color system, try each palette's Light, Dim, and Dark buttons. Each button selects both that palette and that appearance. Look for readable text, useful surface depth, and a color family you would want to use daily. Hue angles and harmony rationale are printed on every palette card.
|
||||
5. In Iconography & controls, try the example name field, all five states, and Preview success. An error keeps the text intact. Loading is a labeled specimen, not a running request. Nothing creates a project or contacts a server.
|
||||
6. In Your review, write notes and download them. Downloads contain the selected logo, font, palette, mode, and your notes. Send the file or your feedback in this conversation. The page does not transmit notes to Dewey.
|
||||
|
||||
Selections and review notes use this browser's local storage when available. Reset preview restores the proposed choices but deliberately keeps your notes and the example name. Clearing site data or using a different file location/browser can lose local notes; download a copy to keep them.
|
||||
|
||||
You can also download the selected palette's tokens for all three modes. This is a design reference, not a production configuration file.
|
||||
|
||||
## What to decide
|
||||
|
||||
- Preferred logo and any shape/spacing revisions.
|
||||
- Preferred font and icon treatment.
|
||||
- Default palette and appearance behavior.
|
||||
- Palettes to change or remove before dashboard exploration.
|
||||
|
||||
Branding approval is a separate human decision. Choosing controls or downloading notes does not approve anything automatically. Dashboard work stays stopped until Jason supplies the branding decision and direction to proceed.
|
||||
|
||||
## Evidence and project records
|
||||
|
||||
- [Verification and limits](VERIFY.md), including reproducible commands.
|
||||
- [Reference observations](RESEARCH.md), including what was and was not inspected on t3.codes and buzz.xyz.
|
||||
- [Requirements](BRIEF.md), [owner decisions](DECISIONS.md), [task list](TASKS.md), [work history](HISTORY.md).
|
||||
- `evidence/verification.json`: 840 token-pair checks and 240 layout combinations.
|
||||
- `evidence/detail-verification.json`: 8,220 rendered-text checks and interaction/failure/download checks.
|
||||
- `evidence/final-verification.json`: local resources, links, targets, breakpoint checks, and skip navigation.
|
||||
- `evidence/board-light-1440.png`, `board-dim-1440.png`, `board-dark-1440.png`: desktop appearance captures.
|
||||
- `evidence/section-identity-light.png`, `section-typography-light.png`, `section-color-ultrawide.png`, `section-components-mobile.png`, and `section-review-light.png`: comparison and component captures.
|
||||
- `assets/logos/`: three standalone original SVG studies.
|
||||
- `assets/palettes.json`: all ten proposed palettes and thirty token sets.
|
||||
- `assets/fonts/`: bundled Latin fonts, OFL licenses, and source receipts. The existing font license notices are unchanged.
|
||||
|
||||
This is a local design-review package. No integration, deployment, commit, push, or independent technical acceptance is implied.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Reference inspection and brand-board plan
|
||||
|
||||
## Live reference inspection, 2026-09-08
|
||||
|
||||
Tool: installed headless Chromium through `checks/browser.mjs`, a dependency-free CDP client. Reproduce with `node agents/dewey/work/wui/checks/references.mjs`. Browser profile is temporary and removed on completion. No login, download, registration, or form submission was performed.
|
||||
|
||||
Evidence: `evidence/references.json`, `evidence/t3-desktop.png`, `evidence/t3-details.png`, `evidence/t3-mobile.png`, and the corresponding Buzz images. Initial screenshots caught entrance animations; the final captures include a bounded four-second animation allowance. Reference screenshots are research evidence, not reusable brand assets.
|
||||
|
||||
### t3.codes
|
||||
|
||||
Observed the public landing page at 1440 px and 390 px. Computed body font is DM Sans, with near-black `#09090b` and white `#fafafa`. The desktop hero uses large, tightly spaced sans-serif type, muted supporting text, and one bright primary download control. Provider icons sit in dark tiles. The embedded application image separates a navigation rail, conversation, and diff pane with restrained boundaries. Further down, provider names and small symbols share a regular comparison row.
|
||||
|
||||
Useful for Mosaic: clear text hierarchy, restrained neutral surfaces, one obvious primary action, consistent inline icon scale, and color localized to meaningful content. Do not copy the landing page's grid decoration, floating logo animation, developer-only framing, or marketing claims. A brand board can use generous comparison space; a working dashboard will need different density.
|
||||
|
||||
### buzz.xyz
|
||||
|
||||
Observed the public landing page at the same widths. Computed font is Cash Sans with a system sans-serif fallback. Its chartreuse `#d7d72e` canvas contrasts with dark `#231e1e` typography. The oversized wordmark and small repeating bee symbol make the identity recognizable at different scales. The desktop product image shows a tinted sidebar, light conversation area, labeled outline icons, grouped channels, and a clearly separated composer. On mobile, the landing composition stacks the wordmark and description while keeping the app action available.
|
||||
|
||||
Useful for Mosaic: a strong one-color symbol, tinted surfaces as part of identity, human/agent collaboration described in ordinary language, and familiar navigation controls. Do not borrow its bee, wordmark, yellow dominance, floating decoration, or proprietary font.
|
||||
|
||||
### Inspection limits
|
||||
|
||||
These are public landing pages and embedded product images, not installed or authenticated applications. T3 application theme switching and Buzz's actual application onboarding wizard were not exercised. The public Buzz waitlist has email/name/company fields and a Next button; no personal information was entered or sent. Jason's praise of in-app onboarding remains owner-provided context, not a verified wizard assessment. No v1 files were inspected.
|
||||
|
||||
## Brand-board design plan
|
||||
|
||||
Prototype only. One local `index.html` page, section anchors for Identity, Type, Color, Components, and Review. No public-site policy/contact pages or real backend are in scope. Navigation stays ordinary links; palette, logo, font, and mode choices use labeled native controls. No fake workspace creation or external submissions.
|
||||
|
||||
Three logo studies will differ in silhouette and construction: a segmented M, a woven angular loop, and an asymmetric cut-paper tile. All use the same selectable palette but also have monochrome samples. Original SVG paths, no borrowed assets. Recognition and trademark clearance are not established by author inspection.
|
||||
|
||||
Three sans-serif candidates will be compared at common sizes with the same UI labels and numerals. Fonts should be bundled locally with their licenses so opening the HTML does not require a CDN. Small outline icons use a common 24-unit grid, 1.75-unit strokes, round caps and joins, with text labels rather than color-only meaning.
|
||||
|
||||
Ten palettes will specify hue relationships rather than simply rotating one hue. Define canvas, surface, raised surface, text, muted text, border, action, on-action, secondary accent, focus, success, warning, and danger roles. Light uses pale tinted canvas and white surfaces. Dim uses middle charcoal with restrained tint. Dark uses deeper chromatic canvas and lighter layered surfaces. Foreground colors are adjusted against the actual background for contrast; hue relationships alone do not guarantee usability.
|
||||
|
||||
The page will use available viewport width with fluid comparison grids and bounded prose. At narrow widths, sections stack with no clipped controls. At ultrawide widths, multiple alternatives remain visible together rather than stretching text across the whole display.
|
||||
|
||||
Verification includes all 30 palette/mode combinations, meaningful text and control contrast, native keyboard operation, viewport widths from 320 to 3440, text expansion, reduced motion, and screenshots. Record limitations rather than claiming WCAG certification or production readiness.
|
||||
@@ -0,0 +1,49 @@
|
||||
# WUI task list
|
||||
|
||||
Owner and author: Dewey. Product decisions and acceptance: Jason.
|
||||
Workspace: /mnt/storage/src/mosaic-stack/agents/dewey/work/wui
|
||||
Requirements: [BRIEF.md](BRIEF.md). Decisions: [DECISIONS.md](DECISIONS.md).
|
||||
Created 2026-09-08. This is the authoritative task list for this assignment, not a second goal lifecycle. The operator-set goal extension owns goal control. No goal was activated by creating this file.
|
||||
|
||||
## Phase 1: brand board ready for Jason's review
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| B01 | Capture audience, product scope, preferences, constraints, and approval boundaries. Evidence: BRIEF.md and DECISIONS.md. | None | done | Preserve subsequent decisions in DECISIONS.md. |
|
||||
| B02 | Inspect t3.codes and buzz.xyz for useful theming, typography, controls, and onboarding ideas. Save observations and distinguish live inspection from owner descriptions. | B01 | done | RESEARCH.md and evidence/* reference images/JSON record live public-page inspection and app/onboarding limits. |
|
||||
| B03 | Produce three distinct original SVG logo studies with wordmark, monochrome, and 16/24/32 px samples. Record tradeoffs; recognition remains subject to Jason's judgment. | B02 | done | Three original studies in brand.js and assets/logos; rendered size/monochrome comparisons in index.html. |
|
||||
| B04 | Compare sans-serif typography and propose consistent iconography, spacing, geometry, and focus/interaction treatment. Use real control labels and numerical samples. | B02 | done | Three local OFL font families and original outline icon specimens implemented. |
|
||||
| B05 | Define ten palettes, each with a color-theory rationale and Light/Dim/Dark tokens. Keep identity and semantic meanings stable. Dark retains color and depth. | B04 | done | Ten rationale-bearing palettes and 30 mode token sets in brand.js and assets/palettes.json. |
|
||||
| B06 | Build a local navigable HTML brand board with logo/type comparisons, ten-palette and three-mode selectors, and representative component states. Clearly mark samples. | B03, B04, B05 | done | index.html, styles.css, board.js provide local interactive brand comparisons and sample states. |
|
||||
| B07 | Verify the brand board at mobile, desktop, and ultrawide widths, keyboard operation, focus, reduced motion where applicable, and contrast across all 30 palette/mode combinations. Save reproducible checks and browser evidence; flag missing checks honestly. | B06 | done | VERIFY.md and three evidence/*verification.json reports record contrast, 240 layouts, keyboard, targets, downloads, fallback and author screenshot checks with explicit limits. |
|
||||
| B08 | Deliver the local entry path, review instructions, recommendation, and evidence/limitations. Brand board is ready for review, not an approved identity. | B07 | done | index.html entry page, README.md review instructions and VERIFY.md limitations delivered as a review-ready design package. No brand approval implied. |
|
||||
| B09 | Obtain Jason's logo, typography, iconography, palette, and mode decisions; iterate the brand board as requested. Acceptance owner: Jason. | B08 | ready | First feedback received: Manrope and themes liked; logo has an unintended robot-crotch association. Revise logo direction, retain palette work, and use Manrope in the next preview. See DECISIONS.md. |
|
||||
|
||||
## Phase 2: five dashboard alternatives
|
||||
|
||||
Jason removed logo/iconography completion as a prerequisite for dashboard design. Proceed with Manrope, the existing ten palettes and three appearances, a text wordmark, and provisional labeled UI icons. B09 remains separate unfinished brand work, not a dashboard blocker. All implementation tasks are owned by Dewey; selection and final acceptance belong to Jason.
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| D01 | Research current docs/plans and save a source-linked feature inventory. Distinguish implemented, planned, and owner-proposed capabilities. | B08 and owner gate revision | ready | Read current foundation feature sources, not the v1 UI. Final logo/iconography is not required. |
|
||||
| D02 | Define shared routes, primary flows, states, sample data, and five genuinely different dashboard/navigation styles. | D01 | waiting | Establish comparable coverage across designs. |
|
||||
| D03 | Build a central HTML index linking all five navigable, feature-rich mockups. Apply Manrope, ten palettes, and three modes consistently; use a text wordmark and provisional labeled icons until identity work is finalized. | D02 | waiting | Build shared prototype resources and distinct layouts. |
|
||||
| D04 | Verify mobile/full-width/ultrawide layout, navigation, primary flows, keyboard access, and loading/empty/error/success/disabled/long-content states. Simulate unsupported features honestly. | D03 | waiting | Exercise each design in a browser and record evidence. |
|
||||
| D05 | Deliver the comparison and obtain Jason's design selection. | D04 | waiting | Provide a short review script and wait for selection. |
|
||||
|
||||
## Phase 3: selected HTML design
|
||||
|
||||
| ID | Task and acceptance evidence | Dependencies | Status | Next action |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| I01 | Record Jason's selected design, change requests, and newly requested features. | D05 | waiting | Turn feedback into bounded prototype tasks. |
|
||||
| I02 | Iterate and reverify the chosen HTML mockup through explicit owner acceptance. Record unsupported backend requirements separately. | I01 | waiting | Implement approved prototype revisions only. |
|
||||
| I03 | Prepare design assets, tokens, interaction specification, and backend dependency notes for a separately authorized coding/integration phase. | I02 | waiting | Deliver handoff without starting integration. |
|
||||
|
||||
## Boundaries and recovery
|
||||
|
||||
- Keep working artifacts here. Preserve unrelated repository work and the CURRENT.md inspector queue.
|
||||
- No v1 UI inspection, backend integration, credentials, live fleet changes, publication, commit, or push.
|
||||
- Do not claim unperformed browser checks, approved branding, or backend support.
|
||||
- No numeric budget supplied. No external requests, watches, or uncertain operations outstanding.
|
||||
- Next action: D01 feature inventory, then D02-D05 five dashboard alternatives. Logo/iconography refinement is non-blocking under Jason's revised direction. The completed first goal has not been restarted.
|
||||
- The operator goal ends at B08, a review-ready brand board with documented limits. The extension owns its completion status. B09 is not automatically accepted. Later authorized work covers brand iteration or subsequent phases.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Brand-board verification
|
||||
|
||||
Author: Dewey. Date: 2026-09-08. Scope: local interactive brand prototype, ready for review, not an integrated product or approved identity.
|
||||
|
||||
## Reproduce
|
||||
|
||||
From `/mnt/storage/src/mosaic-stack`:
|
||||
|
||||
```sh
|
||||
node --check agents/dewey/work/wui/brand.js
|
||||
node --check agents/dewey/work/wui/board.js
|
||||
node agents/dewey/work/wui/checks/verify.mjs
|
||||
node agents/dewey/work/wui/checks/detail-checks.mjs
|
||||
node agents/dewey/work/wui/checks/final-checks.mjs
|
||||
```
|
||||
|
||||
Environment used: Node 26.8.1, Chromium 152.0.7977.75 on Arch Linux. The CDP helper uses `/usr/bin/chromium`, or the `CHROMIUM` environment variable. Tests create a fresh temporary browser profile, open the entry page by file URL, save evidence here, and remove the profile at completion. No authenticated browser profile or Mosaic backend is used. There are no npm dependencies for this board or these tests.
|
||||
|
||||
`checks/assets.mjs` regenerates standalone SVGs and palette JSON from brand.js. `checks/fetch-fonts.py` refreshes the public Google Fonts WOFF2/OFL assets and needs network access. Neither is needed just to view or verify the current board. `checks/references.mjs` repeats the public reference-site inspection and also needs network access.
|
||||
|
||||
## Results
|
||||
|
||||
| Requirement | Result | Evidence |
|
||||
| --- | --- | --- |
|
||||
| Three distinct logo concepts | pass for review presentation | Assembly, Relay, Aperture rendered in identity cards, standalone SVGs, small monochrome samples, and selectable header/wordmark specimen. Author inspected screenshots. Recognition and final brand suitability are still Jason's decision. |
|
||||
| Sans-serif alternatives and coherent icons | pass | Three locally loaded families with four weights each and license files. Eight labeled navigation icons use one original 24-unit/1.75-stroke family, shared with feedback symbols. |
|
||||
| Ten theory-backed palettes, all three appearances | pass | Ten palettes carry named harmony relationships and explicit hue anchors in brand.js and the board. Thirty token sets generated. Palette/mode controls tested through their real change/click handlers. |
|
||||
| Token contrast, A11Y-01/02 | pass for declared pairs | 840 unrounded ratio checks in verification.json. Normal text/action-label minimum observed 4.503925718129755:1, boundary/focus minimum 3.704773189325781:1. Checks cover normal/muted/action/accent/status text across canvas, surface, raised surfaces; borders/focus across those surfaces; on-action text on action. Decorative dividers and disabled controls use criterion exceptions. |
|
||||
| Rendered text contrast | pass for sampled DOM text | 274 text/control samples per combination, 8,220 total. Computed foreground and solid ancestor backgrounds, qualifying large-text thresholds, minimum observed 4.504658476260286:1. This is not an image/gradient algorithm or a full accessibility audit. |
|
||||
| Color-independent meaning, A11Y-03 | pass | Selection uses Selected labels and pressed/checked state. Status uses text plus icons. Author inspected the rendered component section. |
|
||||
| Keyboard operation and focus, A11Y-05/06 | pass for tested flows | Native select and radio-arrow changes; Enter/Space selection buttons; Tab/Shift+Tab between sample fields; error recovery; skip link moves focus to main. Focus ring measured at 3 px solid. Short-screen sidebar focus stayed inside the 768 px viewport after adding sidebar scrolling. Chromium accessibility tree reports accessible names for all 60 interactive elements. This is not a screen-reader test. |
|
||||
| Target size, A11Y-04 | pass for measured controls | Enabled buttons, fields, radio inputs, sidebar links, brand link and review link meet 24 px dimensions at 320 and 1440 widths. Inline footer/prose links use the inline-text exception. Most controls are at least 44 px tall. |
|
||||
| Responsive/full-width layout, A11Y-07 | pass for measured layouts | All 30 combinations at 320, 390, 768, 960, 1440, 1920, 2560, 3440 CSS px. 240 layout checks, no page overflow. Ten additional checks cover both sides of the 700, 960, 1200, 1700, and 2400 breakpoints. Author inspected mobile, desktop, and 3440-wide screenshots; five palette columns use ultrawide space without unbounded prose. |
|
||||
| Text enlargement/spacing, A11Y-07/08 | pass for tested overrides | 320 px reflow with WCAG text-spacing overrides, 200% root size, and separately doubling every element's computed font size including authored px values. Initial full-text expansion overflowed the review grid and small-size labels; minimum-width/wrapping fixes passed retest and screenshot inspection. Browser-native zoom was not exercised. |
|
||||
| Reduced motion, A11Y-10 | pass | Emulated reduced-motion media preference; theme and keyboard controls remain usable. Board state changes do not rely on animation. |
|
||||
| Loading/empty/error/success/disabled states | pass as labeled specimens | Tested all five selectable states, loading's disabled action, success button, preserved input after error. No actual request or project creation occurs. |
|
||||
| Long content and failure fallback | pass for tested cases | Wrapped headings/labels and doubled text; font loads blocked and browser storage denied in isolated Chromium, with working controls and no page overflow at 320. Malformed saved preference JSON recovers to default. Input has a 160-character limit and native horizontal text-field scrolling. |
|
||||
| Review notes and token downloads | pass | Browser downloads captured in evidence/downloads and contents checked. Note download contains test review text; palette JSON includes Light/Dim/Dark tokens. These files are author test fixtures, not Jason's decisions. |
|
||||
| Offline resource/link integrity | pass | CDP resource tree showed 11 loaded resources, all local file URLs. Source asset links and internal anchors resolve. Three real font families load. No remote font/CDN/backend request belongs to the board. |
|
||||
| Public site completeness | not applicable to this local review artifact | Single file with section anchors, no public deployment or app router. About/Contact/legal pages, HTTP 404 behavior, consent management, billing and account workflows are outside this prototype. Storage use is explained beside controls and notes. No legal compliance claim. |
|
||||
| Reference sites | pass for public-page inspection, app behavior not verified | RESEARCH.md and reference screenshots. No v1 inspection. T3 application theme switching and Buzz's installed application wizard were not exercised. |
|
||||
|
||||
## Visual fixes and test corrections
|
||||
|
||||
- Early reference screenshots captured entrance animations, not completed pages. Re-captured after a bounded animation allowance.
|
||||
- CDP Enter needed its character value, and Shift uses modifier bit 8. Fixed the test driver rather than changing working controls to accommodate a faulty test.
|
||||
- Added scrollable desktop preview controls so keyboard focus remains available on short screens.
|
||||
- Doubled text exposed a review-column minimum-width problem and logo-size row wrapping. Fixed and rechecked.
|
||||
- A 320 px screenshot exposed a clipped Conversations label. Changed narrow icon layout to two columns and allowed long labels to wrap. Rechecked the actual rendering.
|
||||
- Corrected draft hue anchors so named complementary/triadic/split-complementary relationships match their stated angles. Regenerated assets and re-ran contrast/layout suites.
|
||||
- File-URL resources did not populate the expected Performance entries. The final resource check uses Chromium's Page.getResourceTree instead; its resource URLs are all local.
|
||||
|
||||
## Verification limits and remaining decisions
|
||||
|
||||
- Tested one installed desktop Chromium engine. Safari, Firefox, native mobile devices, touchscreen behavior, assistive technology, OS forced colors, and browser-native zoom are not verified.
|
||||
- Browser viewport emulation and doubled text are useful evidence, not equivalent to testing every physical display and accessibility configuration.
|
||||
- Contrast assertions and Chromium accessibility-tree names do not establish full WCAG conformance. They cover the listed cases.
|
||||
- Only Latin font subsets are bundled. Multilingual typography and fallback coverage need a later product brief.
|
||||
- Logos are original author studies, not independently reviewed, user-tested, trademark-cleared, or final production assets. Small-size optical refinements await selection.
|
||||
- No independent technical review, issue lifecycle, repository delivery suites, integration, deployment, commits, or pushes were performed for this standalone design package. Those delivery gates remain separate if the prototype is promoted into product work.
|
||||
- Jason has not approved branding. B09 and all dashboard tasks remain behind that human gate.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,7 @@
|
||||
DM Sans
|
||||
CSS: https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap
|
||||
License: https://raw.githubusercontent.com/google/fonts/main/ofl/dmsans/OFL.txt
|
||||
400: https://fonts.gstatic.com/s/dmsans/v17/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2
|
||||
500: https://fonts.gstatic.com/s/dmsans/v17/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2
|
||||
600: https://fonts.gstatic.com/s/dmsans/v17/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2
|
||||
700: https://fonts.gstatic.com/s/dmsans/v17/rP2Yp2ywxg089UriI5-g4vlH9VoD8Cmcqbu0-K4.woff2
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,7 @@
|
||||
IBM Plex Sans
|
||||
CSS: https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap
|
||||
License: https://raw.githubusercontent.com/google/fonts/main/ofl/ibmplexsans/OFL.txt
|
||||
400: https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2
|
||||
500: https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2
|
||||
600: https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2
|
||||
700: https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Manrope Project Authors (https://github.com/sharanda/manrope)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,7 @@
|
||||
Manrope
|
||||
CSS: https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap
|
||||
License: https://raw.githubusercontent.com/google/fonts/main/ofl/manrope/OFL.txt
|
||||
400: https://fonts.gstatic.com/s/manrope/v20/xn7gYHE41ni1AdIRggexSg.woff2
|
||||
500: https://fonts.gstatic.com/s/manrope/v20/xn7gYHE41ni1AdIRggexSg.woff2
|
||||
600: https://fonts.gstatic.com/s/manrope/v20/xn7gYHE41ni1AdIRggexSg.woff2
|
||||
700: https://fonts.gstatic.com/s/manrope/v20/xn7gYHE41ni1AdIRggexSg.woff2
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" fill="#215fac" role="img" aria-label="Mosaic Stack Aperture concept"><path fill-rule="evenodd" d="M4 4h30v10H14v28H4zm34 0 14 14v34H18V18h16v10h-6v14h14V22l-4-4z"/></svg>
|
||||
|
After Width: | Height: | Size: 231 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" fill="#215fac" role="img" aria-label="Mosaic Stack Assembly concept"><path d="M4 4h10v24H4zM18 4l12 12-7 7-5-5zM34 4v14l-5 5-7-7zM38 4h10v24H38z" transform="translate(2 10)"/></svg>
|
||||
|
After Width: | Height: | Size: 242 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" fill="#215fac" role="img" aria-label="Mosaic Stack Relay concept"><path fill-rule="evenodd" d="M4 18 18 4h12l10 10-8 8-8-8h-2L14 22v2l8 8-8 8L4 30zm48 8L38 40H26L16 30l8-8 8 8h2l8-8v-2l-8-8 8-8 10 10z" transform="translate(0 6)"/></svg>
|
||||
|
After Width: | Height: | Size: 297 B |
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"prototype": true,
|
||||
"palettes": [
|
||||
{
|
||||
"id": "harbor",
|
||||
"name": "Harbor",
|
||||
"hue": 214,
|
||||
"sat": 70,
|
||||
"accent": 34,
|
||||
"accentSat": 63,
|
||||
"theory": "Complementary",
|
||||
"note": "Clear blue with a restrained copper counterpoint. The warm accent balances cool navigation and gives selected details a second voice.",
|
||||
"use": "Proposed default. Blue without the blue-purple gradient.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f5f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6eaef",
|
||||
"text": "#434d5b",
|
||||
"muted": "#5e6978",
|
||||
"line": "#ced5df",
|
||||
"border": "#636e7e",
|
||||
"action": "#2266bf",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#8d5e20",
|
||||
"focus": "#2266bf",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#242d38",
|
||||
"surface": "#2e3947",
|
||||
"raised": "#384556",
|
||||
"text": "#d6dbe1",
|
||||
"muted": "#a9b1bc",
|
||||
"line": "#47576c",
|
||||
"border": "#a4acb7",
|
||||
"action": "#8ab4ea",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#8ab4ea",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12161c",
|
||||
"surface": "#1c232b",
|
||||
"raised": "#28323e",
|
||||
"text": "#b9c1cb",
|
||||
"muted": "#a4acb7",
|
||||
"line": "#384556",
|
||||
"border": "#a4acb7",
|
||||
"action": "#74a6e7",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#74a6e7",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "carmine",
|
||||
"name": "Carmine",
|
||||
"hue": 350,
|
||||
"sat": 65,
|
||||
"accent": 176,
|
||||
"accentSat": 42,
|
||||
"theory": "Near-complementary",
|
||||
"note": "A red-led identity with a cool turquoise counterpoint. Rose-tinted neutrals keep the red from covering every surface.",
|
||||
"use": "For a red preference. Errors still use an icon and explicit wording.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f7f3f3",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#efe7e8",
|
||||
"text": "#5e4549",
|
||||
"muted": "#7b6065",
|
||||
"line": "#deced1",
|
||||
"border": "#7e6367",
|
||||
"action": "#b92740",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#2e706c",
|
||||
"focus": "#b92740",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#372528",
|
||||
"surface": "#462f33",
|
||||
"raised": "#55393e",
|
||||
"text": "#ded3d5",
|
||||
"muted": "#bca9ac",
|
||||
"line": "#6b484e",
|
||||
"border": "#b7a4a7",
|
||||
"action": "#e996a4",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8bd0cb",
|
||||
"focus": "#e996a4",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e99995"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1b1214",
|
||||
"surface": "#2b1d1f",
|
||||
"raised": "#3d292c",
|
||||
"text": "#c9b6b9",
|
||||
"muted": "#b7a4a7",
|
||||
"line": "#55393e",
|
||||
"border": "#b7a4a7",
|
||||
"action": "#e2788a",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8bd0cb",
|
||||
"focus": "#e2788a",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "atlantic",
|
||||
"name": "Atlantic",
|
||||
"hue": 199,
|
||||
"sat": 72,
|
||||
"accent": 170,
|
||||
"accentSat": 52,
|
||||
"theory": "Analogous",
|
||||
"note": "Ocean blue and teal sit close on the hue wheel. Their shared cool bias makes a quieter combination than an opposing accent.",
|
||||
"use": "A cohesive blue-green alternative with low visual rivalry.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f6f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6ecef",
|
||||
"text": "#3f4e55",
|
||||
"muted": "#5c6d75",
|
||||
"line": "#cdd9df",
|
||||
"border": "#63757e",
|
||||
"action": "#19719a",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#257467",
|
||||
"focus": "#19719a",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#243238",
|
||||
"surface": "#2e3f47",
|
||||
"raised": "#384d57",
|
||||
"text": "#e2e7e9",
|
||||
"muted": "#afbbc0",
|
||||
"line": "#46606d",
|
||||
"border": "#a4b1b7",
|
||||
"action": "#73c3e8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#83d8ca",
|
||||
"focus": "#73c3e8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#edaaa6"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12191c",
|
||||
"surface": "#1c272b",
|
||||
"raised": "#28373e",
|
||||
"text": "#bcc8cd",
|
||||
"muted": "#a4b1b7",
|
||||
"line": "#384d57",
|
||||
"border": "#a4b1b7",
|
||||
"action": "#73c3e8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#83d8ca",
|
||||
"focus": "#73c3e8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e4817c"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "terracotta",
|
||||
"name": "Terracotta",
|
||||
"hue": 18,
|
||||
"sat": 60,
|
||||
"accent": 198,
|
||||
"accentSat": 44,
|
||||
"theory": "Complementary",
|
||||
"note": "Fired-clay orange meets a desaturated steel blue. Warm neutral surfaces connect the palette without needing a beige wash everywhere.",
|
||||
"use": "An earthier option for writing and personal work.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f7f4f3",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#eee9e7",
|
||||
"text": "#584841",
|
||||
"muted": "#78665e",
|
||||
"line": "#ded3cf",
|
||||
"border": "#7e6b63",
|
||||
"action": "#a74f2a",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#356f88",
|
||||
"focus": "#a74f2a",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#362b26",
|
||||
"surface": "#453630",
|
||||
"raised": "#54423b",
|
||||
"text": "#e5dfdc",
|
||||
"muted": "#beb2ac",
|
||||
"line": "#695349",
|
||||
"border": "#b7a9a4",
|
||||
"action": "#e2a68d",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#89bcd1",
|
||||
"focus": "#e2a68d",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eba29e"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1b1513",
|
||||
"surface": "#2a211d",
|
||||
"raised": "#3c2f2a",
|
||||
"text": "#cbbeb9",
|
||||
"muted": "#b7a9a4",
|
||||
"line": "#54423b",
|
||||
"border": "#b7a9a4",
|
||||
"action": "#de9a7c",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#89bcd1",
|
||||
"focus": "#de9a7c",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aubergine",
|
||||
"name": "Aubergine",
|
||||
"hue": 287,
|
||||
"sat": 38,
|
||||
"accent": 47,
|
||||
"accentSat": 46,
|
||||
"theory": "Triadic pair",
|
||||
"note": "Muted plum and ochre sit 120 degrees apart on the hue wheel. The third triadic hue, teal, is left out so the interface has two brand accents rather than three.",
|
||||
"use": "A deliberate purple option for users who want it, without the AI gradient.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f5f4f6",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#ece8ed",
|
||||
"text": "#59455e",
|
||||
"muted": "#75607b",
|
||||
"line": "#d9d2db",
|
||||
"border": "#78637e",
|
||||
"action": "#88469b",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#77672c",
|
||||
"focus": "#88469b",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#312933",
|
||||
"surface": "#3e3441",
|
||||
"raised": "#4c3f50",
|
||||
"text": "#e1d9e3",
|
||||
"muted": "#baacbe",
|
||||
"line": "#5f4f63",
|
||||
"border": "#b3a4b7",
|
||||
"action": "#cba4d6",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d3c388",
|
||||
"focus": "#cba4d6",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#18141a",
|
||||
"surface": "#262028",
|
||||
"raised": "#362d39",
|
||||
"text": "#c7b9cb",
|
||||
"muted": "#b3a4b7",
|
||||
"line": "#4c3f50",
|
||||
"border": "#b3a4b7",
|
||||
"action": "#bf8ecc",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d3c388",
|
||||
"focus": "#bf8ecc",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mineral",
|
||||
"name": "Mineral",
|
||||
"hue": 171,
|
||||
"sat": 47,
|
||||
"accent": 351,
|
||||
"accentSat": 42,
|
||||
"theory": "Complementary",
|
||||
"note": "Deep teal and dusty rose oppose each other while keeping saturation restrained. Blue-green tinted layers carry the identity in dark mode.",
|
||||
"use": "Cool surfaces with a small warm punctuation.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f6f6",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e8eded",
|
||||
"text": "#3d524f",
|
||||
"muted": "#586f6c",
|
||||
"line": "#d0dcda",
|
||||
"border": "#637e7a",
|
||||
"action": "#2a7469",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#9f414f",
|
||||
"focus": "#2a7469",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#273432",
|
||||
"surface": "#324340",
|
||||
"raised": "#3d514e",
|
||||
"text": "#e5ebea",
|
||||
"muted": "#b2c2c0",
|
||||
"line": "#4d6662",
|
||||
"border": "#a4b7b4",
|
||||
"action": "#87d4c8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e0b3ba",
|
||||
"focus": "#87d4c8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eeaeaa"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#141a19",
|
||||
"surface": "#1f2927",
|
||||
"raised": "#2c3a38",
|
||||
"text": "#b9cbc8",
|
||||
"muted": "#a4b7b4",
|
||||
"line": "#3d514e",
|
||||
"border": "#a4b7b4",
|
||||
"action": "#87d4c8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d18f99",
|
||||
"focus": "#87d4c8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e58580"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cobalt",
|
||||
"name": "Cobalt",
|
||||
"hue": 229,
|
||||
"sat": 75,
|
||||
"accent": 49,
|
||||
"accentSat": 63,
|
||||
"theory": "Complementary",
|
||||
"note": "A stronger royal blue meets a small amber accent. The high hue separation is controlled by using amber only in secondary details.",
|
||||
"use": "The most assertive blue option. No large yellow surfaces.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f3f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6e8ef",
|
||||
"text": "#454a5e",
|
||||
"muted": "#60657b",
|
||||
"line": "#cdd1df",
|
||||
"border": "#63687e",
|
||||
"action": "#1c3bc4",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#79671b",
|
||||
"focus": "#1c3bc4",
|
||||
"success": "#277255",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#242838",
|
||||
"surface": "#2e3248",
|
||||
"raised": "#383d57",
|
||||
"text": "#d0d2dc",
|
||||
"muted": "#a6aab9",
|
||||
"line": "#464d6d",
|
||||
"border": "#a4a7b7",
|
||||
"action": "#94a5f0",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1ce7a",
|
||||
"focus": "#94a5f0",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e89591"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12141c",
|
||||
"surface": "#1c1f2c",
|
||||
"raised": "#282c3e",
|
||||
"text": "#b6b9c9",
|
||||
"muted": "#a4a7b7",
|
||||
"line": "#383d57",
|
||||
"border": "#a4a7b7",
|
||||
"action": "#798eec",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1ce7a",
|
||||
"focus": "#798eec",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rosewood",
|
||||
"name": "Rosewood",
|
||||
"hue": 329,
|
||||
"sat": 47,
|
||||
"accent": 209,
|
||||
"accentSat": 39,
|
||||
"theory": "Triadic pair",
|
||||
"note": "Dusty pink and steel blue occupy two points of a triadic relationship. The third, yellow-green, is omitted to avoid an overly colorful interface.",
|
||||
"use": "A softer warm identity that does not become pastel text.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f6f3f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#ede8eb",
|
||||
"text": "#5e4552",
|
||||
"muted": "#7b606e",
|
||||
"line": "#dcd0d6",
|
||||
"border": "#7e6371",
|
||||
"action": "#a53b72",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#416c95",
|
||||
"focus": "#a53b72",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#34272e",
|
||||
"surface": "#43323b",
|
||||
"raised": "#513d48",
|
||||
"text": "#e1d6db",
|
||||
"muted": "#beacb5",
|
||||
"line": "#664d5a",
|
||||
"border": "#b7a4ae",
|
||||
"action": "#dc9ebe",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#95b3d0",
|
||||
"focus": "#dc9ebe",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1a1417",
|
||||
"surface": "#291f24",
|
||||
"raised": "#3a2c33",
|
||||
"text": "#cbb9c2",
|
||||
"muted": "#b7a4ae",
|
||||
"line": "#513d48",
|
||||
"border": "#b7a4ae",
|
||||
"action": "#d487af",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8eaecd",
|
||||
"focus": "#d487af",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "graphite",
|
||||
"name": "Graphite",
|
||||
"hue": 216,
|
||||
"sat": 9,
|
||||
"accent": 216,
|
||||
"accentSat": 12,
|
||||
"theory": "Monochromatic",
|
||||
"note": "One blue-gray hue uses value and saturation changes instead of a second brand hue. Semantic status colors remain independent.",
|
||||
"use": "Minimal chroma for content-heavy work. Hierarchy comes from contrast.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f5f5f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#eaeaeb",
|
||||
"text": "#434d5b",
|
||||
"muted": "#5e6978",
|
||||
"line": "#d5d6d7",
|
||||
"border": "#636e7e",
|
||||
"action": "#616975",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#5e6978",
|
||||
"focus": "#616975",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#2d2e2f",
|
||||
"surface": "#393a3c",
|
||||
"raised": "#454749",
|
||||
"text": "#dfe2e7",
|
||||
"muted": "#afb6c0",
|
||||
"line": "#57595c",
|
||||
"border": "#a4abb7",
|
||||
"action": "#b1b6be",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#afb6c0",
|
||||
"focus": "#b1b6be",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eba29e"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#161718",
|
||||
"surface": "#232425",
|
||||
"raised": "#323334",
|
||||
"text": "#bcc3cd",
|
||||
"muted": "#a4abb7",
|
||||
"line": "#454749",
|
||||
"border": "#a4abb7",
|
||||
"action": "#a6acb5",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#a4abb7",
|
||||
"focus": "#a6acb5",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "grove",
|
||||
"name": "Grove",
|
||||
"hue": 146,
|
||||
"sat": 37,
|
||||
"accent": 356,
|
||||
"accentSat": 39,
|
||||
"theory": "Split-complementary pair",
|
||||
"note": "Muted forest green meets dusty rose, 30 degrees to one side of its magenta complement. The second split accent is omitted. Low-saturation surfaces keep green from dominating.",
|
||||
"use": "An optional green palette for other preferences, not the proposed default.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f4f6f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e8edea",
|
||||
"text": "#3d5246",
|
||||
"muted": "#586f62",
|
||||
"line": "#d2dbd6",
|
||||
"border": "#637e6e",
|
||||
"action": "#377752",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#9c444a",
|
||||
"focus": "#377752",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#29332d",
|
||||
"surface": "#34413a",
|
||||
"raised": "#3f4f46",
|
||||
"text": "#e2e9e5",
|
||||
"muted": "#afc0b6",
|
||||
"line": "#4f6358",
|
||||
"border": "#a4b7ac",
|
||||
"action": "#8fcca9",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#ddb1b4",
|
||||
"focus": "#8fcca9",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#edaaa6"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#141917",
|
||||
"surface": "#202823",
|
||||
"raised": "#2d3932",
|
||||
"text": "#b9cbc1",
|
||||
"muted": "#a4b7ac",
|
||||
"line": "#3f4f46",
|
||||
"border": "#a4b7ac",
|
||||
"action": "#8fcca9",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#cd8e92",
|
||||
"focus": "#8fcca9",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e58580"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const B = window.Brand;
|
||||
const $ = id => document.getElementById(id);
|
||||
const modes = ['light','dim','dark'];
|
||||
const modeName = mode => mode[0].toUpperCase() + mode.slice(1);
|
||||
const defaults = {palette:'harbor',font:'dm',mark:'mosaic',mode:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'};
|
||||
let state = {...defaults};
|
||||
let storageAvailable = true;
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem('mosaic-brand-preview-v1') || '{}');
|
||||
for (const [key, items] of Object.entries({palette:B.palettes.map(p=>p.id),font:B.fonts.map(f=>f.id),mark:B.logos.map(l=>l.id),mode:modes})) {
|
||||
if (items.includes(saved?.[key])) state[key]=saved[key];
|
||||
}
|
||||
$('review-notes').value = localStorage.getItem('mosaic-brand-notes-v1') || '';
|
||||
} catch { storageAvailable = false; }
|
||||
function announce(text) {$('announcement').textContent=text;}
|
||||
function persist() {
|
||||
try {localStorage.setItem('mosaic-brand-preview-v1',JSON.stringify(state));}
|
||||
catch {storageAvailable=false;announce('Browser storage is unavailable. Download your review notes before closing.');}
|
||||
}
|
||||
for(const [id, data] of [['palette',B.palettes],['font',B.fonts],['mark',B.logos]]) {
|
||||
$(id).innerHTML=data.map(x=>`<option value="${x.id}">${x.name}</option>`).join('');
|
||||
$(id).addEventListener('change',event=>{state[id]=event.target.value;update();});
|
||||
}
|
||||
document.querySelectorAll('[name=mode]').forEach(input=>input.addEventListener('change',()=>{state.mode=input.value;update();}));
|
||||
$('logo-grid').innerHTML=B.logos.map((l,i)=>`<article class="study-card" data-mark-card="${l.id}">
|
||||
<div class="card-kicker"><span>${String(i+1).padStart(2,'0')} / ${l.family}</span><span class="selection-label" data-mark-label="${l.id}"></span></div>
|
||||
<div class="logo-display">${B.logo(l.id)}</div><h3>${l.name}</h3><p>${l.note}</p>
|
||||
<div class="size-strip" aria-label="${l.name} monochrome size samples">${[16,24,32].map(size=>`<span>${B.logo(l.id,size)}${size} px</span>`).join('')}<span>${B.logo(l.id,24).replace('class="logo"','class="logo reverse"')}Reverse</span></div>
|
||||
<p class="tradeoff">${l.tradeoff}</p><button type="button" class="choose" data-choose-mark="${l.id}" aria-pressed="false">Use ${l.name}</button></article>`).join('');
|
||||
$('font-grid').innerHTML=B.fonts.map(f=>`<article class="study-card" data-font-card="${f.id}"><div class="card-kicker"><span>${f.detail}</span><span class="selection-label" data-font-label="${f.id}"></span></div>
|
||||
<div style='font-family:${f.family}'><h3 class="font-title">${f.name}</h3><div class="type-specimen"><p class="type-heading">A place for your next idea.</p><p>Plan a project, write a chapter, or ask an agent to help. Start with what you need.</p><p class="numerals">0123456789<br>Il1 O0 · 12:48 · 1,024</p></div></div><p>${f.note}</p><button type="button" class="choose" data-choose-font="${f.id}" aria-pressed="false">Use ${f.name}</button></article>`).join('');
|
||||
$('palette-grid').innerHTML=B.palettes.map(p=>{
|
||||
const t=B.tokens(p,'light');
|
||||
return `<article class="palette-card" data-palette-card="${p.id}"><h3>${p.name}<span data-palette-label="${p.id}"></span></h3><div class="palette-swatches" aria-hidden="true">${['action','accent','raised','canvas'].map(role=>`<i style="background:${t[role]}"></i>`).join('')}</div><p class="theory">${p.theory} · ${p.hue}° / ${p.accent}°</p><p>${p.note}</p><p class="palette-use">${p.use}</p><div class="palette-modes" aria-label="${p.name} appearance">${modes.map(mode=>{const t=B.tokens(p,mode);return `<button type="button" data-palette="${p.id}" data-mode="${mode}" aria-label="Preview ${p.name} in ${modeName(mode)}" aria-pressed="false" style="--mini-surface:${t.surface};--mini-text:${t.text};--mini-border:${t.border};--mini-raised:${t.raised}">${modeName(mode)}</button>`;}).join('')}</div></article>`;
|
||||
}).join('');
|
||||
$('icon-grid').innerHTML=[['home','Home'],['conversation','Conversations'],['projects','Projects'],['agents','Agents'],['people','People'],['board','Board'],['extensions','Extensions'],['search','Search']].map(([id,label])=>`<div class="icon-specimen">${B.icon(id)}<span>${label}</span></div>`).join('');
|
||||
document.querySelectorAll('[data-icon]').forEach(el=>el.innerHTML=B.icon(el.dataset.icon));
|
||||
document.querySelectorAll('[data-choose-mark]').forEach(button=>button.addEventListener('click',()=>{state.mark=button.dataset.chooseMark;update();}));
|
||||
document.querySelectorAll('[data-choose-font]').forEach(button=>button.addEventListener('click',()=>{state.font=button.dataset.chooseFont;update();}));
|
||||
document.querySelectorAll('[data-palette][data-mode]').forEach(button=>button.addEventListener('click',()=>{state.palette=button.dataset.palette;state.mode=button.dataset.mode;update();}));
|
||||
function update(shouldAnnounce=true) {
|
||||
const p=B.palettes.find(p=>p.id===state.palette), f=B.fonts.find(f=>f.id===state.font), l=B.logos.find(l=>l.id===state.mark);
|
||||
const t=B.tokens(p,state.mode);
|
||||
for(const [role,value] of Object.entries(t)) document.documentElement.style.setProperty('--'+role,value);
|
||||
document.documentElement.style.setProperty('--font',f.family);
|
||||
document.documentElement.style.colorScheme=state.mode==='light'?'light':'dark';
|
||||
document.documentElement.dataset.mode=state.mode;
|
||||
document.documentElement.dataset.palette=state.palette;
|
||||
for(const id of ['palette','font','mark']) $(id).value=state[id];
|
||||
document.querySelectorAll('[name=mode]').forEach(input=>input.checked=input.value===state.mode);
|
||||
document.querySelectorAll('[data-logo]').forEach(el=>el.innerHTML=B.logo(state.mark));
|
||||
$('current-palette').textContent=p.name;
|
||||
$('current-mode').textContent=modeName(state.mode);
|
||||
for(const [kind,data] of [['mark',B.logos],['font',B.fonts],['palette',B.palettes]]) {
|
||||
data.forEach(item=>{
|
||||
const chosen=item.id===state[kind];
|
||||
document.querySelector(`[data-${kind}-card="${item.id}"]`).classList.toggle('is-selected',chosen);
|
||||
document.querySelector(`[data-${kind}-label="${item.id}"]`).textContent=chosen?'Selected':'';
|
||||
document.querySelector(`[data-choose-${kind}="${item.id}"]`)?.setAttribute('aria-pressed',String(chosen));
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('[data-palette][data-mode]').forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.palette===state.palette && button.dataset.mode===state.mode)));
|
||||
$('token-title').textContent=`${p.name} / ${modeName(state.mode)} tokens`;
|
||||
$('tokens').innerHTML=Object.entries(t).map(([role,value])=>`<div><dt><i style="background:${value}" aria-hidden="true"></i>${role.replace('onAction','On action')}</dt><dd>${value.toUpperCase()}</dd></div>`).join('');
|
||||
const summary=`${l.name} · ${f.name} · ${p.name} · ${modeName(state.mode)}`;
|
||||
$('selection-summary').textContent=`Current preview: ${summary}.`;
|
||||
persist();
|
||||
if(shouldAnnounce) announce(`Preview changed to ${summary}.`);
|
||||
}
|
||||
const messages={ready:['conversation','Ready. Try a local success preview.'],loading:['dim','Loading specimen. No request is running. Choose another state to continue.'],empty:['plus','No projects in this example. Your next step would be to create one.'],error:['error','Could not save this example. Your text is unchanged. Use Preview success to try again.'],success:['check','Success preview. No project was created or saved to a server.']};
|
||||
function sample() {
|
||||
const state=$('sample-state').value, [icon,message]=messages[state];
|
||||
$('sample-feedback').dataset.state=state;
|
||||
$('sample-feedback').innerHTML=B.icon(icon)+`<span>${message}</span>`;
|
||||
$('sample-action').disabled=state==='loading';
|
||||
$('sample-title').setAttribute('aria-describedby','sample-help sample-feedback');
|
||||
}
|
||||
$('sample-state').addEventListener('change',sample);
|
||||
$('sample-action').addEventListener('click',()=>{$('sample-state').value='success';sample();});
|
||||
$('reset').addEventListener('click',()=>{state={...defaults};update();announce('Preview reset. Your review notes and example project name are unchanged.');});
|
||||
function download(name,type,content) {
|
||||
const url=URL.createObjectURL(new Blob([content],{type}));
|
||||
const a=document.createElement('a');a.href=url;a.download=name;a.click();
|
||||
// Browser downloads consume the URL before the next task; retain briefly for portability.
|
||||
setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||
announce(`${name} prepared for download. Check your browser downloads.`);
|
||||
}
|
||||
$('export').addEventListener('click',()=>{
|
||||
const p=B.palettes.find(p=>p.id===state.palette);
|
||||
download(`mosaic-${p.id}-tokens.json`,'application/json',JSON.stringify({prototype:true,palette:p.name,theory:p.theory,rationale:p.note,modes:Object.fromEntries(modes.map(mode=>[mode,B.tokens(p,mode)]))},null,2));
|
||||
});
|
||||
$('review-notes').addEventListener('input',()=>{
|
||||
try{localStorage.setItem('mosaic-brand-notes-v1',$('review-notes').value);}catch{storageAvailable=false;announce('Notes cannot be stored in this browser. Download them before closing.');}
|
||||
});
|
||||
$('download-review').addEventListener('click',()=>{
|
||||
download('mosaic-brand-review.txt','text/plain',`Mosaic Stack branding review\n${$('selection-summary').textContent}\n\n${$('review-notes').value||'No written notes yet.'}\n\nThese selections are review preferences, not an automatic approval.\n`);
|
||||
});
|
||||
update(false);sample();
|
||||
if(!storageAvailable) announce('Browser storage is unavailable. The preview still works. Download notes to keep them.');
|
||||
})();
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Original brand studies. This file contains prototype design data, not an API. */
|
||||
(function (root) {
|
||||
const palettes = [
|
||||
{id:'harbor', name:'Harbor', hue:214, sat:70, accent:34, accentSat:63, theory:'Complementary', note:'Clear blue with a restrained copper counterpoint. The warm accent balances cool navigation and gives selected details a second voice.', use:'Proposed default. Blue without the blue-purple gradient.'},
|
||||
{id:'carmine', name:'Carmine', hue:350, sat:65, accent:176, accentSat:42, theory:'Near-complementary', note:'A red-led identity with a cool turquoise counterpoint. Rose-tinted neutrals keep the red from covering every surface.', use:'For a red preference. Errors still use an icon and explicit wording.'},
|
||||
{id:'atlantic', name:'Atlantic', hue:199, sat:72, accent:170, accentSat:52, theory:'Analogous', note:'Ocean blue and teal sit close on the hue wheel. Their shared cool bias makes a quieter combination than an opposing accent.', use:'A cohesive blue-green alternative with low visual rivalry.'},
|
||||
{id:'terracotta', name:'Terracotta', hue:18, sat:60, accent:198, accentSat:44, theory:'Complementary', note:'Fired-clay orange meets a desaturated steel blue. Warm neutral surfaces connect the palette without needing a beige wash everywhere.', use:'An earthier option for writing and personal work.'},
|
||||
{id:'aubergine', name:'Aubergine', hue:287, sat:38, accent:47, accentSat:46, theory:'Triadic pair', note:'Muted plum and ochre sit 120 degrees apart on the hue wheel. The third triadic hue, teal, is left out so the interface has two brand accents rather than three.', use:'A deliberate purple option for users who want it, without the AI gradient.'},
|
||||
{id:'mineral', name:'Mineral', hue:171, sat:47, accent:351, accentSat:42, theory:'Complementary', note:'Deep teal and dusty rose oppose each other while keeping saturation restrained. Blue-green tinted layers carry the identity in dark mode.', use:'Cool surfaces with a small warm punctuation.'},
|
||||
{id:'cobalt', name:'Cobalt', hue:229, sat:75, accent:49, accentSat:63, theory:'Complementary', note:'A stronger royal blue meets a small amber accent. The high hue separation is controlled by using amber only in secondary details.', use:'The most assertive blue option. No large yellow surfaces.'},
|
||||
{id:'rosewood', name:'Rosewood', hue:329, sat:47, accent:209, accentSat:39, theory:'Triadic pair', note:'Dusty pink and steel blue occupy two points of a triadic relationship. The third, yellow-green, is omitted to avoid an overly colorful interface.', use:'A softer warm identity that does not become pastel text.'},
|
||||
{id:'graphite', name:'Graphite', hue:216, sat:9, accent:216, accentSat:12, theory:'Monochromatic', note:'One blue-gray hue uses value and saturation changes instead of a second brand hue. Semantic status colors remain independent.', use:'Minimal chroma for content-heavy work. Hierarchy comes from contrast.'},
|
||||
{id:'grove', name:'Grove', hue:146, sat:37, accent:356, accentSat:39, theory:'Split-complementary pair', note:'Muted forest green meets dusty rose, 30 degrees to one side of its magenta complement. The second split accent is omitted. Low-saturation surfaces keep green from dominating.', use:'An optional green palette for other preferences, not the proposed default.'},
|
||||
];
|
||||
const fonts = [
|
||||
{id:'dm', name:'DM Sans', family:'"DM Sans", system-ui, sans-serif', note:'Open, rounded forms without becoming playful. My first choice for a product that spans everyday work and professional tools.', detail:'Recommended balance'},
|
||||
{id:'plex', name:'IBM Plex Sans', family:'"IBM Plex Sans", system-ui, sans-serif', note:'More engineered letterforms and a distinctive rhythm. Strong for dense labels and data; its technical character is more noticeable.', detail:'Precise and structured'},
|
||||
{id:'manrope', name:'Manrope', family:'Manrope, system-ui, sans-serif', note:'Broad geometric forms give the wordmark more presence. Compare long control labels carefully because the wider forms use more space.', detail:'Geometric and expressive'},
|
||||
];
|
||||
const logos = [
|
||||
{id:'mosaic', name:'Assembly', family:'An assembled M', note:'Four solid pieces form an M with an open center. The gaps suggest independent tools working together without using a puzzle-piece cliché.', tradeoff:'Most direct link to Mosaic. At 16 px, the two-unit gaps become fine seams.', paths:'<path d="M4 4h10v24H4zM18 4l12 12-7 7-5-5zM34 4v14l-5 5-7-7zM38 4h10v24H38z" transform="translate(2 10)"/>', recommended:true},
|
||||
{id:'weave', name:'Relay', family:'An interlocking ribbon', note:'Two angular links cross to make a compact woven loop. The open counters suggest handoffs between people, agents, and tools.', tradeoff:'Strong standalone symbol, but less obviously an M. Check for resemblance to existing link marks before final adoption.', paths:'<path fill-rule="evenodd" d="M4 18 18 4h12l10 10-8 8-8-8h-2L14 22v2l8 8-8 8L4 30zm48 8L38 40H26L16 30l8-8 8 8h2l8-8v-2l-8-8 8-8 10 10z" transform="translate(0 6)"/>'},
|
||||
{id:'fold', name:'Aperture', family:'A cut-paper tile', note:'An asymmetric square folds around an open center. A single diagonal cut gives the mark direction without an arrow, robot, or sparkle.', tradeoff:'The most abstract and artistic candidate. Its relationship to Mosaic will rely on repeated use with the wordmark.', paths:'<path fill-rule="evenodd" d="M4 4h30v10H14v28H4zm34 0 14 14v34H18V18h16v10h-6v14h14V22l-4-4z"/>'},
|
||||
];
|
||||
const icons = {
|
||||
home:'<path d="m3 10 9-7 9 7v10H3zM9 20v-7h6v7"/>',
|
||||
conversation:'<path d="M4 4h16v12H9l-5 4zM8 8h8M8 12h5"/>',
|
||||
projects:'<path d="M3 6h7l2 3h9v11H3zM3 6V4h7l2 2h7v3"/>',
|
||||
agents:'<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M12 3v4M9 12h.01M15 12h.01M9 16h6M2 11v5M22 11v5"/>',
|
||||
people:'<circle cx="9" cy="7" r="3"/><path d="M3 21v-3a6 6 0 0 1 12 0v3M16 4a3 3 0 0 1 0 6M18 14a5 5 0 0 1 3 4v3"/>',
|
||||
board:'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M6 7v4M12 7v7M18 7v2"/>',
|
||||
extensions:'<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/>',
|
||||
search:'<circle cx="10" cy="10" r="6"/><path d="m15 15 6 6"/>',
|
||||
check:'<path d="m5 12 4 4L19 6"/>',
|
||||
warning:'<path d="m12 3 10 18H2zM12 9v5M12 17h.01"/>',
|
||||
error:'<circle cx="12" cy="12" r="9"/><path d="m9 9 6 6m0-6-6 6"/>',
|
||||
plus:'<path d="M12 4v16M4 12h16"/>',
|
||||
arrow:'<path d="M4 12h16m-6-6 6 6-6 6"/>',
|
||||
sun:'<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/>',
|
||||
dim:'<circle cx="12" cy="12" r="9"/><path d="M12 3v18M12 7h6M12 12h9M12 17h6"/>',
|
||||
moon:'<path d="M20 14A9 9 0 0 1 10 3a9 9 0 1 0 10 11z"/>',
|
||||
};
|
||||
function hsl(h,s,l) {
|
||||
s/=100; l/=100;
|
||||
const c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs(h/60%2-1)), m=l-c/2;
|
||||
const rgb=h<60?[c,x,0]:h<120?[x,c,0]:h<180?[0,c,x]:h<240?[0,x,c]:h<300?[x,0,c]:[c,0,x];
|
||||
return '#'+rgb.map(v=>Math.round((v+m)*255).toString(16).padStart(2,'0')).join('');
|
||||
}
|
||||
function luminance(hex) {
|
||||
const a=hex.slice(1).match(/../g).map(x=>parseInt(x,16)/255).map(v=>v<=0.04045?v/12.92:((v+0.055)/1.055)**2.4);
|
||||
return a[0]*.2126+a[1]*.7152+a[2]*.0722;
|
||||
}
|
||||
function contrast(a,b) { const x=luminance(a), y=luminance(b);return (Math.max(x,y)+.05)/(Math.min(x,y)+.05); }
|
||||
function foreground(h,s,start, backgrounds, minimum, light) {
|
||||
for(let l=start; l>=0 && l<=100; l+=light?-1:1) {
|
||||
const c=hsl(h,s,l);
|
||||
if(backgrounds.every(bg=>contrast(c,bg)>=minimum)) return c;
|
||||
}
|
||||
throw new Error('No contrast-compliant color');
|
||||
}
|
||||
function tokens(p, mode) {
|
||||
const light=mode==='light', dim=mode==='dim', s=Math.min(p.sat*.3,22);
|
||||
const canvas=hsl(p.hue,s,light?96:dim?18:9);
|
||||
const surface=light?'#ffffff':hsl(p.hue,s,dim?23:14);
|
||||
const raised=hsl(p.hue,s,light?92:dim?28:20);
|
||||
const backgrounds=[canvas,surface,raised];
|
||||
const color=(h,s,min=4.5)=>foreground(h,s,light?44:68,backgrounds,min,light);
|
||||
const action=color(p.hue,p.sat);
|
||||
const onAction=contrast('#ffffff',action)>=4.5?'#ffffff':'#10141a';
|
||||
return {canvas,surface,raised,text:color(p.hue,15,7),muted:color(p.hue,12),
|
||||
line:hsl(p.hue,s,light?84:dim?35:28),border:color(p.hue,12,3),action,onAction,
|
||||
accent:color(p.accent,p.accentSat),focus:action,success:color(157,49),warning:color(34,70),danger:color(3,66)};
|
||||
}
|
||||
function logo(id, size=56) {const l=logos.find(l=>l.id===id)||logos[0];return `<svg class="logo" width="${size}" height="${size}" viewBox="0 0 56 56" fill="currentColor" aria-hidden="true">${l.paths}</svg>`;}
|
||||
function icon(id) {return `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[id]||icons.home}</svg>`;}
|
||||
root.Brand = {palettes,fonts,logos,icons,tokens,contrast,logo,icon};
|
||||
})(typeof window === 'undefined' ? globalThis : window);
|
||||
@@ -0,0 +1,10 @@
|
||||
import '../brand.js';
|
||||
import {mkdir,writeFile} from 'node:fs/promises';
|
||||
const out=new URL('../assets/logos/',import.meta.url);
|
||||
await mkdir(out,{recursive:true});
|
||||
for(const l of Brand.logos){
|
||||
await writeFile(new URL(`${l.name.toLowerCase()}.svg`,out),`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56" fill="#215fac" role="img" aria-label="Mosaic Stack ${l.name} concept">${l.paths}</svg>\n`);
|
||||
}
|
||||
const palettes=Brand.palettes.map(p=>({...p,modes:Object.fromEntries(['light','dim','dark'].map(m=>[m,Brand.tokens(p,m)]))}));
|
||||
await writeFile(new URL('../assets/palettes.json',import.meta.url),JSON.stringify({prototype:true,palettes},null,2)+'\n');
|
||||
console.log('Generated 3 original SVG assets and 30 palette/mode token sets.');
|
||||
@@ -0,0 +1,78 @@
|
||||
// Small CDP client for the installed Chromium. No application dependencies.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export async function browser() {
|
||||
const profile = await mkdtemp(join(tmpdir(), 'dewey-brand-browser-'));
|
||||
const child = spawn(process.env.CHROMIUM || '/usr/bin/chromium', [
|
||||
'--headless', '--no-first-run', '--no-default-browser-check',
|
||||
'--disable-dev-shm-usage', '--remote-debugging-pipe', `--user-data-dir=${profile}`,
|
||||
], { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] });
|
||||
let id = 0, buffer = '';
|
||||
const pending = new Map();
|
||||
child.stdio[4].on('data', chunk => {
|
||||
buffer += chunk.toString();
|
||||
let end;
|
||||
while ((end = buffer.indexOf('\0')) !== -1) {
|
||||
const message = JSON.parse(buffer.slice(0, end));
|
||||
buffer = buffer.slice(end + 1);
|
||||
if (pending.has(message.id)) {
|
||||
const { resolve, reject, timeout } = pending.get(message.id);
|
||||
clearTimeout(timeout);
|
||||
pending.delete(message.id);
|
||||
message.error ? reject(new Error(JSON.stringify(message.error))) : resolve(message.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
function send(method, params = {}, sessionId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const key = ++id;
|
||||
const timeout = setTimeout(() => { pending.delete(key); reject(new Error(`CDP timeout: ${method}`)); }, 25000);
|
||||
pending.set(key, { resolve, reject, timeout });
|
||||
child.stdio[3].write(JSON.stringify({ id: key, method, params, ...(sessionId ? {sessionId} : {}) }) + '\0');
|
||||
});
|
||||
}
|
||||
const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
|
||||
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
|
||||
const call = (method, params) => send(method, params, sessionId);
|
||||
await call('Page.enable');
|
||||
await call('Runtime.enable');
|
||||
const evaluate = async expression => {
|
||||
const r = await call('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.text + ': ' + r.result.description);
|
||||
return r.result.value;
|
||||
};
|
||||
return {
|
||||
call, evaluate,
|
||||
async viewport(width, height = 1000) {
|
||||
await call('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false });
|
||||
},
|
||||
async navigate(url) {
|
||||
const result = await call('Page.navigate', { url });
|
||||
if (result.errorText) throw new Error(result.errorText);
|
||||
// Bounded page-load check, not an agent wake/watch loop.
|
||||
for (let n = 0; n < 50; n++) {
|
||||
if (await evaluate('document.readyState === "complete"')) break;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
await evaluate('document.fonts.ready.then(() => true)');
|
||||
return result;
|
||||
},
|
||||
async screenshot(path) {
|
||||
const { data } = await call('Page.captureScreenshot', { format: 'png' });
|
||||
await writeFile(path, Buffer.from(data, 'base64'));
|
||||
},
|
||||
async key(key, code = key, modifiers = 0) {
|
||||
const virtual = { Tab: 9, Enter: 13, Escape: 27, ArrowDown: 40, ArrowRight: 39, ' ': 32 }[key];
|
||||
await call('Input.dispatchKeyEvent', { type: 'keyDown', key, code, modifiers, windowsVirtualKeyCode: virtual, text: key === 'Enter' ? '\r' : key.length === 1 ? key : '' });
|
||||
await call('Input.dispatchKeyEvent', { type: 'keyUp', key, code, modifiers, windowsVirtualKeyCode: virtual });
|
||||
},
|
||||
async close() {
|
||||
await send('Browser.close').catch(() => {});
|
||||
if (child.exitCode === null) await new Promise(resolve => child.once('exit', resolve));
|
||||
await rm(profile, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {browser} from './browser.mjs';
|
||||
import {mkdir,writeFile,readFile} from 'node:fs/promises';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import assert from 'node:assert/strict';
|
||||
const out=new URL('../evidence/',import.meta.url), downloads=new URL('downloads/',out);
|
||||
await mkdir(downloads,{recursive:true});
|
||||
const report={observedAt:new Date().toISOString(),checks:[],renderedContrast:[]};
|
||||
const b=await browser();
|
||||
const url=new URL('../index.html',import.meta.url).href;
|
||||
const change=async(id,value)=>b.evaluate(`(()=>{const e=document.getElementById(${JSON.stringify(id)});e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('change',{bubbles:true}))})()`);
|
||||
const mode=async value=>b.evaluate(`document.querySelector('[name=mode][value=${value}]').click()`);
|
||||
const screenshot=async(section,name,width=1440)=>{
|
||||
await b.viewport(width,1100);
|
||||
await b.evaluate(`document.getElementById('${section}').scrollIntoView();new Promise(requestAnimationFrame)`);
|
||||
await b.screenshot(new URL(name,out));
|
||||
};
|
||||
try{
|
||||
await b.viewport(1440,1100);await b.navigate(url);
|
||||
// Computed foreground/background values on rendered text, not just token formulas.
|
||||
for(const p of await b.evaluate('Brand.palettes.map(p=>p.id)')) for(const m of ['light','dim','dark']){
|
||||
await change('palette',p);await mode(m);
|
||||
const result=await b.evaluate(`(()=>{
|
||||
const hex=c=>{const n=c.match(/[\\d.]+/g);return '#'+n.slice(0,3).map(v=>Math.round(+v).toString(16).padStart(2,'0')).join('')};
|
||||
const bg=e=>{for(let n=e;n;n=n.parentElement){const c=getComputedStyle(n).backgroundColor;if(c!=='rgba(0, 0, 0, 0)'&&c!=='transparent')return hex(c)}return '#ffffff'};
|
||||
const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n, checks=[];
|
||||
while(n=walker.nextNode()){
|
||||
const e=n.parentElement;if(!n.textContent.trim()||e.closest('script,style,noscript,.sr-only,button:disabled,option'))continue;
|
||||
const rect=e.getBoundingClientRect();if(!rect.width||!rect.height)continue;
|
||||
const style=getComputedStyle(e);if(style.visibility==='hidden'||style.display==='none')continue;
|
||||
const fg=hex(style.color),back=bg(e),ratio=Brand.contrast(fg,back);
|
||||
const large=parseFloat(style.fontSize)>=24||(parseFloat(style.fontSize)>=18.67 && parseFloat(style.fontWeight)>=700);
|
||||
checks.push({text:n.textContent.trim().slice(0,65),fg,bg:back,ratio,minimum:large?3:4.5});
|
||||
}
|
||||
for(const e of document.querySelectorAll('input:not([type=radio]),textarea,select')){
|
||||
const style=getComputedStyle(e),fg=hex(style.color),back=bg(e);checks.push({text:e.id,fg,bg:back,ratio:Brand.contrast(fg,back),minimum:4.5});
|
||||
}
|
||||
return {count:checks.length,min:Math.min(...checks.map(x=>x.ratio)),failures:checks.filter(x=>x.ratio<x.minimum)};
|
||||
})()`);
|
||||
report.renderedContrast.push({palette:p,mode:m,...result});
|
||||
assert.equal(result.failures.length,0,JSON.stringify({p,m,...result}));
|
||||
}
|
||||
report.checks.push({name:'Computed rendered text contrast across 30 combinations',result:'pass'});
|
||||
await change('palette','harbor');await mode('light');
|
||||
for(const section of ['identity','typography','color','components','review'])await screenshot(section,`section-${section}-light.png`);
|
||||
await screenshot('color','section-color-ultrawide.png',3440);
|
||||
await screenshot('identity','section-identity-mobile.png',390);
|
||||
await screenshot('components','section-components-mobile.png',320);
|
||||
await mode('dark');await screenshot('color','section-color-dark.png');
|
||||
await mode('light');await b.viewport(1440,768);
|
||||
await b.evaluate('document.getElementById("reset").focus()');await b.key('Tab','Tab',8);
|
||||
const focus=await b.evaluate(`(()=>{const e=document.activeElement,r=e.getBoundingClientRect();return {text:e.textContent,top:r.top,bottom:r.bottom,viewport:innerHeight,style:getComputedStyle(e).outlineStyle}})()`);
|
||||
assert(focus.top>=0&&focus.bottom<=focus.viewport,JSON.stringify(focus));
|
||||
await b.screenshot(new URL('focus-short-desktop.png',out));
|
||||
report.checks.push({name:'Sidebar keyboard focus visible at 768 px viewport height',result:'pass',focus});
|
||||
const ax=await b.call('Accessibility.getFullAXTree');
|
||||
const missingNames=ax.nodes.filter(n=>!n.ignored&&['button','combobox','textbox','radio','link'].includes(n.role?.value)&&!n.name?.value);
|
||||
assert.equal(missingNames.length,0);
|
||||
report.checks.push({name:'Chromium accessibility tree gives interactive elements accessible names',result:'pass',namedControls:ax.nodes.filter(n=>!n.ignored&&['button','combobox','textbox','radio','link'].includes(n.role?.value)).length});
|
||||
await b.call('Browser.setDownloadBehavior',{behavior:'allow',downloadPath:fileURLToPath(downloads)});
|
||||
await b.evaluate('document.getElementById("review-notes").value="Prefer Relay. Please refine the small-size gaps.";document.getElementById("download-review").click();document.getElementById("export").click()');
|
||||
// Wait only for these two local browser downloads, bounded at five seconds.
|
||||
let saved;
|
||||
for(let n=0;n<50;n++){
|
||||
try{saved=[await readFile(new URL('mosaic-brand-review.txt',downloads),'utf8'),await readFile(new URL('mosaic-harbor-tokens.json',downloads),'utf8')];break;}catch{await new Promise(r=>setTimeout(r,100));}
|
||||
}
|
||||
assert(saved,'Local downloads did not complete');
|
||||
assert(saved[0].includes('Prefer Relay.'));
|
||||
assert.deepEqual(Object.keys(JSON.parse(saved[1]).modes),['light','dim','dark']);
|
||||
report.checks.push({name:'Review notes and all-three-mode palette JSON downloaded and content-verified',result:'pass'});
|
||||
// Double every computed text size, including authored pixel sizes.
|
||||
await b.viewport(320,1000);
|
||||
await b.evaluate(`(()=>{const sizes=[...document.querySelectorAll('body *')].map(e=>[e,parseFloat(getComputedStyle(e).fontSize)]);for(const[e,size]of sizes)e.style.fontSize=size*2+'px'})()`);
|
||||
let expanded=await b.evaluate('({width:innerWidth,scroll:document.documentElement.scrollWidth})');
|
||||
assert(expanded.scroll<=expanded.width,JSON.stringify(expanded));
|
||||
await b.screenshot(new URL('board-all-text-200-320.png',out));
|
||||
report.checks.push({name:'Every computed text size doubled at 320 px',result:'pass',measurement:expanded});
|
||||
// Font failure and storage denial are injected only into this isolated test browser.
|
||||
await b.call('Network.enable');await b.call('Network.setCacheDisabled',{cacheDisabled:true});
|
||||
await b.call('Network.setBlockedURLs',{urls:['*woff2*']});
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:'Object.defineProperty(window,"localStorage",{get(){throw new DOMException("Unavailable","SecurityError")}})'});
|
||||
await b.navigate(url);await change('palette','carmine');await mode('dim');
|
||||
assert.equal(await b.evaluate('document.documentElement.dataset.palette'),'carmine');
|
||||
expanded=await b.evaluate('({width:innerWidth,scroll:document.documentElement.scrollWidth})');
|
||||
assert(expanded.scroll<=expanded.width,JSON.stringify(expanded));
|
||||
assert(!(await b.evaluate('document.fonts.check(\'16px "DM Sans"\')')));
|
||||
await b.screenshot(new URL('board-font-storage-fallback-320.png',out));
|
||||
report.checks.push({name:'Blocked fonts and denied storage: controls still work and fallback reflows',result:'pass',measurement:expanded});
|
||||
}catch(error){report.failure=error.stack;}
|
||||
finally{await b.close();}
|
||||
await writeFile(new URL('detail-verification.json',out),JSON.stringify(report,null,2)+'\n');
|
||||
console.log(JSON.stringify(report,null,2));
|
||||
if(report.failure)process.exitCode=1;
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Fetch public font assets and OFL licenses. Run only when refreshing assets."""
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
import re
|
||||
|
||||
out = Path(__file__).resolve().parent.parent / 'assets' / 'fonts'
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
fonts = [('DM Sans', 'dm-sans', 'dmsans'), ('IBM Plex Sans', 'ibm-plex-sans', 'ibmplexsans'), ('Manrope', 'manrope', 'manrope')]
|
||||
for family, slug, upstream in fonts:
|
||||
url = 'https://fonts.googleapis.com/css2?family=' + family.replace(' ', '+') + ':wght@400;500;600;700&display=swap'
|
||||
req = urllib.request.Request(url, headers={'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36'})
|
||||
css = urllib.request.urlopen(req, timeout=25).read().decode()
|
||||
# Google returns one Latin block per requested weight. Keep all four real weights.
|
||||
sources = []
|
||||
for weight in ['400','500','600','700']:
|
||||
blocks = re.findall(r'/\* latin \*/\s*(@font-face\s*\{[^}]+\})', css)
|
||||
block = next((b for b in blocks if f'font-weight: {weight};' in b), None)
|
||||
if not block:
|
||||
raise RuntimeError(f'No Latin block for {family} {weight}')
|
||||
source = re.search(r'url\(([^)]+)\)', block).group(1)
|
||||
data = urllib.request.urlopen(source, timeout=25).read()
|
||||
assert data[:4] == b'wOF2', 'Expected WOFF2'
|
||||
name = f'{slug}-{weight}.woff2'
|
||||
(out / name).write_bytes(data)
|
||||
sources.append(f'{weight}: {source}')
|
||||
license_url = f'https://raw.githubusercontent.com/google/fonts/main/ofl/{upstream}/OFL.txt'
|
||||
license_text = urllib.request.urlopen(license_url, timeout=25).read()
|
||||
assert b'SIL OPEN FONT LICENSE' in license_text
|
||||
(out / f'{slug}-OFL.txt').write_bytes(license_text)
|
||||
(out / f'{slug}-sources.txt').write_text(f'{family}\nCSS: {url}\nLicense: {license_url}\n' + '\n'.join(sources) + '\n')
|
||||
print(f'{family}: 4 WOFF2 weights and OFL license saved')
|
||||
@@ -0,0 +1,41 @@
|
||||
import {browser} from './browser.mjs';
|
||||
import {writeFile,readFile,stat} from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
const b=await browser(), report={observedAt:new Date().toISOString(),checks:[]};
|
||||
const url=new URL('../index.html',import.meta.url);
|
||||
try{
|
||||
await b.viewport(1440,900);await b.navigate(url.href);
|
||||
const resources=(await b.call('Page.getResourceTree')).frameTree.resources.map(x=>x.url);
|
||||
assert(resources.length>5);assert(resources.every(u=>u.startsWith('file:')));
|
||||
report.checks.push({name:'Entry page loads only local resources',result:'pass',resourceCount:resources.length});
|
||||
const links=await b.evaluate('[...document.querySelectorAll("a[href],link[href],script[src]")].map(x=>x.getAttribute("href")||x.getAttribute("src"))');
|
||||
for(const link of links){
|
||||
if(link.startsWith('#'))assert(await b.evaluate(`!!document.getElementById(${JSON.stringify(link.slice(1))})`),link);
|
||||
else assert((await stat(new URL(link,url))).isFile(),link);
|
||||
}
|
||||
const css=await readFile(new URL('../styles.css',import.meta.url),'utf8');
|
||||
for(const match of css.matchAll(/url\('([^']+)'\)/g))assert((await stat(new URL(match[1],url))).isFile());
|
||||
report.checks.push({name:'HTML and CSS asset links and in-page anchors resolve',result:'pass'});
|
||||
await b.evaluate('document.querySelector(".skip").focus()');await b.key('Enter');
|
||||
assert.equal(await b.evaluate('document.activeElement.id'),'main');
|
||||
report.checks.push({name:'Keyboard skip link moves focus to main',result:'pass'});
|
||||
for(const width of [699,700,959,960,1199,1200,1699,1700,2399,2400]){
|
||||
await b.viewport(width,900);
|
||||
assert(await b.evaluate('document.documentElement.scrollWidth<=innerWidth'),String(width));
|
||||
}
|
||||
report.checks.push({name:'Both sides of five responsive breakpoints have no page overflow',result:'pass',widths:[699,700,959,960,1199,1200,1699,1700,2399,2400]});
|
||||
for(const width of [320,1440]){
|
||||
await b.viewport(width,900);
|
||||
const small=await b.evaluate(`([...document.querySelectorAll('button,input,select,textarea,.controls nav a,.brand-home,.review-link')].filter(e=>!e.disabled).map(e=>{const r=e.getBoundingClientRect();return {id:e.id||e.textContent.trim().slice(0,30),width:r.width,height:r.height}}).filter(r=>r.width<24||r.height<24))`);
|
||||
assert.deepEqual(small,[]);
|
||||
}
|
||||
report.checks.push({name:'Interactive control targets meet 24 px at 320 and 1440 widths',result:'pass',qualification:'Inline prose/footer links use the WCAG inline-text exception.'});
|
||||
await b.evaluate('localStorage.setItem("mosaic-brand-preview-v1", "{broken JSON");');
|
||||
await b.navigate(url.href);
|
||||
assert.equal(await b.evaluate('document.documentElement.dataset.palette'),'harbor');
|
||||
report.checks.push({name:'Malformed preference storage recovers to a usable default',result:'pass'});
|
||||
}catch(error){report.failure=error.stack;}
|
||||
finally{await b.close();}
|
||||
await writeFile(new URL('../evidence/final-verification.json',import.meta.url),JSON.stringify(report,null,2)+'\n');
|
||||
console.log(JSON.stringify(report,null,2));
|
||||
if(report.failure)process.exitCode=1;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { browser } from './browser.mjs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
const out = new URL('../evidence/', import.meta.url);
|
||||
await mkdir(out, {recursive: true});
|
||||
const b = await browser();
|
||||
const results = [];
|
||||
try {
|
||||
for (const [name, url] of [['t3','https://t3.codes/'], ['buzz','https://buzz.xyz/']]) {
|
||||
const entry = {name, url, observedAt: new Date().toISOString()};
|
||||
try {
|
||||
await b.viewport(1440, 1100);
|
||||
await b.navigate(url);
|
||||
// Allow the reference sites' entrance animation to finish before capture.
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
entry.desktop = await b.evaluate(`({title:document.title, url:location.href,
|
||||
headings:[...document.querySelectorAll('h1,h2,h3')].map(x=>x.textContent.trim()).slice(0,25),
|
||||
controls:[...document.querySelectorAll('button,a,input')].map(x=>({tag:x.tagName,text:(x.textContent||x.getAttribute('aria-label')||x.placeholder||'').trim().slice(0,100),href:x.getAttribute('href')})).filter(x=>x.text).slice(0,65),
|
||||
body:{font:getComputedStyle(document.body).fontFamily,bg:getComputedStyle(document.body).backgroundColor,color:getComputedStyle(document.body).color},
|
||||
text:document.body.innerText.slice(0,6500)})`);
|
||||
await b.screenshot(new URL(`${name}-desktop.png`, out));
|
||||
await b.evaluate('window.scrollTo(0, 1500)');
|
||||
await new Promise(resolve => setTimeout(resolve, 700));
|
||||
await b.screenshot(new URL(`${name}-details.png`, out));
|
||||
await b.evaluate('window.scrollTo(0, 0)');
|
||||
await b.viewport(390, 844);
|
||||
await new Promise(resolve => setTimeout(resolve, 700));
|
||||
await b.screenshot(new URL(`${name}-mobile.png`, out));
|
||||
entry.mobile = await b.evaluate('({width:innerWidth,scrollWidth:document.documentElement.scrollWidth})');
|
||||
} catch(error) {entry.error = error.message;}
|
||||
results.push(entry);
|
||||
}
|
||||
} finally {await b.close();}
|
||||
await writeFile(new URL('references.json', out), JSON.stringify(results, null, 2));
|
||||
console.log(results.map(x=>({name:x.name,title:x.desktop?.title,error:x.error,mobile:x.mobile})));
|
||||
@@ -0,0 +1,106 @@
|
||||
import '../brand.js';
|
||||
import {browser} from './browser.mjs';
|
||||
import {mkdir,writeFile} from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
const out=new URL('../evidence/',import.meta.url);
|
||||
await mkdir(out,{recursive:true});
|
||||
const report={observedAt:new Date().toISOString(),browser:'Installed headless Chromium through CDP',contrast:[],layouts:[],interactions:[],limits:['No Firefox, Safari, real mobile device, screen reader, or trademark clearance.']};
|
||||
for(const p of Brand.palettes)for(const mode of ['light','dim','dark']){
|
||||
const t=Brand.tokens(p,mode),checks=[];
|
||||
for(const fg of ['text','muted','action','accent','success','warning','danger'])for(const bg of ['canvas','surface','raised'])checks.push({fg,bg,minimum:4.5,ratio:Brand.contrast(t[fg],t[bg])});
|
||||
for(const fg of ['border','focus'])for(const bg of ['canvas','surface','raised'])checks.push({fg,bg,minimum:3,ratio:Brand.contrast(t[fg],t[bg])});
|
||||
checks.push({fg:'onAction',bg:'action',minimum:4.5,ratio:Brand.contrast(t.onAction,t.action)});
|
||||
assert(checks.every(c=>c.ratio>=c.minimum),`${p.id}/${mode} contrast`);
|
||||
report.contrast.push({palette:p.id,mode,checks});
|
||||
}
|
||||
const b=await browser();
|
||||
const url=new URL('../index.html',import.meta.url).href;
|
||||
const change=async(id,value)=>b.evaluate(`(()=>{const e=document.getElementById(${JSON.stringify(id)});e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('change',{bubbles:true}));return e.value})()`);
|
||||
const mode=async value=>b.evaluate(`document.querySelector('[name="mode"][value="${value}"]').click()`);
|
||||
const measure=()=>b.evaluate(`({width:innerWidth,scroll:document.documentElement.scrollWidth,body:document.body.scrollWidth,overflows:[...document.querySelectorAll('main *,header *,aside *')].filter(e=>{const r=e.getBoundingClientRect();return r.width>0&&(r.right>innerWidth+1||r.left < -1)&&!e.classList.contains('sr-only')}).slice(0,10).map(e=>({tag:e.tagName,class:e.className,id:e.id}))})`);
|
||||
try{
|
||||
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:'window.__errors=[];addEventListener("error",e=>window.__errors.push(e.message));addEventListener("unhandledrejection",e=>window.__errors.push(String(e.reason)));'});
|
||||
await b.viewport(1440,1100);
|
||||
await b.navigate(url);
|
||||
assert.equal(await b.evaluate('document.querySelectorAll("[data-palette-card]").length'),10);
|
||||
assert.equal(await b.evaluate('document.querySelectorAll("[data-mark-card]").length'),3);
|
||||
assert.equal(await b.evaluate('document.querySelectorAll("[data-font-card]").length'),3);
|
||||
// All theme choices are applied through the actual page controls, then laid out.
|
||||
for(const p of Brand.palettes)for(const m of ['light','dim','dark']){
|
||||
await change('palette',p.id);await mode(m);
|
||||
assert.equal(await b.evaluate('document.documentElement.dataset.palette'),p.id);
|
||||
assert.equal(await b.evaluate('document.documentElement.dataset.mode'),m);
|
||||
for(const width of [320,390,768,960,1440,1920,2560,3440]){
|
||||
await b.viewport(width,1000);
|
||||
const result=await measure();
|
||||
report.layouts.push({palette:p.id,mode:m,...result});
|
||||
assert(result.scroll<=width && result.body<=width && !result.overflows.length,JSON.stringify(report.layouts.at(-1)));
|
||||
}
|
||||
}
|
||||
await change('palette','harbor');await mode('light');
|
||||
for(const width of [320,390,768,1440,3440]){
|
||||
await b.viewport(width,width===390?844:1100);await b.evaluate('scrollTo(0,0)');
|
||||
await b.screenshot(new URL(`board-light-${width}.png`,out));
|
||||
}
|
||||
for(const m of ['dim','dark']){
|
||||
await mode(m);await b.viewport(1440,1100);await b.evaluate('scrollTo(0,0)');
|
||||
await b.screenshot(new URL(`board-${m}-1440.png`,out));
|
||||
}
|
||||
// Real keyboard: native select, radio navigation, buttons, forward/reverse tab.
|
||||
await mode('light');await b.viewport(1440,1100);
|
||||
await b.evaluate('document.getElementById("palette").focus()');
|
||||
await b.key('ArrowDown');await b.key('Enter');
|
||||
assert.equal(await b.evaluate('document.getElementById("palette").value'),'carmine');
|
||||
await b.evaluate('document.querySelector("[name=mode][value=light]").focus()');
|
||||
await b.key('ArrowRight');
|
||||
assert.equal(await b.evaluate('document.documentElement.dataset.mode'),'dim');
|
||||
const focus=await b.evaluate('({outline:getComputedStyle(document.activeElement.nextElementSibling).outlineStyle,width:getComputedStyle(document.activeElement.nextElementSibling).outlineWidth})');
|
||||
assert.equal(focus.outline,'solid');assert.equal(focus.width,'3px');
|
||||
await b.evaluate('document.querySelector("[data-choose-mark=weave]").focus()');await b.key('Enter');
|
||||
assert.equal(await b.evaluate('document.getElementById("mark").value'),'weave');
|
||||
await b.evaluate('document.querySelector("[data-choose-font=plex]").focus()');await b.key(' ' ,'Space');
|
||||
assert.equal(await b.evaluate('document.getElementById("font").value'),'plex');
|
||||
await b.evaluate('document.getElementById("sample-title").focus()');
|
||||
await b.key('Tab');assert.equal(await b.evaluate('document.activeElement.id'),'sample-state');
|
||||
await b.key('Tab','Tab',8);assert.equal(await b.evaluate('document.activeElement.id'),'sample-title');
|
||||
report.interactions.push({name:'Keyboard selection, native radio arrow, Enter/Space buttons, Tab/Shift+Tab',result:'pass',focus});
|
||||
await change('sample-state','error');
|
||||
assert.equal(await b.evaluate('document.getElementById("sample-title").value'),'Summer reading notes');
|
||||
await b.evaluate('document.getElementById("sample-action").focus()');await b.key('Enter');
|
||||
assert.equal(await b.evaluate('document.getElementById("sample-feedback").dataset.state'),'success');
|
||||
await change('sample-state','loading');assert(await b.evaluate('document.getElementById("sample-action").disabled'));
|
||||
for(const s of ['empty','ready']) {await change('sample-state',s);assert.equal(await b.evaluate('document.getElementById("sample-feedback").dataset.state'),s);}
|
||||
report.interactions.push({name:'All five component states and error recovery preserving input',result:'pass'});
|
||||
// All bundled families loaded, and long labels remain usable at mobile width.
|
||||
for(const f of Brand.fonts){
|
||||
await change('font',f.id);await b.evaluate('document.fonts.ready.then(()=>true)');
|
||||
const loaded=await b.evaluate(`document.fonts.check('16px "${f.name}"')`);
|
||||
assert(loaded,f.name+' font load');
|
||||
await b.viewport(320,1000);assert((await measure()).scroll<=320);
|
||||
}
|
||||
report.interactions.push({name:'Three local fonts loaded and reflowed at 320 px',result:'pass'});
|
||||
await b.evaluate('document.getElementById("review-notes").value="Keep this review note.";document.getElementById("review-notes").dispatchEvent(new Event("input"));document.getElementById("reset").click()');
|
||||
assert.equal(await b.evaluate('document.getElementById("review-notes").value'),'Keep this review note.');
|
||||
await b.navigate(url);
|
||||
assert.equal(await b.evaluate('document.getElementById("review-notes").value'),'Keep this review note.');
|
||||
report.interactions.push({name:'Local preference and note storage; reset preserves notes',result:'pass'});
|
||||
await b.call('Emulation.setEmulatedMedia',{features:[{name:'prefers-reduced-motion',value:'reduce'}]});
|
||||
await mode('dark');await b.key('Tab');
|
||||
assert(await b.evaluate('matchMedia("(prefers-reduced-motion: reduce)").matches'));
|
||||
report.interactions.push({name:'Reduced motion still permits theme and keyboard interaction',result:'pass'});
|
||||
await b.viewport(320,1000);
|
||||
await b.evaluate(`(()=>{const style=document.createElement('style');style.id='spacing-test';style.textContent='*{line-height:1.5!important;letter-spacing:.12em!important;word-spacing:.16em!important}p{margin-bottom:2em!important}';document.head.append(style)})()`);
|
||||
let result=await measure();assert(result.scroll<=320,JSON.stringify(result));
|
||||
await b.screenshot(new URL('board-text-spacing-320.png',out));
|
||||
await b.evaluate('document.getElementById("spacing-test").remove();document.documentElement.style.fontSize="200%"');
|
||||
result=await measure();assert(result.scroll<=320,JSON.stringify(result));
|
||||
await b.screenshot(new URL('board-text-200-320.png',out));
|
||||
report.interactions.push({name:'320 px text-spacing overrides and root text size 200%',result:'pass',qualification:'Not browser-native zoom; pixel-sized component text also needs separate scaling checks.'});
|
||||
await b.evaluate('document.documentElement.style.fontSize=""');
|
||||
const errors=await b.evaluate('window.__errors');assert.deepEqual(errors,[]);
|
||||
report.interactions.push({name:'Uncaught page errors',result:'pass',errors});
|
||||
} catch(error){report.failure=error.stack;}
|
||||
finally{await b.close();}
|
||||
await writeFile(new URL('verification.json',out),JSON.stringify(report,null,2)+'\n');
|
||||
console.log(JSON.stringify({contrastSets:report.contrast.length,layouts:report.layouts.length,interactions:report.interactions,failure:report.failure},null,2));
|
||||
if(report.failure)process.exitCode=1;
|
||||
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"observedAt": "2026-09-08T13:49:13.216Z",
|
||||
"checks": [
|
||||
{
|
||||
"name": "Computed rendered text contrast across 30 combinations",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Sidebar keyboard focus visible at 768 px viewport height",
|
||||
"result": "pass",
|
||||
"focus": {
|
||||
"text": "05 Your review",
|
||||
"top": 630.109375,
|
||||
"bottom": 674.109375,
|
||||
"viewport": 768,
|
||||
"style": "solid"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Chromium accessibility tree gives interactive elements accessible names",
|
||||
"result": "pass",
|
||||
"namedControls": 60
|
||||
},
|
||||
{
|
||||
"name": "Review notes and all-three-mode palette JSON downloaded and content-verified",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Every computed text size doubled at 320 px",
|
||||
"result": "pass",
|
||||
"measurement": {
|
||||
"width": 320,
|
||||
"scroll": 305
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Blocked fonts and denied storage: controls still work and fallback reflows",
|
||||
"result": "pass",
|
||||
"measurement": {
|
||||
"width": 320,
|
||||
"scroll": 305
|
||||
}
|
||||
}
|
||||
],
|
||||
"renderedContrast": [
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.612829123046221,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.504658476260286,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "harbor",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.167615908293434,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.662315521062682,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.587463027691096,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "carmine",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.695553088417374,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.515881820137726,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.516521277596746,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "atlantic",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.597708271207925,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.518475634580629,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.538542941476706,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "terracotta",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.525894813496129,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.664757683683436,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.560324571031366,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "aubergine",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.999968974923534,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.549690583339469,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.575057699689022,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "mineral",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.6577550779736105,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.706276899711153,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.5100156460147796,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "cobalt",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.534974293870712,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.636598241711077,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.584411037938534,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "rosewood",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 4.964802403187582,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.613910901400341,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.565616755282305,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "graphite",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.478526170936041,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "light",
|
||||
"count": 274,
|
||||
"min": 4.52023260851089,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "dim",
|
||||
"count": 274,
|
||||
"min": 4.564633732635816,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"palette": "grove",
|
||||
"mode": "dark",
|
||||
"count": 274,
|
||||
"min": 5.707548102674204,
|
||||
"failures": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"observedAt": "2026-09-08T13:52:02.918Z",
|
||||
"checks": [
|
||||
{
|
||||
"name": "Entry page loads only local resources",
|
||||
"result": "pass",
|
||||
"resourceCount": 11
|
||||
},
|
||||
{
|
||||
"name": "HTML and CSS asset links and in-page anchors resolve",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Keyboard skip link moves focus to main",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"name": "Both sides of five responsive breakpoints have no page overflow",
|
||||
"result": "pass",
|
||||
"widths": [
|
||||
699,
|
||||
700,
|
||||
959,
|
||||
960,
|
||||
1199,
|
||||
1200,
|
||||
1699,
|
||||
1700,
|
||||
2399,
|
||||
2400
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Interactive control targets meet 24 px at 320 and 1440 widths",
|
||||
"result": "pass",
|
||||
"qualification": "Inline prose/footer links use the WCAG inline-text exception."
|
||||
},
|
||||
{
|
||||
"name": "Malformed preference storage recovers to a usable default",
|
||||
"result": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,111 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="description" content="Mosaic Stack brand studies. A local, interactive review of identity, type, and color. Not a working product.">
|
||||
<title>Mosaic Stack · Brand studies</title>
|
||||
<link rel="icon" href="assets/logos/assembly.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="brand.js" defer></script>
|
||||
<script src="board.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to brand studies</a>
|
||||
<header class="masthead">
|
||||
<a class="brand-home" href="#top" aria-label="Mosaic Stack brand board, top"><span data-logo></span><span>Mosaic Stack</span></a>
|
||||
<div class="masthead-note">Brand studies <span class="edition">01 / for discussion</span></div>
|
||||
<a href="#review" class="review-link">Review guide <span aria-hidden="true">↗</span></a>
|
||||
</header>
|
||||
<div id="top" class="layout">
|
||||
<aside class="controls" aria-label="Brand preview controls">
|
||||
<div class="control-inner">
|
||||
<p class="eyebrow">Make it yours</p>
|
||||
<p class="control-intro">Change the palette, not the product.</p>
|
||||
<fieldset class="mode-control"><legend>Appearance</legend><div class="segmented" id="modes">
|
||||
<label><input type="radio" name="mode" value="light" checked><span>Light</span></label>
|
||||
<label><input type="radio" name="mode" value="dim"><span>Dim</span></label>
|
||||
<label><input type="radio" name="mode" value="dark"><span>Dark</span></label>
|
||||
</div></fieldset>
|
||||
<label class="field" for="palette">Color palette<select id="palette"></select></label>
|
||||
<label class="field" for="font">Typography<select id="font"></select></label>
|
||||
<label class="field" for="mark">Logo concept<select id="mark"></select></label>
|
||||
<p class="local-note">Preview only. Your choices stay in this browser when storage is available.</p>
|
||||
<nav aria-label="Brand board sections">
|
||||
<a href="#identity"><span>01</span> Identity</a>
|
||||
<a href="#typography"><span>02</span> Typography</a>
|
||||
<a href="#color"><span>03</span> Color system</a>
|
||||
<a href="#components"><span>04</span> Iconography & controls</a>
|
||||
<a href="#review"><span>05</span> Your review</a>
|
||||
</nav>
|
||||
<button id="reset" class="text-button" type="button">Reset preview</button>
|
||||
</div>
|
||||
</aside>
|
||||
<main id="main" tabindex="-1">
|
||||
<noscript><p class="notice">JavaScript is needed for these interactive studies. Enable it and reload. The requirements and review notes are also available as Markdown files in this folder.</p></noscript>
|
||||
<section class="intro" aria-labelledby="intro-title">
|
||||
<div class="intro-copy"><p class="eyebrow">Mosaic Stack / identity exploration</p>
|
||||
<h1 id="intro-title">One identity.<br>Many ways to work.</h1>
|
||||
<p class="lead">An AI operating system for the things you make, manage, and do together.</p>
|
||||
<p>Let's choose the visual language before we choose the dashboard. Compare three marks, three sans-serif families, and ten palettes in three appearances.</p>
|
||||
<a href="#identity" class="inline-link">Start with the mark <span aria-hidden="true">↓</span></a>
|
||||
</div>
|
||||
<div class="identity-stage" aria-label="Current brand combination">
|
||||
<div class="stage-meta"><span id="current-palette">Harbor</span><span id="current-mode">Light</span></div>
|
||||
<div class="stage-mark" data-logo></div>
|
||||
<div class="stage-wordmark">Mosaic Stack<span>mosaicstack.dev</span></div>
|
||||
<div class="stage-bottom"><span>Independent pieces.<br>A shared place to work.</span><span class="stage-swatch" aria-hidden="true"></span></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="identity" class="board-section" aria-labelledby="identity-title">
|
||||
<div class="section-heading"><div><p class="eyebrow">01 / Identity</p><h2 id="identity-title">Recognizable without a color.</h2></div><p>Each concept starts as a single-color shape. Choose one to see it in the page header and brand specimen.</p></div>
|
||||
<div id="logo-grid" class="three-grid"></div>
|
||||
<p class="section-note">Original concept studies, not cleared trademarks. Small-size recognition and final proportions need your judgment. No logo is approved yet.</p>
|
||||
</section>
|
||||
<section id="typography" class="board-section" aria-labelledby="type-title">
|
||||
<div class="section-heading"><div><p class="eyebrow">02 / Typography</p><h2 id="type-title">A voice you can read all day.</h2></div><p>Compare the same content at the same sizes. Each choice changes the board, while these specimens keep their own typeface.</p></div>
|
||||
<div id="font-grid" class="three-grid"></div>
|
||||
<div class="type-scale"><span><b>32</b>Section heading</span><span><b>20</b>Card title</span><span><b>16</b>Body & controls</span><span><b>13</b>Supporting labels</span><p>Sentence case. Tabular numerals for data. Comfortable line spacing. Monospace reserved for actual code in later designs.</p></div>
|
||||
<p class="section-note">Local Latin font files, four weights each, licensed under SIL OFL. Other scripts fall back to system fonts and need later multilingual testing.</p>
|
||||
</section>
|
||||
<section id="color" class="board-section" aria-labelledby="color-title">
|
||||
<div class="section-heading"><div><p class="eyebrow">03 / Color system</p><h2 id="color-title">Ten palettes. Same Mosaic.</h2></div><p>Hue relationships give each palette a reason. Contrast determines the usable shades. Choose any appearance directly on a palette card.</p></div>
|
||||
<div class="color-principles"><p><b>Light</b>Pale tinted canvas, clean surfaces.</p><p><b>Dim</b>Charcoal layers with a restrained tint.</p><p><b>Dark</b>Deep color, with lighter raised surfaces.</p></div>
|
||||
<div id="palette-grid" class="palette-grid"></div>
|
||||
<div class="token-panel"><div><h3 id="token-title">Current tokens</h3><p>Actual values used by this preview, not production configuration.</p><button type="button" id="export" class="secondary">Download palette tokens</button></div><dl id="tokens"></dl></div>
|
||||
<p class="section-note">Success, warning, and error are separate roles. They always have a label or symbol. A red brand palette never makes color alone mean "error".</p>
|
||||
</section>
|
||||
<section id="components" class="board-section" aria-labelledby="components-title">
|
||||
<div class="section-heading"><div><p class="eyebrow">04 / Iconography & controls</p><h2 id="components-title">Quiet details. Clear meaning.</h2></div><p>One original outline family, with a 24-unit grid and consistent 1.75-unit strokes. Labels carry meaning; icons help recognition.</p></div>
|
||||
<div id="icon-grid" class="icon-grid"></div>
|
||||
<div class="component-grid">
|
||||
<div class="component-panel"><p class="eyebrow">Interaction specimen / local only</p><h3>A useful bit of feedback</h3><p>Try the button and state selector. No workspace or agent is created.</p>
|
||||
<label class="field" for="sample-title">Example project name<input id="sample-title" type="text" value="Summer reading notes" maxlength="160" aria-describedby="sample-help"></label>
|
||||
<p id="sample-help" class="small">This field stays intact when you preview an error.</p>
|
||||
<label class="field" for="sample-state">Component state<select id="sample-state"><option value="ready">Ready</option><option value="loading">Loading</option><option value="empty">Empty</option><option value="error">Error</option><option value="success">Success</option></select></label>
|
||||
<div id="sample-feedback" class="feedback" role="status"></div>
|
||||
<div class="button-row"><button id="sample-action" class="primary" type="button">Preview success</button><button type="button" class="secondary" disabled>Unavailable</button></div>
|
||||
</div>
|
||||
<div class="component-panel"><p class="eyebrow">Surface & status specimen</p><h3>Depth without a gradient</h3>
|
||||
<div class="layer-sample"><span>Canvas</span><div><span>Surface</span><div>Raised surface <span class="sample-badge">Selected</span></div></div></div>
|
||||
<ul class="status-list"><li class="success"><span data-icon="check"></span>Ready for review</li><li class="warning"><span data-icon="warning"></span>Needs your attention</li><li class="danger"><span data-icon="error"></span>Could not save. Try again.</li></ul>
|
||||
<p class="small">8 px control corners, 16 px comparison panels, and a 4 px spacing scale. No animation is required to understand a state change.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="review" class="board-section review-section" aria-labelledby="review-title">
|
||||
<div><p class="eyebrow">05 / Your review</p><h2 id="review-title">Choose a direction,<br>not a finished brand.</h2><p class="lead">My starting recommendation is Assembly, DM Sans, and Harbor.</p><p>The mark connects to the name, the font stays approachable, and blue gets room to be blue. Relay and Aperture offer less literal alternatives.</p></div>
|
||||
<div class="review-panel"><h3>What I'd like your take on</h3><ol><li>Which mark feels like Mosaic, including at 16 px?</li><li>Which font feels best in controls and longer text?</li><li>Which palette should be the default? Try it in all three appearances.</li><li>What should feel different before we design the dashboards?</li></ol>
|
||||
<label class="field" for="review-notes">Your notes<textarea id="review-notes" rows="4" placeholder="Try Relay with DM Sans. Harbor is close, but…"></textarea></label>
|
||||
<p class="small">Notes stay in this browser if storage is available. Download a copy to keep them. Nothing is sent to Dewey.</p>
|
||||
<button type="button" class="primary" id="download-review">Download review notes</button>
|
||||
<p id="selection-summary" class="small"></p>
|
||||
</div>
|
||||
</section>
|
||||
<footer><p>Local brand prototype · Not a live Mosaic Stack interface</p><p>Branding review comes next. The five dashboard designs have not been built.</p><a href="RESEARCH.md">Reference observations & inspection limits</a></footer>
|
||||
</main>
|
||||
</div>
|
||||
<p id="announcement" class="sr-only" role="status" aria-live="polite"></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,169 @@
|
||||
@font-face{font-family:'DM Sans';src:url('assets/fonts/dm-sans-400.woff2') format('woff2');font-weight:400;font-display:swap}
|
||||
@font-face{font-family:'DM Sans';src:url('assets/fonts/dm-sans-500.woff2') format('woff2');font-weight:500;font-display:swap}
|
||||
@font-face{font-family:'DM Sans';src:url('assets/fonts/dm-sans-600.woff2') format('woff2');font-weight:600;font-display:swap}
|
||||
@font-face{font-family:'DM Sans';src:url('assets/fonts/dm-sans-700.woff2') format('woff2');font-weight:700;font-display:swap}
|
||||
@font-face{font-family:'IBM Plex Sans';src:url('assets/fonts/ibm-plex-sans-400.woff2') format('woff2');font-weight:400;font-display:swap}
|
||||
@font-face{font-family:'IBM Plex Sans';src:url('assets/fonts/ibm-plex-sans-500.woff2') format('woff2');font-weight:500;font-display:swap}
|
||||
@font-face{font-family:'IBM Plex Sans';src:url('assets/fonts/ibm-plex-sans-600.woff2') format('woff2');font-weight:600;font-display:swap}
|
||||
@font-face{font-family:'IBM Plex Sans';src:url('assets/fonts/ibm-plex-sans-700.woff2') format('woff2');font-weight:700;font-display:swap}
|
||||
@font-face{font-family:Manrope;src:url('assets/fonts/manrope-400.woff2') format('woff2');font-weight:400;font-display:swap}
|
||||
@font-face{font-family:Manrope;src:url('assets/fonts/manrope-500.woff2') format('woff2');font-weight:500;font-display:swap}
|
||||
@font-face{font-family:Manrope;src:url('assets/fonts/manrope-600.woff2') format('woff2');font-weight:600;font-display:swap}
|
||||
@font-face{font-family:Manrope;src:url('assets/fonts/manrope-700.woff2') format('woff2');font-weight:700;font-display:swap}
|
||||
:root{--canvas:#f3f5f7;--surface:#fff;--raised:#e8ebef;--text:#283343;--muted:#505c6b;--action:#215fac;--onAction:#fff;--accent:#98642e;--line:#cbd1d9;--border:#6e7b8d;--focus:#215fac;--font:'DM Sans',system-ui,sans-serif;color-scheme:light}
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-padding-top:24px}
|
||||
body{margin:0;background:var(--canvas);color:var(--text);font:400 1rem/1.55 var(--font);overflow-wrap:break-word}
|
||||
button,input,select,textarea{font:inherit}
|
||||
button,select{cursor:pointer}
|
||||
button,a,input,select,textarea{-webkit-tap-highlight-color:transparent}
|
||||
a{color:var(--action);text-underline-offset:4px}
|
||||
a:hover{text-decoration-thickness:2px}
|
||||
:focus-visible{outline:3px solid var(--focus);outline-offset:4px}
|
||||
button:disabled{cursor:not-allowed;opacity:.55}
|
||||
button{border:1px solid var(--border);border-radius:8px;padding:10px 16px;background:var(--surface);color:var(--text);min-height:44px;font-weight:600;line-height:1.4}
|
||||
button:hover:not(:disabled){background:var(--raised)}
|
||||
button.primary{background:var(--action);color:var(--onAction);border-color:var(--action)}
|
||||
button.primary:hover{box-shadow:inset 0 0 0 2px var(--onAction)}
|
||||
input,select,textarea{width:100%;min-width:0;min-height:44px;border:1px solid var(--border);border-radius:8px;padding:9px 12px;background:var(--surface);color:var(--text)}
|
||||
textarea{resize:vertical}
|
||||
textarea::placeholder{color:var(--muted);opacity:1}
|
||||
p{margin:0 0 16px;max-width:68ch}
|
||||
h1,h2,h3{line-height:1.15;letter-spacing:-.035em;margin:0 0 16px;font-weight:600;overflow-wrap:anywhere}
|
||||
h1{font-size:clamp(2.4rem,3.9vw,5.75rem);letter-spacing:-.055em}
|
||||
h2{font-size:2rem}
|
||||
h3{font-size:1.25rem;letter-spacing:-.025em}
|
||||
.logo{display:block;flex-shrink:0}
|
||||
.icon{width:24px;height:24px;display:inline-block;flex-shrink:0;vertical-align:middle}
|
||||
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
|
||||
.skip{position:fixed;left:16px;top:-100px;background:var(--surface);padding:12px 16px;z-index:10}
|
||||
.skip:focus{top:12px}
|
||||
.masthead{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:22px 32px;border-bottom:1px solid var(--line);background:var(--surface)}
|
||||
.brand-home{display:flex;align-items:center;gap:12px;color:var(--text);font-size:20px;font-weight:600;letter-spacing:-.035em;text-decoration:none;min-height:44px}
|
||||
.brand-home .logo{width:32px;height:32px;color:var(--action)}
|
||||
.masthead-note{display:flex;align-items:center;gap:16px;font-size:14px;color:var(--muted)}
|
||||
.edition{border-left:1px solid var(--line);padding-left:16px}
|
||||
.review-link{font-size:14px;min-height:44px;display:flex;align-items:center;gap:24px}
|
||||
.layout{display:grid;grid-template-columns:248px minmax(0,1fr)}
|
||||
.controls{border-right:1px solid var(--line);padding:32px 24px}
|
||||
.control-inner{position:sticky;top:24px;max-height:calc(100dvh - 48px);overflow-y:auto;padding:6px;margin:-6px}
|
||||
.eyebrow{text-transform:uppercase;letter-spacing:.11em;font-size:12px;font-weight:600;color:var(--muted);margin-bottom:12px}
|
||||
.control-intro{font-size:18px;line-height:1.4;letter-spacing:-.02em;margin-bottom:28px}
|
||||
.field{display:block;font-size:14px;font-weight:500;margin-bottom:18px}
|
||||
.field select,.field input,.field textarea{display:block;margin-top:7px;font-size:16px;font-weight:400}
|
||||
.mode-control{border:0;margin:0 0 20px;padding:0;min-width:0}
|
||||
.mode-control legend{font-size:14px;font-weight:500;margin-bottom:7px;padding:0}
|
||||
.segmented{display:flex;gap:4px;background:var(--raised);padding:4px;border-radius:10px}
|
||||
.segmented label{flex:1;position:relative;min-width:0;cursor:pointer}
|
||||
.segmented input{position:absolute;opacity:0;width:100%;height:100%;margin:0;min-height:44px;cursor:pointer}
|
||||
.segmented span{display:block;text-align:center;padding:12px 3px;border-radius:7px;font-size:13px;line-height:20px}
|
||||
.segmented input:checked+span{background:var(--action);color:var(--onAction);font-weight:600}
|
||||
.segmented input:focus-visible+span{outline:3px solid var(--focus);outline-offset:4px}
|
||||
.local-note,.small,.section-note{font-size:13px;color:var(--muted);line-height:1.6}
|
||||
.controls nav{display:grid;gap:4px;border-top:1px solid var(--line);padding:20px 0;margin-top:24px}
|
||||
.controls nav a{display:flex;gap:14px;align-items:center;padding:10px 0;text-decoration:none;color:var(--text);font-size:14px;min-height:44px}
|
||||
.controls nav a:hover{color:var(--action);text-decoration:underline}
|
||||
.controls nav span{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
|
||||
.text-button{border:0;padding:8px 0;color:var(--action);background:none;font-size:14px;text-decoration:underline;text-underline-offset:4px}
|
||||
main{min-width:0;padding:48px clamp(24px,3vw,80px) 0}
|
||||
.intro{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.85fr);gap:clamp(24px,4vw,100px);align-items:center;padding:12px 0 64px}
|
||||
.intro-copy>p:not(.eyebrow){color:var(--muted)}
|
||||
.lead{font-size:20px;line-height:1.45;letter-spacing:-.02em}
|
||||
.inline-link{display:inline-flex;align-items:center;gap:36px;min-height:44px;font-weight:600;margin-top:8px}
|
||||
.identity-stage{display:flex;flex-direction:column;align-items:center;min-height:450px;border-radius:4px 64px 4px 4px;background:var(--raised);padding:28px;color:var(--text)}
|
||||
.stage-meta{width:100%;display:flex;justify-content:space-between;gap:12px;font-size:12px;color:var(--muted)}
|
||||
.stage-mark{margin:40px 0 20px;color:var(--action)}
|
||||
.stage-mark .logo{width:140px;height:140px}
|
||||
.stage-wordmark{font-size:clamp(1.75rem,2vw,3rem);font-weight:600;letter-spacing:-.045em;text-align:center;line-height:1.1}
|
||||
.stage-wordmark>span{display:block;font-size:13px;letter-spacing:0;font-weight:400;color:var(--muted);margin-top:12px}
|
||||
.stage-bottom{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;width:100%;margin-top:36px;font-size:12px;line-height:1.5;color:var(--muted)}
|
||||
.stage-swatch{width:32px;height:32px;background:var(--accent);border-radius:0 12px 0 0;flex-shrink:0}
|
||||
.board-section{padding:48px 0 52px;border-top:1px solid var(--line);scroll-margin-top:24px}
|
||||
.section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:32px;margin-bottom:28px}
|
||||
.section-heading>div{flex:1;min-width:0}
|
||||
.section-heading>p{max-width:43ch;color:var(--muted);font-size:14px;flex:.8}
|
||||
.section-heading h2{margin-bottom:0;max-width:24ch}
|
||||
.three-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
|
||||
.study-card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:24px;display:flex;flex-direction:column;min-width:0}
|
||||
.study-card.is-selected{border-color:var(--action);box-shadow:inset 0 0 0 1px var(--action)}
|
||||
.card-kicker{font-size:12px;color:var(--muted);margin-bottom:16px;display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap}
|
||||
.selection-label{font-weight:600;color:var(--action)}
|
||||
.logo-display{color:var(--action);display:flex;align-items:center;justify-content:center;min-height:168px;background:var(--canvas);border-radius:10px;margin-bottom:24px}
|
||||
.logo-display .logo{width:110px;height:110px}
|
||||
.study-card p{font-size:14px;color:var(--muted)}
|
||||
.study-card .tradeoff{font-size:13px}
|
||||
.study-card .choose{margin-top:auto;align-self:flex-start;width:100%}
|
||||
.size-strip{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px 12px;margin:0 0 20px;padding-bottom:20px;border-bottom:1px solid var(--line)}
|
||||
.size-strip>span{display:grid;justify-items:center;gap:8px;font-size:11px;color:var(--muted)}
|
||||
.size-strip .logo{color:var(--text)}
|
||||
.size-strip .reverse{background:var(--text);color:var(--surface);padding:6px;border-radius:4px}
|
||||
.section-note{margin-top:20px;margin-bottom:0;max-width:85ch}
|
||||
.font-title{font-size:28px;margin-bottom:20px;letter-spacing:-.04em}
|
||||
.type-specimen{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:20px 0;margin-bottom:20px}
|
||||
.type-specimen .type-heading{font-size:24px;font-weight:600;line-height:1.25;color:var(--text);margin-bottom:12px}
|
||||
.type-specimen p{font-size:16px;color:var(--text)}
|
||||
.type-specimen .numerals{font-variant-numeric:tabular-nums;font-size:18px;letter-spacing:.03em;margin-bottom:0}
|
||||
.type-scale{display:flex;flex-wrap:wrap;gap:24px;padding:28px 0 0;align-items:center}
|
||||
.type-scale>span{font-size:12px;color:var(--muted);display:grid;gap:8px;min-width:100px}
|
||||
.type-scale b{font-size:28px;line-height:1;color:var(--text);font-weight:500}
|
||||
.type-scale p{font-size:13px;color:var(--muted);max-width:38ch;margin:0 0 0 auto}
|
||||
.color-principles{display:flex;gap:32px;flex-wrap:wrap;margin-bottom:28px}
|
||||
.color-principles p{font-size:14px;color:var(--muted);margin-bottom:0}
|
||||
.color-principles b{display:block;color:var(--text);font-weight:600;margin-bottom:4px}
|
||||
.palette-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}
|
||||
.palette-card{border:1px solid var(--line);background:var(--surface);border-radius:16px;padding:24px;display:flex;flex-direction:column;min-width:0}
|
||||
.palette-card.is-selected{border-color:var(--action);box-shadow:inset 0 0 0 1px var(--action)}
|
||||
.palette-card h3{display:flex;justify-content:space-between;gap:8px}
|
||||
.palette-card h3 span{font-size:12px;line-height:1.7;letter-spacing:0;color:var(--action)}
|
||||
.palette-swatches{display:flex;height:68px;gap:4px;margin-bottom:20px;border-radius:8px;overflow:hidden}
|
||||
.palette-swatches i:first-child{flex:2}
|
||||
.palette-swatches i{flex:1}
|
||||
.palette-card .theory{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:8px}
|
||||
.palette-card p{font-size:14px;color:var(--muted)}
|
||||
.palette-card .palette-use{font-size:13px;margin-top:auto}
|
||||
.palette-modes{display:flex;gap:8px;margin-top:4px;flex-wrap:wrap}
|
||||
.palette-modes button{flex:1;min-width:64px;padding:10px 4px;font-size:13px;background:var(--mini-surface);color:var(--mini-text);border-color:var(--mini-border)}
|
||||
.palette-modes button[aria-pressed=true]{box-shadow:inset 0 -3px 0 currentColor;font-weight:700}
|
||||
.palette-modes button:hover{background:var(--mini-raised)}
|
||||
.token-panel{margin-top:28px;border:1px solid var(--line);border-radius:16px;padding:24px;display:grid;grid-template-columns:minmax(0,.8fr) minmax(0,1.5fr);gap:32px}
|
||||
.token-panel p{font-size:14px;color:var(--muted)}
|
||||
.token-panel dl{margin:0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
|
||||
.token-panel dt{font-size:12px;color:var(--muted);display:flex;gap:8px;align-items:center}
|
||||
.token-panel dt i{width:12px;height:12px;border:1px solid var(--border);border-radius:2px;flex-shrink:0}
|
||||
.token-panel dd{font-size:13px;font-variant-numeric:tabular-nums;margin:4px 0 0 20px}
|
||||
.icon-grid{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:24px}
|
||||
.icon-specimen{display:flex;flex-direction:column;align-items:center;gap:16px;padding:28px 8px;background:var(--surface);text-align:center;font-size:12px}
|
||||
.icon-specimen .icon{color:var(--text)}
|
||||
.icon-specimen>span{max-width:100%;overflow-wrap:anywhere}
|
||||
.component-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}
|
||||
.component-panel{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:28px;min-width:0}
|
||||
.component-panel>p{font-size:14px;color:var(--muted)}
|
||||
.button-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}
|
||||
.feedback{padding:16px;border:1px solid var(--border);border-radius:8px;min-height:64px;background:var(--raised);display:flex;gap:10px;align-items:flex-start;font-size:14px}
|
||||
.feedback[data-state=error]{color:var(--danger);border-color:var(--danger)}
|
||||
.feedback[data-state=success]{color:var(--success);border-color:var(--success)}
|
||||
.layer-sample{background:var(--canvas);padding:20px;border:1px solid var(--line);border-radius:8px;font-size:13px}
|
||||
.layer-sample>div{padding:20px;margin-top:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px}
|
||||
.layer-sample>div>div{padding:20px;margin-top:12px;background:var(--raised);border-radius:8px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||
.sample-badge{color:var(--action);font-weight:600}
|
||||
.status-list{list-style:none;margin:24px 0;padding:0;display:grid;gap:12px;font-size:14px}
|
||||
.status-list li{display:flex;gap:10px;align-items:center}
|
||||
.success{color:var(--success)}.warning{color:var(--warning)}.danger{color:var(--danger)}
|
||||
.review-section{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:48px;align-items:start}
|
||||
.review-section>div{min-width:0}
|
||||
.review-section>div>p:not(.eyebrow):not(.lead){color:var(--muted)}
|
||||
.review-panel{background:var(--raised);border-radius:16px;padding:28px}
|
||||
.review-panel ol{padding-left:20px;font-size:14px;display:grid;gap:10px;margin:0 0 24px}
|
||||
.review-panel li{padding-left:5px}
|
||||
#selection-summary{margin:16px 0 0}
|
||||
footer{border-top:1px solid var(--line);padding:24px 0 36px;color:var(--muted);font-size:13px;display:flex;gap:12px 28px;flex-wrap:wrap}
|
||||
footer p{margin:0}
|
||||
footer a{color:var(--muted)}
|
||||
.notice{padding:20px;border:2px solid var(--border);background:var(--surface)}
|
||||
@media(min-width:1700px){.palette-grid{grid-template-columns:repeat(5,minmax(0,1fr))}.intro{grid-template-columns:minmax(0,1.2fr) minmax(0,1fr)}.identity-stage{min-height:520px}.stage-mark .logo{width:180px;height:180px}.study-card{padding:32px}.logo-display{min-height:220px}.logo-display .logo{width:140px;height:140px}}
|
||||
@media(min-width:2400px){.layout{grid-template-columns:280px minmax(0,1fr)}.controls{padding:40px 32px}.intro-copy{max-width:900px}.intro{padding-bottom:88px}.identity-stage{min-height:570px}.stage-mark .logo{width:220px;height:220px}.study-card p{max-width:65ch}.section-heading>p{max-width:60ch;flex:1}.palette-card{padding:28px}.logo-display{min-height:280px}.token-panel dl{grid-template-columns:repeat(5,minmax(0,1fr))}}
|
||||
@media(max-width:1199px){.layout{grid-template-columns:220px minmax(0,1fr)}.controls{padding:28px 16px}main{padding:32px 24px 0}.intro{gap:24px}.identity-stage{padding:20px;min-height:420px}.three-grid{gap:12px}.study-card{padding:16px}.section-heading{display:block}.section-heading>p{max-width:70ch;margin:16px 0 0}.font-title{font-size:24px}.icon-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.token-panel{grid-template-columns:1fr}.component-panel{padding:20px}.review-section{gap:24px}}
|
||||
@media(max-width:959px){.masthead{padding:16px 24px}.masthead-note{display:none}.layout{grid-template-columns:1fr}.controls{border-right:0;border-bottom:1px solid var(--line);padding:20px 24px}.control-inner{position:static;max-height:none;overflow:visible;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.control-inner>.eyebrow,.control-intro,.local-note,.controls nav,.control-inner>.text-button{grid-column:1/-1}.control-inner>.eyebrow{margin:0}.control-intro{display:none}.mode-control,.field{margin-bottom:0}.local-note{margin:0}.controls nav{display:flex;flex-wrap:wrap;gap:8px 20px;padding:8px 0 0;margin:0}.controls nav a{font-size:13px}.controls nav span{display:none}.control-inner>.text-button{justify-self:start;padding:0;min-height:30px}.intro{padding:16px 0 40px}.three-grid{gap:16px}.component-panel .field{margin-bottom:18px}.review-panel .field{margin-bottom:18px}h1{font-size:clamp(2.5rem,5.5vw,4rem)}}
|
||||
@media(max-width:699px){.masthead{padding:12px 20px;gap:12px}.brand-home{font-size:18px;gap:8px}.review-link{gap:8px;font-size:13px}.brand-home .logo{width:28px;height:28px}.controls{padding:20px}.control-inner{grid-template-columns:repeat(auto-fit,minmax(min(100%,9em),1fr));gap:16px}.mode-control{grid-column:1/-1}.control-inner>.field:last-of-type{grid-column:1/-1}main{padding:28px 20px 0}.intro{grid-template-columns:1fr;gap:24px}.intro h1{font-size:clamp(2.5rem,9vw,4rem)}.intro-copy>p{max-width:48ch}.identity-stage{min-height:380px}.stage-mark{margin:24px 0 16px}.stage-wordmark{font-size:32px}.three-grid,.palette-grid,.component-grid,.review-section{grid-template-columns:1fr}.study-card{padding:24px}.logo-display{min-height:160px}.size-strip{justify-content:flex-start;gap:32px}.board-section{padding:32px 0}h2{font-size:28px}.type-scale{gap:24px}.type-scale p{margin:0}.token-panel dl{grid-template-columns:repeat(2,minmax(0,1fr))}.token-panel{padding:20px}.icon-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.icon-specimen{padding:20px 8px;font-size:12px}.review-panel{padding:20px}.color-principles{gap:16px}.color-principles p{width:100%}.color-principles b{display:inline-block;min-width:52px;margin:0}.layer-sample,.layer-sample>div,.layer-sample>div>div{padding:12px}.review-section{gap:20px}}
|
||||
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
|
||||
@media(forced-colors:active){.study-card.is-selected,.palette-card.is-selected{outline:2px solid Highlight}.segmented input:checked+span{outline:2px solid Highlight}.palette-swatches{border:1px solid CanvasText}}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
===== FILBERT NATIVE DEVELOPMENT CONTEXT =====
|
||||
|
||||
Your identity is Filbert. This launch runs Pi directly on the host, in the
|
||||
Mosaic Stack development repository. The injected SOUL defines your persona;
|
||||
CONSTITUTION and STANDARDS supply governance, USER supplies user context,
|
||||
and AGENTS.md supplies repository instructions.
|
||||
|
||||
You have host read, bash, edit, write, grep, find, and ls tools. This is a
|
||||
development TUI with the operator's OS access, not a sandbox or a registered
|
||||
managed fleet seat. Use repository scripts for Mosaic operations and inspect
|
||||
their effects before running them. Container paths in skills describe worker
|
||||
deployments, not your current workspace. A tool's presence is not authority
|
||||
to change unrelated files, other agents' work, or the live fleet.
|
||||
|
||||
For an assigned improvement, inspect the implementation, reproduce the issue,
|
||||
make the smallest useful change, verify it, and continue through the authorized
|
||||
outcome. Read docs/plans/CURRENT.md to reconcile ownership and existing gates;
|
||||
a new user assignment does not silently resume unrelated queued work.
|
||||
|
||||
The local /goal extension is loaded and owns any operator-set goal lifecycle.
|
||||
Use ms-proactive-agent for work selection and ms-goal for recovery guidance;
|
||||
do not create a competing goal loop. Follow goal_report's actual schema and
|
||||
reporting instructions. Its text format is Just Completed / Next Step /
|
||||
Blocked, with '* none' for empty sections. No external reporting skill is
|
||||
needed to discover that format. Native development packaging supersedes
|
||||
older skill statements that this extension is unavailable.
|
||||
|
||||
The canonical checkout is /mnt/storage/src/mosaic-stack; v1/ is archived
|
||||
legacy source. Work on the current foundation unless the user explicitly
|
||||
assigns legacy work. Your general development responsibilities are defined in SOUL.
|
||||
Darkwing is the Mosaic Stack development team lead.
|
||||
|
||||
Conversation history persists across launcher restarts. Goals belong to a
|
||||
single process incarnation; recover the assignment from verified records and
|
||||
the operator's direction after a restart. No goal is started by this launcher.
|
||||
Context is captured anew at launch; source edits do not update this process's
|
||||
injected snapshot. Relaunch to load approved context changes.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Filbert development TUI
|
||||
|
||||
From any terminal, run:
|
||||
|
||||
```sh
|
||||
/mnt/storage/src/mosaic-stack/agents/filbert/launch.sh
|
||||
```
|
||||
|
||||
The agent launcher is a thin shim to `scripts/agent.sh --host-dev filbert`,
|
||||
forwarding context/session arguments and fixing the model to
|
||||
`openai-codex/gpt-6-astra` with `low` thinking. `scripts/agent.sh` is the common entry
|
||||
point; `scripts/agent-host-dev.sh` implements its native development mode.
|
||||
The host launcher opens the repository as Filbert's workspace.
|
||||
It uses the repository-pinned Pi, OpenAI Codex GPT-6 Astra with low thinking, and
|
||||
native Pi authentication (normal `~/.pi/agent`, or `PI_CODING_AGENT_DIR` if
|
||||
explicitly set). It never copies credentials. Install dependencies with
|
||||
`npm ci --ignore-scripts --no-audit --no-fund` if needed.
|
||||
|
||||
`--check` validates configuration and required inputs without opening Pi or
|
||||
calling a model. `--fresh` starts a new conversation without deleting earlier
|
||||
ones. Normal launches continue the latest conversation under
|
||||
`.pi/state/filbert/sessions/`; the first launch creates one. A launcher lock
|
||||
rejects simultaneous launches through this script. It does not exclude Pi
|
||||
processes started another way. Damaged JSONL history refuses automatic resume;
|
||||
`--fresh` is an explicit escape hatch that preserves the damaged evidence.
|
||||
|
||||
The current files are combined into a private launch snapshot under
|
||||
`.pi/state/filbert/launches/`:
|
||||
|
||||
- `contracts/CONSTITUTION.md` and `contracts/STANDARDS.md`
|
||||
- `agents/filbert/SOUL.md`
|
||||
- `<configured dataRoot>/user/USER.md`, the deployment's live user profile
|
||||
- the repository's `AGENTS.md` and Filbert's `CONTEXT.md`
|
||||
|
||||
Use `--soul FILE`, `--constitution FILE`, or `--user FILE` to select alternate
|
||||
inputs, including a future `contracts/USER.md`. Relative paths resolve from
|
||||
the repository root. Missing or empty inputs refuse launch. Snapshots can
|
||||
contain personal context and remain local, with private file permissions.
|
||||
Context edits take effect on relaunch, including when resuming a conversation.
|
||||
|
||||
The launcher enables coding/search tools, `goal_report`, ten explicit local
|
||||
skills, and the canonical goal extension through `scripts/sync-dev-extensions.sh`.
|
||||
Ambient context, skills, extensions, templates, and themes are disabled.
|
||||
The normal Pi coding prompt is retained with the Mosaic context appended.
|
||||
Enter `/goal <assignment and acceptance criteria>` to start continuing work;
|
||||
`/goal stop`, `/goal resume`, and `/goal` pause, resume, and inspect it. A new
|
||||
process does not automatically adopt a previous process's goal.
|
||||
|
||||
This TUI has the operator's host access, including repository edits and host
|
||||
commands. Its tool list is not OS isolation. It creates no managed role or
|
||||
fleet registration. Worker dispatch still uses the governed Mosaic task runner.
|
||||
The user supplies the assignment; launch alone does not start self-modification.
|
||||
|
||||
## Deployment findings
|
||||
|
||||
The existing `scripts/agent.sh` launches a Docker container, defaults to the
|
||||
`agent-<name>` session directory, and asks Pi to continue when that directory
|
||||
is nonempty. Its default workspace is `<dataRoot>/workspaces/<name>`, not this
|
||||
checkout. `src/load-contracts.sh` loads image-baked governance, an optional
|
||||
seat SOUL override, live user Markdown, and mission context into a shared
|
||||
prompt path. A seat override requires `agent.json`; a standalone SOUL is not
|
||||
discovered. `adapters/pi/adapter.sh` disables extensions. The temporary host
|
||||
launcher follows the existing native development path to provide repository
|
||||
access and `/goal`, and keeps its conversations separate from container and
|
||||
live fleet sessions. It does not invoke release alignment on startup.
|
||||
@@ -0,0 +1,21 @@
|
||||
# SOUL — Filbert
|
||||
|
||||
You are Filbert, a general development collaborator on Mosaic Stack.
|
||||
Darkwing is the team lead for Mosaic Stack development. Work on the concrete
|
||||
assignments Jason or Darkwing gives you: implementation, investigation,
|
||||
testing, debugging, documentation, or review as the project needs.
|
||||
|
||||
Investigate before guessing. Carry authorized work through implementation
|
||||
and appropriate verification. State what changed, the evidence that it
|
||||
works, and any remaining limitations. Distinguish observed behavior from
|
||||
assumptions and report blockers with the next decision needed.
|
||||
|
||||
Coordinate scope and ownership with Darkwing. Preserve other agents' edits
|
||||
and append-only records, and avoid overlapping changes without reconciling
|
||||
ownership. Dewey owns frontend design and UX; involve that role when a task
|
||||
needs interface design direction. Review independently when assigned and
|
||||
base conclusions on evidence rather than the author's confidence.
|
||||
|
||||
Follow repository governance and the user's current direction. Team roles
|
||||
do not grant new policy, deployment, credential, or push authority. Launch
|
||||
alone does not assign a task or resume unrelated queued work.
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Filbert's native development mode through the Mosaic agent entry point.
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
exec "$REPO/scripts/agent.sh" --host-dev filbert "$@" \
|
||||
--provider openai-codex --model gpt-6-astra --thinking low
|
||||
@@ -0,0 +1,21 @@
|
||||
// Refuse damaged history before Pi's --continue can silently skip it.
|
||||
import { readFileSync, lstatSync } from 'node:fs';
|
||||
|
||||
try {
|
||||
for (const file of process.argv.slice(2)) {
|
||||
if (!lstatSync(file).isFile()) throw new Error(`not a regular session file: ${file}`);
|
||||
const lines = readFileSync(file, 'utf8').trim().split('\n');
|
||||
const entries = lines.map((line) => JSON.parse(line));
|
||||
const header = entries[0];
|
||||
if (header?.type !== 'session' || typeof header.id !== 'string' || !header.id ||
|
||||
typeof header.version !== 'number' || typeof header.cwd !== 'string' ||
|
||||
!Number.isFinite(Date.parse(header.timestamp)) ||
|
||||
entries.slice(1).some((entry) => !entry || typeof entry.type !== 'string')) {
|
||||
throw new Error(`invalid session structure: ${file}`);
|
||||
}
|
||||
if (header.cwd !== process.cwd()) throw new Error(`session belongs to another workspace: ${file}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`filbert: cannot safely resume: ${error.message}; inspect history or explicitly use --fresh`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# SOUL - researcher
|
||||
|
||||
You are the researcher seat of the Mosaic fleet. You are curious, methodical,
|
||||
and precise. You cite what you know, admit what you do not, and never guess
|
||||
when you can verify.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"agentVersion": 1,
|
||||
"name": "researcher",
|
||||
"role": "researcher",
|
||||
"capabilities": { "tools": ["read", "bash"] }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
===== ROCKO NATIVE DEVELOPMENT CONTEXT =====
|
||||
|
||||
Your identity is Rocko. This is Claude Code running on the host in the Mosaic
|
||||
Stack repository, using the Sonnet model. Darkwing is the development team
|
||||
lead; your SOUL defines your general project responsibilities. CONSTITUTION
|
||||
and STANDARDS supply governance, USER supplies user context, and AGENTS.md
|
||||
supplies repository instructions.
|
||||
|
||||
Use the actual Claude Code tools and capabilities available in this session.
|
||||
This launcher does not load Pi extensions or Pi's goal_report tool. Do not
|
||||
claim that Pi's /goal lifecycle is available here. Claude Code retains its
|
||||
normal authentication, settings, discovered context, and permission behavior.
|
||||
Use project skills from skills/ when relevant and follow their actual inputs.
|
||||
|
||||
Read docs/plans/CURRENT.md to reconcile current ownership and existing gates.
|
||||
A new user assignment does not silently resume unrelated queued work. The
|
||||
canonical checkout is /mnt/storage/src/mosaic-stack; v1/ is archived legacy
|
||||
source. Preserve other agents' work and coordinate scope with Darkwing.
|
||||
|
||||
The launcher records Rocko's own Claude session ID and resumes that ID on
|
||||
subsequent launches. Context is captured anew on launch; relaunch to load
|
||||
approved context edits. Launching alone does not assign work. This process
|
||||
has host access and is not a sandbox or a registered managed fleet seat.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Rocko development TUI
|
||||
|
||||
Run from any terminal:
|
||||
|
||||
```sh
|
||||
/mnt/storage/src/mosaic-stack/agents/rocko/launch.sh
|
||||
```
|
||||
|
||||
Rocko handles general Mosaic Stack development assignments under team lead
|
||||
Darkwing. The launcher opens Claude Code in the repository with `--model sonnet`
|
||||
and the display name Rocko. Sonnet is the selected CLI model alias; Rocko's
|
||||
project persona is supplied by SOUL.md, not a separate custom Claude agent.
|
||||
|
||||
Use `--check` to validate the executable and context inputs without a model
|
||||
call. This does not verify authentication or account access to Sonnet. Claude
|
||||
Code uses its normal authentication, settings, permissions, and discovered
|
||||
resources; this script does not copy credentials or bypass permissions.
|
||||
|
||||
Normal launches resume the exact Claude UUID recorded locally in
|
||||
`.pi/state/rocko/session-id`. First launch creates a UUID; `--fresh` creates
|
||||
another while retaining previous conversations in Claude's native storage.
|
||||
Each private launch receipt under `.pi/state/rocko/launches/` retains its UUID
|
||||
and context snapshot. The launcher lock refuses concurrent Rocko launches
|
||||
through this script; it does not lock sessions opened by other means.
|
||||
|
||||
If Claude exits before saving its first conversation, or its history is
|
||||
removed, the saved UUID may not be resumable. Inspect the failure and use
|
||||
`--fresh` explicitly to start again. The launcher does not fall back to the
|
||||
latest unrelated project conversation. Old UUIDs in launch receipts can be
|
||||
opened manually with `claude --resume UUID` from the project directory.
|
||||
|
||||
The prompt snapshot includes CONSTITUTION, STANDARDS, Rocko's SOUL, the live
|
||||
Mosaic user profile, root AGENTS.md, and Rocko's CONTEXT.md. `--soul FILE`,
|
||||
`--constitution FILE`, and `--user FILE` override individual inputs; relative
|
||||
paths resolve from the repository root. Missing or empty inputs refuse launch.
|
||||
The snapshot is appended to Claude's system prompt on each launch.
|
||||
|
||||
This is a host development session, not a managed fleet registration. It
|
||||
loads no Pi goal extension. The user or team lead supplies the assignment.
|
||||
@@ -0,0 +1,21 @@
|
||||
# SOUL — Rocko
|
||||
|
||||
You are Rocko, a general development collaborator on Mosaic Stack.
|
||||
Darkwing is the team lead for Mosaic Stack development. Work on the concrete
|
||||
assignments Jason or Darkwing gives you: implementation, investigation,
|
||||
testing, debugging, documentation, or review as the project needs.
|
||||
|
||||
Investigate before guessing. Carry authorized work through implementation
|
||||
and appropriate verification. State what changed, the evidence that it
|
||||
works, and any remaining limitations. Distinguish observed behavior from
|
||||
assumptions and report blockers with the next decision needed.
|
||||
|
||||
Coordinate scope and ownership with Darkwing. Preserve other agents' edits
|
||||
and append-only records, and avoid overlapping changes without reconciling
|
||||
ownership. Dewey owns frontend design and UX; involve that role when a task
|
||||
needs interface design direction. Review independently when assigned and
|
||||
base conclusions on evidence rather than the author's confidence.
|
||||
|
||||
Follow repository governance and the user's current direction. Team roles
|
||||
do not grant new policy, deployment, credential, or push authority. Launch
|
||||
alone does not assign a task or resume unrelated queued work.
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rocko's native Claude Code development session.
|
||||
set -euo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$REPO"
|
||||
fail() { echo "rocko: $*" >&2; exit 1; }
|
||||
usage() {
|
||||
echo 'Usage: agents/rocko/launch.sh [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE]'
|
||||
}
|
||||
FRESH=false
|
||||
CHECK=false
|
||||
SOUL="$REPO/agents/rocko/SOUL.md"
|
||||
CONSTITUTION="$REPO/contracts/CONSTITUTION.md"
|
||||
USER_FILE=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--fresh) FRESH=true; shift ;;
|
||||
--check) CHECK=true; shift ;;
|
||||
--soul|--constitution|--user)
|
||||
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "$1 needs a file"
|
||||
case "$1" in
|
||||
--soul) SOUL="$2" ;;
|
||||
--constitution) CONSTITUTION="$2" ;;
|
||||
--user) USER_FILE="$2" ;;
|
||||
esac
|
||||
shift 2 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) usage >&2; fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
source "$REPO/scripts/common.sh"
|
||||
load_config
|
||||
USER_FILE="${USER_FILE:-$MOSAIC_DATA_ROOT/user/USER.md}"
|
||||
command -v claude >/dev/null || fail 'Claude Code is required on PATH'
|
||||
command -v node >/dev/null || fail 'Node.js is required'
|
||||
command -v flock >/dev/null || fail 'flock is required'
|
||||
VERSION="$(claude --version)"
|
||||
CONTEXT_FILES=("$CONSTITUTION" "$REPO/contracts/STANDARDS.md" "$SOUL" "$USER_FILE" "$REPO/AGENTS.md" "$REPO/agents/rocko/CONTEXT.md")
|
||||
for file in "${CONTEXT_FILES[@]}"; do
|
||||
[ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ] || fail "missing, unreadable or empty context: $file"
|
||||
done
|
||||
STATE="$REPO/.pi/state/rocko"
|
||||
echo "rocko: native host TUI | $VERSION | model sonnet"
|
||||
echo "rocko: workspace $REPO | team lead Darkwing"
|
||||
if "$CHECK"; then
|
||||
echo 'rocko: configuration checks passed (no TUI, authentication request or model call)'
|
||||
exit 0
|
||||
fi
|
||||
[ -t 0 ] && [ -t 1 ] || fail 'launch from an interactive terminal (or use --check)'
|
||||
umask 077
|
||||
mkdir -p "$STATE/launches"
|
||||
exec 8>"$STATE/launch.lock"
|
||||
flock -n 8 || fail 'another rocko TUI from this launcher is active'
|
||||
SESSION_FILE="$STATE/session-id"
|
||||
if ! "$FRESH" && [ -e "$SESSION_FILE" ]; then
|
||||
[ -f "$SESSION_FILE" ] && [ -r "$SESSION_FILE" ] || fail 'invalid session-id file; inspect it or use --fresh'
|
||||
SESSION_ID="$(cat "$SESSION_FILE")"
|
||||
[[ "$SESSION_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || fail 'invalid session-id; inspect it or use --fresh'
|
||||
SESSION_ARGS=(--resume "$SESSION_ID")
|
||||
echo "rocko: resuming conversation $SESSION_ID"
|
||||
else
|
||||
SESSION_ID="$(node -e 'console.log(require("node:crypto").randomUUID())')"
|
||||
SESSION_ARGS=(--session-id "$SESSION_ID")
|
||||
echo "rocko: starting conversation $SESSION_ID; earlier conversations are preserved"
|
||||
fi
|
||||
LAUNCH="$(mktemp -d "$STATE/launches/launch.XXXXXXXX")"
|
||||
PROMPT="$LAUNCH/context.md"
|
||||
for file in "${CONTEXT_FILES[@]}"; do
|
||||
printf '\n===== %s (%s) =====\n' "$(basename "$file")" "$file" >> "$PROMPT"
|
||||
cat "$file" >> "$PROMPT"
|
||||
printf '\n' >> "$PROMPT"
|
||||
done
|
||||
sha256sum "$PROMPT" > "$LAUNCH/context.sha256"
|
||||
printf '%s\n' "$SESSION_ID" > "$LAUNCH/session-id"
|
||||
# Retain each prior ID in its launch receipt; replace only the current pointer.
|
||||
printf '%s\n' "$SESSION_ID" > "$STATE/session-id.tmp"
|
||||
mv "$STATE/session-id.tmp" "$SESSION_FILE"
|
||||
echo "rocko: context snapshot $PROMPT"
|
||||
unset MOSAIC_LAUNCH_INCARNATION
|
||||
export MOSAIC_AGENT_NAME=rocko
|
||||
exec claude --model sonnet --name Rocko \
|
||||
--append-system-prompt "$(cat "$PROMPT")" "${SESSION_ARGS[@]}"
|
||||
@@ -1,58 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* Resolves a user-provided path and verifies it is inside the allowed sandbox directory.
|
||||
* Throws SandboxEscapeError if the resolved path is outside the sandbox.
|
||||
*
|
||||
* Uses realpathSync to resolve symlinks in the sandbox root. The user-supplied path
|
||||
* is checked for containment AFTER lexical resolution but BEFORE resolving any symlinks
|
||||
* within the user path — so symlink escape attempts are caught too.
|
||||
*
|
||||
* @param userPath - The path provided by the agent (may be relative or absolute)
|
||||
* @param sandboxDir - The allowed root directory (already validated on session creation)
|
||||
* @returns The resolved absolute path, guaranteed to be within sandboxDir
|
||||
*/
|
||||
export function guardPath(userPath: string, sandboxDir: string): string {
|
||||
const resolved = path.resolve(sandboxDir, userPath);
|
||||
const sandboxResolved = fs.realpathSync.native(sandboxDir);
|
||||
|
||||
// Normalize both paths to resolve any symlinks in the sandbox root itself.
|
||||
// For the user path, we check containment BEFORE resolving symlinks in the path
|
||||
// (so we catch symlink escape attempts too — the resolved path must still be under sandbox)
|
||||
if (!resolved.startsWith(sandboxResolved + path.sep) && resolved !== sandboxResolved) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a path without resolving symlinks in the user-provided portion.
|
||||
* Use for paths that may not exist yet (creates, writes).
|
||||
*
|
||||
* Performs a lexical containment check only using path.resolve.
|
||||
*/
|
||||
export function guardPathUnsafe(userPath: string, sandboxDir: string): string {
|
||||
const resolved = path.resolve(sandboxDir, userPath);
|
||||
const sandboxAbs = path.resolve(sandboxDir);
|
||||
|
||||
if (!resolved.startsWith(sandboxAbs + path.sep) && resolved !== sandboxAbs) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export class SandboxEscapeError extends Error {
|
||||
constructor(
|
||||
public readonly userPath: string,
|
||||
public readonly sandboxDir: string,
|
||||
public readonly resolvedPath: string,
|
||||
) {
|
||||
super(
|
||||
`Path escape attempt blocked: "${userPath}" resolves to "${resolvedPath}" which is outside sandbox "${sandboxDir}"`,
|
||||
);
|
||||
this.name = 'SandboxEscapeError';
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { TeamsService } from './teams.service.js';
|
||||
|
||||
@Controller('api/teams')
|
||||
@UseGuards(AuthGuard)
|
||||
export class TeamsController {
|
||||
constructor(private readonly teams: TeamsService) {}
|
||||
|
||||
@Get()
|
||||
async list() {
|
||||
return this.teams.findAll();
|
||||
}
|
||||
|
||||
@Get(':teamId')
|
||||
async findOne(@Param('teamId') teamId: string) {
|
||||
return this.teams.findById(teamId);
|
||||
}
|
||||
|
||||
@Get(':teamId/members')
|
||||
async listMembers(@Param('teamId') teamId: string) {
|
||||
return this.teams.listMembers(teamId);
|
||||
}
|
||||
|
||||
@Get(':teamId/members/:userId')
|
||||
async checkMembership(@Param('teamId') teamId: string, @Param('userId') userId: string) {
|
||||
const isMember = await this.teams.isMember(teamId, userId);
|
||||
return { isMember };
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAs, TEST_USER } from './helpers/auth.js';
|
||||
|
||||
test.describe('Chat page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAs(page, TEST_USER.email, TEST_USER.password);
|
||||
// If login failed (no seeded user in env) we may be on /login — skip
|
||||
const url = page.url();
|
||||
test.skip(!url.includes('/chat'), 'No seeded test user — skipping authenticated tests');
|
||||
});
|
||||
|
||||
test('chat page loads and shows the welcome message or conversation list', async ({ page }) => {
|
||||
await page.goto('/chat');
|
||||
// Either there are conversations listed or the welcome empty-state is shown
|
||||
const hasWelcome = await page
|
||||
.getByRole('heading', { name: /welcome to mosaic chat/i })
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const hasConversationPanel = await page
|
||||
.locator('[data-testid="conversation-list"], nav, aside')
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
expect(hasWelcome || hasConversationPanel).toBe(true);
|
||||
});
|
||||
|
||||
test('new conversation button is visible', async ({ page }) => {
|
||||
await page.goto('/chat');
|
||||
// "Start new conversation" button or a "+" button in the sidebar
|
||||
const newConvButton = page.getByRole('button', { name: /new conversation|start new/i }).first();
|
||||
await expect(newConvButton).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('clicking new conversation shows a chat input area', async ({ page }) => {
|
||||
await page.goto('/chat');
|
||||
// Find any button that creates a new conversation
|
||||
const newBtn = page.getByRole('button', { name: /new conversation|start new/i }).first();
|
||||
await newBtn.click();
|
||||
// After creating, a text input for sending messages should appear
|
||||
const chatInput = page.getByRole('textbox').or(page.locator('textarea')).first();
|
||||
await expect(chatInput).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('sidebar navigation is present on chat page', async ({ page }) => {
|
||||
await page.goto('/chat');
|
||||
// The app-shell sidebar should be visible
|
||||
await expect(page.getByRole('link', { name: /chat/i }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export const TEST_USER = {
|
||||
email: process.env['E2E_USER_EMAIL'] ?? '[email protected]',
|
||||
password: process.env['E2E_USER_PASSWORD'] ?? 'password123',
|
||||
name: 'E2E Test User',
|
||||
};
|
||||
|
||||
export const ADMIN_USER = {
|
||||
email: process.env['E2E_ADMIN_EMAIL'] ?? '[email protected]',
|
||||
password: process.env['E2E_ADMIN_PASSWORD'] ?? 'adminpass123',
|
||||
name: 'E2E Admin User',
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill the login form and submit. Waits for navigation after success.
|
||||
*/
|
||||
export async function loginAs(page: Page, email: string, password: string): Promise<void> {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill(email);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||