docs: plan model activity signal filter
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
# Model Activity Signal Filter Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Keep the left Model activity panel focused on prompt, genuine reasoning, meaningful status, and reviewer gates while hiding assistant narration, tool lifecycle, and turn lifecycle rows.
|
||||
|
||||
**Architecture:** Preserve the complete chronological `activityLog` and every existing backend/SSE/store contract. Add a strict allowlist at the `ModelActivityPanel` rendering boundary, derive the visible tail for stable bottom-follow behavior, and leave the central transcript untouched.
|
||||
|
||||
**Tech Stack:** React 18, TypeScript, Zustand, Vitest, Testing Library, Vite, Docker Compose, nginx-unprivileged.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Visible kinds are exactly `prompt`, `thinking`, `status`, and `gate`.
|
||||
- Hidden kinds are exactly `assistant`, `tool`, and `lifecycle`; unknown future kinds are hidden by default.
|
||||
- Do not filter by text, language, tool name, or heuristic content matching.
|
||||
- Do not change backend emission, SSE subscriptions, `sessionStore` ingestion, the central transcript, or workflow state.
|
||||
- Base the empty state and bottom-follow behavior on visible entries only.
|
||||
- Preserve warning/error labels, Markdown rendering, phase labels, accessibility, close behavior, and the 48 px near-bottom threshold.
|
||||
- Do not touch the user-owned untracked `.vite/` directory.
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
- `frontend/src/shell/ModelActivityPanel.tsx`: owns the visibility predicate, visible projection, rendering, empty state, and bottom-follow dependency.
|
||||
- `frontend/src/shell/ModelActivityPanel.test.tsx`: proves the allowlist, transcript isolation, hidden-only empty state, Markdown/status rendering, and scroll behavior.
|
||||
- `brain/codebase/workflow-ui-contracts.md`: records the durable distinction between the complete internal log and its user-facing projection.
|
||||
- `PROJECT_STATE.md`: records final verification, container image/asset, and Qwen smoke evidence.
|
||||
|
||||
### Task 1: Filter the user-facing activity projection
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/shell/ModelActivityPanel.test.tsx`
|
||||
- Modify: `frontend/src/shell/ModelActivityPanel.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ActivityEntry` and `ActivityKind` from `frontend/src/api/types.ts`; the unchanged `activityLog` array from `useSessionStore`.
|
||||
- Produces: `isVisibleModelActivity(entry: ActivityEntry): boolean`; a panel projection containing only prompt/thinking/status/gate rows.
|
||||
|
||||
- [ ] **Step 1: Write failing behavior tests before production code**
|
||||
|
||||
In `frontend/src/shell/ModelActivityPanel.test.tsx`, import the activity kind type and the module namespace so the not-yet-created predicate can fail as an assertion rather than a module-load error:
|
||||
|
||||
```tsx
|
||||
import type { ActivityEntry, ActivityKind } from "../api/types";
|
||||
import * as activityPanelModule from "./ModelActivityPanel";
|
||||
import { isNearBottom, ModelActivityPanel } from "./ModelActivityPanel";
|
||||
```
|
||||
|
||||
Replace the existing complete-sequence test with the approved mixed-sequence contract:
|
||||
|
||||
```tsx
|
||||
test("renders only prompt, thinking, status, and gate from a mixed F1 sequence", () => {
|
||||
const store = useSessionStore.getState();
|
||||
store.setPhase("F1");
|
||||
store.setLastUserEntry({ kind: "input", text: "How many patients?" });
|
||||
store.applyEvent({ type: "system_event", event: "agent_start" });
|
||||
store.applyEvent({
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "running" },
|
||||
});
|
||||
store.applyEvent({
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" },
|
||||
});
|
||||
store.applyEvent({ type: "text_delta", text: "Let me run another bash search." });
|
||||
store.applyEvent({ type: "activity_delta", text: "Inspecting **schema**." });
|
||||
store.applyEvent({ type: "info", level: "warning", text: "Retrying schema lookup" });
|
||||
store.applyEvent({
|
||||
type: "ui_request",
|
||||
ui_request: { id: "gate-1", widget: "select", phase: "F1_review", title: "Confirm cohort" },
|
||||
});
|
||||
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.getAllByRole("article")).toHaveLength(4);
|
||||
expect(screen.getByText("How many patients?")).toBeInTheDocument();
|
||||
expect(screen.getByText("schema").tagName).toBe("STRONG");
|
||||
expect(screen.getByText("Retrying schema lookup")).toBeInTheDocument();
|
||||
expect(screen.getByText("Confirm cohort")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Agent started")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("bash")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("completed")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Let me run another bash search.")).not.toBeInTheDocument();
|
||||
expect(useSessionStore.getState().transcript).toEqual([
|
||||
{ role: "assistant", text: "Let me run another bash search." },
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
||||
Add direct allowlist/default-deny and hidden-only empty-state tests:
|
||||
|
||||
```tsx
|
||||
test("uses an explicit default-deny activity-kind allowlist", () => {
|
||||
const predicate = (activityPanelModule as unknown as {
|
||||
isVisibleModelActivity?: (entry: ActivityEntry) => boolean;
|
||||
}).isVisibleModelActivity;
|
||||
expect(predicate).toBeTypeOf("function");
|
||||
if (!predicate) return;
|
||||
|
||||
for (const kind of ["prompt", "thinking", "status", "gate"] satisfies ActivityKind[]) {
|
||||
expect(predicate({ kind, phase: "F1", text: kind })).toBe(true);
|
||||
}
|
||||
for (const kind of ["assistant", "tool", "lifecycle"] satisfies ActivityKind[]) {
|
||||
expect(predicate({ kind, phase: "F1", text: kind })).toBe(false);
|
||||
}
|
||||
expect(predicate({ kind: "future-kind" as ActivityKind, phase: "F1", text: "future" })).toBe(false);
|
||||
});
|
||||
|
||||
test("shows the empty state when the raw log contains only hidden entries", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [
|
||||
{ kind: "assistant", phase: "F1", text: "Let me try another command." },
|
||||
{ kind: "tool", phase: "F1", text: "bash", toolCallId: "tool-1", status: "completed" },
|
||||
{ kind: "lifecycle", phase: "F1", text: "Turn end" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("No activity yet.")).toBeInTheDocument();
|
||||
expect(screen.queryAllByRole("article")).toHaveLength(0);
|
||||
});
|
||||
```
|
||||
|
||||
Change the assistant/thinking Markdown test so only reasoning is rendered:
|
||||
|
||||
```tsx
|
||||
test("renders thinking markdown without assistant narration", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [
|
||||
{ kind: "thinking", phase: "F2", text: "Considering **history**." },
|
||||
{ kind: "assistant", phase: "F2", text: "Use *current* records." },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText("history").tagName).toBe("STRONG");
|
||||
expect(screen.queryByText("current")).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole("article")).toHaveLength(1);
|
||||
});
|
||||
```
|
||||
|
||||
Add the hidden-update scroll regression:
|
||||
|
||||
```tsx
|
||||
test("does not bottom-follow when only a hidden event arrives", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [{ kind: "prompt", phase: "F1", text: "Visible prompt" }],
|
||||
});
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
const viewport = screen.getByTestId("activity-scroll");
|
||||
setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 });
|
||||
|
||||
act(() => useSessionStore.getState().applyEvent({
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-2", toolName: "bash", status: "running" },
|
||||
}));
|
||||
|
||||
expect(viewport.scrollTop).toBe(300);
|
||||
});
|
||||
```
|
||||
|
||||
Update both existing scroll tests to start with a visible prompt and append a visible status rather
|
||||
than calling `recordLifecycle`, which is intentionally hidden:
|
||||
|
||||
```tsx
|
||||
useSessionStore.setState({
|
||||
activityLog: [{ kind: "prompt", phase: "F1", text: "First" }],
|
||||
});
|
||||
// ...existing geometry and scroll setup...
|
||||
act(() => useSessionStore.getState().applyEvent({ type: "info", text: "Second" }));
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx vitest run src/shell/ModelActivityPanel.test.tsx
|
||||
```
|
||||
|
||||
Expected: FAIL because the predicate is absent, the panel renders seven mixed-sequence rows instead
|
||||
of four, hidden-only input does not show the empty state, assistant text remains visible, and a
|
||||
hidden tool append moves the bottom-follow viewport.
|
||||
|
||||
- [ ] **Step 3: Implement the strict rendering-boundary filter**
|
||||
|
||||
In `frontend/src/shell/ModelActivityPanel.tsx`, extend the type import and add the pure predicate:
|
||||
|
||||
```tsx
|
||||
import type { ActivityEntry, ActivityKind } from "../api/types";
|
||||
|
||||
const VISIBLE_MODEL_ACTIVITY_KINDS: ReadonlySet<ActivityKind> = new Set([
|
||||
"prompt",
|
||||
"thinking",
|
||||
"status",
|
||||
"gate",
|
||||
]);
|
||||
|
||||
export function isVisibleModelActivity(entry: ActivityEntry): boolean {
|
||||
return VISIBLE_MODEL_ACTIVITY_KINDS.has(entry.kind);
|
||||
}
|
||||
```
|
||||
|
||||
Inside `ModelActivityPanel`, derive the projection and a stable visible tail:
|
||||
|
||||
```tsx
|
||||
const activityLog = useSessionStore((s) => s.activityLog);
|
||||
const visibleActivity = activityLog.filter(isVisibleModelActivity);
|
||||
const visibleCount = visibleActivity.length;
|
||||
const visibleTail = visibleActivity.at(-1);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const followRef = useRef(true);
|
||||
```
|
||||
|
||||
Change the layout effect dependency so hidden appends do not run it, while a streamed visible tail
|
||||
still changes object identity and follows correctly:
|
||||
|
||||
```tsx
|
||||
useLayoutEffect(() => {
|
||||
const viewport = scrollRef.current;
|
||||
if (viewport && followRef.current) viewport.scrollTop = viewport.scrollHeight;
|
||||
}, [visibleCount, visibleTail]);
|
||||
```
|
||||
|
||||
Finally, use `visibleActivity` for both empty-state and row rendering:
|
||||
|
||||
```tsx
|
||||
{visibleActivity.length === 0 ? (
|
||||
<p className="py-3 text-sm text-muted-foreground">No activity yet.</p>
|
||||
) : (
|
||||
visibleActivity.map((entry, index) => (
|
||||
<ActivityRow key={`${index}-${entry.toolCallId ?? entry.kind}`} entry={entry} />
|
||||
))
|
||||
)}
|
||||
```
|
||||
|
||||
Do not modify `sessionStore.ts`, `useSessionStream.ts`, backend files, or `ActivityEntry`.
|
||||
|
||||
- [ ] **Step 4: Run focused GREEN and adjacent store regressions**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx vitest run src/shell/ModelActivityPanel.test.tsx src/store/sessionStore.test.ts
|
||||
npx tsc -b
|
||||
```
|
||||
|
||||
Expected: both test files pass, the complete store fold still retains all kinds, and TypeScript
|
||||
exits 0.
|
||||
|
||||
- [ ] **Step 5: Inspect scope and commit Task 1**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
git diff -- frontend/src/shell/ModelActivityPanel.tsx frontend/src/shell/ModelActivityPanel.test.tsx
|
||||
git status --short
|
||||
git add frontend/src/shell/ModelActivityPanel.tsx frontend/src/shell/ModelActivityPanel.test.tsx
|
||||
git commit -m "fix(frontend): hide model activity execution noise"
|
||||
```
|
||||
|
||||
Expected: only the panel and its test are committed; `.vite/` remains untracked and untouched.
|
||||
|
||||
### Task 2: Verify, document, deploy, and smoke the filtered panel
|
||||
|
||||
**Files:**
|
||||
- Modify: `brain/codebase/workflow-ui-contracts.md`
|
||||
- Modify: `PROJECT_STATE.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1's `isVisibleModelActivity` contract and production frontend bundle.
|
||||
- Produces: deployed frontend image/entry evidence and a durable record of the complete-log versus visible-projection distinction.
|
||||
|
||||
- [ ] **Step 1: Run the complete frontend verification gate**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npx vitest run
|
||||
npx tsc -b
|
||||
npm run build
|
||||
cd ..
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all frontend tests pass (baseline before this task: 285 tests), TypeScript/build exit 0,
|
||||
and only the repository's pre-existing MSW/ref/chunk-size warnings remain.
|
||||
|
||||
- [ ] **Step 2: Record the running asset and runtime state before deployment**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker inspect -f '{{.Name}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.Image}}|{{.State.StartedAt}}' thothii-core-1 thothii-frontend-1 omics_portal-web-1
|
||||
docker exec thothii-frontend-1 cat /usr/share/nginx/html/.vite/manifest.json | rg -n -B 3 '"isEntry": true'
|
||||
docker top thothii-core-1 -eo pid,args
|
||||
```
|
||||
|
||||
Expected: core is healthy, frontend/portal are running, the active entry is recorded, and no
|
||||
`pi --mode rpc` process is active before the frontend-only recreate.
|
||||
|
||||
- [ ] **Step 3: Build and recreate only the frontend service**
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```bash
|
||||
docker compose build frontend
|
||||
docker compose up -d --no-deps --force-recreate --wait --wait-timeout 60 frontend
|
||||
docker inspect -f '{{.Name}}|{{.State.Status}}|{{.Image}}|{{.State.StartedAt}}' thothii-frontend-1
|
||||
docker exec thothii-frontend-1 cat /usr/share/nginx/html/.vite/manifest.json | rg -n -B 3 '"isEntry": true'
|
||||
```
|
||||
|
||||
Expected: the build exits 0, frontend is running on the newly built image, and the active Vite
|
||||
entry differs from the value recorded in Step 2.
|
||||
|
||||
If and only if the entry changed, refresh the portal's indefinite manifest cache:
|
||||
|
||||
```bash
|
||||
docker restart omics_portal-web-1
|
||||
docker inspect -f '{{.Name}}|{{.State.Status}}|{{.State.StartedAt}}' omics_portal-web-1
|
||||
```
|
||||
|
||||
Expected: only `omics_portal-web-1` receives a new start timestamp; core and unrelated portal
|
||||
services are not restarted.
|
||||
|
||||
- [ ] **Step 4: Run a real no-COT Qwen smoke with exact settings restore**
|
||||
|
||||
Run the probe inside the core container so it can reach the private backend listener. It preserves
|
||||
the full settings object, creates one uniquely named session, waits for sanitized tool activity and
|
||||
the first gate, asserts Qwen emitted no reasoning delta, cleans up only its own session, and restores
|
||||
settings in `finally`:
|
||||
|
||||
```bash
|
||||
docker exec -i thothii-core-1 node - <<'NODE'
|
||||
const base = "http://127.0.0.1:8787";
|
||||
const canonical = (value) => JSON.stringify(value, Object.keys(value).sort());
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(base + path, {
|
||||
...options,
|
||||
headers: { "content-type": "application/json", ...(options.headers ?? {}) },
|
||||
});
|
||||
if (!response.ok) throw new Error(`${options.method ?? "GET"} ${path}: ${response.status}`);
|
||||
return response.status === 204 ? null : response.json();
|
||||
}
|
||||
|
||||
const saved = await request("/settings");
|
||||
const stamp = new Date().toISOString().replaceAll(/[:.]/g, "-");
|
||||
const question = `activity-filter-smoke-${stamp}: quanti record sono disponibili?`;
|
||||
let sessionId = null;
|
||||
let reader = null;
|
||||
let activityDeltaCount = 0;
|
||||
let toolCount = 0;
|
||||
let gateSeen = false;
|
||||
let timeout = null;
|
||||
let cleanupError = null;
|
||||
|
||||
try {
|
||||
await request("/settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
...saved,
|
||||
provider: "local-qwen",
|
||||
model: "qwen3.6-35b-a3b",
|
||||
thinking: "low",
|
||||
}),
|
||||
});
|
||||
const created = await request("/sessions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ question }),
|
||||
});
|
||||
sessionId = created.id;
|
||||
|
||||
const response = await fetch(`${base}/sessions/${encodeURIComponent(sessionId)}/events`);
|
||||
if (!response.ok || !response.body) throw new Error(`SSE open failed: ${response.status}`);
|
||||
reader = response.body.getReader();
|
||||
timeout = setTimeout(() => { void reader.cancel("Qwen smoke timeout"); }, 180_000);
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let eventName = "message";
|
||||
let dataLines = [];
|
||||
|
||||
while (!gateSeen) {
|
||||
const read = await reader.read();
|
||||
if (read.done) throw new Error("SSE ended before the first gate");
|
||||
buffer += decoder.decode(read.value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.replace(/\r$/, "");
|
||||
if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
||||
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
||||
else if (line === "") {
|
||||
if (dataLines.length > 0) {
|
||||
const payload = JSON.parse(dataLines.join("\n"));
|
||||
if (eventName === "activity_delta") activityDeltaCount += 1;
|
||||
if (eventName === "activity_event") {
|
||||
const keys = Object.keys(payload.activity ?? {}).sort();
|
||||
const allowed = ["kind", "status", "toolCallId", "toolName"];
|
||||
if (JSON.stringify(keys) !== JSON.stringify(allowed)) {
|
||||
throw new Error(`forbidden tool fields: ${keys.join(",")}`);
|
||||
}
|
||||
toolCount += 1;
|
||||
}
|
||||
if (eventName === "ui_request") gateSeen = true;
|
||||
}
|
||||
eventName = "message";
|
||||
dataLines = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toolCount === 0) throw new Error("no sanitized tool activity observed");
|
||||
if (activityDeltaCount !== 0) throw new Error(`expected no COT, got ${activityDeltaCount}`);
|
||||
console.log(JSON.stringify({ sessionId, activityDeltaCount, toolCount, gateSeen }));
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
if (reader) await reader.cancel().catch(() => undefined);
|
||||
if (sessionId) {
|
||||
try {
|
||||
const close = await fetch(`${base}/sessions/${encodeURIComponent(sessionId)}/close`, { method: "POST" });
|
||||
if (!close.ok) throw new Error(`smoke close failed: ${close.status}`);
|
||||
const remove = await fetch(`${base}/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
||||
if (remove.status !== 204) throw new Error(`smoke delete failed: ${remove.status}`);
|
||||
} catch (error) {
|
||||
cleanupError = error;
|
||||
}
|
||||
}
|
||||
await request("/settings", { method: "PUT", body: JSON.stringify(saved) });
|
||||
const restored = await request("/settings");
|
||||
const sessions = await request("/sessions");
|
||||
if (canonical(restored) !== canonical(saved)) throw new Error("settings restore mismatch");
|
||||
if (sessionId && sessions.some((session) => session.id === sessionId)) {
|
||||
throw new Error("smoke session still exists");
|
||||
}
|
||||
if (cleanupError) throw cleanupError;
|
||||
console.log(JSON.stringify({ settingsRestored: true, smokePresent: false }));
|
||||
}
|
||||
NODE
|
||||
```
|
||||
|
||||
Expected: `activityDeltaCount` is 0, `toolCount` is greater than 0, `gateSeen` is true, tool payloads
|
||||
contain only the four public fields, settings restore is exact, and the smoke session is absent.
|
||||
|
||||
- [ ] **Step 5: Update durable project state with the new projection contract and evidence**
|
||||
|
||||
In `brain/codebase/workflow-ui-contracts.md`, replace the complete-panel statement with:
|
||||
|
||||
```markdown
|
||||
- `activityLog` remains the complete in-memory chronological fold of prompt, thinking, assistant,
|
||||
sanitized tool lifecycle, reviewer gates, status, and turn lifecycle. The left Model activity
|
||||
panel is a strict projection: it renders only prompt, thinking, status, and gate; assistant,
|
||||
tool, lifecycle, and unknown future kinds are hidden. The central transcript and workflow state
|
||||
continue to consume their existing events independently.
|
||||
```
|
||||
|
||||
In `PROJECT_STATE.md`, add a dated resolved-state paragraph containing the exact final frontend test
|
||||
count, TypeScript/build result, new frontend image id, old/new Vite entry, portal restart decision,
|
||||
Qwen smoke session id/counts, exact settings restore, cleanup, and absence of residual Pi runtime.
|
||||
|
||||
- [ ] **Step 6: Recheck runtime, review scope, and commit the evidence**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker inspect -f '{{.Name}}|{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}|{{.Image}}' thothii-core-1 thothii-frontend-1 omics_portal-web-1
|
||||
docker top thothii-core-1 -eo pid,args
|
||||
docker logs --since 15m --tail 300 thothii-core-1
|
||||
docker logs --since 15m --tail 200 thothii-frontend-1
|
||||
git diff --check
|
||||
git status --short
|
||||
git diff -- brain/codebase/workflow-ui-contracts.md PROJECT_STATE.md
|
||||
git add brain/codebase/workflow-ui-contracts.md PROJECT_STATE.md
|
||||
git commit -m "docs: record filtered model activity deployment"
|
||||
```
|
||||
|
||||
Expected: containers are healthy/running, no smoke/Pi process remains, logs contain no raw tool
|
||||
payloads/credentials/fatal crash, only the two documentation files are committed, and `.vite/`
|
||||
remains untouched.
|
||||
|
||||
---
|
||||
|
||||
## Final review gate
|
||||
|
||||
Before integration, request an independent review of the complete implementation range. The review
|
||||
must verify the allowlist is at the rendering boundary, assistant text remains in the central
|
||||
transcript, hidden updates cannot move the panel scroll, unknown kinds default to hidden, tests
|
||||
exercise real store/panel behavior, and deployment evidence matches the live containers. Fix every
|
||||
Critical or Important finding before declaring the work complete.
|
||||
Reference in New Issue
Block a user