Skip to content

Commit 565de7f

Browse files
committed
v0.7.1: behavioral framework system + 3 new CLI adapters + identity polish
Combined release covering v0.6.0 -> v0.7.0 -> v0.7.1 work landed on the private dev branch since public's last sync at v0.5.2. ## v0.7.1 highlights - OpenWar v0.3.0 vendored at presets/frameworks/openwar.md with vendor-trace header - npm run update-frameworks: pulls pinned upstream tags, lints fetched content (em-dashes + forker-extensible sanity patterns), writes to presets/frameworks/ - Mid-conversation framework switch confirmation modal - 'Framework not found' graceful degrade with inline chat toast (no more 500s) - Demo seed acme-website thread now opens with Phase 0 brief + Confirmation Summary, demonstrating OpenWar gating before Phase 1 - Electron bundle now includes presets/ (fixes the missing-framework crash on packaged installs) ## v0.7.0 highlights - Framework registry + per-channel framework override + global default - OpenWar bundled and auto-seeded for cold-clone installs - Framework picker in channel header chip and onboarding wizard - API: GET /api/frameworks, POST /api/frameworks - 3 new CLI adapters: OpenClaw, Hermes, SemaClaw. All probe binary on PATH for genuine isConfigured() reporting - Brand-mark SVGs at public/agent-logos/ for all 7 adapters - New <AgentAvatar> component renders the right mark per bubble/seat - sendMessage now layers framework preamble / cross-agent context / user prompt as three opt-in overlays ## v0.6.0 highlights - Right-click rail-server context menu (rename, icon, color) - Personal workspace icon auto-derives from display name - useIdentityVersion + IdentityHydrator: name/label changes propagate to chat / boardroom / team-presence live, no page reload - Customizable agent label (onboarding.agentName) ## Release-session cleanup - update-frameworks script's sanity-pattern list rewritten generic (was operator-specific in the live build). Forkers extend via WAR_ROOM_FRAMEWORK_SANITY_PATTERNS env var. Zero personal-data hits on the public sweep. - DEFERRED.md added to .gitignore (builder-session-internal, never public). - README OpenWar cross-link preserved from earlier public commit. Sanity regex clean. Em-dash sweep clean across all shipped markdown. Local build verified.
1 parent b136440 commit 565de7f

61 files changed

