Compare commits

..

161 Commits

Author SHA1 Message Date
Jarvis
f3d5ef8d7d feat: unified first-run flow — merge wizard + gateway install (IUH-M03)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Collapse `mosaic wizard` and `mosaic gateway install` into a single cohesive
first-run experience. Gateway config and admin bootstrap now run as terminal
stages of `runWizard`, sharing `WizardState` with the framework stages and
eliminating the fragile 10-minute `$XDG_RUNTIME_DIR/mosaic-install-state.json`
session-file bridge.

- Extract `gatewayConfigStage` and `gatewayBootstrapStage` as first-class
  wizard stages with full spec coverage (headless + interactive paths).
- `mosaic gateway install` becomes a thin wrapper that invokes the same
  two stages — the CLI entry point is preserved for operators who only
  need to (re)configure the daemon.
- Honor explicit `--port` override even on resume: when the override
  differs from the saved GATEWAY_PORT, force a config regeneration so
  `.env` and `meta.json` cannot drift.
- Honor `state.hooks.accepted === false` in the finalize stage and in
  `mosaic-link-runtime-assets`: declined hooks are now actually opted-out,
  with a stable `mosaic-managed: true` marker in the template so cleanup
  survives template updates without touching user-owned configs.
- Headless rerun of an already-bootstrapped gateway with no local token
  cache is a successful no-op (no more false-positive install failures).
- `tools/install.sh` calls `mosaic wizard` only — the follow-up
  `mosaic gateway install` auto-launch is removed.

