docs: establish canonical documentation architecture (#1210)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/publish Pipeline failed
This commit was merged in pull request #1210.
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
# Gateway Security Hardening Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Finish the requested gateway security hardening fixes in the existing `fix/gateway-security` worktree and produce a PR-ready branch.
|
||||
|
||||
**Architecture:** Tighten NestJS gateway boundaries in-place by enforcing auth guards, session validation, ownership checks, DTO validation, and Fastify security defaults. Preserve the current module structure and existing ESM import conventions.
|
||||
|
||||
**Tech Stack:** NestJS 11, Fastify, Socket.IO, Better Auth, class-validator, Vitest, pnpm, TypeScript ESM
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Reconcile Security Tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/gateway/src/chat/__tests__/chat-security.test.ts`
|
||||
- Modify: `apps/gateway/src/__tests__/resource-ownership.test.ts`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
- Encode the requested DTO constraints and socket-auth contract exactly.
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts`
|
||||
|
||||
Expected: FAIL on current DTO/helper mismatch.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- Update DTO/helper/controller code only where tests prove a gap.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run the same command and require green.
|
||||
|
||||
### Task 2: Align Gateway Runtime Hardening
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/gateway/src/conversations/conversations.dto.ts`
|
||||
- Modify: `apps/gateway/src/chat/chat.dto.ts`
|
||||
- Modify: `apps/gateway/src/chat/chat.gateway-auth.ts`
|
||||
- Modify: `apps/gateway/src/chat/chat.gateway.ts`
|
||||
- Modify: `apps/gateway/src/main.ts`
|
||||
- Modify: `apps/gateway/src/app.module.ts`
|
||||
|
||||
**Step 1: Verify remaining requested deltas**
|
||||
|
||||
- Confirm code matches requested guard, rate limit, helmet, body limit, env validation, and CORS settings.
|
||||
|
||||
**Step 2: Apply minimal patch**
|
||||
|
||||
- Keep changes scoped to requested behavior only.
|
||||
|
||||
**Step 3: Run targeted tests**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/gateway test -- src/chat/__tests__/chat-security.test.ts src/__tests__/resource-ownership.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Verification, Review, and Delivery
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `docs/reports/code-review/gateway-security-20260313.md`
|
||||
- Create: `docs/reports/qa/gateway-security-20260313.md`
|
||||
- Modify: `docs/scratchpads/gateway-security-20260313.md`
|
||||
|
||||
**Step 1: Run baseline gates**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
**Step 2: Perform manual code review**
|
||||
|
||||
- Record correctness/security/testing/doc findings.
|
||||
|
||||
**Step 3: Commit and publish**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix(gateway): security hardening — auth guards, ownership checks, validation, rate limiting"
|
||||
git push origin fix/gateway-security
|
||||
```
|
||||
|
||||
**Step 4: Open PR and notify**
|
||||
|
||||
- Open PR titled `fix(gateway): security hardening — auth guards, ownership checks, validation, rate limiting`
|
||||
- Run `openclaw system event --text "PR ready: mosaic-mono-v1 fix/gateway-security — 7 security fixes" --mode now`
|
||||
- Remove worktree after PR is created.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,998 +0,0 @@
|
||||
# Wave 2 — TUI Layout & Navigation Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add conversation sidebar, keybindings, scrollable message history, and search to the Mosaic TUI.
|
||||
|
||||
**Architecture:** The TUI gains a sidebar panel for conversation management (list/create/switch) fetched via REST from the gateway. A `useConversations` hook manages REST calls. A `useScrollableViewport` hook wraps the message list with virtual viewport logic. An app-level focus/mode state machine (`useAppMode`) controls which panel receives input. All new socket events for conversation listing use the existing REST API (`GET /api/conversations`).
|
||||
|
||||
**Tech Stack:** Ink 5, React 18, socket.io-client, fetch (for REST), @mosaicstack/types
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
TUI-010 (scrollable history) ← TUI-011 (search)
|
||||
TUI-008 (sidebar) ← TUI-009 (keybindings)
|
||||
```
|
||||
|
||||
TUI-008 and TUI-010 are independent — can be built in parallel.
|
||||
TUI-009 depends on TUI-008. TUI-011 depends on TUI-010.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: TUI-010 — Scrollable Message History
|
||||
|
||||
### 1A: Create `use-viewport` hook
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/tui/hooks/use-viewport.ts`
|
||||
|
||||
**Step 1: Write the hook**
|
||||
|
||||
This hook tracks a scroll offset and viewport height for the message list.
|
||||
Ink's `useStdout` gives us terminal rows. We calculate visible slice.
|
||||
|
||||
```ts
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useStdout } from 'ink';
|
||||
|
||||
export interface UseViewportOptions {
|
||||
/** Total number of renderable lines (message count as proxy) */
|
||||
totalItems: number;
|
||||
/** Lines reserved for chrome (top bar, input bar, bottom bar) */
|
||||
reservedLines?: number;
|
||||
}
|
||||
|
||||
export interface UseViewportReturn {
|
||||
/** Index of first visible item (0-based) */
|
||||
scrollOffset: number;
|
||||
/** Number of items that fit in viewport */
|
||||
viewportSize: number;
|
||||
/** Whether user has scrolled up from bottom */
|
||||
isScrolledUp: boolean;
|
||||
/** Scroll to bottom (auto-follow mode) */
|
||||
scrollToBottom: () => void;
|
||||
/** Scroll by delta (negative = up, positive = down) */
|
||||
scrollBy: (delta: number) => void;
|
||||
/** Scroll to a specific offset */
|
||||
scrollTo: (offset: number) => void;
|
||||
/** Whether we can scroll up/down */
|
||||
canScrollUp: boolean;
|
||||
canScrollDown: boolean;
|
||||
}
|
||||
|
||||
export function useViewport(opts: UseViewportOptions): UseViewportReturn {
|
||||
const { totalItems, reservedLines = 10 } = opts;
|
||||
const { stdout } = useStdout();
|
||||
const terminalRows = stdout?.rows ?? 24;
|
||||
|
||||
// Viewport = terminal height minus chrome
|
||||
const viewportSize = Math.max(1, terminalRows - reservedLines);
|
||||
|
||||
const maxOffset = Math.max(0, totalItems - viewportSize);
|
||||
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
// Track if user explicitly scrolled up
|
||||
const [autoFollow, setAutoFollow] = useState(true);
|
||||
|
||||
// Effective offset: if auto-following, always show latest
|
||||
const effectiveOffset = autoFollow ? maxOffset : Math.min(scrollOffset, maxOffset);
|
||||
|
||||
const scrollBy = useCallback(
|
||||
(delta: number) => {
|
||||
setAutoFollow(false);
|
||||
setScrollOffset((prev) => {
|
||||
const next = Math.max(0, Math.min(prev + delta, maxOffset));
|
||||
// If scrolled to bottom, re-enable auto-follow
|
||||
if (next >= maxOffset) {
|
||||
setAutoFollow(true);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[maxOffset],
|
||||
);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
setAutoFollow(true);
|
||||
setScrollOffset(maxOffset);
|
||||
}, [maxOffset]);
|
||||
|
||||
const scrollTo = useCallback(
|
||||
(offset: number) => {
|
||||
const clamped = Math.max(0, Math.min(offset, maxOffset));
|
||||
setAutoFollow(clamped >= maxOffset);
|
||||
setScrollOffset(clamped);
|
||||
},
|
||||
[maxOffset],
|
||||
);
|
||||
|
||||
return {
|
||||
scrollOffset: effectiveOffset,
|
||||
viewportSize,
|
||||
isScrolledUp: !autoFollow,
|
||||
scrollToBottom,
|
||||
scrollBy,
|
||||
scrollTo,
|
||||
canScrollUp: effectiveOffset > 0,
|
||||
canScrollDown: effectiveOffset < maxOffset,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/hooks/use-viewport.ts
|
||||
git commit -m "feat(cli): add use-viewport hook for scrollable message history"
|
||||
```
|
||||
|
||||
### 1B: Integrate viewport into MessageList and wire PgUp/PgDn
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/tui/components/message-list.tsx`
|
||||
- Modify: `packages/cli/src/tui/app.tsx`
|
||||
|
||||
**Step 1: Update MessageList to accept viewport props and slice messages**
|
||||
|
||||
In `message-list.tsx`, add viewport props and render only the visible slice. Add a scroll indicator when scrolled up.
|
||||
|
||||
```tsx
|
||||
// Add to MessageListProps:
|
||||
export interface MessageListProps {
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
currentStreamText: string;
|
||||
currentThinkingText: string;
|
||||
activeToolCalls: ToolCall[];
|
||||
// New viewport props
|
||||
scrollOffset: number;
|
||||
viewportSize: number;
|
||||
isScrolledUp: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
In the component body, slice messages:
|
||||
|
||||
```tsx
|
||||
const visibleMessages = messages.slice(scrollOffset, scrollOffset + viewportSize);
|
||||
```
|
||||
|
||||
Replace `messages.map(...)` with `visibleMessages.map(...)`. Add a scroll-up indicator at the top:
|
||||
|
||||
```tsx
|
||||
{
|
||||
isScrolledUp && (
|
||||
<Box justifyContent="center">
|
||||
<Text dimColor>↑ {scrollOffset} more messages ↑</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Wire viewport hook + keybindings in app.tsx**
|
||||
|
||||
In `app.tsx`:
|
||||
|
||||
1. Import and call `useViewport({ totalItems: socket.messages.length })`
|
||||
2. Pass viewport props to `<MessageList>`
|
||||
3. Add PgUp/PgDn/Home/End keybindings in the existing `useInput`:
|
||||
- `key.pageUp` → `viewport.scrollBy(-viewport.viewportSize)`
|
||||
- `key.pageDown` → `viewport.scrollBy(viewport.viewportSize)`
|
||||
- Shift+Up → `viewport.scrollBy(-1)` (line scroll)
|
||||
- Shift+Down → `viewport.scrollBy(1)` (line scroll)
|
||||
|
||||
Note: Ink's `useInput` key object supports `pageUp`, `pageDown`. For Home/End, check `key.meta && ch === '<'` / `key.meta && ch === '>'` as Ink doesn't have built-in home/end.
|
||||
|
||||
**Step 3: Typecheck and lint**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/components/message-list.tsx packages/cli/src/tui/app.tsx
|
||||
git commit -m "feat(cli): scrollable message history with PgUp/PgDn viewport"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: TUI-008 — Conversation Sidebar
|
||||
|
||||
### 2A: Create `use-conversations` hook (REST client)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/tui/hooks/use-conversations.ts`
|
||||
|
||||
**Step 1: Write the hook**
|
||||
|
||||
This hook fetches conversations from the gateway REST API and provides create/switch actions.
|
||||
|
||||
```ts
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export interface ConversationSummary {
|
||||
id: string;
|
||||
title: string | null;
|
||||
archived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UseConversationsOptions {
|
||||
gatewayUrl: string;
|
||||
sessionCookie?: string;
|
||||
/** Currently active conversation ID from socket */
|
||||
activeConversationId: string | undefined;
|
||||
}
|
||||
|
||||
export interface UseConversationsReturn {
|
||||
conversations: ConversationSummary[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
createConversation: (title?: string) => Promise<ConversationSummary | null>;
|
||||
deleteConversation: (id: string) => Promise<boolean>;
|
||||
renameConversation: (id: string, title: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function useConversations(opts: UseConversationsOptions): UseConversationsReturn {
|
||||
const { gatewayUrl, sessionCookie } = opts;
|
||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(sessionCookie ? { Cookie: sessionCookie } : {}),
|
||||
};
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`${gatewayUrl}/api/conversations`, { headers });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = (await res.json()) as ConversationSummary[];
|
||||
if (mountedRef.current) {
|
||||
setConversations(data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mountedRef.current) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, [gatewayUrl, sessionCookie]);
|
||||
|
||||
const createConversation = useCallback(
|
||||
async (title?: string): Promise<ConversationSummary | null> => {
|
||||
try {
|
||||
const res = await fetch(`${gatewayUrl}/api/conversations`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ title: title ?? 'New Conversation' }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const conv = (await res.json()) as ConversationSummary;
|
||||
await refresh();
|
||||
return conv;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[gatewayUrl, sessionCookie, refresh],
|
||||
);
|
||||
|
||||
const deleteConversation = useCallback(
|
||||
async (id: string): Promise<boolean> => {
|
||||
try {
|
||||
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
await refresh();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[gatewayUrl, sessionCookie, refresh],
|
||||
);
|
||||
|
||||
const renameConversation = useCallback(
|
||||
async (id: string, title: string): Promise<boolean> => {
|
||||
try {
|
||||
const res = await fetch(`${gatewayUrl}/api/conversations/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
await refresh();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[gatewayUrl, sessionCookie, refresh],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
void refresh();
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
conversations,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
createConversation,
|
||||
deleteConversation,
|
||||
renameConversation,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/hooks/use-conversations.ts
|
||||
git commit -m "feat(cli): add use-conversations hook for REST conversation management"
|
||||
```
|
||||
|
||||
### 2B: Create `use-app-mode` hook (focus/mode state machine)
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/tui/hooks/use-app-mode.ts`
|
||||
|
||||
**Step 1: Write the hook**
|
||||
|
||||
This manages which panel has focus and the current UI mode.
|
||||
|
||||
```ts
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export type AppMode = 'chat' | 'sidebar' | 'search';
|
||||
|
||||
export interface UseAppModeReturn {
|
||||
mode: AppMode;
|
||||
setMode: (mode: AppMode) => void;
|
||||
toggleSidebar: () => void;
|
||||
/** Whether sidebar panel should be visible */
|
||||
sidebarOpen: boolean;
|
||||
}
|
||||
|
||||
export function useAppMode(): UseAppModeReturn {
|
||||
const [mode, setModeState] = useState<AppMode>('chat');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const setMode = useCallback((m: AppMode) => {
|
||||
setModeState(m);
|
||||
if (m === 'sidebar') setSidebarOpen(true);
|
||||
}, []);
|
||||
|
||||
const toggleSidebar = useCallback(() => {
|
||||
setSidebarOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
// Closing sidebar → return to chat mode
|
||||
setModeState('chat');
|
||||
} else {
|
||||
setModeState('sidebar');
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { mode, setMode, toggleSidebar, sidebarOpen };
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/hooks/use-app-mode.ts
|
||||
git commit -m "feat(cli): add use-app-mode hook for panel focus state machine"
|
||||
```
|
||||
|
||||
### 2C: Create `Sidebar` component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/tui/components/sidebar.tsx`
|
||||
|
||||
**Step 1: Write the component**
|
||||
|
||||
The sidebar shows a scrollable list of conversations with the active one highlighted. It handles keyboard navigation when focused.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { ConversationSummary } from '../hooks/use-conversations.js';
|
||||
|
||||
export interface SidebarProps {
|
||||
conversations: ConversationSummary[];
|
||||
activeConversationId: string | undefined;
|
||||
selectedIndex: number;
|
||||
onSelectIndex: (index: number) => void;
|
||||
onSwitchConversation: (id: string) => void;
|
||||
onDeleteConversation: (id: string) => void;
|
||||
loading: boolean;
|
||||
focused: boolean;
|
||||
width: number;
|
||||
}
|
||||
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen - 1) + '…';
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
if (diffDays === 0) {
|
||||
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
}
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
conversations,
|
||||
activeConversationId,
|
||||
selectedIndex,
|
||||
onSelectIndex,
|
||||
onSwitchConversation,
|
||||
onDeleteConversation,
|
||||
loading,
|
||||
focused,
|
||||
width,
|
||||
}: SidebarProps) {
|
||||
useInput(
|
||||
(ch, key) => {
|
||||
if (!focused) return;
|
||||
|
||||
if (key.upArrow) {
|
||||
onSelectIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
onSelectIndex(Math.min(conversations.length - 1, selectedIndex + 1));
|
||||
} else if (key.return) {
|
||||
const conv = conversations[selectedIndex];
|
||||
if (conv) onSwitchConversation(conv.id);
|
||||
} else if (ch === 'd' || ch === 'D') {
|
||||
const conv = conversations[selectedIndex];
|
||||
if (conv && conv.id !== activeConversationId) {
|
||||
onDeleteConversation(conv.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: focused },
|
||||
);
|
||||
|
||||
const titleWidth = width - 4; // padding + borders
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={width}
|
||||
borderStyle="single"
|
||||
borderColor={focused ? 'cyan' : 'gray'}
|
||||
>
|
||||
<Box paddingX={1}>
|
||||
<Text bold color={focused ? 'cyan' : undefined}>
|
||||
Conversations
|
||||
</Text>
|
||||
{loading && <Text dimColor> …</Text>}
|
||||
</Box>
|
||||
|
||||
{conversations.length === 0 && (
|
||||
<Box paddingX={1}>
|
||||
<Text dimColor>No conversations</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{conversations.map((conv, i) => {
|
||||
const isActive = conv.id === activeConversationId;
|
||||
const isSelected = i === selectedIndex && focused;
|
||||
const title = conv.title ?? `Untitled (${conv.id.slice(0, 6)})`;
|
||||
const displayTitle = truncate(title, titleWidth);
|
||||
|
||||
return (
|
||||
<Box key={conv.id} paddingX={1}>
|
||||
<Text
|
||||
bold={isActive}
|
||||
color={isSelected ? 'cyan' : isActive ? 'green' : undefined}
|
||||
inverse={isSelected}
|
||||
>
|
||||
{isActive ? '● ' : ' '}
|
||||
{displayTitle}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{focused && (
|
||||
<Box paddingX={1} marginTop={1}>
|
||||
<Text dimColor>↑↓ navigate · ↵ switch · d delete</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Typecheck**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/components/sidebar.tsx
|
||||
git commit -m "feat(cli): add conversation sidebar component"
|
||||
```
|
||||
|
||||
### 2D: Wire sidebar + conversation switching into app.tsx
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/tui/app.tsx`
|
||||
- Modify: `packages/cli/src/tui/hooks/use-socket.ts`
|
||||
|
||||
**Step 1: Add `switchConversation` to useSocket**
|
||||
|
||||
In `use-socket.ts`, add a method to switch conversations. When switching, clear local messages and set the new conversation ID. The socket will pick up the new conversation on next `message` emit.
|
||||
|
||||
Add to `UseSocketReturn`:
|
||||
|
||||
```ts
|
||||
switchConversation: (id: string) => void;
|
||||
clearMessages: () => void;
|
||||
```
|
||||
|
||||
Implementation:
|
||||
|
||||
```ts
|
||||
const switchConversation = useCallback((id: string) => {
|
||||
setConversationId(id);
|
||||
setMessages([]);
|
||||
setIsStreaming(false);
|
||||
setCurrentStreamText('');
|
||||
setCurrentThinkingText('');
|
||||
setActiveToolCalls([]);
|
||||
}, []);
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessages([]);
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Step 2: Update app.tsx layout to include sidebar**
|
||||
|
||||
1. Import `useAppMode`, `useConversations`, `Sidebar`
|
||||
2. Add `useAppMode()` call
|
||||
3. Add `useConversations({ gatewayUrl, sessionCookie, activeConversationId: socket.conversationId })`
|
||||
4. Track `sidebarSelectedIndex` state
|
||||
5. Wrap the main content area in a horizontal `<Box>`:
|
||||
|
||||
```tsx
|
||||
<Box flexDirection="row" flexGrow={1}>
|
||||
{appMode.sidebarOpen && (
|
||||
<Sidebar
|
||||
conversations={convos.conversations}
|
||||
activeConversationId={socket.conversationId}
|
||||
selectedIndex={sidebarSelectedIndex}
|
||||
onSelectIndex={setSidebarSelectedIndex}
|
||||
onSwitchConversation={(id) => {
|
||||
socket.switchConversation(id);
|
||||
appMode.setMode('chat');
|
||||
}}
|
||||
onDeleteConversation={(id) => void convos.deleteConversation(id)}
|
||||
loading={convos.loading}
|
||||
focused={appMode.mode === 'sidebar'}
|
||||
width={30}
|
||||
/>
|
||||
)}
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<MessageList ... />
|
||||
</Box>
|
||||
</Box>
|
||||
```
|
||||
|
||||
6. InputBar should be disabled (or readonly placeholder) when mode is not 'chat'
|
||||
|
||||
**Step 3: Typecheck and lint**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/app.tsx packages/cli/src/tui/hooks/use-socket.ts
|
||||
git commit -m "feat(cli): wire conversation sidebar with create/switch/delete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: TUI-009 — Keybinding System
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/cli/src/tui/app.tsx`
|
||||
|
||||
**Step 1: Add global keybindings in the existing useInput**
|
||||
|
||||
Add these bindings to the `useInput` in `app.tsx`:
|
||||
|
||||
| Binding | Action |
|
||||
| ----------- | ----------------------------------------- |
|
||||
| `Ctrl+L` | Toggle sidebar visibility |
|
||||
| `Ctrl+N` | Create new conversation + switch to it |
|
||||
| `Ctrl+K` | Toggle search mode (TUI-011) |
|
||||
| `Escape` | Return to chat mode from any panel |
|
||||
| `Ctrl+T` | Cycle thinking level (already exists) |
|
||||
| `PgUp/PgDn` | Scroll viewport (already wired in Task 1) |
|
||||
|
||||
```ts
|
||||
useInput((ch, key) => {
|
||||
if (key.ctrl && ch === 'c') {
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Global keybindings (work in any mode)
|
||||
if (key.ctrl && ch === 'l') {
|
||||
appMode.toggleSidebar();
|
||||
if (!appMode.sidebarOpen) void convos.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && ch === 'n') {
|
||||
void convos.createConversation().then((conv) => {
|
||||
if (conv) {
|
||||
socket.switchConversation(conv.id);
|
||||
appMode.setMode('chat');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && ch === 'k') {
|
||||
appMode.setMode(appMode.mode === 'search' ? 'chat' : 'search');
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
if (appMode.mode !== 'chat') {
|
||||
appMode.setMode('chat');
|
||||
return;
|
||||
}
|
||||
// In chat mode, Escape could scroll to bottom
|
||||
viewport.scrollToBottom();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+T: cycle thinking (existing)
|
||||
if (key.ctrl && ch === 't') {
|
||||
const levels = socket.availableThinkingLevels;
|
||||
if (levels.length > 0) {
|
||||
const currentIdx = levels.indexOf(socket.thinkingLevel);
|
||||
const nextIdx = (currentIdx + 1) % levels.length;
|
||||
const next = levels[nextIdx];
|
||||
if (next) socket.setThinkingLevel(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Viewport scrolling (only in chat mode)
|
||||
if (appMode.mode === 'chat') {
|
||||
if (key.pageUp) {
|
||||
viewport.scrollBy(-viewport.viewportSize);
|
||||
} else if (key.pageDown) {
|
||||
viewport.scrollBy(viewport.viewportSize);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Add keybinding hints to bottom bar**
|
||||
|
||||
In `bottom-bar.tsx`, add a hints line above the status lines (or integrate into line 1):
|
||||
|
||||
```tsx
|
||||
<Text dimColor>^L sidebar · ^N new · ^K search · ^T thinking · PgUp/Dn scroll</Text>
|
||||
```
|
||||
|
||||
**Step 3: Typecheck and lint**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/app.tsx packages/cli/src/tui/components/bottom-bar.tsx
|
||||
git commit -m "feat(cli): keybinding system — Ctrl+L sidebar, Ctrl+N new, Ctrl+K search, Escape"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: TUI-011 — Message Search
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/cli/src/tui/components/search-bar.tsx`
|
||||
- Create: `packages/cli/src/tui/hooks/use-search.ts`
|
||||
- Modify: `packages/cli/src/tui/app.tsx`
|
||||
- Modify: `packages/cli/src/tui/components/message-list.tsx`
|
||||
|
||||
### 4A: Create `use-search` hook
|
||||
|
||||
**Step 1: Write the hook**
|
||||
|
||||
```ts
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { Message } from './use-socket.js';
|
||||
|
||||
export interface SearchMatch {
|
||||
messageIndex: number;
|
||||
/** Character offset within message content */
|
||||
charOffset: number;
|
||||
}
|
||||
|
||||
export interface UseSearchReturn {
|
||||
query: string;
|
||||
setQuery: (q: string) => void;
|
||||
matches: SearchMatch[];
|
||||
currentMatchIndex: number;
|
||||
nextMatch: () => void;
|
||||
prevMatch: () => void;
|
||||
clear: () => void;
|
||||
/** Total match count */
|
||||
totalMatches: number;
|
||||
}
|
||||
|
||||
export function useSearch(messages: Message[]): UseSearchReturn {
|
||||
const [query, setQuery] = useState('');
|
||||
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
||||
|
||||
const matches = useMemo(() => {
|
||||
if (!query || query.length < 2) return [];
|
||||
const q = query.toLowerCase();
|
||||
const result: SearchMatch[] = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const content = messages[i]!.content.toLowerCase();
|
||||
let pos = 0;
|
||||
while ((pos = content.indexOf(q, pos)) !== -1) {
|
||||
result.push({ messageIndex: i, charOffset: pos });
|
||||
pos += q.length;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [query, messages]);
|
||||
|
||||
const nextMatch = useCallback(() => {
|
||||
if (matches.length === 0) return;
|
||||
setCurrentMatchIndex((prev) => (prev + 1) % matches.length);
|
||||
}, [matches.length]);
|
||||
|
||||
const prevMatch = useCallback(() => {
|
||||
if (matches.length === 0) return;
|
||||
setCurrentMatchIndex((prev) => (prev - 1 + matches.length) % matches.length);
|
||||
}, [matches.length]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setQuery('');
|
||||
setCurrentMatchIndex(0);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
query,
|
||||
setQuery,
|
||||
matches,
|
||||
currentMatchIndex: matches.length > 0 ? currentMatchIndex % matches.length : 0,
|
||||
nextMatch,
|
||||
prevMatch,
|
||||
clear,
|
||||
totalMatches: matches.length,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/hooks/use-search.ts
|
||||
git commit -m "feat(cli): add use-search hook for message search"
|
||||
```
|
||||
|
||||
### 4B: Create SearchBar component
|
||||
|
||||
**Step 1: Write the component**
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
|
||||
export interface SearchBarProps {
|
||||
query: string;
|
||||
onQueryChange: (q: string) => void;
|
||||
totalMatches: number;
|
||||
currentMatch: number;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
onClose: () => void;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
query,
|
||||
onQueryChange,
|
||||
totalMatches,
|
||||
currentMatch,
|
||||
onClose,
|
||||
focused,
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<Box paddingX={1} borderStyle="single" borderColor={focused ? 'yellow' : 'gray'}>
|
||||
<Text color="yellow">🔍 </Text>
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={onQueryChange}
|
||||
placeholder="search messages…"
|
||||
focus={focused}
|
||||
/>
|
||||
<Box marginLeft={1}>
|
||||
{query.length >= 2 ? (
|
||||
<Text dimColor>
|
||||
{totalMatches > 0 ? `${currentMatch + 1}/${totalMatches}` : 'no matches'}
|
||||
{' · ↑↓ navigate · Esc close'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text dimColor>type to search…</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/components/search-bar.tsx
|
||||
git commit -m "feat(cli): add search bar component"
|
||||
```
|
||||
|
||||
### 4C: Wire search into app.tsx and message-list
|
||||
|
||||
**Step 1: Integrate**
|
||||
|
||||
In `app.tsx`:
|
||||
|
||||
1. Import `useSearch` and `SearchBar`
|
||||
2. Call `useSearch(socket.messages)`
|
||||
3. When mode is 'search', render `<SearchBar>` above `<InputBar>`
|
||||
4. In search mode, Up/Down arrows call `search.nextMatch()`/`search.prevMatch()` and scroll the viewport to the matched message
|
||||
5. Pass `searchHighlights` to `MessageList` — the set of message indices that match
|
||||
|
||||
In `message-list.tsx`:
|
||||
|
||||
1. Add optional `highlightedMessageIndices?: Set<number>` and `currentHighlightIndex?: number` props
|
||||
2. Highlighted messages get a yellow left border or background tint
|
||||
3. The current match gets a brighter highlight
|
||||
|
||||
**Step 2: Typecheck and lint**
|
||||
|
||||
Run: `pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/cli/src/tui/app.tsx packages/cli/src/tui/components/message-list.tsx packages/cli/src/tui/components/search-bar.tsx
|
||||
git commit -m "feat(cli): wire message search with highlight and viewport scroll"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Final Integration & Quality Gates
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/TASKS-TUI_Improvements.md` (update status)
|
||||
|
||||
**Step 1: Full typecheck across all affected packages**
|
||||
|
||||
```bash
|
||||
pnpm --filter @mosaicstack/cli typecheck && pnpm --filter @mosaicstack/cli lint
|
||||
pnpm --filter @mosaicstack/types typecheck
|
||||
```
|
||||
|
||||
Expected: All PASS
|
||||
|
||||
**Step 2: Manual smoke test (held)**
|
||||
|
||||
This historical TUI smoke test is unavailable until KBN-101-02 supplies a fail-closed Gateway local
|
||||
startup route. Do not start current Compose PostgreSQL or infer a local Gateway from PGlite support.
|
||||
A future reviewed test must use the correct Mosaic CLI package and an independently verified Gateway.
|
||||
|
||||
Verify:
|
||||
|
||||
- [ ] Messages scroll with PgUp/PgDn
|
||||
- [ ] Ctrl+L opens/closes sidebar
|
||||
- [ ] Sidebar shows conversations from REST API
|
||||
- [ ] Arrow keys navigate sidebar when focused
|
||||
- [ ] Enter switches conversation, clears messages
|
||||
- [ ] Ctrl+N creates new conversation
|
||||
- [ ] Ctrl+K opens search bar
|
||||
- [ ] Typing in search highlights matches
|
||||
- [ ] Up/Down in search mode cycles through matches
|
||||
- [ ] Escape returns to chat from any mode
|
||||
- [ ] Ctrl+T still cycles thinking levels
|
||||
- [ ] Auto-scroll follows new messages at bottom
|
||||
|
||||
**Step 3: Update task tracker**
|
||||
|
||||
Mark TUI-008, TUI-009, TUI-010, TUI-011 as ✅ done in `docs/TASKS-TUI_Improvements.md`
|
||||
|
||||
**Step 4: Commit and push**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "docs: mark Wave 2 tasks complete"
|
||||
git push
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
|
||||
| Action | Path |
|
||||
| ------ | -------------------------------------------------- |
|
||||
| Create | `packages/cli/src/tui/hooks/use-viewport.ts` |
|
||||
| Create | `packages/cli/src/tui/hooks/use-conversations.ts` |
|
||||
| Create | `packages/cli/src/tui/hooks/use-app-mode.ts` |
|
||||
| Create | `packages/cli/src/tui/hooks/use-search.ts` |
|
||||
| Create | `packages/cli/src/tui/components/sidebar.tsx` |
|
||||
| Create | `packages/cli/src/tui/components/search-bar.tsx` |
|
||||
| Modify | `packages/cli/src/tui/app.tsx` |
|
||||
| Modify | `packages/cli/src/tui/hooks/use-socket.ts` |
|
||||
| Modify | `packages/cli/src/tui/components/message-list.tsx` |
|
||||
| Modify | `packages/cli/src/tui/components/bottom-bar.tsx` |
|
||||
| Modify | `docs/TASKS-TUI_Improvements.md` |
|
||||
@@ -1,238 +0,0 @@
|
||||
# Hermes-Mosaic Alignment Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Package Mosaic's mechanical coordination primitives as a native Hermes toolset so any Hermes profile gets mission management, task decomposition, handoff, and session continuity without depending on the Mosaic gateway or OpenClaw runtime.
|
||||
|
||||
**Architecture:** Extract the coordination logic from Mosaic's `packages/coord` (TypeScript, file-first) into a Hermes Python toolset that wraps the same file conventions. The Mosaic Stack repo remains the canonical upstream for the file formats (TASKS.md schema, mission.json schema, handoff packet schema). Hermes implements native Python tools that read/write those same files, plus tool-calls for churn detection and handoff generation that have no Mosaic equivalent today.
|
||||
|
||||
**Tech Stack:** Python (Hermes toolset), SQLite (Hermes Kanban), JSON + Markdown (Mosaic file conventions)
|
||||
|
||||
---
|
||||
|
||||
## Alignment Map
|
||||
|
||||
### What Mosaic has that Hermes needs
|
||||
|
||||
| Mosaic Component | What it does | Natural Hermes home | Why |
|
||||
| -------------------------------- | --------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/coord` (mission.ts) | Mission CRUD, session tracking, milestone state | **Hermes toolset: `mission`** | Mission state is session-scoped, not gateway-scoped. Hermes sessions already have identity, process tracking, and context windows. |
|
||||
| `packages/coord` (tasks-file.ts) | Parse/write TASKS.md tables | **Hermes toolset: `mission`** (same) | Hermes already reads/writes files. The TASKS.md parser is ~300 lines of pure string manipulation — trivial Python port. |
|
||||
| `packages/coord` (runner.ts) | Spawn claude/codex workers with continuation prompts | **Already covered by `delegate_task`** | Hermes delegate_task already does isolated subagent spawning with restricted toolsets. The runner's "find next task and build continuation prompt" logic moves into a tool-call. |
|
||||
| `packages/coord` (status.ts) | Mission health, task progress, next task | **Hermes toolset: `mission`** (same) | Status readout fits naturally as a tool-call. No gateway needed. |
|
||||
| `packages/prdy` | PRD generation wizard | **Hermes skill: `prdy`** | PRD generation is a prompt + template problem, not infrastructure. A Hermes skill with templates is the right fit. |
|
||||
| `plugins/mosaic-framework` | before_agent_start + subagent_spawning hooks | **Hermes system prompt injection** | Hermes already injects system context via skills and config. The framework preamble and worktree rules become standard Hermes skills loaded by the orchestrator profile. |
|
||||
| `plugins/macp` | OpenClaw ACP bridge (spawn codex/claude) | **Already covered by `delegate_task` + ACP** | Hermes already has ACP support and delegate_task. The MACP bridge is redundant when running natively in Hermes. |
|
||||
| Churn detection (planned) | Detect compaction loops, repeated tool calls, no progress | **Hermes middleware** | This needs to live inside Hermes's turn loop where it can observe tool-call patterns. Mosaic can't see this from outside. |
|
||||
| Handoff packet (planned) | Structured context summary for session rotation | **Hermes toolset: `mission`** | Handoff is a serialization of mission + session state. Hermes owns the session, so it should own the handoff. |
|
||||
|
||||
### What Hermes already has that replaces Mosaic infrastructure
|
||||
|
||||
| Mosaic concept | Hermes equivalent | Notes |
|
||||
| -------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| Gateway (NestJS) | Hermes gateway | Hermes already has a gateway with WebSocket, Discord, Telegram, CLI. No need for a second one. |
|
||||
| Pi SDK agent runtime | Hermes agent loop | Hermes IS the agent runtime. OpenClaw's Pi SDK is a different runtime that Mosaic targets. |
|
||||
| MACP ACP bridge | `delegate_task` + ACP tools | Same capability, already native. |
|
||||
| Session identity | Hermes session IDs + process_registry | Hermes already tracks session identity, PIDs, and background processes. |
|
||||
| Task execution board | Hermes Kanban | Fully functional SQLite-backed Kanban with dispatcher, triage, events, comments. |
|
||||
| Worker spawning | Hermes dispatcher + cron | Kanban dispatcher + cron already handle this. |
|
||||
| Context injection | Hermes skills + system prompt | Skills are loaded at session start and injected into context. Exactly what mosaic-framework plugin does. |
|
||||
| File checkpoints | Hermes checkpoint_manager | Already tracks file mutations with shadow git. |
|
||||
|
||||
### What Mosaic keeps as its own entity
|
||||
|
||||
| Component | Why it stays in Mosaic |
|
||||
| --------------------- | --------------------------------------------------- |
|
||||
| `apps/gateway` | NestJS API surface — Mosaic's web platform offering |
|
||||
| `apps/web` | Next.js dashboard — Mosaic's UI offering |
|
||||
| `packages/types` | Shared TS contracts for Mosaic gateway plugins |
|
||||
| `packages/db` | Drizzle ORM + PG — Mosaic's data layer |
|
||||
| `packages/auth` | BetterAuth — Mosaic's auth system |
|
||||
| `packages/brain` | PG-backed data layer for Mosaic web app |
|
||||
| `packages/queue` | Valkey task queue for Mosaic gateway |
|
||||
| `plugins/discord` | OpenClaw Discord plugin |
|
||||
| `plugins/telegram` | OpenClaw Telegram plugin |
|
||||
| `packages/mosaic` CLI | The `mosaic` CLI — Mosaic's own command surface |
|
||||
|
||||
---
|
||||
|
||||
## Architecture: `mission` Toolset for Hermes
|
||||
|
||||
### New files under `/opt/hermes/tools/`
|
||||
|
||||
```
|
||||
mission_tools.py — Tool-call surface (mission_create, mission_status,
|
||||
mission_next_task, mission_update_task, mission_handoff,
|
||||
mission_resume)
|
||||
mission_state.py — State management (read/write mission.json, parse TASKS.md,
|
||||
parse MISSION-MANIFEST.md)
|
||||
mission_churn.py — Churn detection (tool-loop counter, compaction counter,
|
||||
progress scorer)
|
||||
mission_handoff.py — Handoff packet generation and loading
|
||||
```
|
||||
|
||||
### Tool-calls exposed to the agent
|
||||
|
||||
| Tool | What it does | When the agent calls it |
|
||||
| --------------------- | --------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| `mission_create` | Initialize mission.json + TASKS.md + MISSION-MANIFEST.md in a project dir | When starting a new mission |
|
||||
| `mission_status` | Read current mission state, milestone progress, next task, active session | At session start, or when checking progress |
|
||||
| `mission_next_task` | Find the next `not-started` task whose dependencies are met, return its full spec | When the agent needs work to do |
|
||||
| `mission_update_task` | Update a task row status in TASKS.md | When completing or blocking a task |
|
||||
| `mission_handoff` | Generate a handoff packet from current session context + mission state | Before session rotation or at session end |
|
||||
| `mission_resume` | Load a handoff packet and inject it as context for the new session | At session start after rotation |
|
||||
|
||||
### Toolset registration
|
||||
|
||||
The `mission` toolset follows the same pattern as `kanban`:
|
||||
|
||||
1. **Gating**: Tools are available when:
|
||||
- The profile has `mission` in its toolsets config, OR
|
||||
- A `HERMES_MISSION_DIR` env var is set (cron/dispatcher spawned workers)
|
||||
2. **File conventions**: The toolset reads/writes the same file formats as Mosaic `packages/coord`:
|
||||
- `.mosaic/orchestrator/mission.json` — mission state
|
||||
- `docs/TASKS.md` — task table
|
||||
- `docs/MISSION-MANIFEST.md` — mission manifest
|
||||
- `docs/scratchpads/<id>.md` — session scratchpad
|
||||
|
||||
3. **Kanban bridge**: Optional bidirectional sync between mission TASKS.md rows and Kanban task cards, so the dashboard sees mission tasks.
|
||||
|
||||
### Churn detection (middleware)
|
||||
|
||||
Churn detection lives in Hermes's turn loop, NOT as a tool-call. It observes:
|
||||
|
||||
- Repeated compaction events (context window pressure)
|
||||
- Identical tool-call sequences (loop detection)
|
||||
- No file state changes across N turns
|
||||
- Repeated permission denials
|
||||
|
||||
When churn score exceeds threshold:
|
||||
|
||||
1. `mission_handoff` is called automatically
|
||||
2. Session is rotated (fresh context window)
|
||||
3. `mission_resume` is called in the new session
|
||||
|
||||
This is new infrastructure that only Hermes can provide (Mosaic runs outside the agent loop).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Tasks
|
||||
|
||||
### Phase 1: Core state management (Python port of coord)
|
||||
|
||||
| Task | Files | Estimate |
|
||||
| -------------------------------------------------- | ----------------------------- | -------- |
|
||||
| 1.1 Port mission.json read/write to Python | `mission_state.py` | 2h |
|
||||
| 1.2 Port TASKS.md parser to Python | `mission_state.py` | 2h |
|
||||
| 1.3 Port MISSION-MANIFEST.md reader to Python | `mission_state.py` | 1h |
|
||||
| 1.4 Implement `mission_create` tool-call | `mission_tools.py` | 1h |
|
||||
| 1.5 Implement `mission_status` tool-call | `mission_tools.py` | 1h |
|
||||
| 1.6 Implement `mission_next_task` tool-call | `mission_tools.py` | 1h |
|
||||
| 1.7 Implement `mission_update_task` tool-call | `mission_tools.py` | 1h |
|
||||
| 1.8 Register `mission` toolset in Hermes registry | `tools/registry.py` | 30m |
|
||||
| 1.9 Add `mission` to orchestrator profile toolsets | `config.yaml` | 10m |
|
||||
| 1.10 Write unit tests for mission_state | `tests/test_mission_state.py` | 2h |
|
||||
| 1.11 Write unit tests for TASKS.md parser | `tests/test_tasks_parser.py` | 1h |
|
||||
|
||||
**Phase 1 estimate:** ~13h
|
||||
|
||||
### Phase 2: Handoff and session continuity
|
||||
|
||||
| Task | Files | Estimate |
|
||||
| ------------------------------------------------- | ---------------------------------------- | -------- |
|
||||
| 2.1 Define handoff packet schema (JSON) | `mission_handoff.py` | 1h |
|
||||
| 2.2 Implement `mission_handoff` tool-call | `mission_handoff.py`, `mission_tools.py` | 2h |
|
||||
| 2.3 Implement `mission_resume` tool-call | `mission_handoff.py`, `mission_tools.py` | 2h |
|
||||
| 2.4 Wire handoff into session start (auto-resume) | agent loop hook | 2h |
|
||||
| 2.5 Write tests for handoff round-trip | `tests/test_mission_handoff.py` | 1h |
|
||||
|
||||
**Phase 2 estimate:** ~8h
|
||||
|
||||
### Phase 3: Churn detection
|
||||
|
||||
| Task | Files | Estimate |
|
||||
| -------------------------------------------------------------- | ----------------------------- | -------- |
|
||||
| 3.1 Define churn signal weights and thresholds | `mission_churn.py` | 1h |
|
||||
| 3.2 Implement tool-loop detector (consecutive identical calls) | `mission_churn.py` | 2h |
|
||||
| 3.3 Implement compaction pressure detector | `mission_churn.py` | 1h |
|
||||
| 3.4 Implement progress scorer (file state delta) | `mission_churn.py` | 2h |
|
||||
| 3.5 Wire churn scoring into agent turn loop | agent loop middleware | 2h |
|
||||
| 3.6 Implement auto-rotation trigger | agent loop + handoff | 2h |
|
||||
| 3.7 Write tests for churn scoring | `tests/test_mission_churn.py` | 1h |
|
||||
|
||||
**Phase 3 estimate:** ~11h
|
||||
|
||||
### Phase 4: Kanban bridge + CLI surface
|
||||
|
||||
| Task | Files | Estimate |
|
||||
| ---------------------------------------------------- | ------------------------ | -------- |
|
||||
| 4.1 Implement TASKS.md → Kanban sync (one-way first) | `mission_kanban_sync.py` | 2h |
|
||||
| 4.2 Add `hermes mission` CLI subcommand | `mission_cli.py` | 2h |
|
||||
| 4.3 Add `hermes mission status` command | `mission_cli.py` | 1h |
|
||||
| 4.4 Add `hermes mission init` command | `mission_cli.py` | 1h |
|
||||
| 4.5 Add `hermes mission handoff` command | `mission_cli.py` | 1h |
|
||||
| 4.6 Add `hermes mission resume` command | `mission_cli.py` | 1h |
|
||||
|
||||
**Phase 4 estimate:** ~8h
|
||||
|
||||
---
|
||||
|
||||
## File Format Compatibility
|
||||
|
||||
The Python implementation MUST read and write the exact same file formats as Mosaic's TypeScript `packages/coord`. This means:
|
||||
|
||||
1. **mission.json** schema is identical to `Mission` type in `packages/coord/src/types.ts`
|
||||
2. **TASKS.md** table format is identical to what `packages/coord/src/tasks-file.ts` parses
|
||||
3. **MISSION-MANIFEST.md** is free-form markdown (no parser needed — just read the file)
|
||||
4. **Handoff packets** are a new JSON format defined in this toolset (Mosaic doesn't have them yet)
|
||||
|
||||
This way a project can use Hermes mission tools OR Mosaic `mosaic coord` commands interchangeably. The files are the contract.
|
||||
|
||||
---
|
||||
|
||||
## Relationship Diagram
|
||||
|
||||
```
|
||||
Mosaic Stack (TypeScript) Hermes Agent (Python)
|
||||
┌─────────────────────────┐ ┌─────────────────────────┐
|
||||
│ packages/coord │ │ tools/mission_tools.py │
|
||||
│ ├─ mission.ts │◄──────►│ ├─ mission_state.py │
|
||||
│ ├─ tasks-file.ts │ same │ ├─ mission_handoff.py │
|
||||
│ ├─ status.ts │ files │ ├─ mission_churn.py │
|
||||
│ └─ runner.ts │ │ └─ mission_tools.py │
|
||||
│ │ │ │
|
||||
│ packages/prdy │ │ skills/prdy/ │
|
||||
│ └─ templates, wizard │◄──────►│ └─ SKILL.md + templates │
|
||||
│ │ │ │
|
||||
│ plugins/mosaic-framework│ │ skills/ (existing) │
|
||||
│ └─ context injection │◄──────►│ └─ kanban-orchestrator │
|
||||
│ │ │ + mosaic-coding-* │
|
||||
│ plugins/macp │ │ tools/delegate_task.py │
|
||||
│ └─ ACP bridge │◄──────►│ └─ already covers this │
|
||||
│ │ │ │
|
||||
│ (stays in Mosaic) │ │ tools/kanban_tools.py │
|
||||
│ apps/gateway │ │ └─ Hermes Kanban DB │
|
||||
│ apps/web │ │ │
|
||||
│ packages/db │ │ tools/cronjob_tools.py │
|
||||
│ packages/queue │ │ └─ already covers cron │
|
||||
└─────────────────────────┘ └─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should the `mission` toolset ship with Hermes core, or as a plugin?**
|
||||
- Recommendation: ship as a **built-in toolset** (like `kanban`) since mission coordination is a core agent capability, not an optional integration. The file formats are stable and the code is small.
|
||||
|
||||
2. **Should churn detection be per-profile configurable?**
|
||||
- Recommendation: yes. Add `mission.churn_threshold` and `mission.churn_weights` to profile config.yaml. Default threshold = 5 consecutive no-progress turns.
|
||||
|
||||
3. **Should handoff packets live in the project dir or in Hermes home?**
|
||||
- Recommendation: **project dir** (`.mosaic/handoffs/<session-id>.json`). This keeps them version-controlled and accessible regardless of which agent runtime picks up the project.
|
||||
|
||||
4. **Bidirectional Kanban sync?**
|
||||
- Recommendation: **one-way first** (TASKS.md → Kanban). Bidirectional adds conflict resolution complexity. Ship one-way, add reverse sync in v2 if needed.
|
||||
|
||||
5. **PRD generation — skill or tool-call?**
|
||||
- Recommendation: **skill** (`prdy`). PRD generation is a prompt engineering problem with templates. Skills already handle this pattern perfectly.
|
||||
@@ -1,236 +0,0 @@
|
||||
# Mosaic Stack ↔ Hermes Coordination Resilience
|
||||
|
||||
> Purpose: document the self-healing coordination patterns that emerged while implementing the Hermes mission toolset, distress-card protocol, and auto-heal watchers, so the same mechanics can be reimplemented in Mosaic Stack or any similar agent platform.
|
||||
|
||||
## Summary
|
||||
|
||||
The coordination layer should be treated as a system of mechanical recovery loops rather than a single interactive agent session.
|
||||
|
||||
## SIBKISS operational summary
|
||||
|
||||
- mission on
|
||||
- heartbeat always
|
||||
- resume from packet
|
||||
- block with `[BLOCKED]`
|
||||
- reassign
|
||||
- keep tasks tiny
|
||||
- auto-heal dead workers
|
||||
|
||||
The design has four parts:
|
||||
|
||||
1. Atomic task decomposition — workers operate only within a small, explicit scope.
|
||||
2. Distress signaling — workers create a standardized `[BLOCKED]` card when they encounter a blocker outside their scope.
|
||||
3. Mechanical fallback — if the worker cannot phone home because of rate limits or dead context, a cron-style watcher synthesizes the distress card for them.
|
||||
4. Auto-heal / reassignment — stale workers are reaped, crash-loops are reset, and rate-limited work is reassigned to a different profile/provider.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Observed failure modes:
|
||||
|
||||
- Scope creep: a worker completes the target fix, then spends the rest of its budget chasing downstream cascade work.
|
||||
- Silent failure / dead worker: the worker PID is gone, but the task remains running or blocked.
|
||||
- Rate-limited worker: the worker is too constrained to create a help card itself, so it spins or fails without a clean handoff.
|
||||
|
||||
The answer is not to raise iteration caps or ask the worker to keep trying longer. The answer is to make the coordination layer self-healing and the work items atomic.
|
||||
|
||||
## Core workflow
|
||||
|
||||
### 1) Atomic task boundaries
|
||||
|
||||
Every task should have:
|
||||
|
||||
- one concern
|
||||
- explicit files/packages in scope
|
||||
- explicit files/packages out of scope
|
||||
- a maximum file count if possible
|
||||
- a stated expected iteration budget
|
||||
|
||||
When a worker discovers work outside scope, it must stop fixing it and hand off.
|
||||
|
||||
### 2) Worker-authored distress card
|
||||
|
||||
If the worker can still report status, it creates a card like:
|
||||
|
||||
- Title: `[BLOCKED] t_<source_id> <blocker_type>`
|
||||
- Assignee: `tuesday` / orchestrator role
|
||||
- Status: `ready`
|
||||
- Body: standardized distress template with source task, blocker type, completed work, cannot-touch scope, and needed action
|
||||
|
||||
The orchestrator receives the card, acts on it, and closes the loop.
|
||||
|
||||
## Routing rules
|
||||
|
||||
### Distress card routing
|
||||
|
||||
- Title: `[BLOCKED] t_<source_id> <blocker_type>`
|
||||
- Assignee: `tuesday` / orchestrator role
|
||||
- Status: `ready`
|
||||
- Body: standardized distress template with source task, blocker type, completed work, cannot-touch scope, and needed action
|
||||
- Source task stays linked to the distress card so the recovery trail is auditable
|
||||
|
||||
The orchestrator receives the card, acts on it, and closes the loop.
|
||||
|
||||
### 3) Mechanical fallback for rate-limited workers
|
||||
|
||||
If the worker is too rate-limited or unstable to create the distress card itself, a no-agent watcher must synthesize the card from the task row and failure metadata.
|
||||
|
||||
That watcher should:
|
||||
|
||||
- inspect running / blocked tasks
|
||||
- detect repeated 429 / 503 / overload errors
|
||||
- create the same standardized `[BLOCKED]` card on behalf of the worker
|
||||
- link the distress card to the source task
|
||||
- add a comment to the source task
|
||||
- allow the dispatcher to pick up the new card immediately
|
||||
|
||||
This is the key fix for the logic issue: the worker does not need to be able to phone home if the watcher can do it mechanically.
|
||||
|
||||
### 4) Auto-heal for dead workers
|
||||
|
||||
A separate no-agent watcher should:
|
||||
|
||||
- reap dead PIDs stuck in `running`
|
||||
- reset crash-loops whose failures are infrastructure-related
|
||||
- escalate tasks that have been reset too many times
|
||||
|
||||
This watcher prevents stale tasks from clogging the board and keeps the dispatch queue moving.
|
||||
|
||||
## Distress card contract
|
||||
|
||||
### Canonical title
|
||||
|
||||
```text
|
||||
[BLOCKED] t_<source_task_id> <blocker_type>
|
||||
```
|
||||
|
||||
### Canonical blocker types
|
||||
|
||||
- `scope_boundary`
|
||||
- `env_blocker`
|
||||
- `credential_failure`
|
||||
- `dependency`
|
||||
- `iteration_budget`
|
||||
- `rate_limited`
|
||||
|
||||
### Canonical body
|
||||
|
||||
```markdown
|
||||
## Distress Signal
|
||||
|
||||
- Blocked task: t_xxx
|
||||
- Worker: <profile_name>
|
||||
- Branch: <git_branch_name>
|
||||
- Workspace: <path>
|
||||
- Blocker type: <type>
|
||||
- Completed: <what was done>
|
||||
- Cannot touch: <out-of-scope packages/files>
|
||||
- Needs: <what the orchestrator should do>
|
||||
- State: committed | uncommitted | stashed(<stash_name>)
|
||||
|
||||
## Scope Guard
|
||||
|
||||
DO NOT touch: anything outside diagnosing and remediating the blocker described above
|
||||
Only fix: assign, split, reassign, or unblock the source task
|
||||
```
|
||||
|
||||
## Routing rules
|
||||
|
||||
### Distress card routing
|
||||
|
||||
- `[BLOCKED]` title prefix should bypass normal triage.
|
||||
- The card should go directly to the orchestration profile.
|
||||
- The orchestrator should start from a clean session each time.
|
||||
|
||||
### Rate-limit fallback
|
||||
|
||||
When the source task is rate-limited:
|
||||
|
||||
- do not keep retrying in the worker
|
||||
- let the watcher synthesize the distress card
|
||||
- have the orchestrator reassign the source task to a different profile/provider combo
|
||||
|
||||
### Provider fallback principle
|
||||
|
||||
Never reassign rate-limited work back to the same provider if the failure was provider pressure. Use a different provider when possible.
|
||||
|
||||
### Suggested fallback order
|
||||
|
||||
1. Keep the current task body and scope guards intact.
|
||||
2. Reassign to a different profile on a different provider.
|
||||
3. If that is impossible, reassign to a different profile on the same provider only for non-rate-limit blockers.
|
||||
4. If repeated failures continue, split the task into a narrower atomic card.
|
||||
|
||||
## Related recovery docs
|
||||
|
||||
- Mission packet recovery contract: `/opt/hermes/docs/mission-toolset-heartbeat.md`
|
||||
- Hermes mission implementation plan: `/opt/hermes/docs/plans/mission-toolset-implementation.md`
|
||||
- The same packet-first resume rule applies: inspect the latest packet before re-reading mission files.
|
||||
- New-session trigger: when a profile config changes, start a fresh session or `/reset` so the updated toolset is actually loaded.
|
||||
|
||||
## Watchers to implement
|
||||
|
||||
### Auto-heal watcher
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- reap stale workers
|
||||
- reset dead-PID crash loops
|
||||
- track reset counts
|
||||
- escalate after repeated resets
|
||||
|
||||
### Distress synthesizer watcher
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- detect rate-limited / stuck workers
|
||||
- create `[BLOCKED]` cards mechanically
|
||||
- link the card to the source task
|
||||
- leave a comment for traceability
|
||||
|
||||
### Iteration-budget watcher
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- detect long-running tasks and repeated failure patterns
|
||||
- recommend splits when a task is clearly over-scoped
|
||||
- report tasks that need human review after multiple resets
|
||||
|
||||
## Operational principle
|
||||
|
||||
If a task cannot cleanly finish within its atomic scope, the right response is to surface a smaller coordination problem, not to keep burning context.
|
||||
|
||||
This is what makes the system robust across compaction, rate limits, and dead workers.
|
||||
|
||||
## Suggested implementation order
|
||||
|
||||
1. Atomic task metadata in task bodies
|
||||
2. Worker-authored distress card protocol
|
||||
3. Mechanical distress synthesizer watcher
|
||||
4. Auto-heal watcher for dead workers
|
||||
5. Orchestrator routing rules for `[BLOCKED]`
|
||||
6. Rate-limit fallback / model reassignment table
|
||||
|
||||
## Where this fits in Hermes
|
||||
|
||||
- Kanban = durable work graph and status engine
|
||||
- Watchers = mechanical healing and distress synthesis
|
||||
- Orchestrator = split / reassign / unblock decision-maker
|
||||
- Workers = execution inside atomic task boundaries
|
||||
|
||||
## Where this fits in Mosaic Stack
|
||||
|
||||
- PRD / coordination infra should encode the same patterns
|
||||
- Mosaic can use the same distress-card contract and watcher logic
|
||||
- The coordination model should be runtime-agnostic: any agent system can use it if it can write a task card and react to a ready queue
|
||||
|
||||
## Cross-project takeaway
|
||||
|
||||
The important pattern is not the specific tool names. It is the mechanical feedback loop:
|
||||
|
||||
- detect failure without requiring the failing worker to succeed
|
||||
- create a standardized help artifact
|
||||
- route that artifact to a fresh orchestrator context
|
||||
- repair the assignment graph
|
||||
- continue the mission
|
||||
|
||||
That pattern is reusable anywhere.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
# Documentation Catalog and Truth Audit Plan
|
||||
|
||||
**Task:** DOCS-IA-002
|
||||
**Internal reference:** `TASKS:DOCS-IA-002`
|
||||
**Goal:** Catalog the existing Mosaic Stack documentation, identify its intended destination in the new structure, and audit validity/truthfulness against repository evidence before moving or rewriting content.
|
||||
|
||||
## Scope
|
||||
|
||||
- Current root-level documentation and newly established structure files.
|
||||
- All Markdown and relevant YAML/API artifacts under `docs/_old_structure/`.
|
||||
- Repository references from source, tests, scripts, guides, and root README files.
|
||||
- Static truth checks for paths, commands, package names, environment variables, API artifacts, and explicit document status.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not move, delete, or rewrite documentation.
|
||||
- Do not decide product requirements that belong in `docs/PRD.md`.
|
||||
- Do not mark a claim true solely because it appears in a document.
|
||||
- Do not modify active `docs/TASKS.md` because it has a single-writer orchestrator policy.
|
||||
|
||||
## Parallel discovery lanes
|
||||
|
||||
1. **File catalog:** path, title, type, size, line count, current/archive location, last repository change.
|
||||
2. **Navigation audit:** Markdown and Obsidian links, target resolution, broken-link clusters, source references.
|
||||
3. **Code-surface audit:** package names, scripts, entry points, referenced docs, paths used by tests and source.
|
||||
4. **Truth triage:** compare current claims against executable code/config/tests and label evidence strength.
|
||||
|
||||
Parallel lanes produce findings only. The coordinator reconciles them into one report so truth labels remain consistent.
|
||||
|
||||
## Evidence statuses
|
||||
|
||||
- `verified`: directly supported by current source/config/tests or a reproducible command.
|
||||
- `partially-verified`: some claims are supported, but the page contains unverified or time-sensitive claims.
|
||||
- `contradicted`: current repository evidence conflicts with a material claim.
|
||||
- `stale`: formerly meaningful but no longer aligned with current paths, APIs, or state.
|
||||
- `historical`: intentionally retained record of past state; not a current instruction.
|
||||
- `draft`: normative proposal or requirement, not a statement of shipped behavior.
|
||||
- `unverified`: not yet checked or insufficient evidence exists.
|
||||
- `incomplete`: empty or structurally insufficient for its stated role.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `docs/reports/documentation/2026-08-10-docs-catalog-audit.md` — human-readable catalog, findings, evidence, and migration recommendations.
|
||||
- `docs/scratchpads/DOCS-IA-002-catalog-audit.md` — task progress and command evidence.
|
||||
|
||||
A machine-readable intermediate inventory may remain under `/tmp`; it is not canonical unless explicitly copied into the report.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Every current documentation file and every archived documentation file is counted and assigned a preliminary disposition.
|
||||
- Broken internal links and repository references are enumerated with evidence.
|
||||
- Truth labels distinguish current behavior, normative intent, historical evidence, and unresolved claims.
|
||||
- High-risk contradictions and source/test dependencies are called out before any migration.
|
||||
- The report recommends migration order and identifies pages requiring human/product-owner validation.
|
||||
- No existing documentation or unrelated working-tree state is modified.
|
||||
@@ -0,0 +1,182 @@
|
||||
# Documentation Information Architecture Design
|
||||
|
||||
**Status:** Approved
|
||||
**Date:** 2026-08-10
|
||||
**Scope:** Establish the canonical structure and authoring rules for `docs/` before migrating or rewriting existing documentation.
|
||||
|
||||
## Goal
|
||||
|
||||
Create a clean, human-readable, Obsidian-compatible documentation system for Mosaic Stack. The system must make the relationships between requirements, architecture, guides, API contracts, operational procedures, evidence, and work tracking visible without duplicating canonical content.
|
||||
|
||||
## Decision
|
||||
|
||||
Use a single root-level documentation atlas in `docs/README.md`, organized around audience-specific guide books and dedicated artifact directories. Retire `docs/mosaic-stack/` as a content boundary; it adds no useful ownership distinction once the documentation system has explicit root guides and cross-links.
|
||||
|
||||
The target structure is:
|
||||
|
||||
```text
|
||||
docs/
|
||||
├── README.md
|
||||
├── PRD.md
|
||||
├── TASKS.md
|
||||
├── SITEMAP.md
|
||||
│
|
||||
├── USER-GUIDE/
|
||||
│ ├── README.md
|
||||
│ ├── getting-started/
|
||||
│ ├── concepts/
|
||||
│ ├── workflows/
|
||||
│ └── troubleshooting/
|
||||
│
|
||||
├── ADMIN-GUIDE/
|
||||
│ ├── README.md
|
||||
│ ├── installation/
|
||||
│ ├── configuration/
|
||||
│ ├── deployment/
|
||||
│ ├── operations/
|
||||
│ ├── security/
|
||||
│ └── recovery/
|
||||
│
|
||||
├── DEVELOPER-GUIDE/
|
||||
│ ├── README.md
|
||||
│ ├── architecture/
|
||||
│ │ ├── README.md
|
||||
│ │ ├── system-overview.md
|
||||
│ │ ├── component-map.md
|
||||
│ │ ├── data-flow.md
|
||||
│ │ ├── security-model.md
|
||||
│ │ ├── decisions/
|
||||
│ │ └── rfcs/
|
||||
│ ├── packages/
|
||||
│ ├── local-development/
|
||||
│ ├── testing/
|
||||
│ ├── contributing/
|
||||
│ └── integrations/
|
||||
│
|
||||
├── API/
|
||||
│ ├── README.md
|
||||
│ ├── OPENAPI.yaml
|
||||
│ └── ENDPOINTS.md
|
||||
│
|
||||
├── assets/
|
||||
├── reports/
|
||||
│ ├── code-review/
|
||||
│ ├── documentation/
|
||||
│ ├── qa/
|
||||
│ ├── security/
|
||||
│ └── deferred/
|
||||
├── tasks/
|
||||
├── plans/
|
||||
├── scratchpads/
|
||||
├── releases/
|
||||
├── archive/
|
||||
└── _old_structure/ # temporary migration quarantine; read-only
|
||||
```
|
||||
|
||||
`docs/plans/` is a workflow directory for approved design and implementation plans. It is not a substitute for the canonical requirements document, active task ledger, or guide books.
|
||||
|
||||
## Information architecture
|
||||
|
||||
### Root control documents
|
||||
|
||||
- `docs/README.md` is the documentation contract, placement guide, and top-level entry point.
|
||||
- `docs/PRD.md` is the canonical product and requirements source. Requirements must not be silently redefined in guides or reports.
|
||||
- `docs/TASKS.md` is the active orchestrator rollup. Its single-writer policy remains authoritative.
|
||||
- `docs/SITEMAP.md` is the complete human navigation index. It must be updated when canonical pages are added, moved, renamed, or retired.
|
||||
|
||||
### Audience books
|
||||
|
||||
- `USER-GUIDE/` contains end-user workflows, user-visible behavior, concepts needed to operate the product, and user troubleshooting.
|
||||
- `ADMIN-GUIDE/` contains installation, configuration, deployment, operations, security controls, recovery, and incident procedures.
|
||||
- `DEVELOPER-GUIDE/` contains architecture, package/component documentation, local development, testing, contribution rules, and integration authoring.
|
||||
- `API/` contains the machine-readable OpenAPI contract and its human-readable endpoint index.
|
||||
|
||||
Audience books are task-oriented. They link to canonical architecture, requirements, API, and operational pages rather than copying those pages.
|
||||
|
||||
### Artifact directories
|
||||
|
||||
- `assets/` contains diagrams and documentation media referenced by canonical pages.
|
||||
- `reports/` contains evidence and findings. Reports are informative and do not override the PRD or normative contracts.
|
||||
- `tasks/` contains archived task snapshots and orchestrator learnings. Active orchestration remains in root `TASKS.md`.
|
||||
- `plans/` contains approved design and implementation plans.
|
||||
- `scratchpads/` contains active, task-specific working notes and verification evidence. Scratchpads are not product documentation.
|
||||
- `releases/` contains release notes and release-specific migration or compatibility notes.
|
||||
- `archive/` contains superseded but intentionally retained documentation. Archived pages must state their replacement or reason for retention.
|
||||
- `_old_structure/` is a temporary migration quarantine. It is read-only, is not indexed as current documentation, and is not an authoring destination.
|
||||
|
||||
## Placement rules
|
||||
|
||||
| Content | Required location | Do not place it in |
|
||||
| ----------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| Product requirements and acceptance criteria | `docs/PRD.md` or an explicitly scoped PRD under a guide/workstream | A scratchpad, report, or README-only note |
|
||||
| Active task status | `docs/TASKS.md` | A guide page or personal scratchpad |
|
||||
| User workflow | `docs/USER-GUIDE/<chapter>/` | The docs root |
|
||||
| Installation, deployment, or recovery procedure | `docs/ADMIN-GUIDE/<chapter>/` | `README.md` or a report |
|
||||
| Architecture, component, package, ADR, or RFC content | `docs/DEVELOPER-GUIDE/architecture/` or its relevant developer chapter | `docs/mosaic-stack/` or the docs root |
|
||||
| API contract | `docs/API/OPENAPI.yaml` and `docs/API/ENDPOINTS.md` | A guide-only description |
|
||||
| Documentation navigation | `docs/SITEMAP.md` | A duplicated ad-hoc index |
|
||||
| Design or implementation plan | `docs/plans/` | `docs/scratchpads/` |
|
||||
| Active task working notes | `docs/scratchpads/<task-id>-<slug>.md` | The docs root or a canonical guide |
|
||||
| Review, QA, audit, security, or deferral evidence | `docs/reports/<category>/` | A canonical guide page |
|
||||
| Archived task snapshot | `docs/tasks/` | Root `TASKS.md` unless it is active |
|
||||
| Release notes | `docs/releases/` | The docs root |
|
||||
| Diagram or image | `docs/assets/` or an owning chapter asset directory | An external personal path |
|
||||
| Superseded documentation | `docs/archive/` | `_old_structure/` after migration completes |
|
||||
|
||||
When a page appears to fit multiple locations, classify it by its primary reader and purpose, then link it from the other relevant indexes. Do not create copies to satisfy multiple audiences.
|
||||
|
||||
## Page conventions
|
||||
|
||||
Every canonical Markdown page should:
|
||||
|
||||
1. Cover one concern or workflow.
|
||||
2. Use a descriptive, lowercase kebab-case filename, except for established root control files and required API filenames.
|
||||
3. Begin with a clear title and a short purpose statement.
|
||||
4. Declare status and audience when the page is more than a simple index.
|
||||
5. Identify prerequisites, source-of-truth dependencies, and related pages.
|
||||
6. State whether examples and commands are current, illustrative, or held/non-operative.
|
||||
7. Include an owner or maintenance responsibility when the content is operationally sensitive.
|
||||
|
||||
Recommended front matter for canonical pages:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Human-readable page title
|
||||
type: guide
|
||||
audience: developer
|
||||
status: current
|
||||
---
|
||||
```
|
||||
|
||||
Allowed `type` values include `guide`, `concept`, `reference`, `decision`, `rfc`, and `runbook`. Allowed `audience` values are `user`, `admin`, `developer`, and `all`. Allowed `status` values are `current`, `draft`, `deprecated`, and `historical`.
|
||||
|
||||
## Obsidian and link conventions
|
||||
|
||||
- Use Obsidian wikilinks for relationship-oriented internal references, for example `[[DEVELOPER-GUIDE/architecture/component-map|Component map]]`.
|
||||
- Use normal relative Markdown links in `SITEMAP.md` and book `README.md` indexes so links render on Git hosting platforms. Obsidian can resolve these links too.
|
||||
- Use `Related`, `Depends on`, and `Referenced by` sections when a page participates in a meaningful documentation relationship.
|
||||
- Link to stable page paths, not transient line numbers or branch URLs.
|
||||
- Omit `.md` in wikilinks. Include an alias when the file path is not a readable label.
|
||||
- Use standard Markdown links for external URLs, source files, commands, and API paths.
|
||||
- Do not rely on a link to `_old_structure/` as a current navigation path. Historical references must explain why the archived page is retained and point to its replacement.
|
||||
|
||||
## Migration rules
|
||||
|
||||
This design phase does not move or rewrite content. During later migration:
|
||||
|
||||
1. Inventory current pages and classify each by audience, purpose, status, and source-of-truth role.
|
||||
2. Move canonical content into the target tree without changing meaning unless the migration task explicitly includes a rewrite.
|
||||
3. Update all repository links, source comments, tests, and `SITEMAP.md` in the same logical change.
|
||||
4. Preserve historical evidence in `reports/`, `tasks/`, `releases/`, or `archive/` rather than mixing it into current guides.
|
||||
5. Treat `_old_structure/` as read-only during migration. It may be removed only after all required links and source references are resolved.
|
||||
6. Do not add new content to `docs/mosaic-stack/`; the empty directory is retired by this design.
|
||||
7. For documents referenced by executable tests or source code, update those references deliberately and verify them before deleting the old path.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `docs/README.md` defines the complete target tree and placement rules.
|
||||
- The target tree has no `docs/mosaic-stack/` content boundary.
|
||||
- Agents can determine where to put product docs, plans, task notes, reports, scratchpads, releases, and archives without guessing.
|
||||
- The rules support both Obsidian graph navigation and Git-hosted Markdown navigation.
|
||||
- The design distinguishes normative sources from evidence and working notes.
|
||||
- Migration can proceed incrementally without treating `_old_structure/` as current documentation.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Documentation Structure README Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the starter `docs/README.md` with the normative documentation structure, placement rules, source-of-truth policy, and Obsidian-compatible navigation conventions approved for Mosaic Stack.
|
||||
|
||||
**Architecture:** Keep `docs/README.md` as the root documentation atlas and authoring contract. Use audience books for current user, administrator, and developer content; keep API contracts and operational artifacts in dedicated directories; retain `_old_structure/` as a read-only migration quarantine. Do not move or rewrite existing documentation in this slice.
|
||||
|
||||
**Tech Stack:** Markdown, YAML front matter examples, Obsidian wikilinks, relative Markdown links, Prettier.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Write the documentation structure contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/README.md`
|
||||
- Reference: `docs/plans/2026-08-10-docs-information-architecture-design.md`
|
||||
|
||||
**Step 1: Confirm the approved design and current transition constraints**
|
||||
|
||||
Verify that the README preserves these decisions:
|
||||
|
||||
- `docs/mosaic-stack/` is not a target content directory.
|
||||
- `docs/README.md` is the documentation atlas and placement contract.
|
||||
- Existing files are not moved or rewritten yet.
|
||||
- `_old_structure/` is read-only migration quarantine.
|
||||
- Root control files, audience books, API, reports, tasks, plans, scratchpads, releases, archive, and assets have distinct responsibilities.
|
||||
|
||||
**Step 2: Replace the starter README**
|
||||
|
||||
Write `docs/README.md` with these sections:
|
||||
|
||||
1. Purpose and scope.
|
||||
2. Reader entry points.
|
||||
3. Complete target directory tree, including the optional `.obsidian/` vault configuration boundary and the workflow-only `plans/` directory.
|
||||
4. Root control document responsibilities.
|
||||
5. Guide book responsibilities and chapter rules.
|
||||
6. Artifact directory responsibilities.
|
||||
7. Placement matrix for agents.
|
||||
8. Source-of-truth and precedence rules.
|
||||
9. Page naming and front matter conventions.
|
||||
10. Obsidian wikilink and Git-hosted Markdown link conventions.
|
||||
11. Authoring workflow for new or changed documentation.
|
||||
12. Migration rules for `_old_structure/`, legacy root files, and repository references.
|
||||
13. Current transitional exceptions and explicit non-goals.
|
||||
|
||||
Use future target paths as a blueprint, but clearly label directories that are not populated yet so readers do not mistake the blueprint for completed migration.
|
||||
|
||||
**Step 3: Preserve the existing Obsidian configuration boundary**
|
||||
|
||||
Document `.obsidian/` as optional vault metadata only. Do not place Markdown content, scratchpads, reports, or source-of-truth files under it, and do not modify its existing files in this task.
|
||||
|
||||
**Step 4: Keep the README portable**
|
||||
|
||||
Use ordinary relative Markdown links for indexes and Git-hosted navigation. Use Obsidian wikilinks for graph-oriented relationships such as `Related`, `Depends on`, and `Referenced by`. Do not make a current navigation path depend solely on a Git-host-incompatible wikilink.
|
||||
|
||||
**Step 5: Review the resulting document**
|
||||
|
||||
Check that an agent can answer all of these without inspecting another file:
|
||||
|
||||
- Where does a user guide go?
|
||||
- Where does an admin runbook go?
|
||||
- Where does architecture or an RFC go?
|
||||
- Where does an API contract go?
|
||||
- Where does an active scratchpad go?
|
||||
- Where does a review or QA report go?
|
||||
- Where does an approved design or implementation plan go?
|
||||
- Which files are normative, working notes, evidence, or historical?
|
||||
- What may be added directly under `docs/`?
|
||||
|
||||
**Step 6: Commit only the README**
|
||||
|
||||
Because `docs/GETTING_STARTED.md` is an unrelated pre-staged deletion, stage and commit only `docs/README.md`:
|
||||
|
||||
```bash
|
||||
git add docs/README.md
|
||||
git commit --only docs/README.md -m "docs: codify documentation structure"
|
||||
```
|
||||
|
||||
Expected: the commit contains only the README change; the existing staged deletion and orchestrator state remain outside the commit.
|
||||
|
||||
### Task 2: Verify the README-only change
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `docs/README.md`
|
||||
|
||||
**Step 1: Run Markdown formatting validation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
pnpm exec prettier --check docs/README.md
|
||||
```
|
||||
|
||||
Expected: Prettier reports the file is formatted.
|
||||
|
||||
**Step 2: Run whitespace validation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check HEAD^ -- docs/README.md
|
||||
```
|
||||
|
||||
Expected: no whitespace errors.
|
||||
|
||||
**Step 3: Validate required structural anchors**
|
||||
|
||||
Run a focused search or script confirming the README names:
|
||||
|
||||
- `PRD.md`, `TASKS.md`, and `SITEMAP.md`;
|
||||
- `USER-GUIDE/`, `ADMIN-GUIDE/`, `DEVELOPER-GUIDE/`, and `API/`;
|
||||
- `reports/`, `tasks/`, `plans/`, `scratchpads/`, `releases/`, `archive/`, and `assets/`;
|
||||
- `_old_structure/` as read-only quarantine;
|
||||
- `docs/mosaic-stack/` as retired/non-authoring;
|
||||
- Obsidian wikilinks and Git-compatible Markdown links.
|
||||
|
||||
Expected: all anchors are present and no section instructs agents to create content under `docs/mosaic-stack/`.
|
||||
|
||||
**Step 4: Confirm scope isolation**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git show --stat --oneline HEAD
|
||||
```
|
||||
|
||||
Expected: the new commit contains only `docs/README.md`; pre-existing `.mosaic/orchestrator/*`, `docs/GETTING_STARTED.md`, and `docs/.obsidian/` states remain untouched.
|
||||
|
||||
**Step 5: Record verification evidence**
|
||||
|
||||
Update the task scratchpad at `docs/scratchpads/DOCS-IA-001.md` with commands, results, known transitional gaps, and the next migration slice. Do not modify active `docs/TASKS.md`; its single-writer policy belongs to the orchestrator.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Documentation Plans
|
||||
|
||||
> **Status:** Current artifact index. Plans record approved intent and execution approach; they are not current product behavior or operational authority.
|
||||
|
||||
## Documentation migration plans
|
||||
|
||||
- [Information architecture design](2026-08-10-docs-information-architecture-design.md) — approved audience books, artifact boundaries, source-of-truth rules, and migration model.
|
||||
- [Documentation structure README implementation](2026-08-10-docs-structure-readme.md) — completed implementation plan for the documentation contract and atlas.
|
||||
- [Documentation catalog and truth audit](2026-08-10-docs-catalog-audit.md) — audit method, evidence statuses, deliverables, and acceptance criteria.
|
||||
|
||||
After a plan is delivered, update the canonical guide, contract, decision, or index. Do not cite a plan as proof that intended behavior shipped.
|
||||
|
||||
## Related
|
||||
|
||||
- [[README|Documentation contract]]
|
||||
- [[SITEMAP|Documentation sitemap]]
|
||||
- [[scratchpads/README|Documentation scratchpads]]
|
||||
@@ -1,173 +0,0 @@
|
||||
# PRD — Agent Reflection Loop (durable kernel)
|
||||
|
||||
**Issue:** [#544](http://git.mosaicstack.dev/mosaicstack/stack/issues/544)
|
||||
**Source design:** jarvis-brain `docs/planning/AGENT-REFLECTION-LOOP.md` (commit df6576fc, debate-hardened v2)
|
||||
**Status:** in-progress
|
||||
**Scope rule:** Build the **durable kernel** only. The closed calibration/skill-synthesis loop
|
||||
(design §7–§8) is **gated** behind Phase-0 experiments P1/P2/P3 and is explicitly out of scope here.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem
|
||||
|
||||
At end-of-run an agent holds context that never reaches the diff or the "done" message —
|
||||
assumptions, shortcuts, untested paths, the single most-likely way the work is wrong. That context
|
||||
is what a lead/human needs to judge trust, and it evaporates when the session ends. Capture it
|
||||
mechanically as **structured data** (`reflection.v1`), and derive a **review risk-floor** from the
|
||||
change surface so risky diffs are flagged for independent review.
|
||||
|
||||
## 2. Non-goals (gated on Phase-0)
|
||||
|
||||
- No closed calibration loop (predicted-vs-actual scoring as a routing input).
|
||||
- No skill synthesis.
|
||||
- No automated reviewer routing/dispatch. The kernel **writes** the sidecar; pickup is future work.
|
||||
|
||||
## 3. Components & exact placement (main-branch truth)
|
||||
|
||||
| # | Component | Path | Mirror |
|
||||
| --- | -------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------- |
|
||||
| a | Stop hook (capture) | `packages/mosaic/framework/tools/qa/reflect-stop-hook.sh` | `tools/qa/prevent-memory-write.sh` |
|
||||
| a | Hook registration | `packages/mosaic/framework/runtime/claude/settings.json` (`hooks.Stop`) | existing `PreToolUse`/`PostToolUse` |
|
||||
| b | JSON Schema | `packages/macp/src/schemas/reflection.v1.schema.json` | `schemas/task.schema.json` |
|
||||
| b | TS types (zod) + DTO | `packages/types/src/reflection/{index.ts,reflection.dto.ts}` + re-export from `src/index.ts` | `packages/types/src/federation/*` |
|
||||
| c | Diff risk-floor | `packages/macp/src/risk-floor.ts` (+ `__tests__/risk-floor.test.ts`, export from `src/index.ts`) | `packages/macp/src/gate-runner.ts` |
|
||||
| d | Phase-0 scripts | `scripts/analysis/reflect-{git-history,board-history,calibration}.sh` | `scripts/publish-npmjs.sh` |
|
||||
|
||||
**Activation note (deliberate deviation):** the `settings-overlays/` directory has **no merge
|
||||
mechanism** (referenced only in docs), so a hooks overlay there would be inert. The Stop hook is
|
||||
registered in the canonical `runtime/claude/settings.json` — the same file the `mosaic` launcher
|
||||
reflects into `~/.claude/settings.json` (verified byte-identical hooks live there). Still fully
|
||||
vendored in-repo.
|
||||
|
||||
## 4. `reflection.v1` schema (authoritative field list)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": "reflection.v1", // literal
|
||||
"task_ref": "string", // canonical task ref; kernel derives from REFLECTION_TASK_REF or repo+branch
|
||||
"agent": "string", // persona/runtime id (REFLECTION_AGENT or "unknown")
|
||||
"session_id": "string", // from Stop payload session_id, else "unknown"
|
||||
"timestamp": "string", // ISO-8601 UTC
|
||||
"repo": "string", // repo root basename
|
||||
"confidence": 0.0, // FLOAT [0,1] — SELF-REPORTED (optional; null if not supplied)
|
||||
"most_likely_wrong": {
|
||||
// SELF-REPORTED (optional)
|
||||
"surface": "auth|data|infra|ui|build|test|docs|none",
|
||||
"description": "string",
|
||||
},
|
||||
"known_not_in_diff": "string|null", // SELF-REPORTED: "what I know that isn't visible in the diff"
|
||||
"risk": {
|
||||
// MECHANICAL — from risk-floor
|
||||
"needs_review": true,
|
||||
"score": 0.0, // [0,1]
|
||||
"surface": "auth|data|infra|ui|build|test|docs|none",
|
||||
"reason": "string",
|
||||
},
|
||||
"files_changed": ["string"], // MECHANICAL — git diff name-only
|
||||
"provenance": {
|
||||
"source": "stop-hook",
|
||||
"reflection_attempt": 1,
|
||||
"degraded": false, // true if self-report inputs missing/unreadable
|
||||
"reflection_mode": "off|solo|orchestrated",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Mechanical vs self-reported.** A bash Stop hook cannot author the agent's self-assessment. The
|
||||
hook populates the **mechanical** fields deterministically (risk, files_changed, provenance, ids).
|
||||
The **self-reported** fields are read from an optional agent-supplied input file
|
||||
(`$REFLECTION_INPUT`, default `<repo>/.mosaic/reflection-input.json`) and merged if present;
|
||||
absent/unreadable → those fields null and `provenance.degraded=true`. This realizes the design's
|
||||
"hook is a pre-seed, not the asker" (§4).
|
||||
|
||||
## 5. Stop hook behavior (fail-closed, non-blocking)
|
||||
|
||||
1. Read Stop payload JSON from stdin.
|
||||
2. **Fail-closed:** if `REFLECTION_MODE` is unset or `off` → `exit 0` immediately (strict no-op). This
|
||||
is the global-registration safety guarantee.
|
||||
3. **Sentinel guard:** if `<sidecar>.lock` exists → `exit 0` (prevents re-fire loops). Create it,
|
||||
`trap` cleanup.
|
||||
4. Determine output dir: `$REFLECTION_DIR` else `<repo>/.mosaic/reflections/`. `mkdir -p`.
|
||||
5. Compute mechanical fields: `git diff --name-only` (HEAD + staged + worktree, best-effort),
|
||||
call risk-floor logic (inline bash port OR `node -e` into `@mosaicstack/macp` — see §6), session
|
||||
ids from payload + env.
|
||||
6. Merge optional `$REFLECTION_INPUT` self-report if readable JSON.
|
||||
7. Write `reflection.v1` to a temp file, `mv` (atomic) to `<dir>/<session>-<ts>.reflection.json`.
|
||||
8. Always `exit 0`. **Never** emit a `decision` field (Stop hooks are observational).
|
||||
|
||||
Hook must never fail the session: wrap risky steps, default to `degraded:true` on any error, exit 0.
|
||||
|
||||
## 6. Risk-floor (`packages/macp/src/risk-floor.ts`)
|
||||
|
||||
Pure, deterministic, no IO. Single source of truth for the verdict; the hook calls it via
|
||||
`node --input-type=module -e` (importing the built package) **or**, to avoid a node dependency in the
|
||||
hook path, the hook ports the same surface table. **Decision:** implement the canonical logic in TS
|
||||
(tested), and have the hook shell out to node when available, else fall back to a minimal inline
|
||||
classifier flagged `degraded:true`. (Keep the TS the authority; the inline path is a safety net.)
|
||||
|
||||
```ts
|
||||
export type ReviewSurface = 'auth' | 'data' | 'infra' | 'ui' | 'build' | 'test' | 'docs' | 'none';
|
||||
export interface RiskFloorInput {
|
||||
filesChanged: string[];
|
||||
insertions?: number;
|
||||
deletions?: number;
|
||||
}
|
||||
export interface RiskFloorVerdict {
|
||||
needs_review: boolean;
|
||||
score: number;
|
||||
surface: ReviewSurface;
|
||||
reason: string;
|
||||
}
|
||||
export function evaluateRiskFloor(input: RiskFloorInput): RiskFloorVerdict;
|
||||
```
|
||||
|
||||
Surface classification by path regex (first match wins, highest-risk surface dominates):
|
||||
|
||||
- `auth` (weight 1.0): `auth`, `login`, `session`, `token`, `permission`, `rbac`, `credential`, `secret`
|
||||
- `data` (0.9): `migration`, `prisma`, `schema`, `\.sql`, `entity`, `repository`, `seed`
|
||||
- `infra` (0.85): `docker`, `\.woodpecker`, `compose`, `traefik`, `deploy`, `helm`, `k8s`, `terraform`
|
||||
- `build` (0.6): `package.json`, `tsconfig`, `turbo.json`, `pnpm-`, `\.config\.`, `eslint`, `vite`
|
||||
- `ui` (0.4): `\.tsx`, `\.css`, `components/`, `apps/web/`
|
||||
- `test` (0.2): `\.spec\.`, `\.test\.`, `__tests__/`
|
||||
- `docs` (0.1): `\.md`, `docs/`
|
||||
- `none` (0.0): anything else
|
||||
|
||||
`needs_review = score >= THRESHOLD` (default `0.5`, overridable). `reason` names the files+surface
|
||||
that tripped it. **Subordinate to CI:** this is a _floor_ (minimum review requirement) only;
|
||||
consumers MUST treat CI/tests as authoritative above the floor (precedence: CI/tests > human merge >
|
||||
reviewer verdict > self-reflection). Documented in the module header.
|
||||
|
||||
## 7. Phase-0 experiment scripts (`scripts/analysis/`)
|
||||
|
||||
Offline, no-infra bash. Each script: `#!/usr/bin/env bash`, `set -euo pipefail`, header `Usage:` +
|
||||
`Requirements:`, flag parsing, **prints its pre-registered kill condition**, emits structured
|
||||
(JSON/markdown) output. They are harnesses + rubrics — real corpora are wired later.
|
||||
|
||||
- `reflect-git-history.sh` (**P2** — only-self-reflection bucket): scan `git log` for failure signals
|
||||
(reverts, `fix:`/`hotfix` shortly after a feature merge) over a window; classify each by which gate
|
||||
would catch it (CI / human-review / only-self-reflection) via a pre-registered heuristic; tally.
|
||||
Kill: bucket-3 near-empty → no §7/§8.
|
||||
- `reflect-board-history.sh` (**P3** — outcome detectability): given a task/board export (or the
|
||||
git history of `data/` task files), measure the fraction of completed tasks with a
|
||||
machine-detectable correct/wrong signal within 30 days. Kill: base-rate < 20% → caveat-notes only.
|
||||
- `reflect-calibration.sh` (**P1** — confidence signal): consume a labeled corpus (JSONL of
|
||||
`{confidence, correct}`), compute discrimination (AUC/lift) on the self-rated-high subset, print
|
||||
the metric vs the pre-registered chance threshold. Kill: AUC ≈ chance on the high subset → no §7/§8.
|
||||
|
||||
## 8. CI / quality gates
|
||||
|
||||
- TS packages: `pnpm typecheck` (tsc --noEmit), `pnpm lint` (eslint), `pnpm format:check`
|
||||
(prettier), `pnpm test` (vitest). ESM, NodeNext, `.js` import specifiers, `*.dto.ts` at boundaries.
|
||||
- New files in existing packages need no CI config change; add ≥1 vitest spec per new TS module.
|
||||
- Bash scripts/hook are dev/runtime tooling, not CI-built; keep them `shellcheck`-clean.
|
||||
|
||||
## 9. Acceptance criteria
|
||||
|
||||
1. `REFLECTION_MODE` unset → hook is a strict no-op (`exit 0`, no file written). **(test)**
|
||||
2. With `REFLECTION_MODE=solo`, hook writes a schema-valid `reflection.v1` with correct mechanical
|
||||
fields; self-report merged when `$REFLECTION_INPUT` present, `degraded:true` when absent.
|
||||
3. `evaluateRiskFloor` deterministic across all surfaces; unit-tested incl. auth/data/infra → review,
|
||||
docs/test → no review, empty → `none`/no review.
|
||||
4. `reflection.v1` zod type + JSON Schema agree; sidecar validates against the schema.
|
||||
5. Phase-0 scripts run offline, print kill conditions, emit structured output, shellcheck-clean.
|
||||
6. `pnpm typecheck && pnpm lint && pnpm format:check && pnpm test` green; independent review passed.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Authentik SSO Setup
|
||||
|
||||
## Create the Authentik application
|
||||
|
||||
1. In Authentik, create an OAuth2/OpenID Provider.
|
||||
2. Create an Application and link it to that provider.
|
||||
3. Copy the generated client ID and client secret.
|
||||
|
||||
## Required environment variables
|
||||
|
||||
Set these values for the gateway/auth runtime:
|
||||
|
||||
```bash
|
||||
AUTHENTIK_CLIENT_ID=your-client-id
|
||||
AUTHENTIK_CLIENT_SECRET=your-client-secret
|
||||
AUTHENTIK_ISSUER=https://authentik.example.com
|
||||
```
|
||||
|
||||
`AUTHENTIK_ISSUER` should be the Authentik base URL, for example `https://authentik.example.com`.
|
||||
|
||||
## Redirect URI
|
||||
|
||||
Configure this redirect URI in the Authentik provider/application:
|
||||
|
||||
```text
|
||||
{BETTER_AUTH_URL}/api/auth/callback/authentik
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
https://mosaic.example.com/api/auth/callback/authentik
|
||||
```
|
||||
|
||||
## Test the flow
|
||||
|
||||
1. Start the gateway with `BETTER_AUTH_URL` and the Authentik environment variables set.
|
||||
2. Open the Mosaic login flow and choose the Authentik provider.
|
||||
3. Complete the Authentik login.
|
||||
4. Confirm the browser returns to Mosaic and a session is created successfully.
|
||||
@@ -1,60 +0,0 @@
|
||||
# Chroot Agent Sandboxing — Process Isolation for Agent Tool Execution
|
||||
|
||||
> **Status:** Stub — deferred. Referenced from `2026-03-15-agent-platform-architecture.md` (Phase 7 Workspaces → Chroot Agent Sandboxing).
|
||||
> Implement after Workspaces (P8-015) is complete. Requires workspace directory structure and `WorkspaceService` to be operational.
|
||||
|
||||
**Date:** 2026-03-15
|
||||
**Packages:** `apps/gateway`
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Agent sessions can use file, git, and shell tools. Path validation in tools is defense-in-depth but insufficient alone — an agent with shell access can run `cat /opt/mosaic/.workspaces/other_user/...` and bypass gateway RBAC.
|
||||
|
||||
Chroot provides OS-level enforcement: tool processes literally cannot see outside their workspace directory.
|
||||
|
||||
---
|
||||
|
||||
## Design (Sweet Spot)
|
||||
|
||||
Chroot strikes the balance between full container isolation (too heavy per session) and path validation only (escape-prone):
|
||||
|
||||
- Gateway spawns tool processes inside a chroot rooted at the session's `sandboxDir`
|
||||
- Requires `CAP_SYS_CHROOT` capability on the gateway process (not full root)
|
||||
- Chroot environment provisioned by `WorkspaceService` on workspace creation (minimal deps: git, shell utils, language runtimes as needed)
|
||||
- Alternative for Docker deployments: Linux `unshare` namespaces (lighter, no chroot env setup)
|
||||
|
||||
---
|
||||
|
||||
## Scope (To Be Designed)
|
||||
|
||||
- [ ] Chroot environment provisioning — `WorkspaceService.provisionChroot(workspacePath)` on project creation
|
||||
- [ ] Minimal chroot deps — identify required binaries/libs per tool type (file: none; git: git binary; shell: bash, common utils)
|
||||
- [ ] Gateway capability — document `CAP_SYS_CHROOT` requirement; Dockerfile and docker-compose.yml changes
|
||||
- [ ] Tool process spawning — modify `createShellTools`, `createFileTools`, `createGitTools` to spawn via chroot wrapper
|
||||
- [ ] Docker alternative — `unshare --mount --pid --user` namespace wrapper as fallback for environments without chroot capability
|
||||
- [ ] Defense-in-depth layering — chroot + path validation both active; neither alone is sufficient
|
||||
- [ ] Chroot cleanup — integrate with `SessionGCService` / workspace deletion
|
||||
- [ ] AppArmor/SELinux profiles (v2) — restrict gateway process file access patterns for multi-tenant hardening
|
||||
|
||||
---
|
||||
|
||||
## Security Constraints
|
||||
|
||||
- What lives **inside** the chroot (agent-accessible): workspace files, git repo, language runtimes
|
||||
- What lives **outside** the chroot (gateway-only, never agent-accessible): Valkey connection, PG connection, other users' workspaces, gateway config, OTEL endpoint, credentials
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Workspaces (P8-015) — chroot is rooted at workspace directory; workspace must exist first
|
||||
- Tool hardening (P8-016) — path validation stays active as defense-in-depth alongside chroot
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Original design context: `docs/plans/2026-03-15-agent-platform-architecture.md` → "Chroot Agent Sandboxing" section
|
||||
- Current tool implementations: `apps/gateway/src/agent/tools/`
|
||||
@@ -1,53 +0,0 @@
|
||||
# Gatekeeper Service — PR Review, Quality Gates & Merge Authority
|
||||
|
||||
> **Status:** Stub — deferred. Referenced from `2026-03-15-agent-platform-architecture.md` (Phase 7 Workspaces).
|
||||
> Implement after Workspaces (P8-015) is complete and the workspace/git infrastructure is operational.
|
||||
|
||||
**Date:** 2026-03-15
|
||||
**Packages:** `apps/gateway`, `packages/types`, `packages/agent`
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Project agents create PRs but cannot review or merge their own work. A separate, isolated agent service with read-only code access and quality gate enforcement is needed to act as the authoritative merge authority.
|
||||
|
||||
The Gatekeeper existed in the old Mosaic codebase and must be ported/redesigned for mosaic-mono-v1.
|
||||
|
||||
---
|
||||
|
||||
## Key Design Constraints
|
||||
|
||||
- **Isolated trust boundary** — project agents cannot invoke Gatekeeper directly; it listens for PR events from the git provider
|
||||
- **`isSystem: true`** — system agent, not editable by users
|
||||
- **Read-only code access** — reads diffs and runs checks; cannot commit or push
|
||||
- **Quality gates required before merge** — lint, typecheck, test results must pass
|
||||
- **Cannot self-approve** — the agent that authored the PR cannot be the Gatekeeper for that PR
|
||||
|
||||
---
|
||||
|
||||
## Scope (To Be Designed)
|
||||
|
||||
- [ ] Gatekeeper agent bootstrap — system agent config, tool set, prompt engineering
|
||||
- [ ] PR event listener — Gitea/GitHub webhook integration (PR opened/updated/ready)
|
||||
- [ ] Quality gate runner — trigger CI checks, poll for results, enforce pass criteria
|
||||
- [ ] Review generation — LLM-driven code review comment generation
|
||||
- [ ] Merge execution — approve + merge when gates pass; reject with comments when they fail
|
||||
- [ ] Configurable strictness — per-project required checks, review depth
|
||||
- [ ] Trust boundary enforcement — gateway rejects Gatekeeper tool calls that exceed read-only scope
|
||||
- [ ] Audit trail — OTEL spans for all Gatekeeper decisions (approve/reject/merge)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Workspaces (P8-015) — Gatekeeper needs project workspace layout to locate code
|
||||
- Git provider API tools — PR creation/review/merge API (Gitea/GitHub/GitLab)
|
||||
- CI/CD tool integration — Woodpecker pipeline status polling
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Original design context: `docs/plans/2026-03-15-agent-platform-architecture.md` → "Gatekeeper Service" section
|
||||
- Workspace RBAC and agent trust model: same document → "RBAC & Filesystem Security"
|
||||
@@ -1,193 +0,0 @@
|
||||
# Gateway Admin Token Recovery — Implementation Plan
|
||||
|
||||
**Mission:** `cli-unification-20260404`
|
||||
**Task:** `CU-03-01` (planning only — no runtime code changes)
|
||||
**Status:** Design locked (Session 1) — BetterAuth cookie-based recovery
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The gateway installer strands operators when the admin user exists but the admin
|
||||
API token is missing. Concrete trigger:
|
||||
|
||||
- `~/.config/mosaic/gateway/meta.json` was deleted / regenerated.
|
||||
- The installer was re-run after a previous successful bootstrap.
|
||||
|
||||
Flow today (`packages/mosaic/src/commands/gateway/install.ts:375-400`):
|
||||
|
||||
1. `bootstrapFirstUser` hits `GET /api/bootstrap/status`.
|
||||
2. Server returns `needsSetup: false` because `users` count > 0.
|
||||
3. Installer logs `Admin user already exists — skipping setup. (No admin token on file — sign in via the web UI to manage tokens.)` and returns.
|
||||
4. The operator now has:
|
||||
- No token in `meta.json`.
|
||||
- No CLI path to mint a new one (`mosaic gateway <anything>` that needs the token fails).
|
||||
- `POST /api/bootstrap/setup` locked out — it only runs when `users` count is zero (`apps/gateway/src/admin/bootstrap.controller.ts:34-37`).
|
||||
- `POST /api/admin/tokens` gated by `AdminGuard` — requires either a bearer token (which they don't have) or a BetterAuth session (which they don't have in the CLI).
|
||||
|
||||
Dead end. The web UI is the only escape hatch today, and for headless installs even that may be inaccessible.
|
||||
|
||||
## 2. Design Summary
|
||||
|
||||
The BetterAuth session cookie is the authority. The operator runs
|
||||
`mosaic gateway login` to sign in with email/password, which persists a session
|
||||
cookie via `saveSession` (reusing `packages/mosaic/src/auth.ts`). With a valid
|
||||
session, `mosaic gateway config recover-token` (stranded-operator entry point)
|
||||
and `mosaic gateway config rotate-token` call the existing authenticated admin
|
||||
endpoint `POST /api/admin/tokens` using the cookie, then persist the returned
|
||||
plaintext to `meta.json` via `writeMeta`. **No new server endpoints are
|
||||
required** — `AdminGuard` already accepts BetterAuth session cookies via its
|
||||
`validateSession` path (`apps/gateway/src/admin/admin.guard.ts:90-120`).
|
||||
|
||||
## 3. Surface Contract
|
||||
|
||||
### 3.1 Server — no changes required
|
||||
|
||||
| Endpoint | Status | Notes |
|
||||
| ------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `POST /api/admin/tokens` | **Reuse as-is** | `admin-tokens.controller.ts:46-72`. Returns `{ id, label, scope, expiresAt, lastUsedAt, createdAt, plaintext }`. |
|
||||
| `GET /api/admin/tokens` | **Reuse** | Useful for `mosaic gateway config tokens list` follow-on (out of scope for CU-03-01, but trivial once auth path exists). |
|
||||
| `DELETE /api/admin/tokens/:id` | **Reuse** | Used by rotate flow for optional old-token revocation. |
|
||||
| `POST /api/bootstrap/setup` | **Unchanged** | Remains first-user-only; not part of recovery. |
|
||||
|
||||
`AdminGuard.validateSession` takes BetterAuth cookies from `request.raw.headers`
|
||||
via `fromNodeHeaders` and calls `auth.api.getSession({ headers })`. It also
|
||||
enforces `role === 'admin'`. This is exactly the path the CLI will hit with
|
||||
`Cookie: better-auth.session_token=...`.
|
||||
|
||||
**Confirmed feasible** during CU-03-01 investigation.
|
||||
|
||||
### 3.2 `mosaic gateway login`
|
||||
|
||||
Thin wrapper over the existing top-level `mosaic login`
|
||||
(`packages/mosaic/src/cli.ts:42-76`) with gateway-specific defaults pulled from
|
||||
`readMeta()`.
|
||||
|
||||
| Aspect | Behavior |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Default gateway URL | `http://${meta.host}:${meta.port}` from `readMeta()`, fallback `http://localhost:14242`. |
|
||||
| Flow | Prompt email + password -> `signIn()` -> `saveSession()`. |
|
||||
| Persistence | `~/.mosaic/session.json` via existing `saveSession` (7-day expiry). |
|
||||
| Decision | **Thin wrapper**, not alias. Rationale: defaults differ (reads `meta.json`), and discoverability under `mosaic gateway --help`. |
|
||||
| Implementation | Share the sign-in logic by extracting a small `runLogin(gatewayUrl, email?, password?)` helper; both commands call it. |
|
||||
|
||||
### 3.3 `mosaic gateway config rotate-token`
|
||||
|
||||
| Aspect | Behavior |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Precondition | Valid session (via `loadSession` + `validateSession`). On failure, print: "Not signed in — run `mosaic gateway login`" and exit non-zero. |
|
||||
| Request | `POST ${gatewayUrl}/api/admin/tokens` with header `Cookie: <session>`, body `{ label: "CLI token (rotated YYYY-MM-DD)" }`. |
|
||||
| On success | Read meta via `readMeta()`, set `meta.adminToken = plaintext`, `writeMeta(meta)`. Print the token banner (reuse `printAdminTokenBanner` shape). |
|
||||
| Old token | **Optional `--revoke-old`** flag. When set and a previous `meta.adminToken` existed, call `DELETE /api/admin/tokens/:id` after rotation. Requires listing first to find the id; punt to CU-03-02 decision. Document as nice-to-have. |
|
||||
| Exit codes | `0` success; `1` network error; `2` auth error; `3` server rejection. |
|
||||
|
||||
### 3.4 `mosaic gateway config recover-token`
|
||||
|
||||
Superset of `rotate-token` with an inline login nudge — the "stranded operator"
|
||||
entry point.
|
||||
|
||||
| Step | Action |
|
||||
| ---- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `readMeta()` — derive gateway URL. If meta is missing entirely, fall back to `--gateway` flag or default. |
|
||||
| 2 | `loadSession(gatewayUrl)` then `validateSession`. If either fails, prompt inline: email + password -> `signIn` -> `saveSession`. |
|
||||
| 3 | `POST /api/admin/tokens` with cookie, label `"Recovered via CLI YYYY-MM-DDTHH:mm"`. |
|
||||
| 4 | Persist plaintext to `meta.json` via `writeMeta`. |
|
||||
| 5 | Print the token banner and next-steps hints (e.g. `mosaic gateway status`). |
|
||||
| 6 | Exit `0`. |
|
||||
|
||||
Key property: this command is **runnable with nothing but email+password in hand**.
|
||||
It assumes the gateway is up but assumes no prior CLI session state.
|
||||
|
||||
### 3.5 File touch list (for CU-03-02..05 execution)
|
||||
|
||||
| File | Change |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `packages/mosaic/src/commands/gateway.ts` | Register `login`, `config recover-token`, `config rotate-token` subcommands under `gw`. |
|
||||
| `packages/mosaic/src/commands/gateway/config.ts` | Add `runRecoverToken`, `runRotateToken` handlers; export from module. |
|
||||
| `packages/mosaic/src/commands/gateway/login.ts` (new) | Thin wrapper calling shared `runLogin` helper with meta-derived default URL. |
|
||||
| `packages/mosaic/src/auth.ts` | No change expected. Possibly export a `requireSession(gatewayUrl)` helper (reuse pattern). |
|
||||
| `packages/mosaic/src/commands/gateway/install.ts` | `bootstrapFirstUser` branch: "user exists, no token" -> offer recovery (see Section 4). |
|
||||
|
||||
## 4. Installer Fix (CU-03-06 preview)
|
||||
|
||||
Current stranding point is `install.ts:388-395`. The fix:
|
||||
|
||||
```
|
||||
if (!status.needsSetup) {
|
||||
if (meta.adminToken) {
|
||||
// unchanged — happy path
|
||||
} else {
|
||||
// NEW: prompt "Admin exists but no token on file. Recover now? [Y/n]"
|
||||
// If yes -> call runRecoverToken(gatewayUrl) inline (interactive):
|
||||
// - prompt email + password
|
||||
// - signIn -> saveSession
|
||||
// - POST /api/admin/tokens
|
||||
// - writeMeta(meta) with returned plaintext
|
||||
// - print banner
|
||||
// If no -> print the current stranded message but include:
|
||||
// "Run `mosaic gateway config recover-token` when ready."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Shape notes (actual code lands in CU-03-06):
|
||||
|
||||
- Extract the recovery body so it can be called **both** from the standalone
|
||||
command and from `bootstrapFirstUser` without duplicating prompts.
|
||||
- Reuse the same `rl` readline interface already open in `bootstrapFirstUser`
|
||||
for the inline prompts.
|
||||
- Preserve non-interactive behavior: if `process.stdin.isTTY` is false, skip the
|
||||
prompt and emit the "run recover-token" hint only.
|
||||
|
||||
## 5. Test Strategy (CU-03-07 scope)
|
||||
|
||||
### 5.1 Happy paths
|
||||
|
||||
| Command | Scenario | Expected |
|
||||
| ------------------------------------- | ------------------------------------------------ | -------------------------------------------------------- |
|
||||
| `mosaic gateway login` | Valid creds | `session.json` written, 7-day expiry, exit 0 |
|
||||
| `mosaic gateway config rotate-token` | Valid session, server reachable | `meta.json` updated, banner printed, new token usable |
|
||||
| `mosaic gateway config recover-token` | No session, valid creds, server reachable | Prompts for creds, writes session + meta, exit 0 |
|
||||
| Installer inline recovery | Re-run after `meta.json` wipe, operator says yes | Meta restored, banner printed, no manual CLI step needed |
|
||||
|
||||
### 5.2 Error paths (must all produce actionable messages and non-zero exit)
|
||||
|
||||
| Failure | Expected handling |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| Invalid email/password | BetterAuth 401 surfaced as "Sign-in failed: <server message>", exit 2 |
|
||||
| Expired stored session | Recover command silently re-prompts; rotate command exits 2 with "run login" hint |
|
||||
| Gateway down / connection refused | "Could not reach gateway at <url>" exit 1 |
|
||||
| Server rejects token creation | Print status + body excerpt, exit 3 |
|
||||
| Meta file missing (recover) | Fall back to `--gateway` flag or default; warn that meta will be created |
|
||||
| Non-admin user | `AdminGuard` 403 surfaced as "User is not an admin", exit 2 |
|
||||
|
||||
### 5.3 Integration test (recommended)
|
||||
|
||||
Spin up gateway in test harness, create admin user via `/api/bootstrap/setup`,
|
||||
wipe `meta.json`, invoke `mosaic gateway config recover-token` programmatically,
|
||||
assert new `meta.adminToken` works against `GET /api/admin/tokens`.
|
||||
|
||||
## 6. Risks & Open Questions
|
||||
|
||||
| # | Item | Severity | Mitigation |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `AdminGuard.validateSession` calls `getSession` with `fromNodeHeaders(request.raw.headers)`. CLI sends `Cookie:` header only. Confirm BetterAuth reads from `Cookie`, not `Set-Cookie`. | Low | Confirmed — `mosaic login` + `mosaic tui` already use this flow successfully (`cli.ts:137-181`). |
|
||||
| 2 | Session cookie local expiry (7d) vs BetterAuth server-side expiry may drift. | Low | `validateSession` hits `get-session`; handle 401 by re-prompting. |
|
||||
| 3 | Label collision / unbounded token growth if operators run `recover-token` repeatedly. | Low | Include ISO timestamp in label. Optional `--revoke-old` in CU-03-02. Add `tokens list/prune` later. |
|
||||
| 4 | `mosaic login` exists at top level and `mosaic gateway login` is a wrapper — risk of confusion. | Low | Document that `gateway login` is the preferred entry for gateway operators; top-level stays for compatibility. |
|
||||
| 5 | `meta.json` write is not atomic. Crash between token creation and `writeMeta` leaves an orphan token server-side with no plaintext on disk. | Medium | Accept for now — re-running `recover-token` mints a fresh token. Document as known limitation. |
|
||||
| 6 | Non-TTY installer runs (CI, headless provisioners) cannot prompt for creds interactively. | Medium | Installer inline recovery must skip prompt when `!process.stdin.isTTY`; emit the recover-token hint. |
|
||||
| 7 | If `BETTER_AUTH_SECRET` rotates between login and recover, the session cookie is invalid — user must re-login. Acceptable but surface a clear error. | Low | Error handler maps 401 on recover -> "Session invalid; re-run `mosaic gateway login`". |
|
||||
| 8 | No MFA today. When MFA lands, BetterAuth sign-in will return a challenge, not a cookie — recovery UX will need a second prompt step. | Future | Out of scope for this mission. Flag for future CLI work. |
|
||||
|
||||
## 7. Downstream Task Hooks
|
||||
|
||||
| Task | Scope |
|
||||
| -------- | -------------------------------------------------------------------------- |
|
||||
| CU-03-02 | Implement `mosaic gateway login` wrapper + shared `runLogin` extraction. |
|
||||
| CU-03-03 | Implement `mosaic gateway config rotate-token`. |
|
||||
| CU-03-04 | Implement `mosaic gateway config recover-token`. |
|
||||
| CU-03-05 | Wire commands into `gateway.ts` registration, update `--help` copy. |
|
||||
| CU-03-06 | Installer inline recovery hook in `bootstrapFirstUser`. |
|
||||
| CU-03-07 | Tests per Section 5. |
|
||||
| CU-03-08 | Docs: update gateway install README + operator runbook with recovery flow. |
|
||||
@@ -1,60 +0,0 @@
|
||||
# Task Queue Unification — @mosaicstack/queue as Unified Orchestration Layer
|
||||
|
||||
> **Status:** Stub — deferred. Referenced from `2026-03-15-agent-platform-architecture.md` (Task Queue & Orchestration section).
|
||||
> Implement after Workspaces (P8-015) is complete. Requires workspace file structure to be in place.
|
||||
|
||||
**Date:** 2026-03-15
|
||||
**Packages:** `packages/queue`, `packages/coord`, `packages/db`, `apps/gateway`
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Two disconnected task systems exist:
|
||||
|
||||
1. **`@mosaicstack/coord`** — file-based missions (`mission.json`, `TASKS.md`), file locks, subprocess spawning. Single-machine orchestrator pattern.
|
||||
2. **PG tables** (`tasks`, `mission_tasks`, `missions`) — DB-backed CRUD, REST API, Brain repos.
|
||||
|
||||
An agent using `coord_mission_status` gets file data. The dashboard shows DB data. They are never in sync.
|
||||
|
||||
---
|
||||
|
||||
## Vision
|
||||
|
||||
`@mosaicstack/queue` becomes the unified task orchestration service bridging PG, workspace files, and Valkey:
|
||||
|
||||
- DB is source of truth for structured state (status, assignees, timestamps)
|
||||
- Workspace files (`TASKS.md`, PRDs) are working copies for agent interaction
|
||||
- Valkey handles real-time assignment queues and agent claim locks
|
||||
- Flatfile fallback for no-DB single-machine deployments (preserves `@mosaicstack/coord` pattern)
|
||||
|
||||
---
|
||||
|
||||
## Scope (To Be Designed)
|
||||
|
||||
- [ ] `@mosaicstack/queue` refactor — elevate from ioredis primitive to task orchestration service
|
||||
- [ ] DB ↔ file sync layer — writes to PG propagate to `TASKS.md`; file edits by agents sync back
|
||||
- [ ] Task assignment queue — Valkey-backed RPUSH/BLPOP for agent task claiming
|
||||
- [ ] Agent claim locks — `mosaic:queue:project:{id}:lock:{taskId}` with TTL
|
||||
- [ ] `@mosaicstack/coord` consolidation — file-based ops ported into queue service; `@mosaicstack/coord` becomes thin adapter or deprecated
|
||||
- [ ] Flatfile fallback — queue service writes JSON manifests when PG unavailable
|
||||
- [ ] Status pub/sub — real-time task status updates via Valkey pub/sub
|
||||
- [ ] Dependency resolution — block task assignment until dependencies are met
|
||||
- [ ] Orchestrator monitor — gateway process watches task queue, assigns next based on dependency graph
|
||||
- [ ] API surface — queue service exposes typed interface used by agents, gateway, and CLI
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Workspaces (P8-015) — file sync targets the workspace directory structure
|
||||
- Teams architecture (P8-007) — project ownership determines queue namespacing
|
||||
- DB schema stable — task/mission tables must not change mid-unification
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Original design context: `docs/plans/2026-03-15-agent-platform-architecture.md` → "Task Queue & Orchestration" section
|
||||
- Current `@mosaicstack/coord` implementation: `packages/coord/src/`
|
||||
- Current `@mosaicstack/queue` implementation: `packages/queue/src/`
|
||||
Reference in New Issue
Block a user