Lines changed: 2617 additions & 186 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ notes.txt
5757
/playwright-report/
5858
/blob-report/
5959
/playwright/.cache/
60+
DEFERRED.md

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,56 @@ For full details on any release, see the corresponding entry on [GitHub Releases
88

99
---
1010

11+
## [0.7.1] - 2026-05-16
12+
13+
Patch on v0.7.0. Bundled OpenWar bumped to upstream v0.3.0, mid-conversation framework switch confirmed via modal, graceful degrade when a framework file is missing.
14+
15+
### Added
16+
- **Vendored OpenWar v0.3.0** at `presets/frameworks/openwar.md` with a vendor-trace header noting upstream tag and commit SHA. Replaces the v0.1-era draft that shipped in v0.7.0.
17+
- **`scripts/update-frameworks.mjs`** (`npm run update-frameworks`). Fetches pinned tags of registered upstream frameworks, lints fetched content for em-dashes and personal-data patterns, writes to `presets/frameworks/`. Manual invocation only. Sanity patterns are forker-extensible via the `WAR_ROOM_FRAMEWORK_SANITY_PATTERNS` env var.
18+
- **Mid-conversation framework switch modal** in the channel header chip. Explains the contract (next turn uses the new framework, existing context stays as-is). Esc cancels, Enter confirms.
19+
- **Inline "framework not found" toast** in chat when a channel pins a framework whose markdown file isn't bundled. Graceful degrade to no framework instead of 500.
20+
- **Demo seed Phase 0 turns:** the acme-website multi-agent thread now opens with a brief + Confirmation Summary exchange, demonstrating the OpenWar framework's gating behavior before the Phase 1 execution continues.
21+
22+
### Changed
23+
- `lib/frameworks.ts` registry gains a `refresh` flag on `listFrameworks()` and an exported `refreshFrameworkCache()` so dev-watcher and test scenarios can drop the in-process cache without restarting the process.
24+
- README "Behavioral framework" section describes the framework system, the update workflow, and the graceful-degrade rules.
25+
26+
### Fixed
27+
- **Bundled frameworks now ship in the packaged Electron installer.** `next.config.ts` adds `presets/**/*` to `outputFileTracingIncludes`, `electron/after-pack.js` copies `presets/` into the standalone bundle, and `lib/frameworks.ts` resolves the dir robustly across cwd and the module's `__dirname` ancestors. Fixes the "Page couldn't load" crash on v0.7.0's first NSIS install where the embedded Next server crashed trying to read missing framework files.
28+
29+
## [0.7.0] - 2026-05-16
30+
31+
Behavioral framework system + three new CLI adapters + brand-mark logos throughout the UI.
32+
33+
### Added
34+
- **Framework registry** + per-channel framework override + global default setting (`channel_overrides.framework_preset`, `default.framework`).
35+
- **OpenWar bundled** as the default framework, auto-seeded for cold-clone installs.
36+
- Per-channel framework picker in the chat-header AI chip (next to the context-mode controls).
37+
- Framework picker in the onboarding wizard's Agent step.
38+
- API: `GET /api/frameworks` (list + default), `POST /api/frameworks` (set global default or per-channel pin).
39+
- OpenWar logo (`public/openwar-logo.svg`). Heater-shield silhouette with a four-bar phase-stack, sibling to the War Room mark.
40+
- **Three new CLI adapters**: OpenClaw, Hermes (Nous Research), SemaClaw (midea-ai). All probe via `where`/`which` so the green-dot signal in the UI means "binary genuinely on PATH," not just "setting non-empty."
41+
- Brand-mark SVGs at `public/agent-logos/` for Claude, OpenAI, Gemini, Grok, OpenClaw, Hermes, SemaClaw. Adapters carry an `iconUrl` field; channel chat bubbles, boardroom seats, and right-panel agent rows render the matching mark.
42+
- Reusable `<AgentAvatar>` component.
43+
44+
### Changed
45+
- `sendMessage` in `lib/agents/index.ts` now layers prompt overlays in three positions: framework preamble (outermost), cross-agent context (middle), user prompt (innermost). Each layer is opt-in per channel; defaults skip them so the single-agent flow is unchanged.
46+
- Onboarding wizard's Agent step is a multi-adapter setup form instead of a single-pick picker. Paste keys / set binary paths for as many providers as you want in one pass.
47+
48+
### Fixed
49+
- `isConfigured()` on all CLI adapters now genuinely probes whether the binary exists on PATH (`where`/`which` with 30s cache), instead of returning true for any non-empty setting. Fixes the earlier confusion where every CLI showed green-dot regardless of install state.
50+
51+
## [0.6.0] - 2026-05-16
52+
53+
Identity polish. Customizable display name, agent label, server icon. Changes propagate live without page reload.
54+
55+
### Added
56+
- Right-click context menu on any rail server icon. Edit modal for rename, change icon, change color. The War Room server stays locked to the brand mark and violet palette.
57+
- Personal workspace icon auto-derives from display name (first letter) on wizard completion. `ServerProvider` listens for `war-room:identity-changed` to refresh in place.
58+
- `useIdentityVersion()` hook + `IdentityHydrator` component so the display name and agent label propagate to chat bubbles, boardroom seats, and team-presence rows on wizard finish without a page reload.
59+
- Customizable agent label (`onboarding.agentName`). Defaults to `${displayName}-Agent`; user can rename in the wizard's Identity step or under Settings → General.
60+
1161
## [0.5.2] - 2026-05-15
1262

1363
Hotfix. v0.5.1 broke production builds across every OS/Node combo in CI.
@@ -141,6 +191,9 @@ First public release.
141191
- Optional Electron desktop wrapper.
142192
- Optional self-hosted LiveKit boardroom voice channel.
143193

194+
[0.7.1]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.1
195+
[0.7.0]: https://github.com/pythonluvr/war-room/releases/tag/v0.7.0
196+
[0.6.0]: https://github.com/pythonluvr/war-room/releases/tag/v0.6.0
144197
[0.5.2]: https://github.com/pythonluvr/war-room/releases/tag/v0.5.2
145198
[0.5.1]: https://github.com/pythonluvr/war-room/releases/tag/v0.5.1
146199
[0.5.0]: https://github.com/pythonluvr/war-room/releases/tag/v0.5.0

app/api/agents/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const SETTINGS_KEYS = [
1414
"agent.cli.claude.bin",
1515
"agent.cli.codex.bin",
1616
"agent.cli.gemini.bin",
17+
"agent.cli.openclaw.bin",
18+
"agent.cli.hermes.bin",
19+
"agent.cli.semaclaw.bin",
1720
"agent.cli.custom.bin",
1821
"agent.cli.custom.template",
1922
// API keys + models
@@ -59,6 +62,7 @@ export async function GET() {
5962
kind: a.kind,
6063
capabilities: a.capabilities,
6164
isConfigured: a.isConfigured(),
65+
iconUrl: a.iconUrl ?? null,
6266
})),
6367
});
6468
}