Closes mosaicstack/mosaic-stack#427.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 14:08:28 -05:00
be917e2496 docs: mark IUH-M02 complete, start IUH-M03 (#432)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 18:02:21 +00:00
cd8b1f666d feat: wizard remediation — password mask, hooks preview, headless (IUH-M02) (#431)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 17:47:53 +00:00
8fa5995bde docs: scaffold install-ux-hardening mission + archive cli-unification (#430)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 17:15:39 +00:00
25cada7735 feat: mosaic uninstall (IUH-M01) (#429)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 17:06:21 +00:00
be6553101c docs: finalize CLI unification mission at mosaic-v0.0.24 (#424)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 14:54:48 +00:00
417805f330 fix: bump memory/queue/storage to 0.0.4 to force republish (#423)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/tag/publish Pipeline was successful
2026-04-05 14:39:15 +00:00
2472ce52e8 fix: bump stale sub-package versions (brain/forge/log) (#422)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 14:26:30 +00:00
597eb232d7 fix: revert mosaic to 0.0.22 alpha + republish macp (#421)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 14:15:46 +00:00
afe997db82 docs: mission cli-unification-20260404 complete (#420)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 07:54:50 +00:00
b9d464de61 docs: CLI unification release v0.1.0 (M8) (#419)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/tag/publish Pipeline was successful
2026-04-05 07:46:00 +00:00
872c124581 feat(mosaic): unified first-run UX wizard -> gateway install -> verify (#418)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 07:29:17 +00:00
a531029c5b feat(mosaic): mosaic telemetry command (M6 CU-06-01..05) (#417)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 07:06:42 +00:00
35ab619bd0 docs: session 2 orchestrator bookkeeping (M3/M4/M5 complete) (#416)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed
2026-04-05 07:06:40 +00:00
831193cdd8 fix(macp): align exports + add CLI smoke test (#415)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 06:57:42 +00:00
df460d5a49 feat(macp): mosaic macp CLI surface (#410)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 06:33:52 +00:00
119ff0eb1b fix(mosaic): gateway token recovery review remediations (#414)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 06:13:29 +00:00
3abd63ea5c Merge pull request 'feat(mosaic): mosaic auth CLI surface' (#413) from feat/mosaic-auth-cli into main
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-05 06:11:33 +00:00
641e4604d5 feat(forge): mosaic forge CLI surface (#412)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-05 06:08:50 +00:00
Jarvis
9b5ecc0171 feat(mosaic): add auth command and stage parallel agent changes
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Picks up auth command and spec written by parallel agent, and updated
mosaic cli.ts wiring from parallel development during cli-unification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 00:58:03 -05:00
Jarvis
a00325da0e feat(forge): add registerForgeCommand for mosaic forge CLI surface
Adds mosaic forge run|status|resume|personas list subcommands to
@mosaicstack/forge, wires registerForgeCommand into the root mosaic CLI,
and ships a smoke test asserting command structure. Ref CU-05-01
cli-unification-20260404.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 00:58:03 -05:00
4ebce3422d feat(log): mosaic log CLI surface (#407)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 05:57:22 +00:00
751e0ee330 feat(storage): mosaic storage CLI surface (#405)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 05:48:13 +00:00
54b2920ef3 feat(memory): mosaic memory CLI surface (#406)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed
2026-04-05 05:44:06 +00:00
5917016509 feat(mosaic): gateway token recovery via BetterAuth cookie (#411)
Some checks are pending
ci/woodpecker/push/ci Pipeline is pending
ci/woodpecker/push/publish Pipeline is pending
2026-04-05 05:43:49 +00:00
7b4f1d249d feat(mosaic): top-level mosaic config command (#408)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-05 05:37:05 +00:00
5425f9268e feat(queue): mosaic queue CLI surface (#404)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-05 05:27:59 +00:00
febd866098 feat(brain): mosaic brain CLI surface (#403)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 05:20:44 +00:00
2446593fff feat(mosaic): alphabetize and group mosaic --help output (#402)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 05:12:32 +00:00
651426cf2e docs(plan): gateway admin token recovery flow (#401)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-05 05:11:33 +00:00
cf46f6e0ae docs: capture planning decisions + session 1 handoff (#400)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 04:57:00 +00:00
6f15a84ccf docs: archive stale mission, scaffold CLI unification mission (#399)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 04:47:54 +00:00
c39433c361 chore: remove legacy @mosaicstack/cli package (#398)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 04:39:46 +00:00
257796ce87 Merge pull request 'chore: bump @mosaicstack/mosaic to 0.0.21 for republish' (#397) from chore/bump-mosaic-0.0.21 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 04:12:05 +00:00
Jarvis
2357602f50 chore: bump @mosaicstack/mosaic to 0.0.21 for publish
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
2026-04-04 23:09:52 -05:00
1230f6b984 ci: fail publish pipeline loudly on registry/auth/network errors (#396)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 03:58:35 +00:00
14b775f1b9 Merge pull request 'fix: populate KNOWN_PACKAGES for mosaic update command' (#395) from fix/populate-known-packages-list into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
Reviewed-on: mosaicstack/mosaic-stack#395
2026-04-05 03:52:57 +00:00
Jarvis
c7691d9807 fix: populate KNOWN_PACKAGES with all workspace packages for 'mosaic update'
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
- Remove @mosaicstack/cli (absorbed into @mosaicstack/mosaic)
- Add all 21 remaining workspace packages so the multi-package
  update checker actually covers every published package
2026-04-04 22:49:45 -05:00
9a53d55678 Merge pull request 'fix: update Gitea org references from mosaic/ to mosaicstack/' (#394) from fix/gitea-org-rename into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaicstack/mosaic-stack#394
2026-04-05 03:35:11 +00:00
Jarvis
31008ef7ff fix: update Gitea org references from mosaic/ to mosaicstack/
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
- Update all package.json repo URLs (mosaic/mosaic-stack → mosaicstack/mosaic-stack)
- Update npm registry URLs (/api/packages/mosaic/npm → /api/packages/mosaicstack/npm)
- Update woodpecker publish destinations
- Update tools/install.sh registry and repo base URLs
2026-04-04 22:31:20 -05:00
621ab260c0 fix(mosaic): resumable gateway install + prominent admin token (#393)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/manual/publish Pipeline failed
ci/woodpecker/manual/ci Pipeline failed
2026-04-05 03:19:07 +00:00
2b1840214e Merge pull request 'fix: rename @mosaic/* packages to @mosaicstack/*' (#392) from fix/rename-mosaic-scope-391 into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 03:11:55 +00:00
Jarvis
5cfccc2ead fix(mosaic): remove unused hasUpdate variable in formatAllPackagesTable
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Fixes lint error:
@typescript-eslint/no-unused-vars on hasUpdate
2026-04-04 22:01:01 -05:00
Jarvis
774b76447d fix: rename all packages from @mosaic/* to @mosaicstack/*
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
- Updated all package.json name fields and dependency references
- Updated all TypeScript/JavaScript imports
- Updated .woodpecker/publish.yml filters and registry paths
- Updated tools/install.sh scope default
- Updated .npmrc registry paths (worktree + host)
- Enhanced update-checker.ts with checkForAllUpdates() multi-package support
- Updated CLI update command to show table of all packages
- Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand
- Marked checkForUpdate() with @deprecated JSDoc

Closes #391
2026-04-04 21:43:23 -05:00
80994bdc8e fix(packages): bump db/memory/queue for PGlite + adapter factories (#389)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 02:20:23 +00:00
2e31626f87 fix: simplify updater to @mosaic/mosaic only, add explicit tea repo/login flags (#388)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 02:09:23 +00:00
255ba46a4d fix(packages): republish @mosaic/config and bump dependents (#386)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 01:56:57 +00:00
10285933a0 fix: retarget updater to @mosaic/mosaic (#384)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-05 01:52:30 +00:00
543388e18b fix(mosaic): resolve framework scripts via import.meta.url (#385)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Fixes #383 — resolveTool now uses fileURLToPath(import.meta.url). Adds package.json/framework subpath exports. Bumps @mosaic/mosaic to 0.0.18.
2026-04-05 01:41:46 +00:00
07a1f5d594 Merge pull request 'feat(mosaic): merge @mosaic/cli into @mosaic/mosaic' (#381) from fix/merge-cli-into-mosaic into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 01:11:33 +00:00
Jarvis
c6fc090c98 feat(mosaic): merge @mosaic/cli into @mosaic/mosaic
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
@mosaic/mosaic is now the single package providing both:
- 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.)
- 'mosaic-wizard' binary (installation wizard)

Changes:
- Move packages/cli/src/* into packages/mosaic/src/
- Convert dynamic @mosaic/mosaic imports to static relative imports
- Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic
- Add jsx: react-jsx to mosaic's tsconfig
- Exclude packages/cli from workspace (pnpm-workspace.yaml)
- Update install.sh to install @mosaic/mosaic instead of @mosaic/cli
- Bump version to 0.0.17

This eliminates the circular dependency between @mosaic/cli and
@mosaic/mosaic that was blocking the build graph.
2026-04-04 20:07:27 -05:00
9723b6b948 chore: bump @mosaic/cli and @mosaic/mosaic to 0.0.16 (#379)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-05 00:52:09 +00:00
c0d0fd44b7 refactor(storage): replace better-sqlite3 with PGlite adapter (#378)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 21:58:14 +00:00
30c0fb1308 fix: remediate npm deprecation warnings in @mosaic/gateway 0.0.3 (#377)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 21:03:54 +00:00
26fac4722f fix: gateway install preserves npm prefix via registry flag (#376)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 20:36:15 +00:00
e3f64c79d9 chore: move gateway default port from 4000 to 14242 (#375)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 20:17:40 +00:00
cbd5e8c626 fix: scope Gitea registry to @mosaic packages only (#374)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 19:09:14 +00:00
7560c7dee7 fix: gateway install uses Gitea registry instead of npmjs (#373)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-04 18:59:40 +00:00
982a0e8f83 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.11 (#372)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 18:47:03 +00:00
fc7fa11923 feat: local tier gateway with PGlite + Gitea-only publishing (#371)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-04 18:39:20 +00:00
86d6c214fe feat: gateway publishability + npmjs publish script (#370)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-04 18:07:05 +00:00
39ccba95d0 feat: mosaic gateway CLI daemon management + admin token auth (#369)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-04 18:03:12 +00:00
202e375f41 Merge pull request 'fix: add build tools to CI install step for better-sqlite3 native bindings' (#368) from feat/task-1775219952-fix-add-build-tools-to-ci-install-step-for-better-sqlite3 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 15:41:23 +00:00
Jarvis
d0378c5723 fix: add Alpine build tools before pnpm install in CI
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-04-03 09:13:25 -05:00
d6f04a0757 Merge pull request 'fix: add build tools to CI install step for better-sqlite3 native bindings' (#366) from fix/storage-sqlite-ci into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 13:41:04 +00:00
afedb8697e Merge pull request 'fix: allow better-sqlite3 build script in pnpm 10' (#367) from fix/pnpm-build-scripts into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 13:11:07 +00:00
Jarvis
1274df7ffc fix: allow better-sqlite3 build script in pnpm 10
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
2026-04-03 08:06:01 -05:00
Jarvis
1b4767bd8b fix: add build tools to CI install step for better-sqlite3 native bindings
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-04-03 07:41:39 -05:00
0b0fe10b37 Merge pull request 'feat: storage abstraction retrofit — adapters for queue, storage, memory (phases 1-4)' (#365) from feat/storage-abstraction into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 04:40:57 +00:00
acfb31f8f6 fix: quality-rails Commander version mismatch + installer defaults (#364)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 02:40:02 +00:00
Jarvis
fd83bd4f2d chore(orchestrator): Phase 4 complete — config schema + CLI lifecycle commands
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
381 tests passing (347 gateway + 34 CLI), 40/40 tasks clean
2026-04-02 21:38:40 -05:00
Jarvis
ce3ca1dbd1 feat(cli): add gateway start/stop/status lifecycle commands
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 21:37:20 -05:00
Jarvis
95e7b071d4 feat(cli): add mosaic gateway init command with tier selection wizard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 21:35:32 -05:00
d4c5797a65 fix: installer copies default framework files (AGENTS.md) to mosaicHome (#363)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:34:43 +00:00
70a51ba711 fix: all CLI script resolution uses bundled-first resolveTool() (#362)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:28:07 +00:00
db8023bdbb fix: fwScript prefers npm-bundled scripts over stale deployed copies (#361)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:21:58 +00:00
9e597ecf87 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.6 (#360)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:13:37 +00:00
a23c117ea4 fix: auto-migrate customized skills to skills-local/ on sync (#359)
Some checks failed
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-03 02:11:03 +00:00
0cf80dab8c fix: stale update banner + skill sync dirty worktree crash (#358)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-03 02:04:05 +00:00
Jarvis
04a80fb9ba feat(config): add MosaicConfig schema + loader with tier auto-detection
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 21:03:00 -05:00
Jarvis
626adac363 chore(orchestrator): Phase 3 complete — local tier implemented (SQLite + keyword search + JSON queue)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
42 new tests: 4 queue, 18 storage, 20 memory
347 total tests passing
2026-04-02 20:56:39 -05:00
Jarvis
35fbd88a1d feat(memory): implement keyword search adapter — no vector dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:55:00 -05:00
381b0eed7b Merge pull request 'chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.4' (#357) from chore/bump-0.0.4 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#357
2026-04-03 01:51:55 +00:00
Jarvis
25383ea645 feat(storage): implement SQLite adapter with better-sqlite3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:51:13 -05:00
Jarvis
e7db9ddf98 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.4
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-04-02 20:50:44 -05:00
Jarvis
7bb878718d feat(queue): implement local adapter with JSON persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:46:11 -05:00
Jarvis
46a31d4e71 chore(orchestrator): Phase 2 complete — existing backends wrapped as adapters
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-04-02 20:44:11 -05:00
Jarvis
e128a7a322 feat(gateway): wire adapter factories + DI tokens alongside existing providers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:11 -05:00
Jarvis
27b1898ec6 refactor(memory): wrap pgvector logic as MemoryAdapter implementation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:11 -05:00
Jarvis
d19ef45bb0 feat(storage): implement Postgres adapter wrapping Drizzle + @mosaic/db
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
5e852df6c3 refactor(queue): wrap ioredis as bullmq adapter behind QueueAdapter interface
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
e0eca771c6 chore(orchestrator): Phase 1 complete — all interfaces defined 2026-04-02 20:44:10 -05:00
Jarvis
9d22ef4cc9 feat: add adapter factory + registry pattern for queue, storage, memory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
41961a6980 feat(memory): define MemoryAdapter interface types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
e797676a02 feat(storage): define StorageAdapter interface types + scaffold package
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
05d61e62be feat(queue): define QueueAdapter interface types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 20:44:10 -05:00
Jarvis
73043773d8 chore(orchestrator): Bootstrap storage abstraction retrofit
Mission: Decouple gateway from hardcoded Postgres/Valkey backends.
20 tasks across 5 phases. Estimated total: ~214K tokens.

Phase 1: Interface extraction (4 tasks)
Phase 2: Wrap existing backends as adapters (5 tasks)
Phase 3: Local tier implementation (4 tasks)
Phase 4: Config + CLI commands (4 tasks)
Phase 5: Migration + docs (3 tasks)
2026-04-02 20:44:10 -05:00
0be9729e40 Merge pull request 'fix: syncDirectory same-path guard, nested .git exclusion, and sync stash handling' (#356) from fix/idempotent-init into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#356
2026-04-03 01:42:18 +00:00
Jarvis
e83674ac51 fix: mosaic sync — auto-stash dirty worktree before pull --rebase
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
git pull --rebase fails with 'cannot pull with rebase: You have
unstaged changes' when the skills repo has local modifications.

Fix: detect dirty index/worktree, stash before pull, restore after.
Also gracefully handle pull failures (warn and continue with existing
checkout) and stash pop conflicts.
2026-04-02 20:41:11 -05:00
Jarvis
a6e59bf829 fix: syncDirectory — guard same-path copy and skip nested .git dirs
Two bugs causing 'EACCES: permission denied, copyfile' when source
and target are the same path (e.g. wizard with sourceDir == mosaicHome):

1. No same-path guard — syncDirectory tried to copy every file onto
   itself; git pack files are read-only (0444) so copyFileSync fails.
2. excludeGit only matched top-level .git — nested .git dirs like
   sources/agent-skills/.git were copied, hitting the same permission
   issue.

Fixes:
- Early return when resolve(source) === resolve(target)
- Match .git dirs at any depth via dirName and relPath checks
- Skip files inside .git/ paths

Added file-ops.test.ts with 4 tests covering all cases.
2026-04-02 20:41:11 -05:00
e46f0641f6 Merge pull request 'fix: make mosaic init idempotent — detect existing config files' (#355) from fix/idempotent-init into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#355
2026-04-03 01:30:01 +00:00
Jarvis
07efaa9580 chore: bump @mosaic/mosaic and @mosaic/cli to 0.0.3
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
2026-04-02 20:26:01 -05:00
Jarvis
361fece023 fix: make mosaic init idempotent — detect existing config files
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
- mosaic-init bash script: detect existing SOUL.md/USER.md/TOOLS.md and
  prompt user to keep, import (re-use values as defaults), or overwrite.
  Non-interactive mode exits cleanly unless --force is passed.
  Overwrite creates timestamped backups before replacing files.

- launch.ts checkSoul(): prefer 'mosaic wizard' over legacy bash script
  when SOUL.md is missing, with fallback to mosaic-init.

- detect-install.ts: pre-populate wizard state with existing values when
  user chooses 'reconfigure', so they see current settings as defaults.

- soul-setup.ts: show existing agent name and communication style as
  defaults during reconfiguration.

- Added tests for reconfigure pre-population and reset non-population.
2026-04-02 20:20:59 -05:00
80e69016b0 Merge pull request 'chore: bump all packages to 0.0.2 — drop alpha prerelease tag' (#354) from chore/bump-0.0.2 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#354
2026-04-03 01:12:24 +00:00
Jarvis
e084a88a9d chore: bump all packages to 0.0.2 — drop alpha prerelease tag
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Switches from 0.0.1-alpha.2 to 0.0.2. Clean semver, no prerelease
suffixes. We're still alpha (0.0.x range).
2026-04-02 20:03:55 -05:00
990a88362f Merge pull request 'feat: complete CLI command parity — coord, prdy, seq, upgrade' (#352) from fix/complete-cli-parity into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#352
2026-04-03 00:52:36 +00:00
Jarvis
ea9782b2dc feat: complete CLI command parity — add coord, prdy, seq, upgrade
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
The #351 merge landed before the force-push with full commands.
This adds the missing subcommands:

- mosaic coord {init,status,mission,continue,run,smoke,resume}
  → delegates to tools/orchestrator/*.sh with --claude/--codex/--pi/--yolo
- mosaic prdy {init,update,validate,status}
  → delegates to tools/prdy/*.sh with --claude/--codex/--pi
- mosaic seq {check,fix,start}
  → sequential-thinking MCP management (native TS)
- mosaic upgrade {release,check,project}
  → delegates to tools/_scripts/mosaic-release-upgrade and mosaic-upgrade

Also removes duplicate prdy registration (was in both launch.ts and
the old registerPrdyCommand — now only in launch.ts).
2026-04-02 19:51:34 -05:00
8efbaf100e Merge pull request 'feat: unify mosaic CLI — single binary, no PATH conflict' (#351) from feat/unify-mosaic-cli into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-03 00:41:06 +00:00
Jarvis
15830e2f2a feat!: unify mosaic CLI — native launcher, no bin/ directory
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
BREAKING CHANGE: ~/.config/mosaic/bin/ is removed entirely.
The mosaic npm CLI is now the only executable.

## What changed

- **bin/ → deleted**: All scripts moved to tools/_scripts/ (internal)
- **mosaic-launch → deleted**: Launcher logic is native TypeScript
  in packages/cli/src/commands/launch.ts
- **mosaic.ps1 → deleted**: PowerShell launcher removed
- **Framework install.sh**: Complete rewrite with migration system
- **Version tracking**: .framework-version file (schema v2)
- **Migration v1→v2**: Auto-removes bin/, cleans old PATH entries
  from shell profiles

## Native TypeScript launcher (commands/launch.ts)

All runtime launch logic ported from bash:
- Runtime prompt builder (AGENTS.md + RUNTIME.md + USER.md + TOOLS.md)
- Mission context injection (reads .mosaic/orchestrator/mission.json)
- PRD status injection (scans docs/PRD.md)
- Pre-flight checks (MOSAIC_HOME, AGENTS.md, SOUL.md, runtime binary)
- Session lock management with signal cleanup
- Per-runtime launch: Claude, Codex, OpenCode, Pi
- Yolo mode flags per runtime
- Pi skill discovery + extension loading
- Framework management (init, doctor, sync, bootstrap) delegates
  to tools/_scripts/ bash implementations

## Installer

- tools/install.sh: detects framework by .framework-version or AGENTS.md
- Framework install.sh: migration system with schema versioning
- Forward-compatible: add migrations as numbered blocks
- No PATH manipulation for framework (npm bin is the only PATH entry)
2026-04-02 19:37:13 -05:00
04db8591af Merge pull request 'docs: add project README' (#350) from docs/readme into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
Reviewed-on: mosaic/mosaic-stack#350
2026-04-03 00:17:10 +00:00
Jarvis
785d30e065 docs: add project README with install, usage, architecture, and dev guide
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
2026-04-02 19:12:28 -05:00
e57a10913d chore: bump all packages to 0.0.1-alpha.2 (#349)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-02 18:21:23 +00:00
0d12471868 feat: add web search, file edit, MCP management, file refs, and /stop to CLI/TUI (#348)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-02 18:08:30 +00:00
ea371d760d Merge pull request 'feat: unified install.sh + auto-update checker (deprecates mosaic/bootstrap)' (#347) from feat/install-update-checker into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-02 05:41:07 +00:00
Jarvis
3b9104429b fix(mosaic): wizard integration test — templates path after monorepo migration
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Templates moved from packages/mosaic/templates/ to
packages/mosaic/framework/templates/ in #345. The test's
existsSync guard silently skipped the copy, causing writeSoul
to early-return without writing SOUL.md.
2026-04-02 00:12:03 -05:00
Jarvis
8a83aed9b1 feat: unify install.sh — single installer for framework + npm CLI
- tools/install.sh now installs both components:
  1. Framework (bash launcher, guides, runtime configs) → ~/.config/mosaic/
  2. @mosaic/cli (TUI, gateway client, wizard) → ~/.npm-global/
- Downloads framework from monorepo archive (no bootstrap repo dependency)
- Supports --framework, --cli, --check, --ref flags
- Delete remote-install.sh and remote-install.ps1 (redundant redirectors)
- Update all stale mosaic/bootstrap references → mosaic/mosaic-stack
- Update README.md with monorepo install instructions

Deprecates: mosaic/bootstrap repo
2026-04-02 00:12:03 -05:00
Jarvis
2f68237046 fix: remove --registry from npm install to avoid 404 on transitive deps
The @mosaic scope registry is configured in ~/.npmrc. Passing --registry
on the install command overrides the default registry for ALL packages,
causing non-@mosaic deps like @clack/prompts to 404 against Gitea.
2026-04-02 00:11:42 -05:00
Jarvis
45f5b9062e feat: install.sh + auto-update checker for CLI
- tools/install.sh: standalone installer/upgrader, curl-pipe safe
  (main() wrapper, process.argv instead of stdin, mkdir -p prefix)
- packages/mosaic/src/runtime/update-checker.ts: version check module
  with 1h cache at ~/.cache/mosaic/update-check.json
- CLI startup: non-blocking background update check on every invocation
- 'mosaic update' command: explicit check + install (--check for CI)
- session-start.sh: warns agents when CLI is outdated
- Proper semver comparison including pre-release precedence
- eslint: allow __tests__ in packages/mosaic for projectService
2026-04-02 00:11:42 -05:00
147f5f1bec Merge pull request 'fix: remove stale bootstrap repo references' (#346) from fix/stale-bootstrap-refs into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
2026-04-02 02:27:49 +00:00
Jason Woltje
f05b198882 fix: remove stale bootstrap repo references from CLI error messages
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
Replace 'cd ~/src/mosaic-bootstrap && bash install.sh' with
'npm install -g @mosaic/mosaic' now that bootstrap is archived.
2026-04-01 21:26:53 -05:00
d0a484cbb7 Merge pull request 'feat: complete bootstrap → monorepo migration (archive-ready)' (#345) from feat/framework-migration-complete into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed
2026-04-02 02:24:33 +00:00
Jason Woltje
6e6ee37da0 feat: complete framework migration — PowerShell, adapters, guides, profiles, tests
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
Completes the bootstrap repo migration with remaining files:
- PowerShell scripts (.ps1) for Windows support (bin/ + tools/)
- Runtime adapters (claude, codex, generic, pi)
- Guides (17 .md files) and profiles (domains, tech-stacks, workflows)
- Wizard test suite (6 test files from bootstrap tests/)
- Memory placeholder, audit history

Bootstrap repo (mosaic/bootstrap) is now fully superseded:
- All 335 files accounted for
- 5 build config files (package.json, tsconfig, etc.) not needed —
  monorepo has its own at packages/mosaic/
- skills-local/ superseded by monorepo skills/ with mosaic-* naming
- src/ already lives at packages/mosaic/src/
2026-04-01 21:23:26 -05:00
53199122d8 Merge pull request 'feat: integrate framework files into monorepo' (#344) from feat/framework-into-monorepo into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-02 02:20:17 +00:00
Jason Woltje
b38cfac760 feat: integrate framework files into monorepo under packages/mosaic/framework/
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Moves all Mosaic framework runtime files from the separate bootstrap repo
into the monorepo as canonical source. The @mosaic/mosaic npm package now
ships the complete framework — bin scripts, runtime configs, tools, and
templates — enabling standalone installation via npm install.

Structure:
  packages/mosaic/framework/
  ├── bin/          28 CLI scripts (mosaic, mosaic-doctor, mosaic-sync-skills, etc.)
  ├── runtime/      Runtime adapters (claude, codex, opencode, pi, mcp)
  ├── tools/        Shell tooling (git, prdy, orchestrator, quality, etc.)
  ├── templates/    Agent and repo templates
  ├── defaults/     Default identity files (AGENTS.md, STANDARDS.md, SOUL.md, etc.)
  ├── install.sh    Legacy bash installer
  └── remote-install.sh  One-liner remote installer

Key files with Pi support and recent fixes:
- bin/mosaic: launch_pi() with skills-local loop
- bin/mosaic-doctor: --fix auto-wiring for all 4 harnesses
- bin/mosaic-sync-skills: Pi as 4th link target, symlink-aware find
- bin/mosaic-link-runtime-assets: Pi settings.json patching
- bin/mosaic-migrate-local-skills: Pi skill roots, symlink find
- runtime/pi/RUNTIME.md + mosaic-extension.ts

Package ships 251 framework files in the npm tarball (278KB compressed).
2026-04-01 21:19:21 -05:00
f3cb3e6852 Merge pull request 'fix(web): add public/ directory — fixes Docker build COPY failure' (#343) from fix/web-public-dir into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-04-01 18:09:40 +00:00
Jason Woltje
e599f5fe38 fix(web): add public/ directory — fixes Docker build COPY failure
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Kaniko fails when COPY --from=builder references a path that doesn't
exist. The web app had no public/ directory, causing build-web to fail
with 'no such file or directory' on the public assets COPY step.
2026-04-01 13:09:03 -05:00
6357a3fc9c Merge pull request 'fix(ci): use gitea_token secret for npm publish' (#342) from fix/ci-npm-secret into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-01 17:51:32 +00:00
Jason Woltje
92998e6e65 fix(ci): use gitea_token secret for npm publish
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
2026-04-01 12:51:06 -05:00
2394a2a0dd Merge pull request 'feat: npm publish pipeline + package versioning (0.0.1-alpha.1)' (#341) from feat/npm-publish-pipeline into main 2026-04-01 17:47:10 +00:00
Jason Woltje
13934d4879 feat: npm publish pipeline + package versioning (0.0.1-alpha.1)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Publish pipeline:
- Add publish-npm step to .woodpecker/publish.yml — publishes all
  @mosaic/* packages to Gitea npm registry on main push/tag
- Requires gitea_npm_token Woodpecker secret (package:write scope)
- publish-npm runs after build, parallel with Docker image builds
- pnpm publish resolves workspace:* to concrete versions automatically

Package configuration:
- All 20 packages versioned at 0.0.1-alpha.1
- publishConfig added to all packages (Gitea registry, public access)
- files field added to all packages (ship only dist/)
- @mosaic/forge includes pipeline/ assets in published package

Meta package (@mosaic/mosaic):
- Now depends on @mosaic/forge, @mosaic/macp, @mosaic/prdy,
  @mosaic/quality-rails, @mosaic/types
- npm install @mosaic/mosaic pulls in the standalone framework

Build fixes:
- Fix forge and macp tsconfig rootDir: '.' -> 'src' so dist/index.js
  resolves correctly (was dist/src/index.js)
- Exclude __tests__ and vitest.config from build includes
- Clean stale build artifacts from old rootDir config

Required Woodpecker secret:
  woodpecker secret add mosaic/mosaic-stack \
    --name gitea_npm_token --value '<token>' \
    --event push,manual,tag
2026-04-01 12:46:13 -05:00
aa80013811 Merge pull request 'feat: mosaic-* skill naming, board/forge/prdy skills, doctor --fix auto-wiring' (#340) from feat/mosaic-skills-doctor-wiring into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-01 17:29:39 +00:00
Jason Woltje
2ee7206c3a feat: mosaic-* skill naming, new board/forge/prdy skills, doctor --fix auto-wiring
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Skills:
- Rename all repo skills to mosaic-<name> convention (jarvis -> mosaic-jarvis, etc.)
- Update frontmatter name: fields to match directory names
- New mosaic-board skill: standalone Board of Directors multi-persona review
- New mosaic-forge skill: standalone Forge specialist pipeline
- New mosaic-prdy skill: PRD lifecycle (init/update/validate/status)

Wizard (packages/mosaic):
- Add mosaic-board, mosaic-forge, mosaic-prdy, mosaic-standards, mosaic-macp
  to RECOMMENDED_SKILLS
- Add new skills to SKILL_CATEGORIES for categorized browsing

Framework scripts (~/.config/mosaic/bin):
- mosaic (launcher): load skills from both skills/ and skills-local/ for Pi
- mosaic-doctor: add --fix flag for auto-wiring skills into all harnesses,
  Pi skill dir checks, Pi settings.json validation, mosaic-* presence checks
- mosaic-sync-skills: add Pi as 4th link target, fix find to follow symlinks
  in skills-local/, harden is_mosaic_skill_name() with -L fallback
- mosaic-link-runtime-assets: add Pi settings.json skills path patching,
  remove duplicate extension copy (launcher --extension is single source)
- mosaic-migrate-local-skills: add Pi to skill_roots, fix find for symlinks

YAML fixes:
- Quote description values containing colons in mosaic-deploy and
  mosaic-woodpecker SKILL.md frontmatter (fixes Pi parse errors)
2026-04-01 12:28:36 -05:00
be74ca3cf9 feat: add Pi as first-class Mosaic runtime (#339)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-04-01 17:02:23 +00:00
35123b21ce Merge pull request 'fix(ci): pass DATABASE_URL through Turbo to test tasks' (#338) from fix/turbo-env-passthrough into main
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
2026-03-31 04:03:29 +00:00
492dc18e14 Merge pull request 'fix(db): add missing migration to Drizzle journal — fixes CI test failures' (#337) from fix/ci-drizzle-migration-journal into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed
2026-03-31 03:03:45 +00:00
Jarvis
a824a43ed1 fix(ci): pass DATABASE_URL through Turbo to test tasks
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 22:02:37 -05:00
Jarvis
9b72f0ea14 fix(db): add CREATE EXTENSION vector before first migration using pgvector
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
The insights table uses vector(1536) but no migration enables the pgvector
extension. CI postgres (pgvector/pgvector:pg17) has the extension available
but it must be explicitly created before use.

Adds CREATE EXTENSION IF NOT EXISTS vector at the top of
0001_cynical_ultimatum.sql (the first migration referencing vector type).
2026-03-30 21:14:44 -05:00
Jarvis
d367f00077 fix(db): add missing 0001_cynical_ultimatum to Drizzle migration journal
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
The migration file 0001_cynical_ultimatum.sql existed on disk but was not
registered in the Drizzle journal (_journal.json). This caused fresh-database
migrations (CI) to skip creating tables (agent_logs, insights, preferences,
skills, summarization_jobs), then 0002_nebulous_mimic.sql would fail trying
to ALTER the non-existent preferences table.

Fix: insert cynical_ultimatum at idx 1 in the journal and shift all
subsequent entries (idx 2-7).

Verified: pnpm test passes (347 tests, 35 tasks).
2026-03-30 21:09:34 -05:00
31a5751c6c Merge pull request 'feat(ci): Docker build+push pipeline for gateway and web images' (#335) from fix/ci-docker-publish-test-dep into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline failed
2026-03-31 01:48:08 +00:00
fa43989cd5 Merge pull request 'fix: parse VALKEY_URL into RedisOptions for BullMQ — fixes ECONNREFUSED 6379' (#336) from fix/bullmq-valkey-url-port into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-31 01:45:37 +00:00
1b317e8a0a style: fix prettier formatting in plugins/macp (consolidation follow-up)
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 20:43:54 -05:00
316807581c fix: parse VALKEY_URL into RedisOptions object for BullMQ connection
BullMQ v5 RedisConnection constructor does:
  Object.assign({ port: 6379, host: '127.0.0.1' }, opts)

When opts is a URL string (via 'as unknown as ConnectionOptions'),
Object.assign only copies character-index properties from the string,
so the default port 6379 was never overridden — causing ECONNREFUSED
against the wrong port instead of the configured 6380.

Fix: parse VALKEY_URL with new URL() and return a plain RedisOptions
object { host, port, ... } so Object.assign merges it correctly.
2026-03-30 20:42:03 -05:00
Jarvis
3321d4575a fix(ci): wait for postgres readiness before migration + tests
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 20:41:46 -05:00
Jarvis
85d4527701 fix(macp): use sh instead of bash in gate-runner — Alpine Linux compatibility
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 20:31:39 -05:00
Jarvis
47b7509288 fix(ci): add postgres service sidecar for integration tests
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 20:25:59 -05:00
Jarvis
34fad9da81 fix(ci): remove build step from ci.yml — build only in publish pipeline
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 20:19:29 -05:00
Jarvis
48be0aa195 fix(ci): separate publish pipeline — Docker builds independent of test failures
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 20:12:23 -05:00
Jarvis
f544cc65d2 fix(ci): switch to Kaniko image builder using global gitea secrets
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 20:04:50 -05:00
Jarvis
41e8f91b2d fix(ci): decouple build/publish from test step — DB test requires external Postgres
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 20:00:35 -05:00
Jarvis
f161e3cb62 feat(ci): add Docker build+push pipeline for gateway and web images
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 19:54:28 -05:00
da41724490 Merge pull request 'fix: remove all hardcoded user paths — dynamic OC SDK resolution' (#333) from fix/macp-dynamic-sdk-resolution into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 19:55:21 +00:00
Mos (Agent)
281e636e4d fix: remove all hardcoded user paths from plugins — dynamic SDK resolution
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
- plugins/macp/src/index.ts: use createRequire + dynamic import() for OC SDK
- plugins/macp/src/acp-runtime-types.ts: local ACP runtime type definitions
- plugins/macp/src/macp-runtime.ts: DEFAULT_REPO_ROOT and PI_RUNNER_PATH use
  os.homedir() instead of hardcoded /home/user/
- plugins/mosaic-framework/src/index.ts: removed hardcoded SDK import
- No hardcoded /home/ paths remain in any plugin source file
- Plugin works on any machine with openclaw installed globally
2026-03-30 19:55:00 +00:00
87dcd12a65 Merge pull request 'fix: update MACP plugin paths from /home/jarvis to local environment' (#332) from fix/macp-plugin-paths into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 19:47:11 +00:00
Mos (Agent)
d3fdc4ff54 fix: update MACP plugin paths from /home/jarvis to dynamic resolution
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
- plugins/macp/src/index.ts: updated OC SDK imports to local paths
- plugins/macp/src/macp-runtime.ts: DEFAULT_REPO_ROOT → mosaic-stack-new, PI_RUNNER_PATH updated
- plugins/macp/openclaw.plugin.json: default repoRoot description updated
- Removed stale tsconfig.tsbuildinfo with old path references
2026-03-30 19:46:52 +00:00
9690aba0f5 Merge pull request 'feat: monorepo consolidation — forge, MACP, framework plugin, profiles/guides/skills' (#331) from feat/monorepo-consolidation into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 19:44:23 +00:00
Mos (Agent)
10689a30d2 feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
Work packages completed:
- WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier,
  persona loader with project-level overrides. 89 tests, 95.62% coverage.
- WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types.
  65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior.
- WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start +
  subagent_spawning hooks for Mosaic contract enforcement).
- WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs),
  skills/ (5 universal skills), forge pipeline assets (48 markdown files).

Board deliberation: docs/reviews/consolidation-board-memo.md
Brief: briefs/monorepo-consolidation.md

Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack.
154 new tests total. Zero Python — all TypeScript/ESM.
2026-03-30 19:43:24 +00:00
40c068fcbc Merge pull request 'fix(oc-plugin): MACP OC bridge — route through controller queue instead of Pi-direct' (#330) from fix/macp-oc-bridge into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 15:48:12 +00:00
Jarvis
a9340adad7 fix(oc-plugin): replace Pi-direct with MACP controller bridge in runTurn
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-30 10:33:32 -05:00
5cb72e8ca6 Merge pull request 'feat(oc-plugin): MACP ACP runtime backend — sessions_spawn(runtime:macp)' (#329) from feat/oc-macp-plugin-v2 into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-30 04:29:47 +00:00
Jarvis
48323e7d6e chore: update pnpm lockfile for plugins/macp
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-29 23:25:36 -05:00
Jarvis
01259f56cd feat(oc-plugin): add MACP ACP runtime backend
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
2026-03-29 23:21:28 -05:00
743 changed files with 81688 additions and 1467 deletions

View File

@@ -23,8 +23,8 @@ VALKEY_URL=redis://localhost:6380
# ─── Gateway ─────────────────────────────────────────────────────────────────
# TCP port the NestJS/Fastify gateway listens on (default: 4000)
GATEWAY_PORT=4000
# 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.
@@ -37,12 +37,12 @@ GATEWAY_CORS_ORIGIN=http://localhost:3000
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:4000
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:4000
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
@@ -121,12 +121,12 @@ OTEL_SERVICE_NAME=mosaic-gateway
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
# DISCORD_BOT_TOKEN=
# DISCORD_GUILD_ID=
# DISCORD_GATEWAY_URL=http://localhost:4000
# DISCORD_GATEWAY_URL=http://localhost:14242
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_GATEWAY_URL=http://localhost:4000
# TELEGRAM_GATEWAY_URL=http://localhost:14242
# ─── SSO Providers (add credentials to enable) ───────────────────────────────

2
.npmrc
View File

@@ -1 +1 @@
@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/

View File

@@ -15,6 +15,7 @@ steps:
image: *node_image
commands:
- corepack enable
- apk add --no-cache python3 make g++
- pnpm install --frozen-lockfile
typecheck:
@@ -44,18 +45,30 @@ steps:
test:
image: *node_image
environment:
DATABASE_URL: postgresql://mosaic:mosaic@postgres:5432/mosaic
commands:
- *enable_pnpm
# Install postgresql-client for pg_isready
- apk add --no-cache postgresql-client
# Wait up to 30s for postgres to be ready
- |
for i in $(seq 1 30); do
pg_isready -h postgres -p 5432 -U mosaic && break
echo "Waiting for postgres ($i/30)..."
sleep 1
done
# Run migrations (DATABASE_URL is set in environment above)
- pnpm --filter @mosaicstack/db run db:migrate
# Run all tests
- pnpm test
depends_on:
- typecheck
build:
image: *node_image
commands:
- *enable_pnpm
- pnpm build
depends_on:
- lint
- format
- test
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: mosaic
POSTGRES_PASSWORD: mosaic
POSTGRES_DB: mosaic

140
.woodpecker/publish.yml Normal file
View File

@@ -0,0 +1,140 @@
# Build, publish npm packages, and push Docker images
# Runs only on main branch push/tag
variables:
- &node_image 'node:22-alpine'
- &enable_pnpm 'corepack enable'
when:
- branch: [main]
event: [push, manual, tag]
steps:
install:
image: *node_image
commands:
- corepack enable
- pnpm install --frozen-lockfile
build:
image: *node_image
commands:
- *enable_pnpm
- pnpm build
depends_on:
- install
publish-npm:
image: *node_image
environment:
NPM_TOKEN:
from_secret: gitea_token
commands:
- *enable_pnpm
# Configure auth for Gitea npm registry
- |
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
# Publish non-private packages to Gitea.
#
# The only publish failure we tolerate is "version already exists" —
# that legitimately happens when only some packages were bumped in
# the merge. Any other failure (registry 404, auth error, network
# error) MUST fail the pipeline loudly: the previous
# `|| echo "... continuing"` fallback silently hid a 404 from the
# Gitea org rename and caused every @mosaicstack/* publish to fall
# on the floor while CI still reported green.
- |
# Portable sh (Alpine ash) — avoid bashisms like PIPESTATUS.
set +e
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" publish --no-git-checks --access public >/tmp/publish.log 2>&1
EXIT=$?
set -e
cat /tmp/publish.log
if [ "$EXIT" -eq 0 ]; then
echo "[publish] all packages published successfully"
exit 0
fi
# Hard registry / auth / network errors → fatal. Match npm's own
# error lines specifically to avoid false positives on arbitrary
# log text that happens to contain "E404" etc.
if grep -qE "npm (error|ERR!) code (E404|E401|ENEEDAUTH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND)" /tmp/publish.log; then
echo "[publish] FATAL: registry/auth/network error detected — failing pipeline" >&2
exit 1
fi
# Only tolerate the explicit "version already published" case.
# npm returns this as E403 with body "You cannot publish over..."
# or EPUBLISHCONFLICT depending on version.
if grep -qE "EPUBLISHCONFLICT|You cannot publish over|previously published" /tmp/publish.log; then
echo "[publish] some packages already at this version — continuing (non-fatal)"
exit 0
fi
echo "[publish] FATAL: publish failed with unrecognized error — failing pipeline" >&2
exit 1
depends_on:
- build
# TODO: Uncomment when ready to publish to npmjs.org
# publish-npmjs:
# image: *node_image
# environment:
# NPM_TOKEN:
# from_secret: npmjs_token
# commands:
# - *enable_pnpm
# - apk add --no-cache jq bash
# - bash scripts/publish-npmjs.sh
# depends_on:
# - build
# when:
# - event: [tag]
build-gateway:
image: gcr.io/kaniko-project/executor:debug
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:latest"
fi
if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/gateway:$CI_COMMIT_TAG"
fi
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
depends_on:
- build
build-web:
image: gcr.io/kaniko-project/executor:debug
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:sha-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:latest"
fi
if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/mosaic-stack/web:$CI_COMMIT_TAG"
fi
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
depends_on:
- build

View File

@@ -21,11 +21,11 @@ Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo
| `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, @mosaic/db |
| `packages/brain` | Data layer (PG-backed) | @mosaic/db |
| `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 | @mosaic/queue |
| `packages/cli` | Unified CLI + Pi TUI | Ink, Pi SDK |
| `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 |
@@ -33,9 +33,9 @@ Mosaic Stack is a self-hosted, multi-user AI agent platform. TypeScript monorepo
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 `@mosaic/types` enforce compile-time contracts
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 `@mosaic/db`
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)

View File

@@ -10,7 +10,7 @@ Self-hosted, multi-user AI agent platform. TypeScript monorepo.
- **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/cli`)
- **Agent**: Pi SDK (`packages/agent`, `packages/mosaic`)
- **Queue**: Valkey 8 (`packages/queue`)
- **Build**: pnpm workspaces + Turborepo
- **CI**: Woodpecker CI
@@ -26,13 +26,13 @@ pnpm test # Vitest (all packages)
pnpm build # Build all packages
# Database
pnpm --filter @mosaic/db db:push # Push schema to PG (dev)
pnpm --filter @mosaic/db db:generate # Generate migrations
pnpm --filter @mosaic/db db:migrate # Run migrations
pnpm --filter @mosaicstack/db db:push # Push schema to PG (dev)
pnpm --filter @mosaicstack/db db:generate # Generate migrations
pnpm --filter @mosaicstack/db db:migrate # Run migrations
# Dev
docker compose up -d # Start PG, Valkey, OTEL, Jaeger
pnpm --filter @mosaic/gateway exec tsx src/main.ts # Start gateway
pnpm --filter @mosaicstack/gateway exec tsx src/main.ts # Start gateway
```
## Conventions

348
README.md Normal file
View File

@@ -0,0 +1,348 @@
# Mosaic Stack
Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.
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.
## Quick Install
```bash
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh)
```
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
```bash
bash <(curl -fsSL …) --yes # Accept all defaults
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
```
This installs both components:
| 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/` |
After install, the wizard runs automatically or you can invoke it manually:
```bash
mosaic wizard # Full guided setup (gateway install → verify)
```
### Requirements
- Node.js ≥ 20
- 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), or [Pi](https://github.com/mariozechner/pi-coding-agent)
## Usage
### Launching Agent Sessions
```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
```
The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments.
### 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
```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 update # Check for and install CLI updates
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
mosaic forge status
mosaic forge resume
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
mosaic storage migrate
```
### 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 ≥ 20
- pnpm 10.6+
- Docker & Docker Compose
### Setup
```bash
git clone git@git.mosaicstack.dev:mosaicstack/mosaic-stack.git
cd mosaic-stack
# Start infrastructure (Postgres, Valkey, Jaeger)
docker compose up -d
# Install dependencies
pnpm install
# Run migrations
pnpm --filter @mosaicstack/db run db:migrate
# Start all services in dev mode
pnpm dev
```
### Infrastructure
Docker Compose provides:
| Service | Port | Purpose |
| --------------------- | --------- | ---------------------- |
| PostgreSQL (pgvector) | 5433 | Primary database |
| Valkey | 6380 | Task queue + caching |
| Jaeger | 16686 | Distributed tracing UI |
| OTEL Collector | 4317/4318 | Telemetry ingestion |
### Quality Gates
```bash
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`
- Database migration against a fresh Postgres
- `pnpm test` (Turbo-orchestrated across all packages)
npm packages are published to the Gitea package registry on main merges.
## Architecture
```
mosaic-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 (synced from agent-skills repo)
├── 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
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-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 --ref v1.0 # Install from a specific git ref
bash tools/install.sh --yes # Non-interactive, accept all defaults
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
```
## 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.

View File

@@ -1,9 +1,23 @@
{
"name": "@mosaic/gateway",
"version": "0.0.0",
"private": true,
"name": "@mosaicstack/gateway",
"version": "0.0.6",
"repository": {
"type": "git",
"url": "https://git.mosaicstack.dev/mosaicstack/mosaic-stack.git",
"directory": "apps/gateway"
},
"type": "module",
"main": "dist/main.js",
"bin": {
"mosaic-gateway": "dist/main.js"
},
"files": [
"dist"
],
"publishConfig": {
"registry": "https://git.mosaicstack.dev/api/packages/mosaicstack/npm/",
"access": "public"
},
"scripts": {
"build": "tsc",
"dev": "tsx watch src/main.ts",
@@ -14,26 +28,28 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.80.0",
"@fastify/helmet": "^13.0.2",
"@mariozechner/pi-ai": "~0.57.1",
"@mariozechner/pi-coding-agent": "~0.57.1",
"@mariozechner/pi-ai": "^0.65.0",
"@mariozechner/pi-coding-agent": "^0.65.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"@mosaic/auth": "workspace:^",
"@mosaic/brain": "workspace:^",
"@mosaic/coord": "workspace:^",
"@mosaic/db": "workspace:^",
"@mosaic/discord-plugin": "workspace:^",
"@mosaic/log": "workspace:^",
"@mosaic/memory": "workspace:^",
"@mosaic/queue": "workspace:^",
"@mosaic/telegram-plugin": "workspace:^",
"@mosaic/types": "workspace:^",
"@mosaicstack/auth": "workspace:^",
"@mosaicstack/brain": "workspace:^",
"@mosaicstack/config": "workspace:^",
"@mosaicstack/coord": "workspace:^",
"@mosaicstack/db": "workspace:^",
"@mosaicstack/discord-plugin": "workspace:^",
"@mosaicstack/log": "workspace:^",
"@mosaicstack/memory": "workspace:^",
"@mosaicstack/queue": "workspace:^",
"@mosaicstack/storage": "workspace:^",
"@mosaicstack/telegram-plugin": "workspace:^",
"@mosaicstack/types": "workspace:^",
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-fastify": "^11.0.0",
"@nestjs/platform-socket.io": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@nestjs/websockets": "^11.0.0",
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.213.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
"@opentelemetry/resources": "^2.6.0",

View File

@@ -12,7 +12,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import type { ConversationHistoryMessage } from '../agent/agent.service.js';
import { ConversationsController } from '../conversations/conversations.controller.js';
import type { Message } from '@mosaic/brain';
import type { Message } from '@mosaicstack/brain';
// ---------------------------------------------------------------------------
// Shared test data

View File

@@ -18,13 +18,13 @@
*/
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { createDb } from '@mosaic/db';
import { createConversationsRepo } from '@mosaic/brain';
import { createAgentsRepo } from '@mosaic/brain';
import { createPreferencesRepo, createInsightsRepo } from '@mosaic/memory';
import { users, conversations, messages, agents, preferences, insights } from '@mosaic/db';
import { eq } from '@mosaic/db';
import type { DbHandle } from '@mosaic/db';
import { createDb } from '@mosaicstack/db';
import { createConversationsRepo } from '@mosaicstack/brain';
import { createAgentsRepo } from '@mosaicstack/brain';
import { createPreferencesRepo, createInsightsRepo } from '@mosaicstack/memory';
import { users, conversations, messages, agents, preferences, insights } from '@mosaicstack/db';
import { eq } from '@mosaicstack/db';
import type { DbHandle } from '@mosaicstack/db';
// ─── Fixed IDs so the afterAll cleanup is deterministic ──────────────────────

View File

@@ -1,6 +1,6 @@
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
import { sql, type Db } from '@mosaic/db';
import { createQueue } from '@mosaic/queue';
import { sql, type Db } from '@mosaicstack/db';
import { createQueue } from '@mosaicstack/queue';
import { DB } from '../database/database.module.js';
import { AgentService } from '../agent/agent.service.js';
import { ProviderService } from '../agent/provider.service.js';

View File

@@ -0,0 +1,90 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Inject,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { randomBytes, createHash } from 'node:crypto';
import { eq, type Db, adminTokens } from '@mosaicstack/db';
import { v4 as uuid } from 'uuid';
import { DB } from '../database/database.module.js';
import { AdminGuard } from './admin.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import type {
CreateTokenDto,
TokenCreatedDto,
TokenDto,
TokenListDto,
} from './admin-tokens.dto.js';
function hashToken(plaintext: string): string {
return createHash('sha256').update(plaintext).digest('hex');
}
function toTokenDto(row: typeof adminTokens.$inferSelect): TokenDto {
return {
id: row.id,
label: row.label,
scope: row.scope,
expiresAt: row.expiresAt?.toISOString() ?? null,
lastUsedAt: row.lastUsedAt?.toISOString() ?? null,
createdAt: row.createdAt.toISOString(),
};
}
@Controller('api/admin/tokens')
@UseGuards(AdminGuard)
export class AdminTokensController {
constructor(@Inject(DB) private readonly db: Db) {}
@Post()
async create(
@Body() dto: CreateTokenDto,
@CurrentUser() user: { id: string },
): Promise<TokenCreatedDto> {
const plaintext = randomBytes(32).toString('hex');
const tokenHash = hashToken(plaintext);
const id = uuid();
const expiresAt = dto.expiresInDays
? new Date(Date.now() + dto.expiresInDays * 24 * 60 * 60 * 1000)
: null;
const [row] = await this.db
.insert(adminTokens)
.values({
id,
userId: user.id,
tokenHash,
label: dto.label ?? 'CLI token',
scope: dto.scope ?? 'admin',
expiresAt,
})
.returning();
return { ...toTokenDto(row!), plaintext };
}
@Get()
async list(@CurrentUser() user: { id: string }): Promise<TokenListDto> {
const rows = await this.db
.select()
.from(adminTokens)
.where(eq(adminTokens.userId, user.id))
.orderBy(adminTokens.createdAt);
return { tokens: rows.map(toTokenDto), total: rows.length };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async revoke(@Param('id') id: string, @CurrentUser() _user: { id: string }): Promise<void> {
await this.db.delete(adminTokens).where(eq(adminTokens.id, id));
}
}

View File

@@ -0,0 +1,33 @@
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
export class CreateTokenDto {
@IsString()
label!: string;
@IsOptional()
@IsString()
scope?: string;
@IsOptional()
@IsInt()
@Min(1)
expiresInDays?: number;
}
export interface TokenDto {
id: string;
label: string;
scope: string;
expiresAt: string | null;
lastUsedAt: string | null;
createdAt: string;
}
export interface TokenCreatedDto extends TokenDto {
plaintext: string;
}
export interface TokenListDto {
tokens: TokenDto[];
total: number;
}

View File

@@ -13,8 +13,8 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { eq, type Db, users as usersTable } from '@mosaic/db';
import type { Auth } from '@mosaic/auth';
import { eq, type Db, users as usersTable } from '@mosaicstack/db';
import type { Auth } from '@mosaicstack/auth';
import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js';
import { AdminGuard } from './admin.guard.js';

View File

@@ -6,10 +6,11 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { createHash } from 'node:crypto';
import { fromNodeHeaders } from 'better-auth/node';
import type { Auth } from '@mosaic/auth';
import type { Db } from '@mosaic/db';
import { eq, users as usersTable } from '@mosaic/db';
import type { Auth } from '@mosaicstack/auth';
import type { Db } from '@mosaicstack/db';
import { eq, adminTokens, users as usersTable } from '@mosaicstack/db';
import type { FastifyRequest } from 'fastify';
import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js';
@@ -19,6 +20,8 @@ interface UserWithRole {
role?: string;
}
type AuthenticatedRequest = FastifyRequest & { user: unknown; session: unknown };
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
@@ -28,8 +31,64 @@ export class AdminGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const headers = fromNodeHeaders(request.raw.headers);
// Try bearer token auth first
const authHeader = request.raw.headers['authorization'];
if (authHeader?.startsWith('Bearer ')) {
return this.validateBearerToken(request, authHeader.slice(7));
}
// Fall back to BetterAuth session
return this.validateSession(request);
}
private async validateBearerToken(request: FastifyRequest, plaintext: string): Promise<boolean> {
const tokenHash = createHash('sha256').update(plaintext).digest('hex');
const [row] = await this.db
.select({
tokenId: adminTokens.id,
userId: adminTokens.userId,
scope: adminTokens.scope,
expiresAt: adminTokens.expiresAt,
userName: usersTable.name,
userEmail: usersTable.email,
userRole: usersTable.role,
})
.from(adminTokens)
.innerJoin(usersTable, eq(adminTokens.userId, usersTable.id))
.where(eq(adminTokens.tokenHash, tokenHash))
.limit(1);
if (!row) {
throw new UnauthorizedException('Invalid API token');
}
if (row.expiresAt && row.expiresAt < new Date()) {
throw new UnauthorizedException('API token expired');
}
if (row.userRole !== 'admin') {
throw new ForbiddenException('Admin access required');
}
// Update last-used timestamp (fire-and-forget)
this.db
.update(adminTokens)
.set({ lastUsedAt: new Date() })
.where(eq(adminTokens.id, row.tokenId))
.then(() => {})
.catch(() => {});
const req = request as AuthenticatedRequest;
req.user = { id: row.userId, name: row.userName, email: row.userEmail, role: row.userRole };
req.session = { id: `token:${row.tokenId}`, userId: row.userId };
return true;
}
private async validateSession(request: FastifyRequest): Promise<boolean> {
const headers = fromNodeHeaders(request.raw.headers);
const result = await this.auth.api.getSession({ headers });
if (!result) {
@@ -38,8 +97,6 @@ export class AdminGuard implements CanActivate {
const user = result.user as UserWithRole;
// Ensure the role field is populated. better-auth should include additionalFields
// in the session, but as a fallback, fetch the role from the database if needed.
let userRole = user.role;
if (!userRole) {
const [dbUser] = await this.db
@@ -48,7 +105,6 @@ export class AdminGuard implements CanActivate {
.where(eq(usersTable.id, user.id))
.limit(1);
userRole = dbUser?.role ?? 'member';
// Update the session user object with the fetched role
(user as UserWithRole).role = userRole;
}
@@ -56,8 +112,9 @@ export class AdminGuard implements CanActivate {
throw new ForbiddenException('Admin access required');
}
(request as FastifyRequest & { user: unknown; session: unknown }).user = result.user;
(request as FastifyRequest & { user: unknown; session: unknown }).session = result.session;
const req = request as AuthenticatedRequest;
req.user = result.user;
req.session = result.session;
return true;
}

View File

@@ -2,10 +2,18 @@ import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller.js';
import { AdminHealthController } from './admin-health.controller.js';
import { AdminJobsController } from './admin-jobs.controller.js';
import { AdminTokensController } from './admin-tokens.controller.js';
import { BootstrapController } from './bootstrap.controller.js';
import { AdminGuard } from './admin.guard.js';
@Module({
controllers: [AdminController, AdminHealthController, AdminJobsController],
controllers: [
AdminController,
AdminHealthController,
AdminJobsController,
AdminTokensController,
BootstrapController,
],
providers: [AdminGuard],
})
export class AdminModule {}

View File

@@ -0,0 +1,101 @@
import {
Body,
Controller,
ForbiddenException,
Get,
Inject,
InternalServerErrorException,
Post,
} from '@nestjs/common';
import { randomBytes, createHash } from 'node:crypto';
import { count, eq, type Db, users as usersTable, adminTokens } from '@mosaicstack/db';
import type { Auth } from '@mosaicstack/auth';
import { v4 as uuid } from 'uuid';
import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js';
import type { BootstrapSetupDto, BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js';
@Controller('api/bootstrap')
export class BootstrapController {
constructor(
@Inject(AUTH) private readonly auth: Auth,
@Inject(DB) private readonly db: Db,
) {}
@Get('status')
async status(): Promise<BootstrapStatusDto> {
const [result] = await this.db.select({ total: count() }).from(usersTable);
return { needsSetup: (result?.total ?? 0) === 0 };
}
@Post('setup')
async setup(@Body() dto: BootstrapSetupDto): Promise<BootstrapResultDto> {
// Only allow setup when zero users exist
const [result] = await this.db.select({ total: count() }).from(usersTable);
if ((result?.total ?? 0) > 0) {
throw new ForbiddenException('Setup already completed — users exist');
}
// Create admin user via BetterAuth API
const authApi = this.auth.api as unknown as {
createUser: (opts: {
body: { name: string; email: string; password: string; role?: string };
}) => Promise<{
user: { id: string; name: string; email: string };
}>;
};
const created = await authApi.createUser({
body: {
name: dto.name,
email: dto.email,
password: dto.password,
role: 'admin',
},
});
// Verify user was created
const [user] = await this.db
.select()
.from(usersTable)
.where(eq(usersTable.id, created.user.id))
.limit(1);
if (!user) throw new InternalServerErrorException('User created but not found');
// Ensure role is admin (createUser may not set it via BetterAuth)
if (user.role !== 'admin') {
await this.db.update(usersTable).set({ role: 'admin' }).where(eq(usersTable.id, user.id));
}
// Generate admin API token
const plaintext = randomBytes(32).toString('hex');
const tokenHash = createHash('sha256').update(plaintext).digest('hex');
const tokenId = uuid();
const [token] = await this.db
.insert(adminTokens)
.values({
id: tokenId,
userId: user.id,
tokenHash,
label: 'Initial setup token',
scope: 'admin',
})
.returning();
return {
user: {
id: user.id,
name: user.name,
email: user.email,
role: 'admin',
},
token: {
id: token!.id,
plaintext,
label: token!.label,
},
};
}
}

View File

@@ -0,0 +1,31 @@
import { IsString, IsEmail, MinLength } from 'class-validator';
export class BootstrapSetupDto {
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
}
export interface BootstrapStatusDto {
needsSetup: boolean;
}
export interface BootstrapResultDto {
user: {
id: string;
name: string;
email: string;
role: string;
};
token: {
id: string;
plaintext: string;
label: string;
};
}

View File

@@ -62,7 +62,7 @@ function restoreEnv(saved: Map<EnvKey, string | undefined>): void {
}
function makeRegistry(): ModelRegistry {
return new ModelRegistry(AuthStorage.inMemory());
return ModelRegistry.inMemory(AuthStorage.inMemory());
}
// ---------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { RoutingService } from '../routing.service.js';
import type { ModelInfo } from '@mosaic/types';
import type { ModelInfo } from '@mosaicstack/types';
const mockModels: ModelInfo[] = [
{

View File

@@ -7,7 +7,7 @@ import type {
IProviderAdapter,
ModelInfo,
ProviderHealth,
} from '@mosaic/types';
} from '@mosaicstack/types';
/**
* Anthropic provider adapter.

View File

@@ -6,7 +6,7 @@ import type {
IProviderAdapter,
ModelInfo,
ProviderHealth,
} from '@mosaic/types';
} from '@mosaicstack/types';
/** Embedding models that Ollama ships with out of the box */
const OLLAMA_EMBEDDING_MODELS: ReadonlyArray<{

View File

@@ -7,7 +7,7 @@ import type {
IProviderAdapter,
ModelInfo,
ProviderHealth,
} from '@mosaic/types';
} from '@mosaicstack/types';
/**
* OpenAI provider adapter.

View File

@@ -6,7 +6,7 @@ import type {
IProviderAdapter,
ModelInfo,
ProviderHealth,
} from '@mosaic/types';
} from '@mosaicstack/types';
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';

View File

@@ -6,7 +6,7 @@ import type {
IProviderAdapter,
ModelInfo,
ProviderHealth,
} from '@mosaic/types';
} from '@mosaicstack/types';
import { getModelCapability } from '../model-capabilities.js';
/**

View File

@@ -13,7 +13,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -7,8 +7,8 @@ import {
type AgentSessionEvent,
type ToolDefinition,
} from '@mariozechner/pi-coding-agent';
import type { Brain } from '@mosaic/brain';
import type { Memory } from '@mosaic/memory';
import type { Brain } from '@mosaicstack/brain';
import type { Memory } from '@mosaicstack/memory';
import { BRAIN } from '../brain/brain.tokens.js';
import { MEMORY } from '../memory/memory.tokens.js';
import { EmbeddingService } from '../memory/embedding.service.js';
@@ -23,6 +23,7 @@ import { createFileTools } from './tools/file-tools.js';
import { createGitTools } from './tools/git-tools.js';
import { createShellTools } from './tools/shell-tools.js';
import { createWebTools } from './tools/web-tools.js';
import { createSearchTools } from './tools/search-tools.js';
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
import { SystemOverrideService } from '../preferences/system-override.service.js';
import { PreferencesService } from '../preferences/preferences.service.js';
@@ -146,6 +147,7 @@ export class AgentService implements OnModuleDestroy {
...createGitTools(sandboxDir),
...createShellTools(sandboxDir),
...createWebTools(),
...createSearchTools(),
];
}

View File

@@ -1,4 +1,4 @@
import type { ModelCapability } from '@mosaic/types';
import type { ModelCapability } from '@mosaicstack/types';
/**
* Comprehensive capability matrix for all target models.

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
import type { Db } from '@mosaic/db';
import { providerCredentials, eq, and } from '@mosaic/db';
import type { Db } from '@mosaicstack/db';
import { providerCredentials, eq, and } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';

View File

@@ -14,7 +14,7 @@ import type {
ModelInfo,
ProviderHealth,
ProviderInfo,
} from '@mosaic/types';
} from '@mosaicstack/types';
import {
AnthropicAdapter,
OllamaAdapter,
@@ -67,7 +67,7 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
async onModuleInit(): Promise<void> {
const authStorage = AuthStorage.inMemory();
this.registry = new ModelRegistry(authStorage);
this.registry = ModelRegistry.inMemory(authStorage);
// Build the default set of adapters that rely on the registry
this.adapters = [

View File

@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, UseGuards } from '@nestjs/common';
import type { RoutingCriteria } from '@mosaic/types';
import type { RoutingCriteria } from '@mosaicstack/types';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import { ProviderService } from './provider.service.js';

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import type { ModelInfo } from '@mosaic/types';
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaic/types';
import type { ModelInfo } from '@mosaicstack/types';
import type { RoutingCriteria, RoutingResult, CostTier } from '@mosaicstack/types';
import { ProviderService } from './provider.service.js';
/** Per-million-token cost thresholds for tier classification */

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
import { routingRules, type Db, sql } from '@mosaic/db';
import { routingRules, type Db, sql } from '@mosaicstack/db';
import { DB } from '../../database/database.module.js';
import type { RoutingCondition, RoutingAction } from './routing.types.js';

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { routingRules, type Db, and, asc, eq, or } from '@mosaic/db';
import { routingRules, type Db, and, asc, eq, or } from '@mosaicstack/db';
import { DB } from '../../database/database.module.js';
import { ProviderService } from '../provider.service.js';
import { classifyTask } from './task-classifier.js';

View File

@@ -13,7 +13,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaic/db';
import { routingRules, type Db, and, asc, eq, or, inArray } from '@mosaicstack/db';
import { DB } from '../../database/database.module.js';
import { AuthGuard } from '../../auth/auth.guard.js';
import { CurrentUser } from '../../auth/current-user.decorator.js';

View File

@@ -1,7 +1,7 @@
/**
* Routing engine types — M4-002 (condition types) and M4-003 (action types).
*
* These types are re-exported from `@mosaic/types` for shared use across packages.
* These types are re-exported from `@mosaicstack/types` for shared use across packages.
*/
// ─── Classification primitives ───────────────────────────────────────────────
@@ -23,7 +23,7 @@ export type Domain = 'frontend' | 'backend' | 'devops' | 'docs' | 'general';
/**
* Cost tier for model selection.
* Extends the existing `CostTier` in `@mosaic/types` with `local` for self-hosted models.
* Extends the existing `CostTier` in `@mosaicstack/types` with `local` for self-hosted models.
*/
export type CostTier = 'cheap' | 'standard' | 'premium' | 'local';

View File

@@ -1,6 +1,6 @@
import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
export function createBrainTools(brain: Brain): ToolDefinition[] {
const listProjects: ToolDefinition = {

View File

@@ -190,5 +190,169 @@ export function createFileTools(baseDir: string): ToolDefinition[] {
},
};
return [readFileTool, writeFileTool, listDirectoryTool];
const editFileTool: ToolDefinition = {
name: 'fs_edit_file',
label: 'Edit File',
description:
'Make targeted text replacements in a file. Each edit replaces an exact match of oldText with newText. ' +
'All edits are matched against the original file content (not incrementally). ' +
'Each oldText must be unique in the file and edits must not overlap.',
parameters: Type.Object({
path: Type.String({
description: 'File path (relative to sandbox base or absolute within it)',
}),
edits: Type.Array(
Type.Object({
oldText: Type.String({
description: 'Exact text to find and replace (must be unique in the file)',
}),
newText: Type.String({ description: 'Replacement text' }),
}),
{ description: 'One or more targeted replacements', minItems: 1 },
),
}),
async execute(_toolCallId, params) {
const { path, edits } = params as {
path: string;
edits: Array<{ oldText: string; newText: string }>;
};
let safePath: string;
try {
safePath = guardPath(path, baseDir);
} catch (err) {
if (err instanceof SandboxEscapeError) {
return {
content: [{ type: 'text' as const, text: `Error: ${err.message}` }],
details: undefined,
};
}
return {
content: [{ type: 'text' as const, text: `Error: ${String(err)}` }],
details: undefined,
};
}
try {
const info = await stat(safePath);
if (!info.isFile()) {
return {
content: [{ type: 'text' as const, text: `Error: path is not a file: ${path}` }],
details: undefined,
};
}
if (info.size > MAX_READ_BYTES) {
return {
content: [
{
type: 'text' as const,
text: `Error: file too large for editing (${info.size} bytes, limit ${MAX_READ_BYTES} bytes)`,
},
],
details: undefined,
};
}
} catch (err) {
return {
content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
details: undefined,
};
}
let content: string;
try {
content = await readFile(safePath, { encoding: 'utf8' });
} catch (err) {
return {
content: [{ type: 'text' as const, text: `Error reading file: ${String(err)}` }],
details: undefined,
};
}
// Validate all edits before applying any
const errors: string[] = [];
for (let i = 0; i < edits.length; i++) {
const edit = edits[i]!;
const occurrences = content.split(edit.oldText).length - 1;
if (occurrences === 0) {
errors.push(`Edit ${i + 1}: oldText not found in file`);
} else if (occurrences > 1) {
errors.push(`Edit ${i + 1}: oldText matches ${occurrences} locations (must be unique)`);
}
}
// Check for overlapping edits
if (errors.length === 0) {
const positions = edits.map((edit, i) => ({
index: i,
start: content.indexOf(edit.oldText),
end: content.indexOf(edit.oldText) + edit.oldText.length,
}));
positions.sort((a, b) => a.start - b.start);
for (let i = 1; i < positions.length; i++) {
if (positions[i]!.start < positions[i - 1]!.end) {
errors.push(
`Edits ${positions[i - 1]!.index + 1} and ${positions[i]!.index + 1} overlap`,
);
}
}
}
if (errors.length > 0) {
return {
content: [
{
type: 'text' as const,
text: `Edit validation failed:\n${errors.join('\n')}`,
},
],
details: undefined,
};
}
// Apply edits: process from end to start to preserve positions
const positions = edits.map((edit) => ({
edit,
start: content.indexOf(edit.oldText),
}));
positions.sort((a, b) => b.start - a.start); // reverse order
let result = content;
for (const { edit } of positions) {
result = result.replace(edit.oldText, edit.newText);
}
if (Buffer.byteLength(result, 'utf8') > MAX_WRITE_BYTES) {
return {
content: [
{
type: 'text' as const,
text: `Error: resulting file too large (limit ${MAX_WRITE_BYTES} bytes)`,
},
],
details: undefined,
};
}
try {
await writeFile(safePath, result, { encoding: 'utf8' });
return {
content: [
{
type: 'text' as const,
text: `File edited successfully: ${path} (${edits.length} edit(s) applied)`,
},
],
details: undefined,
};
} catch (err) {
return {
content: [{ type: 'text' as const, text: `Error writing file: ${String(err)}` }],
details: undefined,
};
}
},
};
return [readFileTool, writeFileTool, listDirectoryTool, editFileTool];
}

View File

@@ -2,6 +2,7 @@ export { createBrainTools } from './brain-tools.js';
export { createCoordTools } from './coord-tools.js';
export { createFileTools } from './file-tools.js';
export { createGitTools } from './git-tools.js';
export { createSearchTools } from './search-tools.js';
export { createShellTools } from './shell-tools.js';
export { createWebTools } from './web-tools.js';
export { createSkillTools } from './skill-tools.js';

View File

@@ -1,7 +1,7 @@
import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import type { Memory } from '@mosaic/memory';
import type { EmbeddingProvider } from '@mosaic/memory';
import type { Memory } from '@mosaicstack/memory';
import type { EmbeddingProvider } from '@mosaicstack/memory';
/**
* Create memory tools bound to the session's authenticated userId.

View File

@@ -0,0 +1,496 @@
import { Type } from '@sinclair/typebox';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
const DEFAULT_TIMEOUT_MS = 15_000;
const MAX_RESULTS = 10;
const MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
// ─── Provider helpers ────────────────────────────────────────────────────────
interface SearchResult {
title: string;
url: string;
snippet: string;
}
interface SearchResponse {
provider: string;
query: string;
results: SearchResult[];
error?: string;
}
async function fetchWithTimeout(
url: string,
init: RequestInit,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
async function readLimited(response: Response): Promise<string> {
const reader = response.body?.getReader();
if (!reader) return '';
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.length;
if (total > MAX_RESPONSE_BYTES) {
chunks.push(value.subarray(0, MAX_RESPONSE_BYTES - (total - value.length)));
reader.cancel();
break;
}
chunks.push(value);
}
const combined = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0));
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(combined);
}
// ─── Brave Search ────────────────────────────────────────────────────────────
async function searchBrave(query: string, limit: number): Promise<SearchResponse> {
const apiKey = process.env['BRAVE_API_KEY'];
if (!apiKey) return { provider: 'brave', query, results: [], error: 'BRAVE_API_KEY not set' };
try {
const params = new URLSearchParams({
q: query,
count: String(Math.min(limit, 20)),
});
const res = await fetchWithTimeout(
`https://api.search.brave.com/res/v1/web/search?${params}`,
{ headers: { 'X-Subscription-Token': apiKey, Accept: 'application/json' } },
DEFAULT_TIMEOUT_MS,
);
if (!res.ok) {
const body = await res.text().catch(() => '');
return { provider: 'brave', query, results: [], error: `HTTP ${res.status}: ${body}` };
}
const data = (await res.json()) as {
web?: { results?: Array<{ title: string; url: string; description: string }> };
};
const results: SearchResult[] = (data.web?.results ?? []).slice(0, limit).map((r) => ({
title: r.title,
url: r.url,
snippet: r.description,
}));
return { provider: 'brave', query, results };
} catch (err) {
return {
provider: 'brave',
query,
results: [],
error: err instanceof Error ? err.message : String(err),
};
}
}
// ─── Tavily Search ───────────────────────────────────────────────────────────
async function searchTavily(query: string, limit: number): Promise<SearchResponse> {
const apiKey = process.env['TAVILY_API_KEY'];
if (!apiKey) return { provider: 'tavily', query, results: [], error: 'TAVILY_API_KEY not set' };
try {
const res = await fetchWithTimeout(
'https://api.tavily.com/search',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: apiKey,
query,
max_results: Math.min(limit, 10),
include_answer: false,
}),
},
DEFAULT_TIMEOUT_MS,
);
if (!res.ok) {
const body = await res.text().catch(() => '');
return { provider: 'tavily', query, results: [], error: `HTTP ${res.status}: ${body}` };
}
const data = (await res.json()) as {
results?: Array<{ title: string; url: string; content: string }>;
};
const results: SearchResult[] = (data.results ?? []).slice(0, limit).map((r) => ({
title: r.title,
url: r.url,
snippet: r.content,
}));
return { provider: 'tavily', query, results };
} catch (err) {
return {
provider: 'tavily',
query,
results: [],
error: err instanceof Error ? err.message : String(err),
};
}
}
// ─── SearXNG (self-hosted) ───────────────────────────────────────────────────
async function searchSearxng(query: string, limit: number): Promise<SearchResponse> {
const baseUrl = process.env['SEARXNG_URL'];
if (!baseUrl) return { provider: 'searxng', query, results: [], error: 'SEARXNG_URL not set' };
try {
const params = new URLSearchParams({
q: query,
format: 'json',
pageno: '1',
});
const res = await fetchWithTimeout(
`${baseUrl.replace(/\/$/, '')}/search?${params}`,
{ headers: { Accept: 'application/json' } },
DEFAULT_TIMEOUT_MS,
);
if (!res.ok) {
const body = await res.text().catch(() => '');
return { provider: 'searxng', query, results: [], error: `HTTP ${res.status}: ${body}` };
}
const data = (await res.json()) as {
results?: Array<{ title: string; url: string; content: string }>;
};
const results: SearchResult[] = (data.results ?? []).slice(0, limit).map((r) => ({
title: r.title,
url: r.url,
snippet: r.content,
}));
return { provider: 'searxng', query, results };
} catch (err) {
return {
provider: 'searxng',
query,
results: [],
error: err instanceof Error ? err.message : String(err),
};
}
}
// ─── DuckDuckGo (lite HTML endpoint) ─────────────────────────────────────────
async function searchDuckDuckGo(query: string, limit: number): Promise<SearchResponse> {
try {
// Use the DuckDuckGo Instant Answer API (JSON, free, no key)
const params = new URLSearchParams({
q: query,
format: 'json',
no_html: '1',
skip_disambig: '1',
});
const res = await fetchWithTimeout(
`https://api.duckduckgo.com/?${params}`,
{ headers: { Accept: 'application/json' } },
DEFAULT_TIMEOUT_MS,
);
if (!res.ok) {
return {
provider: 'duckduckgo',
query,
results: [],
error: `HTTP ${res.status}`,
};
}
const text = await readLimited(res);
const data = JSON.parse(text) as {
AbstractText?: string;
AbstractURL?: string;
AbstractSource?: string;
RelatedTopics?: Array<{
Text?: string;
FirstURL?: string;
Result?: string;
Topics?: Array<{ Text?: string; FirstURL?: string }>;
}>;
};
const results: SearchResult[] = [];
// Main abstract result
if (data.AbstractText && data.AbstractURL) {
results.push({
title: data.AbstractSource ?? 'DuckDuckGo Abstract',
url: data.AbstractURL,
snippet: data.AbstractText,
});
}
// Related topics
for (const topic of data.RelatedTopics ?? []) {
if (results.length >= limit) break;
if (topic.Text && topic.FirstURL) {
results.push({
title: topic.Text.slice(0, 120),
url: topic.FirstURL,
snippet: topic.Text,
});
}
// Sub-topics
for (const sub of topic.Topics ?? []) {
if (results.length >= limit) break;
if (sub.Text && sub.FirstURL) {
results.push({
title: sub.Text.slice(0, 120),
url: sub.FirstURL,
snippet: sub.Text,
});
}
}
}
return { provider: 'duckduckgo', query, results: results.slice(0, limit) };
} catch (err) {
return {
provider: 'duckduckgo',
query,
results: [],
error: err instanceof Error ? err.message : String(err),
};
}
}
// ─── Provider resolution ─────────────────────────────────────────────────────
type SearchProvider = 'brave' | 'tavily' | 'searxng' | 'duckduckgo' | 'auto';
function getAvailableProviders(): SearchProvider[] {
const available: SearchProvider[] = [];
if (process.env['BRAVE_API_KEY']) available.push('brave');
if (process.env['TAVILY_API_KEY']) available.push('tavily');
if (process.env['SEARXNG_URL']) available.push('searxng');
// DuckDuckGo is always available (no API key needed)
available.push('duckduckgo');
return available;
}
async function executeSearch(
provider: SearchProvider,
query: string,
limit: number,
): Promise<SearchResponse> {
switch (provider) {
case 'brave':
return searchBrave(query, limit);
case 'tavily':
return searchTavily(query, limit);
case 'searxng':
return searchSearxng(query, limit);
case 'duckduckgo':
return searchDuckDuckGo(query, limit);
case 'auto': {
// Try providers in priority order: Brave > Tavily > SearXNG > DuckDuckGo
const available = getAvailableProviders();
for (const p of available) {
const result = await executeSearch(p, query, limit);
if (!result.error && result.results.length > 0) return result;
}
// Fall back to DuckDuckGo if everything failed
return searchDuckDuckGo(query, limit);
}
}
}
function formatSearchResults(response: SearchResponse): string {
const lines: string[] = [];
lines.push(`Search provider: ${response.provider}`);
lines.push(`Query: "${response.query}"`);
if (response.error) {
lines.push(`Error: ${response.error}`);
}
if (response.results.length === 0) {
lines.push('No results found.');
} else {
lines.push(`Results (${response.results.length}):\n`);
for (let i = 0; i < response.results.length; i++) {
const r = response.results[i]!;
lines.push(`${i + 1}. ${r.title}`);
lines.push(` URL: ${r.url}`);
lines.push(` ${r.snippet}`);
lines.push('');
}
}
return lines.join('\n');
}
// ─── Tool exports ────────────────────────────────────────────────────────────
export function createSearchTools(): ToolDefinition[] {
const webSearch: ToolDefinition = {
name: 'web_search',
label: 'Web Search',
description:
'Search the web using configured search providers. ' +
'Supports Brave, Tavily, SearXNG, and DuckDuckGo. ' +
'Use "auto" provider to pick the best available. ' +
'DuckDuckGo is always available as a fallback (no API key needed).',
parameters: Type.Object({
query: Type.String({ description: 'Search query' }),
provider: Type.Optional(
Type.String({
description:
'Search provider: "auto" (default), "brave", "tavily", "searxng", or "duckduckgo"',
}),
),
limit: Type.Optional(
Type.Number({ description: `Max results to return (default 5, max ${MAX_RESULTS})` }),
),
}),
async execute(_toolCallId, params) {
const { query, provider, limit } = params as {
query: string;
provider?: string;
limit?: number;
};
const effectiveProvider = (provider ?? 'auto') as SearchProvider;
const validProviders = ['auto', 'brave', 'tavily', 'searxng', 'duckduckgo'];
if (!validProviders.includes(effectiveProvider)) {
return {
content: [
{
type: 'text' as const,
text: `Invalid provider "${provider}". Valid: ${validProviders.join(', ')}`,
},
],
details: undefined,
};
}
const effectiveLimit = Math.min(Math.max(limit ?? 5, 1), MAX_RESULTS);
try {
const response = await executeSearch(effectiveProvider, query, effectiveLimit);
return {
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
details: undefined,
};
} catch (err) {
return {
content: [
{
type: 'text' as const,
text: `Search failed: ${err instanceof Error ? err.message : String(err)}`,
},
],
details: undefined,
};
}
},
};
const webSearchNews: ToolDefinition = {
name: 'web_search_news',
label: 'Web Search (News)',
description:
'Search for recent news articles. Uses Brave News API if available, falls back to standard search with news keywords.',
parameters: Type.Object({
query: Type.String({ description: 'News search query' }),
limit: Type.Optional(
Type.Number({ description: `Max results (default 5, max ${MAX_RESULTS})` }),
),
}),
async execute(_toolCallId, params) {
const { query, limit } = params as { query: string; limit?: number };
const effectiveLimit = Math.min(Math.max(limit ?? 5, 1), MAX_RESULTS);
// Try Brave News API first (dedicated news endpoint)
const braveKey = process.env['BRAVE_API_KEY'];
if (braveKey) {
try {
const newsParams = new URLSearchParams({
q: query,
count: String(effectiveLimit),
});
const res = await fetchWithTimeout(
`https://api.search.brave.com/res/v1/news/search?${newsParams}`,
{
headers: {
'X-Subscription-Token': braveKey,
Accept: 'application/json',
},
},
DEFAULT_TIMEOUT_MS,
);
if (res.ok) {
const data = (await res.json()) as {
results?: Array<{
title: string;
url: string;
description: string;
age?: string;
}>;
};
const results: SearchResult[] = (data.results ?? [])
.slice(0, effectiveLimit)
.map((r) => ({
title: r.title + (r.age ? ` (${r.age})` : ''),
url: r.url,
snippet: r.description,
}));
const response: SearchResponse = { provider: 'brave-news', query, results };
return {
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
details: undefined,
};
}
} catch {
// Fall through to generic search
}
}
// Fallback: standard search with "news" appended
const newsQuery = `${query} news latest`;
const response = await executeSearch('auto', newsQuery, effectiveLimit);
return {
content: [{ type: 'text' as const, text: formatSearchResults(response) }],
details: undefined,
};
},
};
const searchProviders: ToolDefinition = {
name: 'web_search_providers',
label: 'List Search Providers',
description: 'List the currently available and configured web search providers.',
parameters: Type.Object({}),
async execute() {
const available = getAvailableProviders();
const allProviders = [
{ name: 'brave', configured: !!process.env['BRAVE_API_KEY'], envVar: 'BRAVE_API_KEY' },
{ name: 'tavily', configured: !!process.env['TAVILY_API_KEY'], envVar: 'TAVILY_API_KEY' },
{ name: 'searxng', configured: !!process.env['SEARXNG_URL'], envVar: 'SEARXNG_URL' },
{ name: 'duckduckgo', configured: true, envVar: '(none — always available)' },
];
const lines = ['Search providers:\n'];
for (const p of allProviders) {
const status = p.configured ? '✓ configured' : '✗ not configured';
lines.push(` ${p.name}: ${status} (${p.envVar})`);
}
lines.push(`\nActive providers for "auto" mode: ${available.join(', ')}`);
return {
content: [{ type: 'text' as const, text: lines.join('\n') }],
details: undefined,
};
},
};
return [webSearch, webSearchNews, searchProviders];
}

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { HealthController } from './health/health.controller.js';
import { ConfigModule } from './config/config.module.js';
import { DatabaseModule } from './database/database.module.js';
import { AuthModule } from './auth/auth.module.js';
import { BrainModule } from './brain/brain.module.js';
@@ -28,6 +29,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
ConfigModule,
DatabaseModule,
AuthModule,
BrainModule,

View File

@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { toNodeHandler } from 'better-auth/node';
import type { Auth } from '@mosaic/auth';
import type { Auth } from '@mosaicstack/auth';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import { AUTH } from './auth.tokens.js';

View File

@@ -6,7 +6,7 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { fromNodeHeaders } from 'better-auth/node';
import type { Auth } from '@mosaic/auth';
import type { Auth } from '@mosaicstack/auth';
import type { FastifyRequest } from 'fastify';
import { AUTH } from './auth.tokens.js';

View File

@@ -1,6 +1,6 @@
import { Global, Module } from '@nestjs/common';
import { createAuth, type Auth } from '@mosaic/auth';
import type { Db } from '@mosaic/db';
import { createAuth, type Auth } from '@mosaicstack/auth';
import type { Db } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import { AUTH } from './auth.tokens.js';
import { SsoController } from './sso.controller.js';
@@ -14,7 +14,7 @@ import { SsoController } from './sso.controller.js';
useFactory: (db: Db): Auth =>
createAuth({
db,
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:4000',
baseURL: process.env['BETTER_AUTH_URL'] ?? 'http://localhost:14242',
secret: process.env['BETTER_AUTH_SECRET'],
}),
inject: [DB],

View File

@@ -1,5 +1,5 @@
import { Controller, Get } from '@nestjs/common';
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaic/auth';
import { buildSsoDiscovery, type SsoProviderDiscovery } from '@mosaicstack/auth';
@Controller('api/sso/providers')
export class SsoController {

View File

@@ -1,6 +1,6 @@
import { Global, Module } from '@nestjs/common';
import { createBrain, type Brain } from '@mosaic/brain';
import type { Db } from '@mosaic/db';
import { createBrain, type Brain } from '@mosaicstack/brain';
import type { Db } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import { BRAIN } from './brain.tokens.js';

View File

@@ -11,14 +11,15 @@ import {
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent';
import type { Auth } from '@mosaic/auth';
import type { Brain } from '@mosaic/brain';
import type { Auth } from '@mosaicstack/auth';
import type { Brain } from '@mosaicstack/brain';
import type {
SetThinkingPayload,
SlashCommandPayload,
SystemReloadPayload,
RoutingDecisionInfo,
} from '@mosaic/types';
AbortPayload,
} from '@mosaicstack/types';
import { AgentService, type ConversationHistoryMessage } from '../agent/agent.service.js';
import { AUTH } from '../auth/auth.tokens.js';
import { BRAIN } from '../brain/brain.tokens.js';
@@ -325,6 +326,38 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
});
}
@SubscribeMessage('abort')
async handleAbort(
@ConnectedSocket() client: Socket,
@MessageBody() data: AbortPayload,
): Promise<void> {
const conversationId = data.conversationId;
this.logger.log(`Abort requested by ${client.id} for conversation ${conversationId}`);
const session = this.agentService.getSession(conversationId);
if (!session) {
client.emit('error', {
conversationId,
error: 'No active session to abort.',
});
return;
}
try {
await session.piSession.abort();
this.logger.log(`Agent session ${conversationId} aborted successfully`);
} catch (err) {
this.logger.error(
`Failed to abort session ${conversationId}`,
err instanceof Error ? err.stack : String(err),
);
client.emit('error', {
conversationId,
error: 'Failed to abort the agent operation.',
});
}
}
@SubscribeMessage('command:execute')
async handleCommandExecute(
@ConnectedSocket() client: Socket,

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CommandExecutorService } from './command-executor.service.js';
import type { SlashCommandPayload } from '@mosaic/types';
import type { SlashCommandPayload } from '@mosaicstack/types';
// Minimal mock implementations
const mockRegistry = {
@@ -82,6 +82,7 @@ function buildService(): CommandExecutorService {
mockBrain as never,
null,
mockChatGateway as never,
null,
);
}

View File

@@ -1,12 +1,13 @@
import { forwardRef, Inject, Injectable, Logger, Optional } from '@nestjs/common';
import type { QueueHandle } from '@mosaic/queue';
import type { Brain } from '@mosaic/brain';
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaic/types';
import type { QueueHandle } from '@mosaicstack/queue';
import type { Brain } from '@mosaicstack/brain';
import type { SlashCommandPayload, SlashCommandResultPayload } from '@mosaicstack/types';
import { AgentService } from '../agent/agent.service.js';
import { ChatGateway } from '../chat/chat.gateway.js';
import { SessionGCService } from '../gc/session-gc.service.js';
import { SystemOverrideService } from '../preferences/system-override.service.js';
import { ReloadService } from '../reload/reload.service.js';
import { McpClientService } from '../mcp-client/mcp-client.service.js';
import { BRAIN } from '../brain/brain.tokens.js';
import { COMMANDS_REDIS } from './commands.tokens.js';
import { CommandRegistryService } from './command-registry.service.js';
@@ -28,6 +29,9 @@ export class CommandExecutorService {
@Optional()
@Inject(forwardRef(() => ChatGateway))
private readonly chatGateway: ChatGateway | null,
@Optional()
@Inject(McpClientService)
private readonly mcpClient: McpClientService | null,
) {}
async execute(payload: SlashCommandPayload, userId: string): Promise<SlashCommandResultPayload> {
@@ -105,6 +109,8 @@ export class CommandExecutorService {
};
case 'tools':
return await this.handleTools(conversationId, userId);
case 'mcp':
return await this.handleMcp(args ?? null, conversationId);
case 'reload': {
if (!this.reloadService) {
return {
@@ -489,4 +495,92 @@ export class CommandExecutorService {
conversationId,
};
}
private async handleMcp(
args: string | null,
conversationId: string,
): Promise<SlashCommandResultPayload> {
if (!this.mcpClient) {
return {
command: 'mcp',
conversationId,
success: false,
message: 'MCP client service is not available.',
};
}
const action = args?.trim().split(/\s+/)[0] ?? 'status';
switch (action) {
case 'status':
case 'servers': {
const statuses = this.mcpClient.getServerStatuses();
if (statuses.length === 0) {
return {
command: 'mcp',
conversationId,
success: true,
message:
'No MCP servers configured. Set MCP_SERVERS env var to connect external tool servers.',
};
}
const lines = ['MCP Server Status:\n'];
for (const s of statuses) {
const status = s.connected ? '✓ connected' : '✗ disconnected';
lines.push(` ${s.name}: ${status}`);
lines.push(` URL: ${s.url}`);
lines.push(` Tools: ${s.toolCount}`);
if (s.error) lines.push(` Error: ${s.error}`);
lines.push('');
}
const tools = this.mcpClient.getToolDefinitions();
if (tools.length > 0) {
lines.push(`Total bridged tools: ${tools.length}`);
lines.push(`Tool names: ${tools.map((t) => t.name).join(', ')}`);
}
return {
command: 'mcp',
conversationId,
success: true,
message: lines.join('\n'),
};
}
case 'reconnect': {
const serverName = args?.trim().split(/\s+/).slice(1).join(' ');
if (!serverName) {
return {
command: 'mcp',
conversationId,
success: false,
message: 'Usage: /mcp reconnect <server-name>',
};
}
try {
await this.mcpClient.reconnectServer(serverName);
return {
command: 'mcp',
conversationId,
success: true,
message: `MCP server "${serverName}" reconnected successfully.`,
};
} catch (err) {
return {
command: 'mcp',
conversationId,
success: false,
message: `Failed to reconnect MCP server "${serverName}": ${err instanceof Error ? err.message : String(err)}`,
};
}
}
default:
return {
command: 'mcp',
conversationId,
success: false,
message: `Unknown MCP action: "${action}". Use: /mcp status, /mcp servers, /mcp reconnect <name>`,
};
}
}
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CommandRegistryService } from './command-registry.service.js';
import type { CommandDef } from '@mosaic/types';
import type { CommandDef } from '@mosaicstack/types';
const mockCmd: CommandDef = {
name: 'test',

View File

@@ -1,5 +1,5 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import type { CommandDef, CommandManifest } from '@mosaic/types';
import type { CommandDef, CommandManifest } from '@mosaicstack/types';
@Injectable()
export class CommandRegistryService implements OnModuleInit {
@@ -260,6 +260,23 @@ export class CommandRegistryService implements OnModuleInit {
execution: 'socket',
available: true,
},
{
name: 'mcp',
description: 'Manage MCP server connections (status/reconnect/servers)',
aliases: [],
args: [
{
name: 'action',
type: 'enum',
optional: true,
values: ['status', 'reconnect', 'servers'],
description: 'Action: status (default), reconnect <name>, servers',
},
],
scope: 'agent',
execution: 'socket',
available: true,
},
{
name: 'reload',
description: 'Soft-reload gateway plugins and command manifest (admin)',

View File

@@ -13,7 +13,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CommandRegistryService } from './command-registry.service.js';
import { CommandExecutorService } from './command-executor.service.js';
import type { SlashCommandPayload } from '@mosaic/types';
import type { SlashCommandPayload } from '@mosaicstack/types';
// ─── Mocks ───────────────────────────────────────────────────────────────────
@@ -65,6 +65,7 @@ function buildExecutor(registry: CommandRegistryService): CommandExecutorService
mockBrain as never,
null, // reloadService (optional)
null, // chatGateway (optional)
null, // mcpClient (optional)
);
}

View File

@@ -1,5 +1,5 @@
import { forwardRef, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
import { createQueue, type QueueHandle } from '@mosaic/queue';
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
import { ChatModule } from '../chat/chat.module.js';
import { GCModule } from '../gc/gc.module.js';
import { ReloadModule } from '../reload/reload.module.js';

View File

@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { loadConfig, type MosaicConfig } from '@mosaicstack/config';
export const MOSAIC_CONFIG = 'MOSAIC_CONFIG';
@Global()
@Module({
providers: [
{
provide: MOSAIC_CONFIG,
useFactory: (): MosaicConfig => loadConfig(),
},
],
exports: [MOSAIC_CONFIG],
})
export class ConfigModule {}

View File

@@ -15,7 +15,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -8,7 +8,7 @@ import {
type MissionStatusSummary,
type MissionTask,
type TaskDetail,
} from '@mosaic/coord';
} from '@mosaicstack/coord';
import { promises as fs } from 'node:fs';
import path from 'node:path';

View File

@@ -1,28 +1,51 @@
import { mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
import { createDb, type Db, type DbHandle } from '@mosaic/db';
import { createDb, createPgliteDb, type Db, type DbHandle } from '@mosaicstack/db';
import { createStorageAdapter, type StorageAdapter } from '@mosaicstack/storage';
import type { MosaicConfig } from '@mosaicstack/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
export const DB_HANDLE = 'DB_HANDLE';
export const DB = 'DB';
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
@Global()
@Module({
providers: [
{
provide: DB_HANDLE,
useFactory: (): DbHandle => createDb(),
useFactory: (config: MosaicConfig): DbHandle => {
if (config.tier === 'local') {
const dataDir = join(homedir(), '.config', 'mosaic', 'gateway', 'pglite');
mkdirSync(dataDir, { recursive: true });
return createPgliteDb(dataDir);
}
return createDb(config.storage.type === 'postgres' ? config.storage.url : undefined);
},
inject: [MOSAIC_CONFIG],
},
{
provide: DB,
useFactory: (handle: DbHandle): Db => handle.db,
inject: [DB_HANDLE],
},
{
provide: STORAGE_ADAPTER,
useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
inject: [MOSAIC_CONFIG],
},
],
exports: [DB],
exports: [DB, STORAGE_ADAPTER],
})
export class DatabaseModule implements OnApplicationShutdown {
constructor(@Inject(DB_HANDLE) private readonly handle: DbHandle) {}
constructor(
@Inject(DB_HANDLE) private readonly handle: DbHandle,
@Inject(STORAGE_ADAPTER) private readonly storageAdapter: StorageAdapter,
) {}
async onApplicationShutdown(): Promise<void> {
await this.handle.close();
await Promise.all([this.handle.close(), this.storageAdapter.close()]);
}
}

View File

@@ -1,5 +1,5 @@
import { Module, type OnApplicationShutdown, Inject } from '@nestjs/common';
import { createQueue, type QueueHandle } from '@mosaic/queue';
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
import { SessionGCService } from './session-gc.service.js';
import { REDIS } from './gc.tokens.js';

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Logger } from '@nestjs/common';
import type { QueueHandle } from '@mosaic/queue';
import type { LogService } from '@mosaic/log';
import type { QueueHandle } from '@mosaicstack/queue';
import type { LogService } from '@mosaicstack/log';
import { SessionGCService } from './session-gc.service.js';
type MockRedis = {

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common';
import type { QueueHandle } from '@mosaic/queue';
import type { LogService } from '@mosaic/log';
import type { QueueHandle } from '@mosaicstack/queue';
import type { LogService } from '@mosaicstack/log';
import { LOG_SERVICE } from '../log/log.tokens.js';
import { REDIS } from './gc.tokens.js';

View File

@@ -1,5 +1,5 @@
import { Body, Controller, Get, Inject, Param, Post, Query, UseGuards } from '@nestjs/common';
import type { LogService } from '@mosaic/log';
import type { LogService } from '@mosaicstack/log';
import { LOG_SERVICE } from './log.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';

View File

@@ -1,6 +1,6 @@
import { Global, Module } from '@nestjs/common';
import { createLogService, type LogService } from '@mosaic/log';
import type { Db } from '@mosaic/db';
import { createLogService, type LogService } from '@mosaicstack/log';
import type { Db } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
import { LOG_SERVICE } from './log.tokens.js';
import { LogController } from './log.controller.js';

View File

@@ -1,11 +1,11 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import type { LogService } from '@mosaic/log';
import type { Memory } from '@mosaic/memory';
import type { LogService } from '@mosaicstack/log';
import type { Memory } from '@mosaicstack/memory';
import { LOG_SERVICE } from './log.tokens.js';
import { MEMORY } from '../memory/memory.tokens.js';
import { EmbeddingService } from '../memory/embedding.service.js';
import type { Db } from '@mosaic/db';
import { sql, summarizationJobs } from '@mosaic/db';
import type { Db } from '@mosaicstack/db';
import { sql, summarizationJobs } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
const SUMMARIZATION_PROMPT = `You are a knowledge extraction assistant. Given the following agent interaction logs, extract the key decisions, learnings, and patterns. Output a concise summary (2-4 sentences) that captures the most important information for future reference. Focus on actionable insights, not raw events.

View File

@@ -1,5 +1,13 @@
#!/usr/bin/env node
import { config } from 'dotenv';
import { resolve } from 'node:path';
import { existsSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { homedir } from 'node:os';
// Load .env from daemon config dir (global install / daemon mode).
// Loaded first so monorepo .env can override for local dev.
const daemonEnv = join(homedir(), '.config', 'mosaic', 'gateway', '.env');
if (existsSync(daemonEnv)) config({ path: daemonEnv });
// Load .env from monorepo root (cwd is apps/gateway when run via pnpm filter)
config({ path: resolve(process.cwd(), '../../.env') });
@@ -11,7 +19,7 @@ import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
import helmet from '@fastify/helmet';
import { listSsoStartupWarnings } from '@mosaic/auth';
import { listSsoStartupWarnings } from '@mosaicstack/auth';
import { AppModule } from './app.module.js';
import { mountAuthHandler } from './auth/auth.controller.js';
import { mountMcpHandler } from './mcp/mcp.controller.js';
@@ -51,7 +59,7 @@ async function bootstrap(): Promise<void> {
mountAuthHandler(app);
mountMcpHandler(app, app.get(McpService));
const port = Number(process.env['GATEWAY_PORT'] ?? 4000);
const port = Number(process.env['GATEWAY_PORT'] ?? 14242);
await app.listen(port, '0.0.0.0');
logger.log(`Gateway listening on port ${port}`);
}

View File

@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { Logger } from '@nestjs/common';
import { fromNodeHeaders } from 'better-auth/node';
import type { Auth } from '@mosaic/auth';
import type { Auth } from '@mosaicstack/auth';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import type { McpService } from './mcp.service.js';
import { AUTH } from '../auth/auth.tokens.js';

View File

@@ -3,8 +3,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import type { Brain } from '@mosaic/brain';
import type { Memory } from '@mosaic/memory';
import type { Brain } from '@mosaicstack/brain';
import type { Memory } from '@mosaicstack/memory';
import { BRAIN } from '../brain/brain.tokens.js';
import { MEMORY } from '../memory/memory.tokens.js';
import { EmbeddingService } from '../memory/embedding.service.js';

View File

@@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import type { EmbeddingProvider } from '@mosaic/memory';
import type { EmbeddingProvider } from '@mosaicstack/memory';
// ---------------------------------------------------------------------------
// Environment-driven configuration

View File

@@ -12,7 +12,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import type { Memory } from '@mosaic/memory';
import type { Memory } from '@mosaicstack/memory';
import { MEMORY } from './memory.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -1,11 +1,29 @@
import { Global, Module } from '@nestjs/common';
import { createMemory, type Memory } from '@mosaic/memory';
import type { Db } from '@mosaic/db';
import { DB } from '../database/database.module.js';
import {
createMemory,
type Memory,
createMemoryAdapter,
type MemoryAdapter,
type MemoryConfig,
} from '@mosaicstack/memory';
import type { Db } from '@mosaicstack/db';
import type { StorageAdapter } from '@mosaicstack/storage';
import type { MosaicConfig } from '@mosaicstack/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
import { DB, STORAGE_ADAPTER } from '../database/database.module.js';
import { MEMORY } from './memory.tokens.js';
import { MemoryController } from './memory.controller.js';
import { EmbeddingService } from './embedding.service.js';
export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter): MemoryConfig {
if (config.memory.type === 'keyword') {
return { type: 'keyword', storage: storageAdapter };
}
return { type: config.memory.type };
}
@Global()
@Module({
providers: [
@@ -14,9 +32,15 @@ import { EmbeddingService } from './embedding.service.js';
useFactory: (db: Db): Memory => createMemory(db),
inject: [DB],
},
{
provide: MEMORY_ADAPTER,
useFactory: (config: MosaicConfig, storageAdapter: StorageAdapter): MemoryAdapter =>
createMemoryAdapter(buildMemoryConfig(config, storageAdapter)),
inject: [MOSAIC_CONFIG, STORAGE_ADAPTER],
},
EmbeddingService,
],
controllers: [MemoryController],
exports: [MEMORY, EmbeddingService],
exports: [MEMORY, MEMORY_ADAPTER, EmbeddingService],
})
export class MemoryModule {}

View File

@@ -12,7 +12,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -6,8 +6,8 @@ import {
type OnModuleDestroy,
type OnModuleInit,
} from '@nestjs/common';
import { DiscordPlugin } from '@mosaic/discord-plugin';
import { TelegramPlugin } from '@mosaic/telegram-plugin';
import { DiscordPlugin } from '@mosaicstack/discord-plugin';
import { TelegramPlugin } from '@mosaicstack/telegram-plugin';
import { PluginService } from './plugin.service.js';
import type { IChannelPlugin } from './plugin.interface.js';
import { PLUGIN_REGISTRY } from './plugin.tokens.js';
@@ -48,7 +48,7 @@ class TelegramChannelPluginAdapter implements IChannelPlugin {
}
}
const DEFAULT_GATEWAY_URL = 'http://localhost:4000';
const DEFAULT_GATEWAY_URL = 'http://localhost:14242';
function createPluginRegistry(): IChannelPlugin[] {
const plugins: IChannelPlugin[] = [];

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { PreferencesService, PLATFORM_DEFAULTS, IMMUTABLE_KEYS } from './preferences.service.js';
import type { Db } from '@mosaic/db';
import type { Db } from '@mosaicstack/db';
/**
* Build a mock Drizzle DB where the select chain supports:

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaic/db';
import { eq, and, sql, type Db, preferences as preferencesTable } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
export const PLATFORM_DEFAULTS: Record<string, unknown> = {

View File

@@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { createQueue, type QueueHandle } from '@mosaic/queue';
import { createQueue, type QueueHandle } from '@mosaicstack/queue';
const SESSION_SYSTEM_KEY = (sessionId: string) => `mosaic:session:${sessionId}:system`;
const SESSION_SYSTEM_FRAGMENTS_KEY = (sessionId: string) =>

View File

@@ -13,7 +13,7 @@ import {
Post,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -1,9 +1,21 @@
import { Global, Module } from '@nestjs/common';
import { createQueueAdapter, type QueueAdapter } from '@mosaicstack/queue';
import type { MosaicConfig } from '@mosaicstack/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';
import { QueueService } from './queue.service.js';
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
@Global()
@Module({
providers: [QueueService],
exports: [QueueService],
providers: [
QueueService,
{
provide: QUEUE_ADAPTER,
useFactory: (config: MosaicConfig): QueueAdapter => createQueueAdapter(config.queue),
inject: [MOSAIC_CONFIG],
},
],
exports: [QueueService, QUEUE_ADAPTER],
})
export class QueueModule {}

View File

@@ -7,7 +7,7 @@ import {
type OnModuleDestroy,
} from '@nestjs/common';
import { Queue, Worker, type Job, type ConnectionOptions } from 'bullmq';
import type { LogService } from '@mosaic/log';
import type { LogService } from '@mosaicstack/log';
import { LOG_SERVICE } from '../log/log.tokens.js';
import type { JobDto, JobStatus } from './queue-admin.dto.js';
@@ -51,16 +51,42 @@ export interface QueueHealthStatus {
// Constants
// ---------------------------------------------------------------------------
export const QUEUE_SUMMARIZATION = 'mosaic:summarization';
export const QUEUE_GC = 'mosaic:gc';
export const QUEUE_TIER_MANAGEMENT = 'mosaic:tier-management';
export const QUEUE_SUMMARIZATION = 'mosaic-summarization';
export const QUEUE_GC = 'mosaic-gc';
export const QUEUE_TIER_MANAGEMENT = 'mosaic-tier-management';
const DEFAULT_VALKEY_URL = 'redis://localhost:6380';
/**
* Parse a Redis URL string into a BullMQ-compatible ConnectionOptions object.
*
* BullMQ v5 does `Object.assign({ port: 6379, host: '127.0.0.1' }, opts)` in
* its RedisConnection constructor. If opts is a URL string, Object.assign only
* copies character-index properties and the defaults survive — so 6379 wins.
* We must parse the URL ourselves and return a plain RedisOptions object.
*/
function getConnection(): ConnectionOptions {
const url = process.env['VALKEY_URL'] ?? DEFAULT_VALKEY_URL;
// BullMQ ConnectionOptions accepts a URL string (ioredis-compatible)
return url as unknown as ConnectionOptions;
try {
const parsed = new URL(url);
const opts: ConnectionOptions = {
host: parsed.hostname || '127.0.0.1',
port: parsed.port ? parseInt(parsed.port, 10) : 6380,
};
if (parsed.password) {
(opts as Record<string, unknown>)['password'] = decodeURIComponent(parsed.password);
}
if (parsed.pathname && parsed.pathname.length > 1) {
const db = parseInt(parsed.pathname.slice(1), 10);
if (!isNaN(db)) {
(opts as Record<string, unknown>)['db'] = db;
}
}
return opts;
} catch {
// Fallback: hope the value is already a host string ioredis understands
return { host: '127.0.0.1', port: 6380 } as ConnectionOptions;
}
}
// ---------------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
import { Controller, HttpCode, HttpStatus, Inject, Post, UseGuards } from '@nestjs/common';
import type { SystemReloadPayload } from '@mosaic/types';
import type { SystemReloadPayload } from '@mosaicstack/types';
import { AdminGuard } from '../admin/admin.guard.js';
import { ChatGateway } from '../chat/chat.gateway.js';
import { ReloadService } from './reload.service.js';

View File

@@ -5,7 +5,7 @@ import {
type OnApplicationBootstrap,
type OnApplicationShutdown,
} from '@nestjs/common';
import type { SystemReloadPayload } from '@mosaic/types';
import type { SystemReloadPayload } from '@mosaicstack/types';
import { CommandRegistryService } from '../commands/command-registry.service.js';
import { isMosaicPlugin } from './mosaic-plugin.interface.js';

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { eq, type Db, skills } from '@mosaic/db';
import { eq, type Db, skills } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
type Skill = typeof skills.$inferSelect;

View File

@@ -14,7 +14,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import type { Brain } from '@mosaic/brain';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { PluginService } from '../plugin/plugin.service.js';
import { WorkspaceService } from './workspace.service.js';

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaic/db';
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
import { DB } from '../database/database.module.js';
@Injectable()

View File

@@ -4,15 +4,15 @@
"rootDir": "../..",
"baseUrl": ".",
"paths": {
"@mosaic/auth": ["../../packages/auth/src/index.ts"],
"@mosaic/brain": ["../../packages/brain/src/index.ts"],
"@mosaic/coord": ["../../packages/coord/src/index.ts"],
"@mosaic/db": ["../../packages/db/src/index.ts"],
"@mosaic/log": ["../../packages/log/src/index.ts"],
"@mosaic/memory": ["../../packages/memory/src/index.ts"],
"@mosaic/types": ["../../packages/types/src/index.ts"],
"@mosaic/discord-plugin": ["../../plugins/discord/src/index.ts"],
"@mosaic/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
"@mosaicstack/auth": ["../../packages/auth/src/index.ts"],
"@mosaicstack/brain": ["../../packages/brain/src/index.ts"],
"@mosaicstack/coord": ["../../packages/coord/src/index.ts"],
"@mosaicstack/db": ["../../packages/db/src/index.ts"],
"@mosaicstack/log": ["../../packages/log/src/index.ts"],
"@mosaicstack/memory": ["../../packages/memory/src/index.ts"],
"@mosaicstack/types": ["../../packages/types/src/index.ts"],
"@mosaicstack/discord-plugin": ["../../plugins/discord/src/index.ts"],
"@mosaicstack/telegram-plugin": ["../../plugins/telegram/src/index.ts"]
}
}
}

View File

@@ -2,7 +2,7 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
transpilePackages: ['@mosaic/design-tokens'],
transpilePackages: ['@mosaicstack/design-tokens'],
// Enable gzip/brotli compression for all responses.
compress: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@mosaic/web",
"version": "0.0.0",
"name": "@mosaicstack/web",
"version": "0.0.2",
"private": true,
"scripts": {
"build": "next build",
@@ -12,7 +12,7 @@
"start": "next start"
},
"dependencies": {
"@mosaic/design-tokens": "workspace:^",
"@mosaicstack/design-tokens": "workspace:^",
"better-auth": "^1.5.5",
"clsx": "^2.1.0",
"next": "^16.0.0",

View File

@@ -5,9 +5,9 @@ import { defineConfig, devices } from '@playwright/test';
*
* Assumes:
* - Next.js web app running on http://localhost:3000
* - NestJS gateway running on http://localhost:4000
* - NestJS gateway running on http://localhost:14242
*
* Run with: pnpm --filter @mosaic/web test:e2e
* Run with: pnpm --filter @mosaicstack/web test:e2e
*/
export default defineConfig({
testDir: './e2e',

0
apps/web/public/.gitkeep Normal file
View File

View File

@@ -1,4 +1,4 @@
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
export interface ApiRequestInit extends Omit<RequestInit, 'body'> {
body?: unknown;

View File

@@ -2,7 +2,7 @@ import { createAuthClient } from 'better-auth/react';
import { adminClient, genericOAuthClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000',
baseURL: process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242',
plugins: [adminClient(), genericOAuthClient()],
});

View File

@@ -1,6 +1,6 @@
import { io, type Socket } from 'socket.io-client';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:4000';
const GATEWAY_URL = process.env['NEXT_PUBLIC_GATEWAY_URL'] ?? 'http://localhost:14242';
let socket: Socket | null = null;

View File

@@ -0,0 +1,231 @@
# Brief: Monorepo Consolidation — mosaic/stack → mosaic/mosaic-stack
## Source
Architecture consolidation — merge the mosaic/stack repo (Forge pipeline, MACP protocol, framework tools) into mosaic/mosaic-stack (Harness Foundation platform). Two repos doing related work that need to converge.
## Context
**mosaic/stack** (OLD) contains:
- Forge progressive refinement pipeline (stages, agents, personas, rails, debate protocol, brief classification)
- MACP protocol (JSON schemas, deterministic Python controller, dispatcher, event system, gate runner)
- Credential resolver (Python — OC config, mosaic files, ambient env, JSON5 parser)
- OC framework plugin (injects Mosaic rails into all agent sessions)
- Profiles (runtime-neutral context packs for tech stacks and domains)
- Stage adapter (Forge→MACP bridge)
- Board tasks (multi-agent board evaluation)
- OpenBrain specialist memory (learning capture/recall)
- 17 guides, 5 universal skills
**mosaic/mosaic-stack** (NEW) contains:
- Harness Foundation platform (NestJS gateway, Next.js web, Drizzle ORM, Pi SDK runtime)
- 5 provider adapters, task classifier, routing rules, model capability matrix
- MACP OC plugin (ACP runtime backend with Pi bridge)
- TS coord package (mission runner, tasks file manager, status tracker — 1635 lines)
- BullMQ job queue, OTEL telemetry, channel plugins (Discord, Telegram)
- CLI with TUI, 65/65 tasks done, v0.2.0
**Decision:** NEW repo is the base. All unique work from OLD gets ported into NEW as packages.
## Scope
### Work Package 1: Forge Pipeline Package (`packages/forge`)
Port the entire Forge progressive refinement pipeline as a TypeScript package.
**From OLD:**
- `forge/pipeline/stages/*.md` — 11 stage definitions
- `forge/pipeline/agents/{board,generalists,specialists,cross-cutting}/*.md` — all persona definitions
- `forge/pipeline/rails/*.md` — debate protocol, dynamic composition, worker rails
- `forge/pipeline/gates/` — gate reviewer definitions
- `forge/pipeline/orchestrator/run-structure.md` — file-based observability spec
- `forge/templates/` — brief and PRD templates
- `forge/pipeline/orchestrator/board_tasks.py` → rewrite in TS
- `forge/pipeline/orchestrator/stage_adapter.py` → rewrite in TS
- `forge/pipeline/orchestrator/pipeline_runner.py` → rewrite in TS
- `forge/forge` CLI (Python) → rewrite in TS, integrate with `packages/cli`
**Package structure:**
```
packages/forge/
├── src/
│ ├── index.ts # Public API
│ ├── pipeline-runner.ts # Orchestrates full pipeline run
│ ├── stage-adapter.ts # Maps stages to MACP/coord tasks
│ ├── board-tasks.ts # Multi-agent board evaluation task generator
│ ├── brief-classifier.ts # strategic/technical/hotfix classification
│ ├── types.ts # Stage specs, run manifest, gate results
│ └── constants.ts # Stage sequence, timeouts, labels
├── pipeline/
│ ├── stages/ # .md stage definitions (copied)
│ ├── agents/ # .md persona definitions (copied)
│ │ ├── board/
│ │ ├── cross-cutting/
│ │ ├── generalists/
│ │ └── specialists/
│ │ ├── language/
│ │ └── domain/
│ ├── rails/ # .md rails (copied)
│ ├── gates/ # .md gate definitions (copied)
│ └── templates/ # brief + PRD templates (copied)
└── package.json
```
**Key design decisions:**
- Pipeline markdown assets are runtime data, not compiled — ship as-is in the package
- `pipeline-runner.ts` calls into `packages/coord` for task execution (not a separate controller)
- Stage adapter generates coord-compatible tasks, not MACP JSON directly
- Board tasks use `depends_on_policy: "all_terminal"` for synthesis
- Per-stage timeouts from `STAGE_TIMEOUTS` map
- Brief classifier supports CLI flag, YAML frontmatter, and keyword auto-detection
- Run output goes to project-scoped `.forge/runs/{run-id}/` (not inside the Forge package)
**Persona override system (new):**
- Base personas ship with the package (read-only)
- Project-level overrides in `.forge/personas/{role}.md` extend (not replace) base personas
- Board composition configurable via `.forge/config.yaml`:
```yaml
board:
additional_members:
- compliance-officer.md
skip_members: []
specialists:
always_include:
- proxmox-expert
```
- OpenBrain integration for cross-run specialist memory (when enabled)
### Work Package 2: MACP Protocol Package (`packages/macp`)
Port the MACP protocol layer, event system, and gate runner as a TypeScript package.
**From OLD:**
- `tools/macp/protocol/task.schema.json` — task JSON schema
- `tools/macp/protocol/` — event schemas
- `tools/macp/controller/gate_runner.py` → rewrite in TS as `gate-runner.ts`
- `tools/macp/events/` — event watcher, webhook adapter, Discord formatter → rewrite in TS
- `tools/macp/dispatcher/credential_resolver.py` → rewrite in TS as `credential-resolver.ts`
- `tools/macp/memory/learning_capture.py` + `learning_recall.py` → rewrite in TS
**Package structure:**
```
packages/macp/
├── src/
│ ├── index.ts # Public API
│ ├── types.ts # Task, event, result, gate types
│ ├── schemas/ # JSON schemas (copied)
│ ├── gate-runner.ts # Mechanical + AI review quality gates
│ ├── credential-resolver.ts # Provider credential resolution (mosaic files, OC config, ambient)
│ ├── event-emitter.ts # Append events to ndjson, structured event types
│ ├── event-watcher.ts # Poll events.ndjson with cursor persistence
│ ├── webhook-adapter.ts # POST events to configurable URL
│ ├── discord-formatter.ts # Human-readable event messages
│ └── learning.ts # OpenBrain capture + recall
└── package.json
```
**Integration with existing packages:**
- `packages/coord` uses `packages/macp` for event emission, gate running, and credential resolution
- `plugins/macp` uses `packages/macp` for protocol types and credential resolution
- `packages/forge` uses `packages/macp` gate types for stage gates
### Work Package 3: OC Framework Plugin (`plugins/mosaic-framework`)
Port the OC framework plugin that injects Mosaic rails into all agent sessions.
**From OLD:**
- `oc-plugins/mosaic-framework/index.ts` — `before_agent_start` + `subagent_spawning` hooks
- `oc-plugins/mosaic-framework/openclaw.plugin.json`
**Structure:**
```
plugins/mosaic-framework/
├── src/
│ └── index.ts # Plugin hooks
└── package.json
```
**This is separate from `plugins/macp`:**
- `mosaic-framework` = injects Mosaic rails/contracts into every OC session (passive enforcement)
- `macp` = provides an ACP runtime backend for MACP task execution (active runtime)
### Work Package 4: Profiles + Guides + Skills
Port reference content as a documentation/config package or top-level directories.
**From OLD:**
- `profiles/domains/*.json` — HIPAA, fintech, crypto context packs
- `profiles/tech-stacks/*.json` — NestJS, Next.js, FastAPI, React conventions
- `profiles/workflows/*.json` — API development, frontend component, testing workflows
- `guides/*.md` — 17 guides (auth, backend, QA, orchestrator, PRD, etc.)
- `skills-universal/` — jarvis, macp, mosaic-standards, prd, setup-cicd skills
**Destination:**
```
profiles/ # Top-level (same as OLD)
guides/ # Top-level (same as OLD)
skills/ # Top-level (renamed from skills-universal)
```
These are runtime-neutral assets consumed by any agent or profile loader — they don't belong in a compiled package.
## Out of Scope
- Rewriting the NestJS orchestrator app from OLD (`apps/orchestrator/`) — its functionality is subsumed by `packages/coord` + `apps/gateway`
- Porting the FastAPI coordinator from OLD (`apps/coordinator/`) — its functionality (webhook receiver, issue parser, quality orchestrator) is handled by `packages/coord` + `apps/gateway` in the new architecture
- Porting the Prisma schema or OLD's `apps/api` — Drizzle migration is complete
- Old Docker Compose configs (Traefik, Matrix, OpenBao) — NEW has its own infra setup
## Success Criteria
1. `packages/forge` exists with all 11 stage definitions, all persona markdowns, all rails, and TS implementations of pipeline-runner, stage-adapter, board-tasks, and brief-classifier
2. `packages/macp` exists with gate-runner, credential-resolver, event system, and learning capture/recall — all in TypeScript
3. `plugins/mosaic-framework` exists and registers OC hooks for rails injection
4. Profiles, guides, and skills are present at top-level
5. `packages/forge` integrates with `packages/coord` for task execution
6. `packages/macp` credential-resolver is used by `plugins/macp` Pi bridge
7. All existing tests pass (no regressions)
8. New packages have test coverage ≥85%
9. `pnpm lint && pnpm typecheck && pnpm build` passes
10. `.forge/runs/` project-scoped output directory works for at least one test run
## Technical Constraints
- All new code is ESM with NodeNext module resolution
- No Python in the new repo — everything rewrites to TypeScript
- Pipeline markdown assets (stages, personas, rails) are shipped as package data, not compiled
- Credential resolver must support: mosaic credential files, OC config (JSON5), ambient environment — same resolution order as the Python version
- Must preserve `depends_on_policy` semantics (all, any, all_terminal)
- Per-stage timeouts must be preserved
- JSON5 stripping must use the placeholder-extraction approach (not naive regex on string content)
## Estimated Complexity
High — crosses 4 work packages with protocol porting, TS rewrites, and integration wiring. Each work package is independently shippable.
**Suggested execution order:**
1. WP4 (profiles/guides/skills) — pure copy, no code, fast win
2. WP2 (packages/macp) — protocol foundation, needed by WP1 and WP3
3. WP1 (packages/forge) — the big one, depends on WP2
4. WP3 (plugins/mosaic-framework) — OC integration, can parallel with WP1
## Dependencies
- `packages/coord` must be stable (it is — WP1 integrates with it)
- `plugins/macp` must be stable (it is — WP2 provides types/credentials to it)
- Pi SDK (`@mariozechner/pi-agent-core`) already in the dependency tree

View File

@@ -1,70 +1,57 @@
# Mission Manifest — Harness Foundation
# Mission Manifest — Install UX Hardening
> Persistent document tracking full mission scope, status, and session history.
> Updated by the orchestrator at each phase transition and milestone completion.
## Mission
**ID:** harness-20260321
**Statement:** Transform Mosaic Stack from a functional demo into a real multi-provider, task-routing AI harness. Persist all conversations, integrate frontier LLM providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama), build granular task-aware agent routing, harden agent sessions, replace cron with BullMQ, and design the channel protocol for future Matrix/remote integration.
**Phase:** Complete
**Current Milestone:** All milestones done
**Progress:** 7 / 7 milestones
**Status:** complete
**Last Updated:** 2026-03-22 UTC
**ID:** install-ux-hardening-20260405
**Statement:** Close the remaining gaps in the Mosaic Stack first-run and teardown experience uncovered by the post-`cli-unification` audit. A user MUST be able to cleanly uninstall the stack; the wizard MUST make security-sensitive surfaces visible (hooks, password entry); and CI/headless installs MUST NOT hang on interactive prompts. The longer-term goal is a single cohesive first-run flow that collapses `mosaic wizard` and `mosaic gateway install` into one state-bridged experience.
**Phase:** Execution
**Current Milestone:** IUH-M03
**Progress:** 2 / 3 milestones
**Status:** active
**Last Updated:** 2026-04-05
**Parent Mission:** [cli-unification-20260404](./archive/missions/cli-unification-20260404/MISSION-MANIFEST.md) (complete)
## Context
Post-merge audit of `cli-unification-20260404` (AC-1, AC-6) validated that the first-run wizard covers first user, password, admin tokens, gateway instance config, skills, and SOUL.md/USER.md init. The audit surfaced six gaps, grouped into three tracks of independent value.
## Success Criteria
- [x] AC-1: Send messages in TUI → restart TUI → resume conversation → agent has full history and context
- [x] AC-2: Route a coding task to Claude Opus 4.6, a simple question to Haiku, a summarization to GLM-5 — all via granular routing rules
- [x] AC-3: Two users exist, User A's memory searches never return User B's data
- [x] AC-4: `/model claude-sonnet-4-6` in TUI switches the active model for subsequent messages
- [x] AC-5: `/agent coding-agent` in TUI switches to a different agent with different system prompt and tools
- [x] AC-6: BullMQ jobs execute on schedule, failures retry with backoff, admin can inspect via `/api/admin/jobs`
- [x] AC-7: Channel protocol document exists with Matrix integration points defined, reviewed, and approved
- [x] AC-8: Embeddings run on Ollama local models (no external API dependency for vector operations)
- [x] AC-9: All five providers (Anthropic, OpenAI, OpenRouter, Z.ai, Ollama) connect, list models, and complete chat requests
- [x] AC-10: Routing transparency — TUI displays which model was selected and the routing reason for each response
- [x] AC-1: `mosaic uninstall` (top-level) cleanly reverses every mutation made by `tools/install.sh` — framework data, npm CLI, nested stack deps, runtime asset injections in `~/.claude/`, npmrc scope mapping, PATH edits. Dry-run supported. `--keep-data` preserves memory + user files + gateway DB. (PR #429)
- [x] AC-2: `curl … | bash -s -- --uninstall` works without requiring a functioning CLI. (PR #429)
- [x] AC-3: Password entry in `bootstrapFirstUser` is masked (no plaintext echo); confirm prompt added. (PR #431)
- [x] AC-4: Wizard has an explicit hooks stage that previews which hooks will be installed, asks for confirmation, and records the user's choice. `mosaic config hooks list|enable|disable` surface exists. (PR #431 — consent recorded in `state.hooks.accepted`; finalize-stage gating is a follow-up)
- [x] AC-5: `runConfigWizard` and `bootstrapFirstUser` accept a headless path (env vars + `--yes`) so `tools/install.sh --yes` + `MOSAIC_ASSUME_YES=1` completes end-to-end in CI without TTY. (PR #431)
- [ ] AC-6: `mosaic wizard` and `mosaic gateway install` are collapsed into a single cohesive entry point with shared state (no two-phase handoff via the 10-minute session file).
- [ ] AC-7: All milestones ship as merged PRs with green CI, closed issues, updated release notes.
## Milestones
| # | ID | Name | Status | Branch | Issue | Started | Completed |
| --- | ------ | ---------------------------------- | ------ | ------ | --------- | ---------- | ---------- |
| 1 | ms-166 | Conversation Persistence & Context | done | — | #224#231 | 2026-03-21 | 2026-03-21 |
| 2 | ms-167 | Security & Isolation | done | — | #232#239 | 2026-03-21 | 2026-03-21 |
| 3 | ms-168 | Provider Integration | done | — | #240#251 | 2026-03-21 | 2026-03-22 |
| 4 | ms-169 | Agent Routing Engine | done | — | #252#264 | 2026-03-22 | 2026-03-22 |
| 5 | ms-170 | Agent Session Hardening | done | — | #265#272 | 2026-03-22 | 2026-03-22 |
| 6 | ms-171 | Job Queue Foundation | done | — | #273#280 | 2026-03-22 | 2026-03-22 |
| 7 | ms-172 | Channel Protocol Design | done | — | #281#288 | 2026-03-22 | 2026-03-22 |
| # | ID | Name | Status | Branch | Issue | Started | Completed |
| --- | ------- | --------------------------------------------------------- | ----------- | ----------------------- | ----- | ---------- | ---------- |
| 1 | IUH-M01 | `mosaic uninstall` — top-level teardown + shell wrapper | done | feat/mosaic-uninstall | #425 | 2026-04-05 | 2026-04-05 |
| 2 | IUH-M02 | Wizard remediation — hooks visibility, pwd mask, headless | done | feat/wizard-remediation | #426 | 2026-04-05 | 2026-04-05 |
| 3 | IUH-M03 | Unified first-run wizard (collapse wizard + gateway) | in-progress | feat/unified-first-run | #427 | 2026-04-05 | — |
## Deployment
## Subagent Delegation Plan
| Target | URL | Method |
| -------------------- | --------- | -------------------------- |
| Docker Compose (dev) | localhost | docker compose up |
| Production | TBD | Docker Swarm via Portainer |
| Milestone | Recommended Tier | Rationale |
| --------- | ---------------- | ---------------------------------------------------------------------- |
| IUH-M01 | sonnet | Standard feature work — new command surface mirroring existing install |
| IUH-M02 | sonnet | Small surgical fixes across 3-4 files |
| IUH-M03 | opus | Architectural refactor; state machine design decisions |
## Coordination
## Risks
- **Primary Agent:** claude-opus-4-6
- **Sibling Agents:** sonnet (workers), haiku (verification)
- **Shared Contracts:** docs/PRD-Harness_Foundation.md, docs/TASKS.md
- **Reversal completeness** — runtime asset linking creates `.mosaic-bak-*` backups; uninstall must honor them vs. when to delete. Ambiguity without an install manifest.
- **npm global nested deps** — `npm uninstall -g @mosaicstack/mosaic` removes nested `@mosaicstack/*`, but ownership conflicts with explicitly installed peer packages (`@mosaicstack/gateway`, `@mosaicstack/memory`) need test coverage.
- **Headless bootstrap** — admin password via env var is a credential on disk; needs clear documentation that `MOSAIC_ADMIN_PASSWORD` is intended for CI-only and should be rotated post-install.
## Token Budget
## Out of Scope
| Metric | Value |
| ------ | ------ |
| Budget | — |
| Used | ~2.5M |
| Mode | normal |
## Session History
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
| ------- | --------------- | ---------- | -------- | ------------ | ----------------- |
| 1 | claude-opus-4-6 | 2026-03-21 | ~6h | complete | M7-008 — all done |
## Scratchpad
Path: `docs/scratchpads/harness-20260321.md`
- `mosaicstack.dev/install.sh` vanity URL (blocked on marketing site work)
- Uninstall for the `@mosaicstack/gateway` database contents — delegated to `mosaic gateway uninstall` semantics already in place
- Signature/checksum verification of install scripts

View File

@@ -153,7 +153,7 @@ for any `<Image>` components added in the future.
```bash
# Run the DB migration (requires a live DB)
pnpm --filter @mosaic/db exec drizzle-kit migrate
pnpm --filter @mosaicstack/db exec drizzle-kit migrate
# Or, in Docker/Swarm — migrations run automatically on gateway startup
# via runMigrations() in packages/db/src/migrate.ts

View File

@@ -57,7 +57,7 @@ Multi-panel layout with keyboard navigation.
- **Ink 5** (React for CLI) — already in deps
- **Component architecture** — break monolithic `app.tsx` into composable components
- **Typed Socket.IO events** — leverage `@mosaic/types` `ServerToClientEvents` / `ClientToServerEvents`
- **Typed Socket.IO events** — leverage `@mosaicstack/types` `ServerToClientEvents` / `ClientToServerEvents`
- **Local state only** (Wave 1) — cwd/branch read from `process.cwd()` and `git` at startup
- **Gateway metadata** (future) — extend socket handshake or add REST endpoint for model info, token usage

View File

@@ -8,7 +8,7 @@
- **Best-Guess Mode:** true
- Repo (target): `git.mosaicstack.dev/mosaic/mosaic-stack`
- Baseline: `~/src/jarvis-old` (jarvis v0.2.0)
- Package source: `~/src/mosaic-mono-v0` (@mosaic/\* packages)
- Package source: `~/src/mosaic-mono-v0` (@mosaicstack/\* packages)
- Agent harness: [pi](https://github.com/badlogic/pi-mono) (v0.57.1)
- Remote control reference: [OpenClaw](https://github.com/openclaw/openclaw) (upstream, canonical)
@@ -16,7 +16,7 @@
## Problem Statement
Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and Next.js frontend. It handles chat, projects, tasks, and LLM routing but lacks orchestration depth, agent coordination, shared memory, and remote access. The Mosaic framework (`~/.config/mosaic`) provides agent guides, shell-based orchestration tools, and quality rails — but these are loose scripts, not an integrated platform. The `@mosaic/*` packages in mosaic-mono-v0 began consolidating these into TypeScript packages (brain, queue, coord, cli, prdy, quality-rails) but have no UI, no auth, and no agent runtime integration.
Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and Next.js frontend. It handles chat, projects, tasks, and LLM routing but lacks orchestration depth, agent coordination, shared memory, and remote access. The Mosaic framework (`~/.config/mosaic`) provides agent guides, shell-based orchestration tools, and quality rails — but these are loose scripts, not an integrated platform. The `@mosaicstack/*` packages in mosaic-mono-v0 began consolidating these into TypeScript packages (brain, queue, coord, cli, prdy, quality-rails) but have no UI, no auth, and no agent runtime integration.
**The gap:** Three codebases with overlapping concerns, no unified runtime, no remote control surface (Discord/Telegram), no gateway orchestrator, and a Python backend that doesn't align with the target TypeScript-everywhere stack.
@@ -32,7 +32,7 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
4. **Gateway orchestrator** — Central routing layer that dispatches tasks to appropriate agents based on capability, cost, and context
5. **Shared memory** — PostgreSQL canonical store + vector DB for semantic search + tiered log summarization to prevent context creep
6. **Multi-user with SSO** — BetterAuth with Authentik/WorkOS/Keycloak SSO, RBAC for family/team/business use
7. **Full @mosaic/\* package integration** — brain, queue, coord, mosaic, prdy, quality-rails, cli all integrated
7. **Full @mosaicstack/\* package integration** — brain, queue, coord, mosaic, prdy, quality-rails, cli all integrated
8. **Extensible** — MCP capability, skill import interface, plugin architecture for LLM providers and remote channels
---
@@ -44,7 +44,7 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
1. Chat/conversation UI (web) — carry forward from jarvis-old, rewrite frontend to work with new backend
2. Pi TUI integration — terminal-based agent interaction using Pi SDK
3. Web dashboard — settings, task management, projects, PRDs, missions, agent status
4. Gateway orchestrator (`@mosaic/gateway`) — central dispatch for agent tasks with routing logic
4. Gateway orchestrator (`@mosaicstack/gateway`) — central dispatch for agent tasks with routing logic
5. Task management — CRUD, kanban, mission-scoped tasks, dependency tracking
6. Project management — projects, milestones, PRDs linked to missions
7. Shared memory system — learned preferences, behaviors, defaults; tiered storage with summarization
@@ -55,13 +55,13 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
12. Agent routing — task-based model/provider selection (cost/capability matrix)
13. MCP capability — server and client, tool registration
14. Skill import interface — browse, install, manage agent skills
15. `@mosaic/brain` — structured data layer (migrated to PG + vector DB backend)
16. `@mosaic/queue` — Valkey-backed task queue with MCP tools
17. `@mosaic/coord` — mission coordination engine
18. `@mosaic/mosaic` — install wizard / bootstrap
19. `@mosaic/prdy` — PRD wizard
20. `@mosaic/quality-rails` — code quality scaffolder
21. `@mosaic/cli` — unified `mosaic` CLI
15. `@mosaicstack/brain` — structured data layer (migrated to PG + vector DB backend)
16. `@mosaicstack/queue` — Valkey-backed task queue with MCP tools
17. `@mosaicstack/coord` — mission coordination engine
18. `@mosaicstack/mosaic` — install wizard / bootstrap
19. `@mosaicstack/prdy` — PRD wizard
20. `@mosaicstack/quality-rails` — code quality scaffolder
21. `@mosaicstack/cli` — unified `mosaic` CLI
22. Docker Compose deployment + bare-metal capability
23. Agent log service — ingest, parse, tier, summarize agent interaction logs
@@ -94,14 +94,14 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
│ └──────────────┴───────┬───────┴────────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ @mosaic/gateway │ ← Central Orchestrator│
│ │ @mosaicstack/gateway │ ← Central Orchestrator│
│ │ (NestJS+Fastify) │ │
│ └────┬────┬────┬─────┘ │
│ │ │ │ │
│ ┌──────────────┤ │ ├──────────────┐ │
│ │ │ │ │ │ │
│ ┌───────▼──────┐ ┌────▼────▼──┐ │ ┌───────────▼────────┐ │
│ │ @mosaic/brain│ │ @mosaic/ │ │ │ Agent Pool │ │
│ │ @mosaicstack/brain│ │ @mosaicstack/ │ │ │ Agent Pool │ │
│ │ (Data Layer) │ │ queue │ │ │ (Pi SDK sessions) │ │
│ └───────┬──────┘ └────────────┘ │ │ - Anthropic │ │
│ │ │ │ - Codex │ │
@@ -111,12 +111,12 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
│ └──────────────┴───────────┘ │ │ - llama.cpp │ │
│ │ └────────────────────┘ │
│ ┌─────────────▼──────┐ │
│ │ @mosaic/coord │ │
│ │ @mosaicstack/coord │ │
│ │ Mission lifecycle │ │
│ └────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ @mosaic/cli │ │ @mosaic/prdy │ │ @mosaic/ │ │
│ │ @mosaicstack/cli │ │ @mosaicstack/prdy │ │ @mosaicstack/ │ │
│ │ │ │ │ │ quality-rails │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
@@ -130,20 +130,20 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
| Layer | Technology | Rationale |
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaic/design-tokens (mosaic-stack-website) |
| **Web Frontend** | Next.js 16 + React 19 + Tailwind CSS | SSR, RSC; design tokens from @mosaicstack/design-tokens (mosaic-stack-website) |
| **API / Gateway** | NestJS + Fastify adapter | Module system, DI, guards/interceptors for complex gateway; Fastify performance underneath |
| **Agent Runtime** | Pi SDK (embedded) | Extensible harness with tools, skills, session management |
| **TUI** | Pi interactive mode | Native terminal agent interaction |
| **Auth** | BetterAuth + SSO adapters | Multi-user RBAC with Authentik/WorkOS/Keycloak |
| **Database** | PostgreSQL 17 + pgvector | Canonical store; pgvector for embedding search |
| **Vector DB** | pgvector + VectorStore interface | pgvector for v0.1.0; `VectorStore` abstraction in @mosaic/memory makes Qdrant a drop-in later |
| **Cache / Queue** | Valkey 8 | Redis-compatible; proven in @mosaic/queue |
| **Vector DB** | pgvector + VectorStore interface | pgvector for v0.1.0; `VectorStore` abstraction in @mosaicstack/memory makes Qdrant a drop-in later |
| **Cache / Queue** | Valkey 8 | Redis-compatible; proven in @mosaicstack/queue |
| **ORM** | Drizzle ORM | TypeScript-native, lightweight, good migration story |
| **Validation** | Zod | Already used across @mosaic/\* packages |
| **Validation** | Zod | Already used across @mosaicstack/\* packages |
| **Build** | pnpm workspaces + Turborepo | Proven in both jarvis-old and mosaic-mono-v0 |
| **Testing** | Vitest + Playwright | Unit/integration via Vitest, E2E via Playwright |
| **Remote Control** | Discord.js + Telegraf | Inspired by OpenClaw plugin architecture |
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaic/brain and @mosaic/queue |
| **MCP** | @modelcontextprotocol/sdk | Already used in @mosaicstack/brain and @mosaicstack/queue |
| **Container** | Docker Compose | Self-hosted; bare-metal also supported |
| **CI** | Woodpecker CI | Existing infrastructure at git.mosaicstack.dev |
| **Observability** | OpenTelemetry + SigNoz | Wide-event logging from day one; OTEL auto-instrumentation for NestJS/PG/HTTP; SigNoz as all-in-one backend |
@@ -158,12 +158,12 @@ The jarvis-old FastAPI backend is not carried forward as code. Its domain logic
Instead of a custom LLM provider abstraction (jarvis-old's `BaseLLMProvider`), Pi SDK manages agent sessions. Pi handles model selection, tool calling, context management, and compaction. The gateway dispatches work to Pi sessions configured with appropriate providers.
**AD-3: Gateway as the central nervous system (NestJS + Fastify adapter)**
`@mosaic/gateway` is the single API surface. The web app, TUI, Discord, and Telegram all talk to the gateway. The gateway routes to brain (data), queue (coordination), agent pool (LLM work), and coord (mission lifecycle). This replaces the direct FastAPI-to-DB pattern from jarvis-old.
`@mosaicstack/gateway` is the single API surface. The web app, TUI, Discord, and Telegram all talk to the gateway. The gateway routes to brain (data), queue (coordination), agent pool (LLM work), and coord (mission lifecycle). This replaces the direct FastAPI-to-DB pattern from jarvis-old.
NestJS was chosen over raw Fastify because the gateway is inherently complex — it hosts channel plugins, agent pool management, routing engine, WebSocket hub, MCP server, auth middleware, and integrates brain, queue, memory, and log services. NestJS provides the module system, dependency injection, guards, and interceptors needed to organize this cleanly. NestJS uses Fastify as its HTTP adapter, so Fastify's performance is preserved. This also aligns with the stated stack preference in USER.md ("NestJS API + Next.js web"). @mosaic/brain's existing Fastify code migrates naturally into a NestJS module with Fastify adapter.
NestJS was chosen over raw Fastify because the gateway is inherently complex — it hosts channel plugins, agent pool management, routing engine, WebSocket hub, MCP server, auth middleware, and integrates brain, queue, memory, and log services. NestJS provides the module system, dependency injection, guards, and interceptors needed to organize this cleanly. NestJS uses Fastify as its HTTP adapter, so Fastify's performance is preserved. This also aligns with the stated stack preference in USER.md ("NestJS API + Next.js web"). @mosaicstack/brain's existing Fastify code migrates naturally into a NestJS module with Fastify adapter.
**AD-4: Brain migrates from JSON files to PostgreSQL**
`@mosaic/brain` currently uses a JSON file store. For Mosaic Stack, brain's data model (tasks, projects, events, agents, missions, tickets) moves to PostgreSQL via Drizzle ORM. Brain's REST + MCP interface is preserved — only the storage backend changes.
`@mosaicstack/brain` currently uses a JSON file store. For Mosaic Stack, brain's data model (tasks, projects, events, agents, missions, tickets) moves to PostgreSQL via Drizzle ORM. Brain's REST + MCP interface is preserved — only the storage backend changes.
**AD-5: Tiered memory with summarization**
Agent interaction logs are ingested into a log service. Raw logs are stored short-term. A summarization pipeline (using a cheap LLM) periodically compresses logs into structured insights stored in the vector DB. This prevents unbounded log growth while preserving searchable context.
@@ -189,8 +189,8 @@ The gateway includes a cron scheduler for recurring tasks: log summarization run
**AD-12: Web search tool (DuckDuckGo MCP)**
Agent sessions include a web search tool for information retrieval. DuckDuckGo via MCP server is the primary option (privacy-respecting, no API key required). Falls back to other search MCP providers if configured. Registered as a standard MCP tool available to all agent sessions.
**AD-13: Design system from @mosaic/design-tokens**
The web dashboard uses the Mosaic Stack design system established in `mosaic-stack-website`. The `@mosaic/design-tokens` package provides CSS custom properties, Tailwind preset, and TS color/font/radius exports. Dark theme default with light theme support. Fonts: Outfit (sans), Fira Code (mono). Color palette: deep blue-grays with blue/purple/teal accents.
**AD-13: Design system from @mosaicstack/design-tokens**
The web dashboard uses the Mosaic Stack design system established in `mosaic-stack-website`. The `@mosaicstack/design-tokens` package provides CSS custom properties, Tailwind preset, and TS color/font/radius exports. Dark theme default with light theme support. Fonts: Outfit (sans), Fira Code (mono). Color palette: deep blue-grays with blue/purple/teal accents.
**AD-14: Multi-tier deployment readiness**
Code is structured assuming eventual multi-node deployment with dedicated roles (gateway nodes, agent worker nodes, brain/DB nodes). Packages communicate via well-defined APIs (HTTP/WS/MCP), not in-process calls where avoidable. Service boundaries are clean: gateway is stateless (state in PG/Valkey), agent pool can scale independently, brain is a separate service. v0.1.0 runs single-node; the architecture doesn't fight horizontal scaling later.
@@ -205,25 +205,25 @@ Code is structured assuming eventual multi-node deployment with dedicated roles
mosaic-mono-v1/
├── apps/
│ ├── web/ Next.js 16 web dashboard
│ └── gateway/ @mosaic/gateway — NestJS API + WebSocket
│ └── gateway/ @mosaicstack/gateway — NestJS API + WebSocket
├── packages/
│ ├── types/ @mosaic/types — shared type contracts
│ ├── brain/ @mosaic/brain — data layer (PG-backed)
│ ├── queue/ @mosaic/queue — Valkey task queue + MCP
│ ├── coord/ @mosaic/coord — mission coordination
│ ├── mosaic/ @mosaic/mosaic — install wizard
│ ├── prdy/ @mosaic/prdy — PRD wizard
│ ├── quality-rails/ @mosaic/quality-rails — code quality scaffolder
│ ├── cli/ @mosaic/cli — unified CLI
│ ├── auth/ @mosaic/auth — BetterAuth config + SSO adapters
│ ├── db/ @mosaic/db — Drizzle schema, migrations, connection
│ ├── agent/ @mosaic/agent — Pi SDK integration, agent pool manager
│ ├── memory/ @mosaic/memory — tiered memory + summarization service
│ ├── log/ @mosaic/log — agent log ingest + processing
│ └── design-tokens/ @mosaic/design-tokens — CSS vars, Tailwind preset, colors
│ ├── types/ @mosaicstack/types — shared type contracts
│ ├── brain/ @mosaicstack/brain — data layer (PG-backed)
│ ├── queue/ @mosaicstack/queue — Valkey task queue + MCP
│ ├── coord/ @mosaicstack/coord — mission coordination
│ ├── mosaic/ @mosaicstack/mosaic — install wizard
│ ├── prdy/ @mosaicstack/prdy — PRD wizard
│ ├── quality-rails/ @mosaicstack/quality-rails — code quality scaffolder
│ ├── cli/ @mosaicstack/cli — unified CLI
│ ├── auth/ @mosaicstack/auth — BetterAuth config + SSO adapters
│ ├── db/ @mosaicstack/db — Drizzle schema, migrations, connection
│ ├── agent/ @mosaicstack/agent — Pi SDK integration, agent pool manager
│ ├── memory/ @mosaicstack/memory — tiered memory + summarization service
│ ├── log/ @mosaicstack/log — agent log ingest + processing
│ └── design-tokens/ @mosaicstack/design-tokens — CSS vars, Tailwind preset, colors
├── plugins/
│ ├── discord/ @mosaic/discord-plugin — Discord channel
│ └── telegram/ @mosaic/telegram-plugin — Telegram channel
│ ├── discord/ @mosaicstack/discord-plugin — Discord channel
│ └── telegram/ @mosaicstack/telegram-plugin — Telegram channel
├── docker/
│ ├── gateway.Dockerfile
│ ├── web.Dockerfile
@@ -244,7 +244,7 @@ mosaic-mono-v1/
### Package Responsibilities
#### `apps/gateway` — @mosaic/gateway (NEW — critical path)
#### `apps/gateway` — @mosaicstack/gateway (NEW — critical path)
The central nervous system. All clients connect here. Built with NestJS (Fastify adapter).
@@ -303,7 +303,7 @@ Carried forward from jarvis-old with significant refactoring.
- User management (admin RBAC panel)
- Auth pages (login, SSO redirect, registration)
#### `packages/types` — @mosaic/types
#### `packages/types` — @mosaicstack/types
Migrated from mosaic-mono-v0. Extended with:
@@ -313,7 +313,7 @@ Migrated from mosaic-mono-v0. Extended with:
- Memory types (preference, insight, summary)
- Plugin channel types (Discord, Telegram message mapping)
#### `packages/brain` — @mosaic/brain
#### `packages/brain` — @mosaicstack/brain
Migrated from mosaic-mono-v0. **Storage backend changes from JSON to PostgreSQL.**
@@ -324,7 +324,7 @@ Migrated from mosaic-mono-v0. **Storage backend changes from JSON to PostgreSQL.
- New: computed endpoints (today, stale, stats, search, audit) run against PG
- New: appreciation collection preserved for family use
#### `packages/queue` — @mosaic/queue
#### `packages/queue` — @mosaicstack/queue
Migrated from mosaic-mono-v0 with minimal changes.
@@ -332,7 +332,7 @@ Migrated from mosaic-mono-v0 with minimal changes.
- MCP server with 8 tools
- Used by gateway for agent task dispatch and coordination
#### `packages/coord` — @mosaic/coord
#### `packages/coord` — @mosaicstack/coord
Migrated from mosaic-mono-v0.
@@ -342,7 +342,7 @@ Migrated from mosaic-mono-v0.
- Continuation prompt generation
- Integration with gateway for mission-driven orchestration
#### `packages/db` — @mosaic/db (NEW)
#### `packages/db` — @mosaicstack/db (NEW)
Shared database package.
@@ -351,7 +351,7 @@ Shared database package.
- Connection pool configuration
- Shared by gateway, brain, auth, memory
#### `packages/auth` — @mosaic/auth (NEW)
#### `packages/auth` — @mosaicstack/auth (NEW)
Authentication and authorization.
@@ -361,7 +361,7 @@ Authentication and authorization.
- API key generation for brain/MCP access
- Session management middleware
#### `packages/agent` — @mosaic/agent (NEW — critical path)
#### `packages/agent` — @mosaicstack/agent (NEW — critical path)
Pi SDK integration layer.
@@ -372,7 +372,7 @@ Pi SDK integration layer.
- Skill management — loads and configures Pi skills for agent sessions
- Session lifecycle — create, monitor, complete, fail, timeout
#### `packages/memory` — @mosaic/memory (NEW)
#### `packages/memory` — @mosaicstack/memory (NEW)
Tiered memory system.
@@ -382,7 +382,7 @@ Tiered memory system.
- Summarization pipeline — compress raw logs into structured insights
- Memory API — used by gateway and agent sessions
#### `packages/log` — @mosaic/log (NEW)
#### `packages/log` — @mosaicstack/log (NEW)
Agent log service.
@@ -392,7 +392,7 @@ Agent log service.
- Summarization trigger — invokes cheap LLM to compress aging logs
- Retention policy — configurable TTLs per tier
#### `packages/mosaic` — @mosaic/mosaic
#### `packages/mosaic` — @mosaicstack/mosaic
Migrated from mosaic-mono-v0, updated for v1.
@@ -400,7 +400,7 @@ Migrated from mosaic-mono-v0, updated for v1.
- Detects existing installations, offers upgrade path
- Configures `~/.config/mosaic/` with guides, tools, runtime configs
#### `packages/prdy` — @mosaic/prdy
#### `packages/prdy` — @mosaicstack/prdy
Migrated from mosaic-mono-v0.
@@ -408,7 +408,7 @@ Migrated from mosaic-mono-v0.
- Template-based PRD creation with Zod validation
- CLI integration via `mosaic prdy`
#### `packages/quality-rails` — @mosaic/quality-rails
#### `packages/quality-rails` — @mosaicstack/quality-rails
Migrated from mosaic-mono-v0.
@@ -416,15 +416,15 @@ Migrated from mosaic-mono-v0.
- Generates ESLint, tsconfig, Woodpecker, husky, lint-staged configs
- Supports project types: monorepo, typescript-node, nextjs
#### `packages/cli` — @mosaic/cli
#### `packages/cli` — @mosaicstack/cli
Migrated from mosaic-mono-v0, extended.
- Unified `mosaic` binary
- Subcommands: `mosaic coord`, `mosaic prdy`, `mosaic queue`, `mosaic quality`, `mosaic gateway`, `mosaic brain`
- Plugin discovery for installed @mosaic/\* packages
- Plugin discovery for installed @mosaicstack/\* packages
#### `plugins/discord` — @mosaic/discord-plugin (NEW — high priority)
#### `plugins/discord` — @mosaicstack/discord-plugin (NEW — high priority)
Discord remote control channel. Architecture inspired by OpenClaw (https://github.com/openclaw/openclaw).
@@ -436,7 +436,7 @@ Discord remote control channel. Architecture inspired by OpenClaw (https://githu
- Bot pairing and permission management (Discord user → Mosaic user mapping)
- DM support for private conversations
#### `plugins/telegram` — @mosaic/telegram-plugin (NEW)
#### `plugins/telegram` — @mosaicstack/telegram-plugin (NEW)
Telegram remote control channel.
@@ -547,7 +547,7 @@ Telegram remote control channel.
- WebSocket hub — real-time updates for chat, agent status, notifications
- Rate limiting and request validation
### FR-3: Agent Pool (@mosaic/agent)
### FR-3: Agent Pool (@mosaicstack/agent)
- Manage concurrent Pi SDK sessions
- Provider configuration: API key management, endpoint URLs, model lists
@@ -582,7 +582,7 @@ Telegram remote control channel.
- Mission CRUD (linked to project and PRD)
- Mission tasks with phases, dependencies, ordering
- Mission summary with computed progress
- Mission coordination via @mosaic/coord
- Mission coordination via @mosaicstack/coord
- Active mission dashboard in web UI
### FR-7: Memory System
@@ -844,7 +844,7 @@ Telegram remote control channel.
- [ ] Database migrations run automatically on first start
- [ ] `.env.example` documents all required configuration
### AC-11: @mosaic/\* Packages
### AC-11: @mosaicstack/\* Packages
- [ ] All 7 migrated packages build, pass tests, and integrate with gateway
- [ ] `mosaic` CLI provides subcommands for each package
@@ -870,7 +870,7 @@ Telegram remote control channel.
| Risk | Likelihood | Impact | Mitigation |
| -------------------------------------------------- | ---------- | ------ | ---------------------------------------------------------------------------------------- |
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaic/agent interface |
| Pi SDK API instability (pre-1.0) | Medium | High | Pin version, abstract behind @mosaicstack/agent interface |
| Brain PG migration complexity | Medium | Medium | Preserve Brain REST/MCP API contract; only storage changes |
| Discord plugin complexity (OpenClaw has ~60 files) | Medium | Medium | Start minimal (DM + mention in channel), single-guild only; expand iteratively post-beta |
| LLM provider subscription auth varies by provider | Medium | Medium | Abstract behind provider interface; implement per-provider adapters |
@@ -882,7 +882,7 @@ Telegram remote control channel.
| # | Question | Priority | Status |
| --- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1 | Pi SDK version to pin for v0.1.0? | High | ✅ Resolved — Pin `@mariozechner/pi-coding-agent@~0.57.1` (current stable). Abstract behind `@mosaic/agent` interface to insulate from breaking changes. Bump deliberately after testing. |
| 1 | Pi SDK version to pin for v0.1.0? | High | ✅ Resolved — Pin `@mariozechner/pi-coding-agent@~0.57.1` (current stable). Abstract behind `@mosaicstack/agent` interface to insulate from breaking changes. Bump deliberately after testing. |
| 2 | Authentik vs WorkOS vs Keycloak — which SSO provider to implement first? | Medium | ✅ Resolved — Authentik first (already in Jason's infrastructure) |
| 3 | Vector DB: pgvector sufficient or need Qdrant from the start? | Medium | ✅ Resolved — pgvector with VectorStore interface abstraction. Qdrant drops in later if needed. |
| 4 | Summarization LLM: which model for log compression? | Medium | ✅ Resolved — Haiku-tier default with structured output guardrails, configurable via routing engine. |
@@ -910,9 +910,9 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
### Phase 0: Foundation (v0.0.1)
- Scaffold monorepo (pnpm + turbo + tsconfig + eslint + vitest)
- `@mosaic/types` — migrate and extend from v0
- `@mosaic/db` — Drizzle schema, PG connection, migrations
- `@mosaic/auth` — BetterAuth setup with email/password
- `@mosaicstack/types` — migrate and extend from v0
- `@mosaicstack/db` — Drizzle schema, PG connection, migrations
- `@mosaicstack/auth` — BetterAuth setup with email/password
- OTEL foundation — `@opentelemetry/sdk-node` setup, SigNoz in docker-compose, trace propagation wired
- Docker Compose (PG 17 + Valkey + SigNoz)
- CI pipeline (Woodpecker)
@@ -921,19 +921,19 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
### Phase 1: Core API (v0.0.2)
- `apps/gateway` — NestJS server (Fastify adapter), auth middleware, health endpoints
- `@mosaic/brain` — migrate from v0, swap JSON store for PG via @mosaic/db
- `@mosaic/queue` — migrate from v0 (minimal changes)
- `@mosaicstack/brain` — migrate from v0, swap JSON store for PG via @mosaicstack/db
- `@mosaicstack/queue` — migrate from v0 (minimal changes)
- Gateway routes: conversations, tasks, projects, missions
- WebSocket server for chat streaming
- Basic agent dispatch (single provider, no routing)
### Phase 2: Agent Layer (v0.0.3)
- `@mosaic/agent` — Pi SDK integration, agent pool manager
- `@mosaicstack/agent` — Pi SDK integration, agent pool manager
- Multi-provider support (Anthropic + Ollama minimum)
- Agent routing engine (cost/capability matrix)
- Tool registration (brain, queue, memory tools injected into agent sessions)
- `@mosaic/coord` — migrate from v0, integrate with gateway
- `@mosaicstack/coord` — migrate from v0, integrate with gateway
### Phase 3: Web Dashboard (v0.0.4)
@@ -946,25 +946,25 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
### Phase 4: Memory & Intelligence (v0.0.5)
- `@mosaic/memory` — preference store, insight store, semantic search
- `@mosaic/log` — log ingest, parsing, tiered storage
- `@mosaicstack/memory` — preference store, insight store, semantic search
- `@mosaicstack/log` — log ingest, parsing, tiered storage
- Summarization pipeline
- Memory integration into agent sessions
- Skill management interface (web UI + CLI)
### Phase 5: Remote Control (v0.0.6)
- `@mosaic/discord-plugin` — Discord channel plugin
- `@mosaic/telegram-plugin` — Telegram channel plugin
- `@mosaicstack/discord-plugin` — Discord channel plugin
- `@mosaicstack/telegram-plugin` — Telegram channel plugin
- Plugin host in gateway
- SSO configuration (Authentik)
### Phase 6: CLI & Tools (v0.0.7)
- `@mosaic/cli` — unified CLI with all subcommands
- `@mosaic/prdy` — migrate from v0
- `@mosaic/quality-rails` — migrate from v0
- `@mosaic/mosaic` — install wizard updated for v1
- `@mosaicstack/cli` — unified CLI with all subcommands
- `@mosaicstack/prdy` — migrate from v0
- `@mosaicstack/quality-rails` — migrate from v0
- `@mosaicstack/mosaic` — install wizard updated for v1
- Pi TUI integration (`mosaic tui`)
### Phase 7: Polish & Beta (v0.0.8 → v0.1.0)
@@ -982,11 +982,11 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
## Assumptions
1. RESOLVED: **pgvector is sufficient** for semantic search at v0.1.0 scale (personal/family/team = thousands to low hundreds-of-thousands of vectors). `@mosaic/memory` defines a `VectorStore` interface with pgvector as the default adapter. The interface boundary makes Qdrant a drop-in migration if PG resource contention or scale demands it later. Zero additional infrastructure for v0.1.0. Rationale: Reduces ops burden; pgvector HNSW indexes are fast at this scale; interface abstraction costs almost nothing now.
1. RESOLVED: **pgvector is sufficient** for semantic search at v0.1.0 scale (personal/family/team = thousands to low hundreds-of-thousands of vectors). `@mosaicstack/memory` defines a `VectorStore` interface with pgvector as the default adapter. The interface boundary makes Qdrant a drop-in migration if PG resource contention or scale demands it later. Zero additional infrastructure for v0.1.0. Rationale: Reduces ops burden; pgvector HNSW indexes are fast at this scale; interface abstraction costs almost nothing now.
2. RESOLVED: **Authentik is the first SSO provider** — confirmed, already running in Jason's infrastructure. WorkOS and Keycloak adapters follow in Phase 7.
3. RESOLVED: **NestJS with Fastify adapter for the gateway.** The gateway's complexity (plugin host, agent pool, routing engine, WebSocket hub, MCP server, auth, brain/queue/memory/log integration) warrants NestJS's module system, DI, and guards. Fastify performance preserved via adapter. Aligns with USER.md stated stack ("NestJS API + Next.js web"). @mosaic/brain's Fastify code migrates into a NestJS module.
3. RESOLVED: **NestJS with Fastify adapter for the gateway.** The gateway's complexity (plugin host, agent pool, routing engine, WebSocket hub, MCP server, auth, brain/queue/memory/log integration) warrants NestJS's module system, DI, and guards. Fastify performance preserved via adapter. Aligns with USER.md stated stack ("NestJS API + Next.js web"). @mosaicstack/brain's Fastify code migrates into a NestJS module.
4. RESOLVED: **OpenTelemetry from Phase 0.** Wide-event logging is required from the start. OTEL auto-instrumentation for NestJS/PG/HTTP via `@opentelemetry/sdk-node`. SigNoz as the all-in-one OTEL backend (single Docker service). Every significant operation emits structured events with rich context. Custom spans for agent dispatch, routing decisions, memory writes. Rationale: Retrofitting observability is painful; baking it in from day one means consistent instrumentation across all services.
@@ -1002,4 +1002,4 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaic/agent — only configuration management.
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.

View File

@@ -108,4 +108,4 @@ The web login page renders provider buttons from `NEXT_PUBLIC_*_ENABLED` flags.
## Failure mode
Provider config is optional, but partial config is rejected at startup. If any provider-specific env var is present without the full required set, `@mosaic/auth` throws a bootstrap error with the missing keys instead of silently registering a broken provider.
Provider config is optional, but partial config is rejected at startup. If any provider-specific env var is present without the full required set, `@mosaicstack/auth` throws a bootstrap error with the missing keys instead of silently registering a broken provider.

View File

@@ -91,15 +91,15 @@ packages/cli/src/tui/
```bash
cd /home/jwoltje/src/mosaic-mono-v1-worktrees/tui-improvements
pnpm --filter @mosaic/cli exec tsx src/cli.ts tui
pnpm --filter @mosaicstack/cli exec tsx src/cli.ts tui
# or after build:
node packages/cli/dist/cli.js tui --gateway http://localhost:4000
node packages/cli/dist/cli.js tui --gateway http://localhost:14242
```
### Quality Gates
```bash
pnpm --filter @mosaic/cli typecheck && pnpm --filter @mosaic/cli lint
pnpm --filter @mosaic/gateway typecheck && pnpm --filter @mosaic/gateway lint
pnpm --filter @mosaic/types typecheck
pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint
pnpm --filter @mosaicstack/gateway typecheck && pnpm --filter @mosaicstack/gateway lint
pnpm --filter @mosaicstack/types typecheck
```

View File

@@ -1,73 +1,40 @@
# Tasks — Harness Foundation
# Tasks — Install UX Hardening
> Single-writer: orchestrator only. Workers read but never modify.
>
> **`agent` column values:** `codex` | `sonnet` | `haiku` | `glm-5` | `opus` | `—` (auto/default)
> **Mission:** install-ux-hardening-20260405
> **Schema:** `| id | status | description | issue | agent | branch | depends_on | estimate | notes |`
> **Status values:** `not-started` | `in-progress` | `done` | `blocked` | `failed` | `needs-qa`
> **Agent values:** `codex` | `sonnet` | `haiku` | `opus` | `—` (auto)
| id | status | agent | milestone | description | pr | notes |
| ------ | ------ | ------ | ------------------ | ------------------------------------------------------------------ | ---- | ----------- |
| M1-001 | done | sonnet | M1: Persistence | Wire ChatGateway → ConversationsRepo for user messages | #292 | #224 closed |
| M1-002 | done | sonnet | M1: Persistence | Wire agent event relay → ConversationsRepo for assistant responses | #292 | #225 closed |
| M1-003 | done | sonnet | M1: Persistence | Store message metadata: model, provider, tokens, tool calls | #292 | #226 closed |
| M1-004 | done | sonnet | M1: Persistence | Load message history into Pi session on resume | #301 | #227 closed |
| M1-005 | done | sonnet | M1: Persistence | Context window management: summarize when >80% | #301 | #228 closed |
| M1-006 | done | sonnet | M1: Persistence | Conversation search endpoint | #299 | #229 closed |
| M1-007 | done | sonnet | M1: Persistence | TUI /history command | #297 | #230 closed |
| M1-008 | done | sonnet | M1: Persistence | Verify persistence — 20 tests | #304 | #231 closed |
| M2-001 | done | sonnet | M2: Security | InsightsRepo userId on searchByEmbedding | #290 | #232 closed |
| M2-002 | done | sonnet | M2: Security | InsightsRepo userId on findByUser/decay | #290 | #233 closed |
| M2-003 | done | sonnet | M2: Security | PreferencesRepo userId verified | #294 | #234 closed |
| M2-004 | done | sonnet | M2: Security | Memory tools userId injection fixed | #294 | #235 closed |
| M2-005 | done | sonnet | M2: Security | ConversationsRepo ownership checks | #293 | #236 closed |
| M2-006 | done | sonnet | M2: Security | AgentsRepo findAccessible scoped | #293 | #237 closed |
| M2-007 | done | sonnet | M2: Security | Cross-user isolation — 28 tests | #305 | #238 closed |
| M2-008 | done | sonnet | M2: Security | Valkey SCAN + /gc admin-only | #298 | #239 closed |
| M3-001 | done | sonnet | M3: Providers | IProviderAdapter + OllamaAdapter | #306 | #240 closed |
| M3-002 | done | sonnet | M3: Providers | AnthropicAdapter | #309 | #241 closed |
| M3-003 | done | sonnet | M3: Providers | OpenAIAdapter | #310 | #242 closed |
| M3-004 | done | sonnet | M3: Providers | OpenRouterAdapter | #311 | #243 closed |
| M3-005 | done | sonnet | M3: Providers | ZaiAdapter (GLM-5) | #314 | #244 closed |
| M3-006 | done | sonnet | M3: Providers | Ollama embedding support | #311 | #245 closed |
| M3-007 | done | sonnet | M3: Providers | Provider health checks | #308 | #246 closed |
| M3-008 | done | sonnet | M3: Providers | Model capability matrix | #303 | #247 closed |
| M3-009 | done | sonnet | M3: Providers | EmbeddingService → Ollama default | #308 | #248 closed |
| M3-010 | done | sonnet | M3: Providers | OAuth token storage (AES-256-GCM) | #317 | #249 closed |
| M3-011 | done | sonnet | M3: Providers | Provider credentials CRUD | #317 | #250 closed |
| M3-012 | done | sonnet | M3: Providers | Verify providers — 40 tests | #319 | #251 closed |
| M4-001 | done | sonnet | M4: Routing | routing_rules DB schema | #315 | #252 closed |
| M4-002 | done | sonnet | M4: Routing | Condition types | #315 | #253 closed |
| M4-003 | done | sonnet | M4: Routing | Action types | #315 | #254 closed |
| M4-004 | done | sonnet | M4: Routing | Default routing rules (11 seeds) | #316 | #255 closed |
| M4-005 | done | sonnet | M4: Routing | Task classifier (60+ tests) | #316 | #256 closed |
| M4-006 | done | sonnet | M4: Routing | Routing decision pipeline | #318 | #257 closed |
| M4-007 | done | sonnet | M4: Routing | /model override | #323 | #258 closed |
| M4-008 | done | sonnet | M4: Routing | Routing transparency in session:info | #323 | #259 closed |
| M4-009 | done | sonnet | M4: Routing | Routing rules CRUD API | #320 | #260 closed |
| M4-010 | done | sonnet | M4: Routing | Per-user routing overrides | #320 | #261 closed |
| M4-011 | done | sonnet | M4: Routing | Agent specialization capabilities | #320 | #262 closed |
| M4-012 | done | sonnet | M4: Routing | Routing wired into ChatGateway | #323 | #263 closed |
| M4-013 | done | sonnet | M4: Routing | Verify routing — 9 E2E tests | #323 | #264 closed |
| M5-001 | done | sonnet | M5: Sessions | Agent config loaded on session create | #323 | #265 closed |
| M5-002 | done | sonnet | M5: Sessions | /model command end-to-end | #323 | #266 closed |
| M5-003 | done | sonnet | M5: Sessions | /agent command mid-session | #323 | #267 closed |
| M5-004 | done | sonnet | M5: Sessions | Session ↔ conversation binding | #321 | #268 closed |
| M5-005 | done | sonnet | M5: Sessions | Session info broadcast | #321 | #269 closed |
| M5-006 | done | sonnet | M5: Sessions | /agent new from TUI | #321 | #270 closed |
| M5-007 | done | sonnet | M5: Sessions | Session metrics | #321 | #271 closed |
| M5-008 | done | sonnet | M5: Sessions | Verify sessions — 28 tests | #324 | #272 closed |
| M6-001 | done | sonnet | M6: Jobs | BullMQ + Valkey config | #324 | #273 closed |
| M6-002 | done | sonnet | M6: Jobs | Queue service with typed jobs | #324 | #274 closed |
| M6-003 | done | sonnet | M6: Jobs | Summarization → BullMQ | #324 | #275 closed |
| M6-004 | done | sonnet | M6: Jobs | GC → BullMQ | #324 | #276 closed |
| M6-005 | done | sonnet | M6: Jobs | Tier management → BullMQ | #324 | #277 closed |
| M6-006 | done | sonnet | M6: Jobs | Admin jobs API | #325 | #278 closed |
| M6-007 | done | sonnet | M6: Jobs | Job event logging | #325 | #279 closed |
| M6-008 | done | sonnet | M6: Jobs | Verify jobs | #324 | #280 closed |
| M7-001 | done | sonnet | M7: Channel Design | IChannelAdapter interface | #325 | #281 closed |
| M7-002 | done | sonnet | M7: Channel Design | Channel message protocol | #325 | #282 closed |
| M7-003 | done | sonnet | M7: Channel Design | Matrix integration design | #326 | #283 closed |
| M7-004 | done | sonnet | M7: Channel Design | Conversation multiplexing | #326 | #284 closed |
| M7-005 | done | sonnet | M7: Channel Design | Remote auth bridging | #326 | #285 closed |
| M7-006 | done | sonnet | M7: Channel Design | Agent-to-agent via Matrix | #326 | #286 closed |
| M7-007 | done | sonnet | M7: Channel Design | Multi-user isolation in Matrix | #326 | #287 closed |
| M7-008 | done | sonnet | M7: Channel Design | channel-protocol.md published | #326 | #288 closed |
## Milestone 1 — `mosaic uninstall` (IUH-M01)
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------- | ----- | ------ | --------------------- | ---------- | -------- | ------------------------------------------------------ |
| IUH-01-01 | done | Design install manifest schema (`~/.config/mosaic/.install-manifest.json`) — what install writes on first success | #425 | sonnet | feat/mosaic-uninstall | — | 8K | v1 schema in `install-manifest.ts` |
| IUH-01-02 | done | `mosaic uninstall` TS command: `--framework`, `--cli`, `--gateway`, `--all`, `--keep-data`, `--yes`, `--dry-run` | #425 | sonnet | feat/mosaic-uninstall | IUH-01-01 | 25K | `uninstall.ts` |
| IUH-01-03 | done | Reverse runtime asset linking in `~/.claude/` — restore `.mosaic-bak-*` if present, remove managed copies otherwise | #425 | sonnet | feat/mosaic-uninstall | IUH-01-02 | 12K | file list hardcoded from mosaic-link-runtime-assets |
| IUH-01-04 | done | Reverse npmrc scope mapping and PATH edits made by `tools/install.sh` | #425 | sonnet | feat/mosaic-uninstall | IUH-01-02 | 8K | npmrc reversed; no PATH edits found in v0.0.24 install |
| IUH-01-05 | done | Shell fallback: `tools/install.sh --uninstall` path for users without a working CLI | #425 | sonnet | feat/mosaic-uninstall | IUH-01-02 | 10K | |
| IUH-01-06 | done | Vitest coverage: dry-run output, `--all`, `--keep-data`, partial state, missing manifest | #425 | sonnet | feat/mosaic-uninstall | IUH-01-05 | 15K | 14 new tests, 170 total |
| IUH-01-07 | done | Code review (independent) + remediation | #425 | sonnet | feat/mosaic-uninstall | IUH-01-06 | 5K | |
| IUH-01-08 | done | PR open, CI green, review, merge to `main`, close issue | #425 | sonnet | feat/mosaic-uninstall | IUH-01-07 | 3K | PR #429, merge 25cada77 |
## Milestone 2 — Wizard Remediation (IUH-M02)
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ------ | -------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----------------------- | ---------- | -------- | ----------------------------------------------- |
| IUH-02-01 | done | Password masking: replace plaintext `rl.question` in `bootstrapFirstUser` with masked TTY read + confirmation | #426 | sonnet | feat/wizard-remediation | IUH-01-08 | 8K | `prompter/masked-prompt.ts` |
| IUH-02-02 | done | Hooks preview stage in wizard: show `framework/runtime/claude/hooks-config.json` entries + confirm prompt | #426 | sonnet | feat/wizard-remediation | IUH-02-01 | 12K | `stages/hooks-preview.ts`; finalize gating TODO |
| IUH-02-03 | done | `mosaic config hooks list\|enable\|disable` subcommands | #426 | sonnet | feat/wizard-remediation | IUH-02-02 | 15K | `commands/config.ts` |
| IUH-02-04 | done | Headless path: env-var driven `runConfigWizard` + `bootstrapFirstUser` (`MOSAIC_ASSUME_YES`, `MOSAIC_ADMIN_*`) | #426 | sonnet | feat/wizard-remediation | IUH-02-03 | 12K | |
| IUH-02-05 | done | Tests + code review + PR merge | #426 | sonnet | feat/wizard-remediation | IUH-02-04 | 10K | PR #431, merge cd8b1f66 |
## Milestone 3 — Unified First-Run Wizard (IUH-M03)
| id | status | description | issue | agent | branch | depends_on | estimate | notes |
| --------- | ----------- | ----------------------------------------------------------------------------------------------------------- | ----- | ----- | ---------------------- | ---------- | -------- | ----- |
| IUH-03-01 | not-started | Design doc: unified state machine; decide whether `mosaic gateway install` becomes an internal wizard stage | #427 | opus | feat/unified-first-run | IUH-02-05 | 10K | |
| IUH-03-02 | not-started | Refactor `runWizard` to invoke gateway install as a stage; drop the 10-minute session-file bridge | #427 | opus | feat/unified-first-run | IUH-03-01 | 25K | |
| IUH-03-03 | not-started | Preserve backward-compat: `mosaic gateway install` still works as a standalone entry point | #427 | opus | feat/unified-first-run | IUH-03-02 | 10K | |
| IUH-03-04 | not-started | Tests + code review + PR merge | #427 | opus | feat/unified-first-run | IUH-03-03 | 12K | |

Some files were not shown because too many files have changed in this diff Show More