Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0afff19c7 | ||
|
|
177406e2d1 | ||
|
|
d405831ac5 | ||
|
|
e878dbd472 | ||
|
|
f31813b19e | ||
|
|
bf380b3718 | ||
|
|
ca24d8e70f | ||
|
|
ceb898ce2c | ||
|
|
865e67cd35 | ||
|
|
b09c9b157b | ||
|
|
03786d6803 | ||
|
|
00eb216480 |
@@ -11,48 +11,87 @@
|
||||
|
||||
## Project Context
|
||||
|
||||
Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo with NestJS gateway, Next.js web dashboard, Pi SDK agent runtime, and plugin architecture for Discord/Telegram.
|
||||
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.
|
||||
|
||||
## Package Map
|
||||
### Stack
|
||||
|
||||
| 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 ORM schema + migrations | drizzle-orm, postgres |
|
||||
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
|
||||
| `packages/brain` | Data layer (PG-backed) | @mosaicstack/db |
|
||||
| `packages/queue` | Valkey task queue + MCP | ioredis |
|
||||
| `packages/coord` | Mission coordination | @mosaicstack/queue |
|
||||
| `packages/mosaic` | Unified `mosaic` CLI + TUI | Ink, Pi SDK, commander |
|
||||
| `plugins/discord` | Discord channel plugin | discord.js |
|
||||
| `plugins/telegram` | Telegram channel plugin | Telegraf |
|
||||
- **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
|
||||
|
||||
## Architecture Rules
|
||||
### Package Map
|
||||
|
||||
1. Gateway is the single API surface — all clients connect through it
|
||||
2. Pi SDK is ESM-only — gateway and CLI must use ESM
|
||||
3. Socket.IO typed events defined in `@mosaicstack/types` enforce compile-time contracts
|
||||
4. OTEL auto-instrumentation loads before NestJS bootstrap
|
||||
5. BetterAuth manages auth tables; schema defined in `@mosaicstack/db`
|
||||
6. Docker Compose provides PG (5433), Valkey (6380), OTEL Collector (4317/4318), Jaeger (16686)
|
||||
7. Explicit `@Inject()` decorators required in NestJS (tsx/esbuild doesn't emit decorator metadata)
|
||||
| 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 |
|
||||
|
||||
## Architecture and Code Conventions
|
||||
|
||||
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.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Requirements: Node.js 20+, pnpm 10.6.2, and Docker Compose when optional local services are needed.
|
||||
|
||||
```bash
|
||||
docker compose up -d # Infrastructure
|
||||
pnpm install # Dependencies
|
||||
pnpm typecheck && pnpm lint && pnpm format:check # Quality gates
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm preflight
|
||||
|
||||
# Optional local queue service only; do not start the full Compose stack.
|
||||
docker compose up -d valkey
|
||||
```
|
||||
|
||||
## Repo-Specific Notes
|
||||
The pre-push hook requires:
|
||||
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution in all tsconfigs
|
||||
- Scratchpads are mandatory for non-trivial tasks
|
||||
```bash
|
||||
pnpm preflight && pnpm typecheck && pnpm lint && pnpm format:check
|
||||
```
|
||||
|
||||
Software delivery also requires the applicable tests. Common repository commands are:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -1,46 +1,5 @@
|
||||
# CLAUDE.md — Mosaic Stack
|
||||
# Claude Compatibility Pointer
|
||||
|
||||
## Project
|
||||
@AGENTS.md
|
||||
|
||||
Self-hosted, multi-user AI agent platform. TypeScript monorepo.
|
||||
|
||||
## Stack
|
||||
|
||||
- **API**: NestJS + Fastify adapter (`apps/gateway`)
|
||||
- **Web**: Next.js 16 + React 19 (`apps/web`)
|
||||
- **ORM**: Drizzle ORM + PostgreSQL 17 + pgvector (`packages/db`)
|
||||
- **Auth**: BetterAuth (`packages/auth`)
|
||||
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
|
||||
- **Queue**: Valkey 8 (`packages/queue`)
|
||||
- **Build**: pnpm workspaces + Turborepo
|
||||
- **CI**: Woodpecker CI
|
||||
- **Observability**: OpenTelemetry → Jaeger
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm typecheck # TypeScript check (all packages)
|
||||
pnpm lint # ESLint (all packages)
|
||||
pnpm format:check # Prettier check
|
||||
pnpm test # Vitest (all packages)
|
||||
pnpm build # Build all packages
|
||||
|
||||
# Database
|
||||
pnpm --filter @mosaicstack/db db:generate # Offline migration artifact generation only
|
||||
# PostgreSQL execution is held until KBN-101-00/-03/-05 land. Do not invoke a runner,
|
||||
# init SQL, or Compose PostgreSQL service from this checkout.
|
||||
|
||||
# Dev: local PGlite data-layer work needs no PostgreSQL. Optional local queue service only:
|
||||
docker compose up -d valkey
|
||||
# Do not start Gateway/Web or root pnpm dev as a local PGlite route: the current unguarded dotenv
|
||||
# loader can inherit a daemon PostgreSQL DSN. KBN-101-02 must make that state fail closed first.
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- ESM everywhere (`"type": "module"`, `.js` extensions in imports)
|
||||
- NodeNext module resolution
|
||||
- Explicit `@Inject()` decorators in NestJS (tsx/esbuild doesn't support emitDecoratorMetadata)
|
||||
- DTOs in `*.dto.ts` files at module boundaries
|
||||
- OTEL tracing imported before NestJS bootstrap (`import './tracing.js'`)
|
||||
- All three gates must pass before push: typecheck, lint, format:check
|
||||
Do not add project guidance here. Keep `AGENTS.md` authoritative so every agent runtime receives the same instructions.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# CI Queue Guard Purpose Semantics
|
||||
|
||||
- **Issue:** #1146
|
||||
- **Target branch:** `next`
|
||||
|
||||
## Problem
|
||||
|
||||
`ci-queue-wait.sh` treats any result other than terminal success as asserted non-readiness. That is correct for merge readiness, but incorrect for the pre-push queue guard: a terminal failure or an empty status set means no pipeline is queued or running, so the queue is clear.
|
||||
|
||||
## Design
|
||||
|
||||
Make final-state handling purpose-sensitive while preserving the existing provider and payload safeguards:
|
||||
|
||||
- `--purpose push`
|
||||
- wait while state is `pending`;
|
||||
- return success for `terminal-success`, `terminal-failure`, and `no-status`;
|
||||
- continue rejecting `malformed`, `unknown`, and unrecognized states.
|
||||
- `--purpose merge`
|
||||
- return success only for `terminal-success`;
|
||||
- continue rejecting `terminal-failure`, `no-status`, malformed, unknown, and unrecognized states.
|
||||
- `--require-status` remains authoritative: `no-status` fails for either purpose when it is supplied.
|
||||
|
||||
Diagnostics will explicitly distinguish a queue-clear push result from successful CI so callers cannot mistake an old failure for a green pipeline.
|
||||
|
||||
## Testing
|
||||
|
||||
Extend the process-level tri-state regression harness with separate push and merge assertions:
|
||||
|
||||
1. Push passes for terminal success, terminal failure, and no status.
|
||||
2. Push still fails for pending, malformed, and unknown states.
|
||||
3. `--require-status` makes push/no-status fail.
|
||||
4. Merge behavior remains fail-closed except for terminal success.
|
||||
5. Existing provider-unavailable audit behavior remains unchanged.
|
||||
@@ -0,0 +1,208 @@
|
||||
# CI Queue Guard Purpose Semantics Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Make the pre-push CI queue guard pass when no pipeline is queued or running while preserving fail-closed merge readiness.
|
||||
|
||||
**Architecture:** Keep provider lookup and tri-state classification unchanged. Make only the final state dispatch purpose-sensitive: push treats valid non-pending states as queue-clear, while merge continues to require terminal success. Preserve `--require-status`, malformed-payload rejection, unknown-state rejection, and audited provider-unavailable behavior.
|
||||
|
||||
**Tech Stack:** Bash, process-level shell regression harnesses, Gitea/GitHub status APIs.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Freeze Purpose-Specific State Semantics
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
|
||||
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
|
||||
|
||||
**Step 1: Add failing push assertions**
|
||||
|
||||
Change push expectations so `terminal-failure` and `no-status` require exit 0 plus an explicit `queue-clear` diagnostic. Add a `--require-status` assertion that keeps push/no-status non-zero.
|
||||
|
||||
**Step 2: Add failing merge assertions**
|
||||
|
||||
Invoke the same harness with `MOSAIC_TEST_PURPOSE=merge` and assert terminal failure and no status remain non-zero while terminal success remains zero.
|
||||
|
||||
**Step 3: Add unknown-state coverage**
|
||||
|
||||
Add a stub payload with a syntactically valid but unsupported status value and assert both purposes reject it.
|
||||
|
||||
**Step 4: Run the focused test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
|
||||
```
|
||||
|
||||
Expected: failures showing push terminal-failure and no-status returned exit 3 instead of exit 0 or lacked `queue-clear` diagnostics.
|
||||
|
||||
**Step 5: Commit the failing tests**
|
||||
|
||||
```bash
|
||||
git add packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
|
||||
git commit -m "test(ci): define purpose-aware queue readiness"
|
||||
```
|
||||
|
||||
### Task 2: Implement Purpose-Sensitive Final-State Dispatch
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/mosaic/framework/tools/git/ci-queue-wait.sh:458-481`
|
||||
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh`
|
||||
- Test: `packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh`
|
||||
|
||||
**Step 1: Implement push queue-clear behavior**
|
||||
|
||||
For `no-status`, retain the existing `--require-status` failure. Otherwise, return success for push with an explicit diagnostic such as:
|
||||
|
||||
```text
|
||||
[ci-queue-wait] queue-clear state=no-status purpose=push branch=<branch>; no queued or running CI.
|
||||
```
|
||||
|
||||
For `terminal-failure`, return success only for push with the same queue-clear wording. Merge must continue returning asserted non-readiness.
|
||||
|
||||
**Step 2: Preserve malformed and unknown rejection**
|
||||
|
||||
Keep `malformed`, `unknown`, and unrecognized states non-zero for both purposes.
|
||||
|
||||
**Step 3: Run focused tests and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
|
||||
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh
|
||||
```
|
||||
|
||||
Expected: both scripts exit 0 and report their regression suites passed.
|
||||
|
||||
**Step 4: Commit implementation**
|
||||
|
||||
```bash
|
||||
git add packages/mosaic/framework/tools/git/ci-queue-wait.sh
|
||||
git commit -m "fix(ci): separate push queue clearance from merge readiness"
|
||||
```
|
||||
|
||||
### Task 3: Verify, Review, and Document Evidence
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/scratchpads/1146-ci-queue-purpose.md`
|
||||
|
||||
**Step 1: Run shell syntax and focused regressions**
|
||||
|
||||
```bash
|
||||
bash -n packages/mosaic/framework/tools/git/ci-queue-wait.sh
|
||||
bash -n packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
|
||||
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh
|
||||
bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh
|
||||
```
|
||||
|
||||
**Step 2: Run repository quality gates**
|
||||
|
||||
```bash
|
||||
pnpm preflight
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm test
|
||||
pnpm format:check
|
||||
```
|
||||
|
||||
Expected: every command exits 0.
|
||||
|
||||
**Step 3: Obtain independent review**
|
||||
|
||||
Request review of the exact branch head. Remediate all blocking findings and rerun focused and baseline gates.
|
||||
|
||||
**Step 4: Record evidence and commit**
|
||||
|
||||
Update the scratchpad with test output, review result, and residual risk, then commit it:
|
||||
|
||||
```bash
|
||||
git add docs/scratchpads/1146-ci-queue-purpose.md
|
||||
git commit -m "docs(ci): record queue guard verification"
|
||||
```
|
||||
|
||||
### Task 4: Keep the Merge Wrapper Aligned with the `next` Lane
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/mosaic/framework/tools/git/pr-merge.sh:97-101`
|
||||
- Test: `packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh`
|
||||
|
||||
**Step 1: Write the failing regression**
|
||||
|
||||
Run the exact-head merge regression with its Gitea fixture targeting `next` and confirm the current wrapper rejects it because it only permits `main`.
|
||||
|
||||
**Step 2: Allow only documented integration targets**
|
||||
|
||||
Permit `main` and `next`; reject every other target. Do not alter exact-head pinning, queue-guard invocation, provider selection, or merge method enforcement.
|
||||
|
||||
**Step 3: Run focused merge regressions**
|
||||
|
||||
```bash
|
||||
bash packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh
|
||||
bash packages/mosaic/framework/tools/git/test-pr-merge-queue-branch.sh
|
||||
bash packages/mosaic/framework/tools/git/test-pr-merge-gitea-empty-uid.sh
|
||||
```
|
||||
|
||||
Expected: all pass, including a Gitea merge fixture targeting `next`.
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/mosaic/framework/tools/git/pr-merge.sh packages/mosaic/framework/tools/git/test-pr-merge-head-pin.sh
|
||||
git commit -m "fix(ci): allow reviewed merges into next"
|
||||
```
|
||||
|
||||
### Task 5: Activate and Deliver Through `next`
|
||||
|
||||
**Files:**
|
||||
|
||||
- Installed output: `~/.config/mosaic/tools/git/ci-queue-wait.sh`
|
||||
|
||||
**Step 1: Activate through the canonical installer**
|
||||
|
||||
From the reviewed worktree, run the framework installer in sync-only keep mode so operator files remain protected:
|
||||
|
||||
```bash
|
||||
MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep MOSAIC_SKIP_SKILLS_SYNC=1 \
|
||||
bash packages/mosaic/framework/install.sh
|
||||
```
|
||||
|
||||
**Step 2: Verify installed/source parity**
|
||||
|
||||
```bash
|
||||
cmp -s \
|
||||
packages/mosaic/framework/tools/git/ci-queue-wait.sh \
|
||||
~/.config/mosaic/tools/git/ci-queue-wait.sh
|
||||
```
|
||||
|
||||
Expected: exit 0.
|
||||
|
||||
**Step 3: Run mandatory pre-push queue guard**
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B fix/1146-ci-queue-purpose
|
||||
```
|
||||
|
||||
Expected: branch-absent or queue-clear success.
|
||||
|
||||
**Step 4: Push and open a PR against `next`**
|
||||
|
||||
```bash
|
||||
git push -u origin fix/1146-ci-queue-purpose
|
||||
~/.config/mosaic/tools/git/pr-create.sh \
|
||||
-t "fix(ci): make queue guard purpose-sensitive" \
|
||||
-b "Closes #1146" \
|
||||
-B next \
|
||||
-H fix/1146-ci-queue-purpose \
|
||||
-i 1146
|
||||
```
|
||||
|
||||
**Step 5: Complete reviewed integration**
|
||||
|
||||
Wait for exact-head terminal-green CI, obtain the required review, merge via the Mosaic wrapper, verify merged CI, and close #1146. Do not bypass any gate.
|
||||
@@ -0,0 +1,65 @@
|
||||
# #1146 — CI Queue Guard Purpose Semantics
|
||||
|
||||
## Objective
|
||||
|
||||
Make the pre-push queue guard wait for queued/running CI without requiring the previous remote head to have successful CI. Preserve fail-closed merge readiness.
|
||||
|
||||
## Scope
|
||||
|
||||
- `packages/mosaic/framework/tools/git/ci-queue-wait.sh`
|
||||
- focused queue-guard regression tests
|
||||
- design and scratchpad documentation
|
||||
- local framework activation required before the fixed guard can authorize this branch's push
|
||||
|
||||
## Plan
|
||||
|
||||
1. Freeze purpose-specific behavior in failing process-level tests.
|
||||
2. Implement the smallest state-dispatch change.
|
||||
3. Run focused shell tests and repository quality gates.
|
||||
4. Obtain independent review and remediate findings.
|
||||
5. Install the reviewed framework source locally, run the mandatory pre-push queue guard, and push.
|
||||
6. Open a PR against `next`, verify terminal-green CI, and close #1146 after merge.
|
||||
|
||||
## Budget
|
||||
|
||||
- ASSUMPTION: no explicit token cap was provided.
|
||||
- Working estimate: 12K tokens.
|
||||
- Scope reduction: change only final-state dispatch and focused tests; do not redesign provider adapters.
|
||||
|
||||
## Progress
|
||||
|
||||
- Confirmed source and installed guards are byte-identical.
|
||||
- Reproduced `terminal-failure` blocking `--purpose push`.
|
||||
- Root cause: final-state dispatch requires terminal success for both push and merge.
|
||||
- Design approved: push is queue-clear on valid non-pending states; merge remains fail-closed.
|
||||
|
||||
## Tests
|
||||
|
||||
- RED confirmed before implementation: the focused tri-state harness reported push `terminal-failure` and `no-status` as `ASSERTED_NOT_READY`.
|
||||
- GREEN: `bash packages/mosaic/framework/tools/git/test-ci-queue-wait-tristate.sh` — all outcome classes passed.
|
||||
- GREEN: `bash packages/mosaic/framework/tools/git/test-ci-queue-wait-github-checks.sh` — 6/6 purpose-aware cases passed.
|
||||
- GREEN: `bash -n` passed for the changed guard and both focused harnesses.
|
||||
- GREEN: `pnpm preflight`, `pnpm typecheck`, and `pnpm lint` passed.
|
||||
- `pnpm test` ran 45/46 workspace test tasks successfully, but the pre-existing Gateway `cross-user-isolation.test.ts` failed during cleanup with PostgreSQL error `28P01` (local `mosaic` password authentication failure). The changed Mosaic framework test task passed within that run.
|
||||
- GREEN: focused queue and merge shell regressions passed after the wrapper change.
|
||||
- GREEN: isolated Mosaic Vitest run passed (81 files, 1,514 tests).
|
||||
- The normal parallel Mosaic Vitest run has an environment-sensitive pre-existing failure in `install-ordering-guard.spec.ts`: the real activation probe changes between two calls while other suites run concurrently. Running the same spec alone and the complete Vitest suite with one fork passes.
|
||||
- The framework shell suite's pre-existing `version_coupling_unittest.py` also fails locally because the newly installed `mosaic` is now on PATH despite the test injecting a nonexistent PATH; CI's clean image does not have this global CLI. All changed queue/merge harnesses pass.
|
||||
- GREEN: `pnpm format:check` passed.
|
||||
- Note: an additional ad hoc Prettier command was not applicable to shell files because Prettier has no shell parser; the repository-wide format check passed using its configured file globs.
|
||||
|
||||
## Review
|
||||
|
||||
- Independent Codex review of the six-file diff: approved, confidence 0.84, zero blockers/should-fix/suggestions.
|
||||
- Review confirmed push queue-clear behavior, merge fail-closed behavior, and `--require-status` coverage.
|
||||
|
||||
## Risks and Blockers
|
||||
|
||||
- Canonical framework activation completed with `MOSAIC_SYNC_ONLY=1 MOSAIC_INSTALL_MODE=keep MOSAIC_SKIP_SKILLS_SYNC=1 bash packages/mosaic/framework/install.sh`.
|
||||
- Source and installed queue guards are byte-identical (`cmp` and SHA-256 parity passed).
|
||||
- The installed pre-push guard now passes for the not-yet-remote feature branch with `queue clear`.
|
||||
- The required merge wrapper then exposed a second bootstrap defect: `pr-merge.sh` hardcoded `main`, contradicting the documented PR-based `next` integration lane. Tracked as #1149 and fixed in the same delivery branch with a regression fixture targeting `next`.
|
||||
- Activation emitted the existing manifest-safety warning that six `fleet/run/*.hb*` operator files were touched then restored; no data loss was observed, but this remains a pre-existing framework-manifest defect to report separately.
|
||||
- The first activation attempt timed out after 600 seconds while copying the 113K-file operator snapshot; the bounded 1,800-second retry completed successfully. It left a partial durable snapshot from the interrupted attempt in the normal backup directory; the completed snapshot is the newer `pre-update-20260810T195317Z` entry.
|
||||
- Full baseline test completion is blocked by the unrelated local PostgreSQL authentication/cleanup failure described above; CI has its own disposable PostgreSQL service.
|
||||
- Existing `.mosaic/orchestrator/*` working-tree changes are unrelated and must remain unstaged.
|
||||
@@ -1,115 +0,0 @@
|
||||
# WebUI Phase P — File / Folder Structure & Migration Map
|
||||
|
||||
> **Status:** living document — first pass. Structure and increment status are verified against
|
||||
> `next` as of merge `8c27024d`. Details (per-surface component inventories, exact route tables,
|
||||
> test matrices) are still being fleshed out; extend the stub sections below rather than rewriting
|
||||
> the verified structure.
|
||||
|
||||
## 1. What Phase P is
|
||||
|
||||
Phase P migrates the Mosaic **web UI** (`apps/web`) from the legacy **Next.js App Router** app to a
|
||||
**Vite + React Router single-page app (SPA)** that the **Gateway serves same-origin** on
|
||||
`:14242`. The RFC splits the work into **six increments (P1–P6)**; the P1 PR title records this as
|
||||
"increment 1/6".
|
||||
|
||||
The migration is deliberately **incremental and non-destructive**: the new SPA is built up
|
||||
*beside* the existing Next app, sharing one `apps/web/src/lib` networking/auth layer, until the
|
||||
final cutover (P5) removes the Next tree. At every point in between, **both app trees exist in the
|
||||
same package** — this is intentional, not drift.
|
||||
|
||||
## 2. Current tree on `next` (dual-app, transitional)
|
||||
|
||||
```
|
||||
apps/web/
|
||||
├── next.config.ts # legacy Next.js config (removed at P5)
|
||||
├── vite.config.ts # SPA build + DEV proxy config (canonical from P5)
|
||||
├── package.json # dev/build default to NEXT today; :vite variants opt in
|
||||
└── src/
|
||||
├── main.tsx # ── SPA entry (Vite)
|
||||
├── routes.tsx # ── SPA React Router route table
|
||||
├── spa/ # ── NEW SPA surfaces
|
||||
│ ├── guards.tsx # guest / authenticated route guards
|
||||
│ ├── pages/ # login, register, sso-callback (P2); chat + error boundary (P3)
|
||||
│ └── chat/ # P3 typed chat: use-chat-connection, commands-panel,
|
||||
│ # session-panel, message-transcript, tool-call-list, composer
|
||||
│
|
||||
├── lib/ # ── SHARED by BOTH trees (origin-relative networking + auth)
|
||||
│ ├── api.ts # fetch wrapper — relative /api/...
|
||||
│ ├── socket.ts # Socket.IO singleton — relative /chat
|
||||
│ ├── auth-client.ts # BetterAuth client — relative /api/auth/...
|
||||
│ ├── auth-redirect.ts # post-auth redirect resolution (protocol-relative rejected)
|
||||
│ ├── chat-contract.ts # P3 typed chat wire contract (runtime-guarded)
|
||||
│ ├── sso.ts · types.ts · cn.ts
|
||||
│
|
||||
├── app/ # ══ LEGACY Next.js App Router (removed at P5)
|
||||
│ ├── (auth)/{login,register}/
|
||||
│ ├── (dashboard)/{admin,chat,projects,projects/[id],settings,tasks}/
|
||||
│ ├── auth/provider/[provider]/
|
||||
│ └── layout.tsx · page.tsx · globals.css
|
||||
│
|
||||
├── components/ # ══ LEGACY Next component library (auth, chat, layout,
|
||||
│ # projects, settings, tasks, ui) — ported into spa/ across P3/P4
|
||||
└── providers/ # ══ theme-provider (legacy; SPA equivalent under providers)
|
||||
```
|
||||
|
||||
Legend: `──` new SPA (keep), `══` legacy Next (removed at P5), shared `lib/` in the middle.
|
||||
|
||||
## 3. Networking / serving model (why it's same-origin)
|
||||
|
||||
- The SPA speaks **origin-relative paths only**: `/api/...`, `/api/auth/...`, `/chat`. No
|
||||
`NEXT_PUBLIC_*` / `VITE_*` origin var, no hard-coded `http://localhost:14242` under
|
||||
`apps/web/src`.
|
||||
- **Dev:** `vite.config.ts` runs a dev-only proxy that forwards those paths to the Gateway (so the
|
||||
SPA on its dev port and the Gateway on `:14242` behave as one origin).
|
||||
- **Prod (target):** the SPA is **same-origin with the Gateway** — the Gateway serves the built
|
||||
static bundle and the API/WS on `:14242`, so no proxy and no CORS. *(The Gateway does not serve
|
||||
the web `dist` yet — adding that is the core of P5; see §5.)*
|
||||
|
||||
## 4. Build scripts (`apps/web/package.json`)
|
||||
|
||||
| Script | Today | Notes |
|
||||
|---|---|---|
|
||||
| `dev` | `next dev` | legacy dev server |
|
||||
| `dev:vite` | `vite` | SPA dev server (+ dev proxy) |
|
||||
| `build` | `node ../../scripts/build-web.mjs` | currently a **Next** build |
|
||||
| `build:vite` | `vite build` | SPA production build → `dist/` |
|
||||
| `lint` / `typecheck` / `test` | `eslint src` / `tsc --noEmit` / `vitest run` | tree-agnostic |
|
||||
|
||||
At **P5** the `:vite` variants become the defaults (`dev`→vite, `build`→vite build) and the Next
|
||||
build path is retired.
|
||||
|
||||
## 5. Increment map (P1–P6)
|
||||
|
||||
| # | Increment | Branch | Status |
|
||||
|---|---|---|---|
|
||||
| **P1** | Vite + React Router skeleton beside Next (entry, router, guards, vitest) | `feat/webui-p1-vite-skeleton` | ✅ merged — PR **#1143** |
|
||||
| **P2** | SPA data layer + same-origin auth (login/register/SSO pages, guards, relative api/socket/auth-client) | `feat/webui-p2-data-auth` | ✅ merged — PR **#1144** |
|
||||
| **P3** | Typed SPA **chat** (`spa/chat/*`, `chat-contract.ts`, chat page + error boundary) | `feat/webui-p3-chat` | 🚧 in progress (unmerged) |
|
||||
| **P4** | Port **projects / tasks / settings / admin** dashboard surfaces into the SPA | _tbd_ | ⏳ not started |
|
||||
| **P5** | **Cutover**: Gateway serves the Vite `dist` on `:14242`; flip `dev`/`build` to vite; **remove** the legacy Next `app/` tree + `next.config.ts` | _tbd_ | ⏳ not started |
|
||||
| **P6** | CI / images (trails): build the SPA in CI, ship images | _tbd_ | ⏳ trails |
|
||||
|
||||
Each increment follows the same delivery pipeline: brief traceable to the RFC → author →
|
||||
**independent** integrator verification (build+test+typecheck+lint) → **independent** code + security
|
||||
review (author ≠ reviewer) → author remediates → branch + PR to `next` → **independent** merge-gate
|
||||
merges. Author self-reports are not trusted; every gate is re-derived independently.
|
||||
|
||||
## 6. Known dependency / blocker
|
||||
|
||||
- **Issue #1145 — Gateway `dist` boot is broken** (DI failure on a defaulted constructor param);
|
||||
the Gateway currently runs **dev-mode only**. This is a **hard precondition for P5**: the Gateway
|
||||
cannot serve the SPA `dist` on `:14242` until `dist` boot works. P3/P4 remain on the dev-proxy
|
||||
topology meanwhile.
|
||||
|
||||
## 7. Not part of Phase P (disambiguation)
|
||||
|
||||
`docs/plans/2026-08-09-webui-fleet-claude-bridge.md` and
|
||||
`docs/scratchpads/webui-fleet-bridge-plan.md` describe a **separate** WebUI ↔ fleet/Claude bridge
|
||||
effort. They are **not** the Phase P SPA migration and should not be conflated with the increments
|
||||
above.
|
||||
|
||||
## 8. Where the detail lives (extend these)
|
||||
|
||||
- Per-increment working notes: `docs/scratchpads/webui-p*-*.md` (e.g. `webui-p2-data-auth.md`).
|
||||
- _Stub — to flesh out:_ per-surface component inventory (which `components/*` port to which
|
||||
`spa/*`), the full SPA route table, the P5 cutover checklist, and the P6 CI/image plan.
|
||||
@@ -465,12 +465,24 @@ while true; do
|
||||
no-status)
|
||||
if [[ "$REQUIRE_STATUS" -eq 1 ]]; then
|
||||
echo "Error: ASSERTED_NOT_READY state=no-status; --require-status was set for ${BRANCH}." >&2
|
||||
else
|
||||
echo "Error: ASSERTED_NOT_READY state=no-status purpose=${PURPOSE} branch=${BRANCH}." >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$PURPOSE" == "push" ]]; then
|
||||
echo "[ci-queue-wait] queue-clear state=no-status purpose=push branch=${BRANCH}; no queued or running CI."
|
||||
exit 0
|
||||
fi
|
||||
echo "Error: ASSERTED_NOT_READY state=no-status purpose=${PURPOSE} branch=${BRANCH}." >&2
|
||||
exit 3
|
||||
;;
|
||||
terminal-failure|malformed|unknown)
|
||||
terminal-failure)
|
||||
if [[ "$PURPOSE" == "push" ]]; then
|
||||
echo "[ci-queue-wait] queue-clear state=terminal-failure purpose=push branch=${BRANCH}; no queued or running CI."
|
||||
exit 0
|
||||
fi
|
||||
echo "Error: ASSERTED_NOT_READY state=terminal-failure purpose=${PURPOSE} branch=${BRANCH}." >&2
|
||||
exit 3
|
||||
;;
|
||||
malformed|unknown)
|
||||
echo "Error: ASSERTED_NOT_READY state=${STATE} purpose=${PURPOSE} branch=${BRANCH}." >&2
|
||||
exit 3
|
||||
;;
|
||||
|
||||
@@ -94,8 +94,8 @@ BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print(
|
||||
HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')"
|
||||
HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')"
|
||||
HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
|
||||
if [[ "$BASE_BRANCH" != "main" ]]; then
|
||||
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
|
||||
if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then
|
||||
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' or 'next' (found '$BASE_BRANCH')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -43,22 +43,22 @@ SH
|
||||
chmod +x "$STUB_DIR/gh"
|
||||
|
||||
run_guard() {
|
||||
local mode="$1"
|
||||
local mode="$1" purpose="${2:-push}"
|
||||
(
|
||||
cd "$REPO_DIR" || exit
|
||||
export PATH="$STUB_DIR:$PATH"
|
||||
export MOSAIC_GH_CHECK_MODE="$mode"
|
||||
export MOSAIC_GH_CALL_LOG="$WORK_DIR/gh-calls.log"
|
||||
export MOSAIC_CI_QUEUE_AUDIT_LOG="$WORK_DIR/audit.jsonl"
|
||||
"$SCRIPT_DIR/ci-queue-wait.sh" --purpose push -t 0 -i 0
|
||||
"$SCRIPT_DIR/ci-queue-wait.sh" --purpose "$purpose" -t 0 -i 0
|
||||
)
|
||||
}
|
||||
|
||||
failures=0
|
||||
assert_case() {
|
||||
local mode="$1" expected_rc="$2" expected_state="$3" output rc
|
||||
local mode="$1" expected_rc="$2" expected_state="$3" purpose="${4:-push}" output rc
|
||||
set +e
|
||||
output=$(run_guard "$mode" 2>&1)
|
||||
output=$(run_guard "$mode" "$purpose" 2>&1)
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$expected_rc" == zero && "$rc" -ne 0 ]]; then
|
||||
@@ -79,10 +79,12 @@ set -e
|
||||
: > "$WORK_DIR/gh-calls.log"
|
||||
assert_case success zero terminal-success
|
||||
assert_case pending nonzero pending
|
||||
assert_case failure nonzero terminal-failure
|
||||
assert_case late-failure nonzero terminal-failure
|
||||
assert_case failure zero terminal-failure
|
||||
assert_case late-failure zero terminal-failure
|
||||
assert_case failure nonzero terminal-failure merge
|
||||
assert_case late-failure nonzero terminal-failure merge
|
||||
|
||||
if [[ $(grep -c 'check-runs?per_page=100&filter=latest' "$WORK_DIR/gh-calls.log") -lt 4 ]]; then
|
||||
if [[ $(grep -c 'check-runs?per_page=100&filter=latest' "$WORK_DIR/gh-calls.log") -lt 6 ]]; then
|
||||
echo "FAIL: expected every case to query all Checks API pages" >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
@@ -92,4 +94,4 @@ if [[ "$failures" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub check-runs regression passed (4/4 cases, including later-page failure)"
|
||||
echo "GitHub check-runs regression passed (6/6 purpose-aware cases, including later-page failure)"
|
||||
|
||||
@@ -53,6 +53,7 @@ case "$url" in
|
||||
malformed) printf '%s' 'not-json' ;;
|
||||
malformed-statuses-type) printf '%s' '{"state":"success","statuses":"corrupt"}' ;;
|
||||
malformed-status-entry) printf '%s' '{"state":"success","statuses":[null]}' ;;
|
||||
unknown) printf '%s' '{"state":"success","statuses":[{"status":"cancelled"}]}' ;;
|
||||
large-success)
|
||||
python3 -c 'import json; print(json.dumps({"state":"success", "statuses":[{"status":"success"}], "padding":"x" * (160 * 1024)}), end="")'
|
||||
;;
|
||||
@@ -128,18 +129,27 @@ run_assertion() {
|
||||
|
||||
set -e
|
||||
: > "$WORK_DIR/urls.log"
|
||||
run_assertion success zero success 'state=terminal-success'
|
||||
run_assertion pending nonzero pending 'ASSERTED_NOT_READY'
|
||||
run_assertion failure nonzero failure 'ASSERTED_NOT_READY'
|
||||
run_assertion no-status nonzero no-status 'ASSERTED_NOT_READY'
|
||||
run_assertion aggregate-success-no-status nonzero aggregate-success-no-status 'ASSERTED_NOT_READY'
|
||||
run_assertion malformed nonzero malformed 'ASSERTED_NOT_READY'
|
||||
run_assertion malformed-statuses-type nonzero malformed-statuses-type 'ASSERTED_NOT_READY'
|
||||
run_assertion malformed-status-entry nonzero malformed-status-entry 'ASSERTED_NOT_READY'
|
||||
run_assertion large-payload not126 large-success 'state=terminal-success'
|
||||
# Push readiness is queue clearance, not proof that prior CI succeeded.
|
||||
run_assertion push-success zero success 'state=terminal-success'
|
||||
run_assertion push-pending nonzero pending 'ASSERTED_NOT_READY'
|
||||
run_assertion push-failure zero failure 'queue-clear state=terminal-failure purpose=push'
|
||||
run_assertion push-no-status zero no-status 'queue-clear state=no-status purpose=push'
|
||||
run_assertion push-aggregate-success-no-status zero aggregate-success-no-status 'queue-clear state=no-status purpose=push'
|
||||
run_assertion push-require-status nonzero no-status 'ASSERTED_NOT_READY state=no-status' --require-status
|
||||
run_assertion push-malformed nonzero malformed 'ASSERTED_NOT_READY'
|
||||
run_assertion push-malformed-statuses-type nonzero malformed-statuses-type 'ASSERTED_NOT_READY'
|
||||
run_assertion push-malformed-status-entry nonzero malformed-status-entry 'ASSERTED_NOT_READY'
|
||||
run_assertion push-unknown nonzero unknown 'ASSERTED_NOT_READY'
|
||||
run_assertion push-large-payload not126 large-success 'state=terminal-success'
|
||||
run_assertion credential-unresolvable zero credential-unresolvable 'CANNOT_ASSERT'
|
||||
run_assertion provider-unreachable zero unreachable 'CANNOT_ASSERT'
|
||||
|
||||
# Merge readiness remains fail-closed and requires exact-head terminal success.
|
||||
MOSAIC_TEST_PURPOSE=merge run_assertion merge-success zero success 'state=terminal-success'
|
||||
MOSAIC_TEST_PURPOSE=merge run_assertion merge-failure nonzero failure 'ASSERTED_NOT_READY state=terminal-failure'
|
||||
MOSAIC_TEST_PURPOSE=merge run_assertion merge-no-status nonzero no-status 'ASSERTED_NOT_READY state=no-status'
|
||||
MOSAIC_TEST_PURPOSE=merge run_assertion merge-unknown nonzero unknown 'ASSERTED_NOT_READY state=unknown'
|
||||
|
||||
if [[ ! -s "$AUDIT_LOG" ]] || ! grep -q '"outcome":"CANNOT_ASSERT"' "$AUDIT_LOG"; then
|
||||
echo "FAIL provider-unreachable-audit: expected durable CANNOT_ASSERT JSONL record" >&2
|
||||
failures=$((failures + 1))
|
||||
|
||||
@@ -17,9 +17,10 @@ make_fixture() {
|
||||
cp "$SCRIPT_DIR/detect-platform.sh" "$tools/detect-platform.sh"
|
||||
git -C "$root/repo" init -q
|
||||
git -C "$root/repo" remote add origin "$remote"
|
||||
local base_branch="${3:-main}"
|
||||
cat > "$tools/pr-metadata.sh" <<SH
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' '{"baseRefName":"main","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}'
|
||||
printf '%s\n' '{"baseRefName":"$base_branch","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}'
|
||||
SH
|
||||
cat > "$tools/ci-queue-wait.sh" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
@@ -29,8 +30,8 @@ SH
|
||||
}
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
make_fixture gitea https://git.example.test/acme/widgets.git
|
||||
make_fixture github https://github.com/acme/widgets.git
|
||||
make_fixture gitea https://git.example.test/acme/widgets.git next
|
||||
make_fixture github https://github.com/acme/widgets.git main
|
||||
|
||||
cat > "$WORK_DIR/gitea/curl" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
|
||||
Reference in New Issue
Block a user