app/api/channel-context/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { setChannelContextSettings, type ChannelContextMode } from "@/lib/db";
3+
4+
export const dynamic = "force-dynamic";
5+
export const runtime = "nodejs";
6+
7+
// Update the cross-agent context mode + budgets for a single channel.
8+
// Channel ids contain slashes (e.g. "user/foo"), so the id rides in the
9+
// body instead of the URL.
10+
export async function POST(req: NextRequest) {
11+
const body = (await req.json()) as {
12+
channelId?: string;
13+
mode?: ChannelContextMode;
14+
messages?: number;
15+
chars?: number;
16+
};
17+
const channelId = body.channelId?.trim();
18+
if (!channelId) {
19+
return NextResponse.json({ error: "channelId required" }, { status: 400 });
20+
}
21+
const patch: { mode?: ChannelContextMode; messages?: number; chars?: number } = {};
22+
if (body.mode === "isolated" || body.mode === "shared") patch.mode = body.mode;
23+
if (typeof body.messages === "number" && Number.isFinite(body.messages)) {
24+
patch.messages = body.messages;
25+
}
26+
if (typeof body.chars === "number" && Number.isFinite(body.chars)) {
27+
patch.chars = body.chars;
28+
}
29+
if (Object.keys(patch).length === 0) {
30+
return NextResponse.json({ error: "no fields to update" }, { status: 400 });
31+
}
32+
setChannelContextSettings(channelId, patch);
33+
return NextResponse.json({ ok: true });
34+
}

app/api/dashboard/route.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ function topSessions(limit = 5) {
210210
}
211211

212212
async function recentFiles(limit = 8) {
213+
if (isDemo()) return demoRecentFiles(limit);
213214
const roots = [
214215
...STATIC_WORKSPACES.map((w) => w.path),
215216
CLIENTS_ROOT,
@@ -222,6 +223,25 @@ async function recentFiles(limit = 8) {
222223
return items.slice(0, limit);
223224
}
224225

226+
function demoRecentFiles(limit: number) {
227+
// Synthetic recently-touched list. Mirrors the project paths used by
228+
// the activity seeder so the file rows feel like they came from the
229+
// same work shown elsewhere on the dashboard.
230+
const now = Date.now();
231+
const m = (h: number) => now - h * 3600 * 1000;
232+
const files = [
233+
{ path: "/demo/projects/acme-website/src/app/contact/schema.ts", name: "schema.ts", mtime: m(0.5), size: 1842 },
234+
{ path: "/demo/projects/q3-redesign/notes/2026-q2-discovery.md", name: "2026-q2-discovery.md", mtime: m(2), size: 12_408 },
235+
{ path: "/demo/projects/studio-motion-pack/scenes/hero-loop-03.aep", name: "hero-loop-03.aep", mtime: m(3.5), size: 4_980_222 },
236+
{ path: "/demo/projects/side-project-beta/copy/waitlist-confirmation.md", name: "waitlist-confirmation.md", mtime: m(6), size: 2_104 },
237+
{ path: "/demo/projects/platform-migration/runbooks/cutover.md", name: "cutover.md", mtime: m(9), size: 8_722 },
238+
{ path: "/demo/projects/acme-website/tailwind.config.ts", name: "tailwind.config.ts", mtime: m(14), size: 3_310 },
239+
{ path: "/demo/projects/studio-brand-system/tokens/oklch-map.json", name: "oklch-map.json", mtime: m(20), size: 1_540 },
240+
{ path: "/demo/projects/sara-onboarding-redesign/research/interviews-week-2.md", name: "interviews-week-2.md", mtime: m(28), size: 18_910 },
241+
];
242+
return files.slice(0, limit);
243+
}
244+
225245
async function walk(
226246
dir: string,
227247
out: Array<{ path: string; name: string; mtime: number; size: number }>,
@@ -247,17 +267,23 @@ async function walk(
247267
}
248268

249269
export async function GET() {
270+
// Demo mode skips the SSH probe entirely — without VPS credentials it
271+
// would just block the dashboard load for the SSH timeout each call.
272+
// KPI numbers are synthesized further below.
273+
const healthPromise = isDemo()
274+
? Promise.resolve(null)
275+
: getHealthReport();
250276
const [clients, health, perServer, files] = await Promise.all([
251277
countActiveClients(),
252-
getHealthReport(),
278+
healthPromise,
253279
perServerStats(),
254280
recentFiles(8),
255281
]);
256282
const sessions = listSessions();
257-
const vpsOnline = health.vps.error
283+
const vpsOnline = !health || health.vps.error
258284
? 0
259285
: health.vps.services.filter((s) => s.status === "online").length;
260-
const vpsTotal = health.vps.error ? 0 : health.vps.services.length;
286+
const vpsTotal = !health || health.vps.error ? 0 : health.vps.services.length;
261287

262288
// Demo mode: synthesize VPS counts + team presence + approvals so the
263289
// KPI strip looks like a real operator's daily view instead of zeros.
@@ -267,7 +293,7 @@ export async function GET() {
267293
const teamOnline = demo ? 3 : 1;
268294
const vpsOnlineFinal = demo ? 4 : vpsOnline;
269295
const vpsTotalFinal = demo ? 5 : vpsTotal;
270-
const vpsError = demo ? null : (health.vps.error ?? null);
296+
const vpsError = demo || !health ? null : (health.vps.error ?? null);
271297

272298
return NextResponse.json({
273299
kpi: {

app/api/frameworks/route.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { listFrameworks, readFramework } from "@/lib/frameworks";
3+
import { getSetting, setChannelFrameworkPreset, setSetting } from "@/lib/db";
4+
5+
export const dynamic = "force-dynamic";
6+
export const runtime = "nodejs";
7+
8+
// GET /api/frameworks
9+
// Lists every bundled framework preset + the current global default.
10+
// GET /api/frameworks?id=openwar&content=1
11+
// Returns the markdown for one preset (so the UI can preview it).
12+
export async function GET(req: NextRequest) {
13+
const id = req.nextUrl.searchParams.get("id");
14+
const wantContent = req.nextUrl.searchParams.get("content") === "1";
15+
if (id) {
16+
const text = readFramework(id);
17+
if (!text) return NextResponse.json({ error: "unknown framework" }, { status: 404 });
18+
return NextResponse.json({ id, content: wantContent ? text : null });
19+
}
20+
return NextResponse.json({
21+
frameworks: listFrameworks(),
22+
defaultId: getSetting("default.framework") ?? null,
23+
});
24+
}
25+
26+
// POST /api/frameworks → set global default
27+
// { defaultId: "openwar" | "none" | null }
28+
// POST /api/frameworks → set per-channel pin
29+
// { channelId, presetId: "openwar" | "none" | null }
30+
export async function POST(req: NextRequest) {
31+
const body = (await req.json()) as {
32+
defaultId?: string | null;
33+
channelId?: string;
34+
presetId?: string | null;
35+
};
36+
37+
if (body.channelId !== undefined) {
38+
const next =
39+
body.presetId === null || body.presetId === undefined ? null : body.presetId.trim() || null;
40+
if (next && next !== "none" && !readFramework(next)) {
41+
return NextResponse.json({ error: `unknown framework ${next}` }, { status: 400 });
42+
}
43+
setChannelFrameworkPreset(body.channelId, next);
44+
return NextResponse.json({ ok: true, channelId: body.channelId, presetId: next });
45+
}
46+
47+
if (body.defaultId !== undefined) {
48+
const next =
49+
body.defaultId === null ? "" : body.defaultId.trim();
50+
if (next && next !== "none" && !readFramework(next)) {
51+
return NextResponse.json({ error: `unknown framework ${next}` }, { status: 400 });
52+
}
53+
setSetting("default.framework", next);
54+
return NextResponse.json({ ok: true, defaultId: next || null });
55+
}
56+
57+
return NextResponse.json({ error: "no fields to update" }, { status: 400 });
58+
}

app/api/livekit/token/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export async function POST(req: NextRequest) {
4949
}
5050

5151
export async function GET() {
52+
// Demo pretends LiveKit is wired so the boardroom doesn't show its
53+
// amber "voice + video not enabled" warning. Actual joins still 503
54+
// from the POST handler — but the surface reads as ready, which is
55+
// what screenshots need.
56+
if (process.env.WAR_ROOM_DEMO === "1") {
57+
return NextResponse.json({ configured: true, room: ROOM_NAME });
58+
}
5259
const ok = !!(
5360
process.env.LIVEKIT_URL &&
5461
process.env.LIVEKIT_API_KEY &&

app/api/onboarding/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const KEYS = [
3232
"onboarding.identity",
3333
"onboarding.displayName",
3434
"onboarding.agentName",
35+
"onboarding.agentIcon",
3536
"onboarding.claudeBin",
3637
"onboarding.workspaceRoot",
3738
"onboarding.syncOptIn",
@@ -56,6 +57,7 @@ export async function POST(req: NextRequest) {
5657
identity: string;
5758
displayName: string;
5859
agentName: string;
60+
agentIcon: string;
5961
claudeBin: string;
6062
workspaceRoot: string;
6163
syncOptIn: boolean;
@@ -64,6 +66,7 @@ export async function POST(req: NextRequest) {
6466
}>;
6567

6668
if (body.agentName !== undefined) setSetting("onboarding.agentName", body.agentName);
69+
if (body.agentIcon !== undefined) setSetting("onboarding.agentIcon", body.agentIcon);
6770
if (body.identity !== undefined) setSetting("onboarding.identity", body.identity);
6871
if (body.displayName !== undefined) {
6972
setSetting("onboarding.displayName", body.displayName);

app/api/services/route.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
11
import { NextResponse } from "next/server";
2-
import { getHealthReport } from "@/lib/services-check";
2+
import { getHealthReport, type HealthReport } from "@/lib/services-check";
33

44
export const dynamic = "force-dynamic";
55
export const runtime = "nodejs";
66

7+
// Synthetic health report used when WAR_ROOM_DEMO=1. Real installs hit
8+
// the SSH probe in services-check.ts; the demo never has VPS credentials
9+
// so the page would otherwise read as "VPS not configured" + zero rows.
10+
function demoHealthReport(): HealthReport {
11+
const now = Date.now();
12+
const uptimeStart = (hours: number) => now - hours * 3600 * 1000;
13+
return {
14+
vps: {
15+
reachable: true,
16+
services: [
17+
{ name: "war-room-sync", status: "online", cpu: 1.2, mem: 142 * 1024 * 1024, uptime: uptimeStart(186), restarts: 0 },
18+
{ name: "agent-router", status: "online", cpu: 0.4, mem: 96 * 1024 * 1024, uptime: uptimeStart(186), restarts: 1 },
19+
{ name: "livekit", status: "online", cpu: 2.1, mem: 312 * 1024 * 1024, uptime: uptimeStart(72), restarts: 0 },
20+
{ name: "render-worker", status: "errored", cpu: 0, mem: 0, uptime: uptimeStart(0.5), restarts: 4 },
21+
{ name: "nginx", status: "online", cpu: 0.1, mem: 28 * 1024 * 1024, uptime: uptimeStart(720), restarts: 0 },
22+
],
23+
},
24+
local: [
25+
{ name: "Next dev server", port: 3031, hint: "the cockpit itself", reachable: true },
26+
{ name: "Discord bridge", port: 7891, hint: "router local agent", reachable: true },
27+
{ name: "Voice tray", port: 7892, hint: "push-to-talk daemon", reachable: false },
28+
],
29+
env: [
30+
{ path: "~/.war-room-demo/.env", exists: true, keys: ["DEMO_KEY", "SHARED_KEY"], size: 184 },
31+
],
32+
checkedAt: new Date().toISOString(),
33+
};
34+
}
35+
736
export async function GET() {
37+
if (process.env.WAR_ROOM_DEMO === "1") {
38+
return NextResponse.json(demoHealthReport());
39+
}
840
const report = await getHealthReport();
941
return NextResponse.json(report);
1042
}

0 commit comments

Comments
 (0)