chapterhouse 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import { existsSync, statSync, readdirSync } from "fs";
5
5
  import { join, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { z } from "zod";
8
- import { sendToOrchestrator, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey, subscribeTaskEvents } from "../copilot/orchestrator.js";
8
+ import { sendToOrchestrator, getAgentInfo, cancelCurrentMessage, getLastRouteResult, getCurrentSessionKey } from "../copilot/orchestrator.js";
9
9
  import { squadEventBus } from "../copilot/squad-event-bus.js";
10
10
  import { getAgentRegistry } from "../copilot/agents.js";
11
11
  import { config, persistModel } from "../config.js";
@@ -21,6 +21,7 @@ import { listSkills, removeSkill } from "../copilot/skills.js";
21
21
  import { restartDaemon } from "../daemon.js";
22
22
  import { API_TOKEN_PATH, resolveWikiRelativePath } from "../paths.js";
23
23
  import { getDb, getSessionMessages, getTaskEvents, normalizeSqliteTsToIso } from "../store/db.js";
24
+ import { getTaskLogEvents, subscribeTaskLog } from "../copilot/task-event-log.js";
24
25
  import { getStatus, onStatusChange } from "../status.js";
25
26
  import { formatSseData, formatSseEvent } from "./sse.js";
26
27
  import { syncDecisionsFileToWiki } from "../squad/mirror.js";
@@ -284,7 +285,9 @@ app.get("/api/workers/:taskId", (req, res) => {
284
285
  completedAt: row.completed_at,
285
286
  });
286
287
  });
287
- // Historical event log for a task (catch-up on page load)
288
+ // Historical event log for a task (catch-up on page load / SSE reconnect).
289
+ // Ring buffer (fast, in-memory) is checked first; falls back to SQLite for
290
+ // completed tasks whose buffer has been cleared.
288
291
  app.get("/api/workers/:taskId/events", (req, res) => {
289
292
  const taskId = req.params.taskId;
290
293
  const afterSeqRaw = req.query.afterSeq;
@@ -295,10 +298,13 @@ app.get("/api/workers/:taskId/events", (req, res) => {
295
298
  if (!taskRow) {
296
299
  throw new NotFoundError("Task not found");
297
300
  }
298
- const events = getTaskEvents(taskId, afterSeq);
301
+ const ringEvents = getTaskLogEvents(taskId, afterSeq);
302
+ const events = ringEvents.length > 0 ? ringEvents : getTaskEvents(taskId, afterSeq);
299
303
  res.json({ taskId, events });
300
304
  });
301
- // SSE stream for per-task live tool-call activity
305
+ // SSE stream for per-task live tool-call activity.
306
+ // Uses the ring-buffer subscriber (subscribeTaskLog) so events fire immediately
307
+ // from in-memory state rather than needing an extra SQLite read.
302
308
  app.get("/api/workers/:taskId/events/stream", (req, res) => {
303
309
  const taskId = req.params.taskId;
304
310
  const taskRow = getDb()
@@ -314,8 +320,9 @@ app.get("/api/workers/:taskId/events/stream", (req, res) => {
314
320
  });
315
321
  res.write(formatSseData({ type: "connected", taskId }));
316
322
  const heartbeat = setInterval(() => { res.write(`:ping\n\n`); }, 20_000);
317
- const unsub = subscribeTaskEvents(taskId, (event) => {
318
- res.write(formatSseData({ type: "task_event", taskId, ...event }));
323
+ // subscribeTaskLog delivers ring-buffer-sourced events as they arrive.
324
+ const unsub = subscribeTaskLog(taskId, (event) => {
325
+ res.write(formatSseData({ type: "task_event", ...event }));
319
326
  });
320
327
  req.on("close", () => {
321
328
  clearInterval(heartbeat);
@@ -19,6 +19,7 @@ import { normalizeProjectPath, setChannelProject } from "../squad/context.js";
19
19
  import { getSquadCoordinatorSystemMessage } from "../squad/charter.js";
20
20
  import { childLogger } from "../util/logger.js";
21
21
  import { squadEventBus } from "./squad-event-bus.js";
22
+ import { initTaskEventLog } from "./task-event-log.js";
22
23
  import { SessionManager, SessionRegistry, SESSION_IDLE_TTL_MS, SESSION_MAX_ACTIVE, } from "./session-manager.js";
23
24
  const log = childLogger("orchestrator");
24
25
  const MAX_RETRIES = 3;
@@ -299,6 +300,8 @@ export async function initOrchestrator(client) {
299
300
  copilotClient = client;
300
301
  // Initialize governance hook pipeline before any session is created.
301
302
  initHookPipeline();
303
+ // Initialize per-task ring buffer — subscribes to squadEventBus for session:tool_call events.
304
+ initTaskEventLog();
302
305
  // (Re-)create the registry — supports multiple initOrchestrator calls in tests
303
306
  if (registry) {
304
307
  await registry.shutdown();
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Per-task in-memory event ring buffer — #116.
3
+ *
4
+ * Maintains a `Map<taskId, RingBuffer<TaskEvent>>` in the daemon process,
5
+ * capped at 500 events per task. Subscribes to the process-wide
6
+ * `squadEventBus` for `session:tool_call` events so the ring buffer stays
7
+ * in sync without the caller needing to wire anything extra.
8
+ *
9
+ * Consumers:
10
+ * 1. `GET /api/workers/:taskId/events` — REST hydration on page-load / SSE reconnect.
11
+ * The ring buffer is checked first (fast path, in-memory); SQLite is the fallback
12
+ * for completed tasks whose ring buffer has been cleared.
13
+ * 2. Per-task SSE subscribers — `subscribeTaskLog` delivers events as they arrive
14
+ * so the SSE frame fires immediately (no SQLite round-trip).
15
+ *
16
+ * Lifecycle:
17
+ * - `initTaskEventLog()` must be called once from `initOrchestrator()`.
18
+ * It returns an unsub function; call it on shutdown / test teardown.
19
+ * - `clearTaskLog(taskId)` should be called when a task is destroyed so the
20
+ * Map doesn't grow unbounded.
21
+ *
22
+ * @module copilot/task-event-log
23
+ */
24
+ import { squadEventBus } from "./squad-event-bus.js";
25
+ // ---------------------------------------------------------------------------
26
+ // Ring buffer
27
+ // ---------------------------------------------------------------------------
28
+ export const RING_BUFFER_CAPACITY = 500;
29
+ export class RingBuffer {
30
+ _capacity;
31
+ _items;
32
+ constructor(capacity) {
33
+ if (capacity < 1)
34
+ throw new RangeError("RingBuffer capacity must be >= 1");
35
+ this._capacity = capacity;
36
+ this._items = [];
37
+ }
38
+ push(item) {
39
+ if (this._items.length >= this._capacity) {
40
+ this._items.shift(); // evict oldest
41
+ }
42
+ this._items.push(item);
43
+ }
44
+ getAll() {
45
+ return this._items.slice();
46
+ }
47
+ get size() {
48
+ return this._items.length;
49
+ }
50
+ get capacity() {
51
+ return this._capacity;
52
+ }
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Singleton state
56
+ // ---------------------------------------------------------------------------
57
+ const taskBuffers = new Map();
58
+ const taskListeners = new Map();
59
+ // ---------------------------------------------------------------------------
60
+ // Internal helpers
61
+ // ---------------------------------------------------------------------------
62
+ function getOrCreateBuffer(taskId) {
63
+ let buf = taskBuffers.get(taskId);
64
+ if (!buf) {
65
+ buf = new RingBuffer(RING_BUFFER_CAPACITY);
66
+ taskBuffers.set(taskId, buf);
67
+ }
68
+ return buf;
69
+ }
70
+ function notifyListeners(taskId, event) {
71
+ const listeners = taskListeners.get(taskId);
72
+ if (!listeners)
73
+ return;
74
+ for (const fn of listeners) {
75
+ try {
76
+ fn(event);
77
+ }
78
+ catch { /* non-fatal */ }
79
+ }
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Public API
83
+ // ---------------------------------------------------------------------------
84
+ /**
85
+ * Subscribe to the squadEventBus and maintain the ring buffer.
86
+ * Call once from initOrchestrator(). Returns an unsub / cleanup function.
87
+ */
88
+ export function initTaskEventLog() {
89
+ const unsubToolCall = squadEventBus.subscribe("session:tool_call", (event) => {
90
+ const taskId = event.sessionId;
91
+ if (!taskId)
92
+ return;
93
+ const p = event.payload;
94
+ const taskEvent = {
95
+ id: 0, // not a DB row — id is meaningless for ring-buffer entries
96
+ taskId,
97
+ seq: p._seq ?? 0,
98
+ ts: p._ts ?? Date.now(),
99
+ kind: p._kind === "tool_complete" ? "tool_complete" : "tool_start",
100
+ toolName: p.toolName ?? null,
101
+ summary: p._summary ?? null,
102
+ };
103
+ const buf = getOrCreateBuffer(taskId);
104
+ buf.push(taskEvent);
105
+ notifyListeners(taskId, taskEvent);
106
+ });
107
+ const unsubDestroyed = squadEventBus.subscribe("session:destroyed", (event) => {
108
+ if (event.sessionId)
109
+ clearTaskLog(event.sessionId);
110
+ });
111
+ return () => {
112
+ unsubToolCall();
113
+ unsubDestroyed();
114
+ };
115
+ }
116
+ /**
117
+ * Return all ring-buffered events for a task, optionally filtered to seq > afterSeq.
118
+ * Returns an empty array if the task has no buffer (task unknown or already cleared).
119
+ */
120
+ export function getTaskLogEvents(taskId, afterSeq = 0) {
121
+ const buf = taskBuffers.get(taskId);
122
+ if (!buf)
123
+ return [];
124
+ const all = buf.getAll();
125
+ return afterSeq === 0 ? all : all.filter((e) => e.seq > afterSeq);
126
+ }
127
+ /**
128
+ * Subscribe to live events for a specific task.
129
+ * Listener is called synchronously when a new event arrives (from the bus subscriber above).
130
+ * Returns an unsubscribe function.
131
+ */
132
+ export function subscribeTaskLog(taskId, listener) {
133
+ let listeners = taskListeners.get(taskId);
134
+ if (!listeners) {
135
+ listeners = new Set();
136
+ taskListeners.set(taskId, listeners);
137
+ }
138
+ listeners.add(listener);
139
+ return () => {
140
+ const ls = taskListeners.get(taskId);
141
+ if (ls) {
142
+ ls.delete(listener);
143
+ if (ls.size === 0)
144
+ taskListeners.delete(taskId);
145
+ }
146
+ };
147
+ }
148
+ /**
149
+ * Remove the ring buffer and any listeners for a task.
150
+ * Called automatically when `session:destroyed` fires; also callable manually.
151
+ */
152
+ export function clearTaskLog(taskId) {
153
+ taskBuffers.delete(taskId);
154
+ taskListeners.delete(taskId);
155
+ }
156
+ /**
157
+ * Number of tasks currently tracked. Useful for testing and diagnostics.
158
+ */
159
+ export function taskLogSize() {
160
+ return taskBuffers.size;
161
+ }
162
+ //# sourceMappingURL=task-event-log.js.map
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Unit tests for src/copilot/task-event-log.ts — #116
3
+ *
4
+ * Covers:
5
+ * 1. RingBuffer capacity and eviction
6
+ * 2. RingBuffer getAll returns a copy
7
+ * 3. Ring buffer singleton: bus events populate the buffer
8
+ * 4. getTaskLogEvents returns [] for unknown taskId
9
+ * 5. getTaskLogEvents filters by afterSeq
10
+ * 6. subscribeTaskLog delivers events as they arrive
11
+ * 7. subscribeTaskLog unsub stops delivery
12
+ * 8. clearTaskLog removes buffer and listeners
13
+ * 9. session:destroyed auto-clears the ring buffer
14
+ * 10. Multiple tasks tracked independently
15
+ * 11. RING_BUFFER_CAPACITY constant: 500 items, evicts oldest
16
+ *
17
+ * Uses node:test (same pattern as hooks.test.ts, agents.squad.test.ts) so the
18
+ * daemon's `npm test` runner (node --test) picks these up from dist/.
19
+ */
20
+ import { describe, it, beforeEach, afterEach } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { EventBus } from "@bradygaster/squad-sdk/runtime/event-bus";
23
+ import { RingBuffer, RING_BUFFER_CAPACITY, } from "./task-event-log.js";
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+ function makeToolCallEvent(opts) {
28
+ return {
29
+ type: "session:tool_call",
30
+ sessionId: opts.sessionId,
31
+ payload: {
32
+ toolName: opts.toolName ?? "bash",
33
+ toolArgs: {},
34
+ _kind: opts.kind ?? "tool_start",
35
+ _seq: opts.seq ?? 1,
36
+ _ts: opts.ts ?? Date.now(),
37
+ _summary: opts.summary ?? null,
38
+ },
39
+ timestamp: new Date(),
40
+ };
41
+ }
42
+ function makeDestroyedEvent(sessionId) {
43
+ return {
44
+ type: "session:destroyed",
45
+ sessionId,
46
+ agentName: "test-agent",
47
+ payload: { agentName: "test-agent", reason: "complete" },
48
+ timestamp: new Date(),
49
+ };
50
+ }
51
+ /**
52
+ * Builds an isolated task-event-log wired to a fresh EventBus so tests don't
53
+ * share state through the process-level squadEventBus singleton.
54
+ */
55
+ function buildIsolatedLog(bus) {
56
+ const CAPACITY = RING_BUFFER_CAPACITY;
57
+ const buffers = new Map();
58
+ const listeners = new Map();
59
+ function getOrCreate(taskId) {
60
+ let b = buffers.get(taskId);
61
+ if (!b) {
62
+ b = new RingBuffer(CAPACITY);
63
+ buffers.set(taskId, b);
64
+ }
65
+ return b;
66
+ }
67
+ function notify(taskId, event) {
68
+ const ls = listeners.get(taskId);
69
+ if (!ls)
70
+ return;
71
+ for (const fn of ls) {
72
+ try {
73
+ fn(event);
74
+ }
75
+ catch { /* non-fatal */ }
76
+ }
77
+ }
78
+ const unsubToolCall = bus.subscribe("session:tool_call", (event) => {
79
+ const taskId = event.sessionId;
80
+ if (!taskId)
81
+ return;
82
+ const p = event.payload;
83
+ const ev = {
84
+ id: 0, taskId,
85
+ seq: p._seq ?? 0,
86
+ ts: p._ts ?? Date.now(),
87
+ kind: p._kind === "tool_complete" ? "tool_complete" : "tool_start",
88
+ toolName: p.toolName ?? null,
89
+ summary: p._summary ?? null,
90
+ };
91
+ getOrCreate(taskId).push(ev);
92
+ notify(taskId, ev);
93
+ });
94
+ const unsubDestroyed = bus.subscribe("session:destroyed", (event) => {
95
+ if (event.sessionId) {
96
+ buffers.delete(event.sessionId);
97
+ listeners.delete(event.sessionId);
98
+ }
99
+ });
100
+ return {
101
+ getEvents: (taskId, afterSeq = 0) => {
102
+ const buf = buffers.get(taskId);
103
+ if (!buf)
104
+ return [];
105
+ const all = buf.getAll();
106
+ return afterSeq === 0 ? all : all.filter((e) => e.seq > afterSeq);
107
+ },
108
+ subscribe: (taskId, fn) => {
109
+ let ls = listeners.get(taskId);
110
+ if (!ls) {
111
+ ls = new Set();
112
+ listeners.set(taskId, ls);
113
+ }
114
+ ls.add(fn);
115
+ return () => {
116
+ const lss = listeners.get(taskId);
117
+ if (lss) {
118
+ lss.delete(fn);
119
+ if (lss.size === 0)
120
+ listeners.delete(taskId);
121
+ }
122
+ };
123
+ },
124
+ clear: (taskId) => {
125
+ buffers.delete(taskId);
126
+ listeners.delete(taskId);
127
+ },
128
+ size: () => buffers.size,
129
+ shutdown: () => { unsubToolCall(); unsubDestroyed(); },
130
+ };
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // RingBuffer — pure class tests
134
+ // ---------------------------------------------------------------------------
135
+ describe("RingBuffer", () => {
136
+ it("starts empty", () => {
137
+ const buf = new RingBuffer(3);
138
+ assert.equal(buf.size, 0);
139
+ assert.deepEqual(buf.getAll(), []);
140
+ });
141
+ it("pushes items up to capacity", () => {
142
+ const buf = new RingBuffer(3);
143
+ buf.push(1);
144
+ buf.push(2);
145
+ buf.push(3);
146
+ assert.equal(buf.size, 3);
147
+ assert.deepEqual(buf.getAll(), [1, 2, 3]);
148
+ });
149
+ it("evicts oldest item when capacity is exceeded", () => {
150
+ const buf = new RingBuffer(3);
151
+ buf.push(1);
152
+ buf.push(2);
153
+ buf.push(3);
154
+ buf.push(4);
155
+ assert.equal(buf.size, 3);
156
+ assert.deepEqual(buf.getAll(), [2, 3, 4]);
157
+ });
158
+ it("evicts multiple oldest for many pushes", () => {
159
+ const buf = new RingBuffer(3);
160
+ for (let i = 1; i <= 10; i++)
161
+ buf.push(i);
162
+ assert.equal(buf.size, 3);
163
+ assert.deepEqual(buf.getAll(), [8, 9, 10]);
164
+ });
165
+ it("getAll returns a copy, not the internal array", () => {
166
+ const buf = new RingBuffer(3);
167
+ buf.push(1);
168
+ const snapshot = buf.getAll();
169
+ buf.push(2);
170
+ assert.deepEqual(snapshot, [1]);
171
+ });
172
+ it("capacity constant is 500", () => {
173
+ const buf = new RingBuffer(RING_BUFFER_CAPACITY);
174
+ assert.equal(buf.capacity, 500);
175
+ });
176
+ it("throws RangeError for capacity < 1", () => {
177
+ assert.throws(() => new RingBuffer(0), { name: "RangeError" });
178
+ });
179
+ it("501 pushes: only last 500 remain, oldest evicted", () => {
180
+ const buf = new RingBuffer(RING_BUFFER_CAPACITY);
181
+ for (let i = 1; i <= 501; i++)
182
+ buf.push(i);
183
+ assert.equal(buf.size, 500);
184
+ assert.equal(buf.getAll()[0], 2); // 1 was evicted
185
+ assert.equal(buf.getAll()[499], 501); // newest retained
186
+ });
187
+ });
188
+ // ---------------------------------------------------------------------------
189
+ // Task event log (bus-wired) tests
190
+ // ---------------------------------------------------------------------------
191
+ describe("task event log — bus-wired", () => {
192
+ let bus;
193
+ let log;
194
+ beforeEach(() => {
195
+ bus = new EventBus();
196
+ log = buildIsolatedLog(bus);
197
+ });
198
+ afterEach(() => {
199
+ log.shutdown();
200
+ });
201
+ it("returns [] for an unknown taskId", () => {
202
+ assert.deepEqual(log.getEvents("no-such-task"), []);
203
+ });
204
+ it("populates ring buffer on session:tool_call bus event", async () => {
205
+ await bus.emit(makeToolCallEvent({ sessionId: "task-001", kind: "tool_start", seq: 1, toolName: "view" }));
206
+ const events = log.getEvents("task-001");
207
+ assert.equal(events.length, 1);
208
+ assert.equal(events[0].kind, "tool_start");
209
+ assert.equal(events[0].toolName, "view");
210
+ assert.equal(events[0].taskId, "task-001");
211
+ assert.equal(events[0].seq, 1);
212
+ });
213
+ it("accumulates multiple events in order", async () => {
214
+ await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 1, toolName: "bash" }));
215
+ await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_complete", seq: 2, summary: "ok" }));
216
+ await bus.emit(makeToolCallEvent({ sessionId: "task-002", kind: "tool_start", seq: 3, toolName: "view" }));
217
+ const events = log.getEvents("task-002");
218
+ assert.equal(events.length, 3);
219
+ assert.equal(events[0].seq, 1);
220
+ assert.equal(events[2].seq, 3);
221
+ assert.equal(events[1].kind, "tool_complete");
222
+ });
223
+ it("getEvents filters by afterSeq", async () => {
224
+ await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 1 }));
225
+ await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 2 }));
226
+ await bus.emit(makeToolCallEvent({ sessionId: "task-003", seq: 3 }));
227
+ assert.equal(log.getEvents("task-003", 1).length, 2);
228
+ assert.equal(log.getEvents("task-003", 2).length, 1);
229
+ assert.equal(log.getEvents("task-003", 3).length, 0);
230
+ });
231
+ it("subscribe delivers events as they arrive", async () => {
232
+ const received = [];
233
+ log.subscribe("task-004", (e) => received.push(e.toolName ?? "?"));
234
+ await bus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "bash" }));
235
+ await bus.emit(makeToolCallEvent({ sessionId: "task-004", toolName: "view" }));
236
+ assert.deepEqual(received, ["bash", "view"]);
237
+ });
238
+ it("subscribe unsub stops delivery", async () => {
239
+ const received = [];
240
+ const unsub = log.subscribe("task-005", (e) => received.push(e.toolName ?? "?"));
241
+ await bus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "bash" }));
242
+ unsub();
243
+ await bus.emit(makeToolCallEvent({ sessionId: "task-005", toolName: "view" }));
244
+ assert.equal(received.length, 1);
245
+ assert.equal(received[0], "bash");
246
+ });
247
+ it("clearTaskLog removes buffer and listeners", async () => {
248
+ await bus.emit(makeToolCallEvent({ sessionId: "task-006", seq: 1 }));
249
+ assert.equal(log.getEvents("task-006").length, 1);
250
+ log.clear("task-006");
251
+ assert.equal(log.getEvents("task-006").length, 0);
252
+ });
253
+ it("session:destroyed auto-clears ring buffer", async () => {
254
+ await bus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 1 }));
255
+ await bus.emit(makeToolCallEvent({ sessionId: "task-007", seq: 2 }));
256
+ assert.equal(log.getEvents("task-007").length, 2);
257
+ await bus.emit(makeDestroyedEvent("task-007"));
258
+ assert.equal(log.getEvents("task-007").length, 0);
259
+ });
260
+ it("tracks multiple tasks independently", async () => {
261
+ await bus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 1, toolName: "bash" }));
262
+ await bus.emit(makeToolCallEvent({ sessionId: "task-B", seq: 1, toolName: "view" }));
263
+ await bus.emit(makeToolCallEvent({ sessionId: "task-A", seq: 2, toolName: "edit" }));
264
+ assert.equal(log.getEvents("task-A").length, 2);
265
+ assert.equal(log.getEvents("task-B").length, 1);
266
+ assert.equal(log.getEvents("task-A")[0].toolName, "bash");
267
+ assert.equal(log.getEvents("task-B")[0].toolName, "view");
268
+ assert.equal(log.size(), 2);
269
+ });
270
+ it("events for wrong taskId are not visible to other tasks", async () => {
271
+ await bus.emit(makeToolCallEvent({ sessionId: "task-X", seq: 1 }));
272
+ assert.equal(log.getEvents("task-Y").length, 0);
273
+ });
274
+ });
275
+ //# sourceMappingURL=task-event-log.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
5
  "bin": {
6
6
  "chapterhouse": "dist/cli.js"
@@ -0,0 +1,10 @@
1
+ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
2
+ Theme: GitHub Dark
3
+ Description: Dark theme as seen on github.com
4
+ Author: github.com
5
+ Maintainer: @Hirse
6
+ Updated: 2021-05-15
7
+
8
+ Outdated base version: https://github.com/primer/github-syntax-dark
9
+ Current colors taken from GitHub's CSS
10
+ */.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}:root{color-scheme:dark;--bg: #0e1116;--bg-elev: #161b22;--bg-elev-2: #21262d;--fg: #e6edf3;--fg-dim: #8b949e;--border: #30363d;--accent: #3b82f6;--accent-fg: #ffffff;--danger: #f87171;--user-bubble: #1e293b}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;font-size:14px;line-height:1.5}a,.link{color:var(--accent);text-decoration:none}a:hover,.link:hover{text-decoration:underline}button,input,textarea,select{font:inherit}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bg-elev-2);padding:1px 5px;border-radius:4px}.dim{color:var(--fg-dim)}.small{font-size:12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-link{position:absolute;left:16px;top:-48px;z-index:10;padding:10px 14px;border-radius:8px;background:var(--accent);color:var(--accent-fg)}.skip-link:focus{top:16px}.layout{display:grid;grid-template-columns:220px 1fr;height:100%}.sidebar{background:var(--bg-elev);border-right:1px solid var(--border);padding:16px 0;display:flex;flex-direction:column}.sidebar-brand{display:flex;align-items:center;gap:10px;padding:0 18px 18px;font-weight:600;font-size:16px;border-bottom:1px solid var(--border);margin-bottom:12px}.sidebar nav{display:flex;flex-direction:column}.nav-link{padding:9px 18px;color:var(--fg);border-left:2px solid transparent}.nav-link:hover{background:var(--bg-elev-2);text-decoration:none}.nav-link.active{background:var(--bg-elev-2);border-left-color:var(--accent);color:var(--fg)}.nav-group{display:flex;flex-direction:column}.nav-group-row{display:flex;align-items:stretch}.nav-group-label{flex:1}.nav-group-toggle{background:none;border:none;cursor:pointer;padding:0 14px 0 4px;color:var(--fg-muted, var(--fg));display:flex;align-items:center;justify-content:center;border-left:2px solid transparent}.nav-group-toggle:hover{background:var(--bg-elev-2)}.nav-chevron{display:inline-block;font-size:18px;line-height:1;transition:transform .18s ease;transform:rotate(0)}.nav-chevron-open{transform:rotate(90deg)}.nav-recents{list-style:none;margin:0;padding:0}.nav-recent-link{display:flex;align-items:baseline;justify-content:space-between;gap:6px;width:100%;background:none;border:none;border-left:2px solid transparent;padding:6px 18px 6px 28px;cursor:pointer;color:var(--fg);text-align:left;font-size:13px}.nav-recent-link:hover{background:var(--bg-elev-2);text-decoration:none;border-left-color:var(--accent)}.nav-recent-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.nav-recent-hint{font-size:11px;flex-shrink:0;opacity:.6}.nav-recents-empty{padding:4px 28px 6px;font-size:12px;margin:0}.main{overflow:hidden;display:flex;flex-direction:column}.app-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;border-bottom:1px solid var(--border);background:var(--bg-elev)}.app-header-title{font-size:16px;font-weight:600;margin:0}.app-header-title-row{display:inline-flex;align-items:center;gap:10px}.app-header-user{color:var(--fg-dim);font-size:13px}.mode-badge{display:inline-flex;align-items:center;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:600;line-height:1;border:1px solid transparent}.mode-standalone{background:var(--bg-elev-2);border-color:var(--border);color:var(--fg-dim)}.mode-team{background:color-mix(in srgb,var(--accent) 14%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,var(--border));color:var(--accent)}.sse-badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:600;line-height:1;border:1px solid transparent}.sse-badge__dot{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.sse-badge--reconnecting{background:color-mix(in srgb,#f59e0b 12%,transparent);border-color:color-mix(in srgb,#f59e0b 35%,var(--border));color:#b45309}.sse-badge--reconnecting .sse-badge__dot{background:#f59e0b;animation:sse-pulse 1.2s ease-in-out infinite}.sse-badge--disconnected{background:color-mix(in srgb,#ef4444 12%,transparent);border-color:color-mix(in srgb,#ef4444 35%,var(--border));color:#b91c1c}.sse-badge--disconnected .sse-badge__dot{background:#ef4444}.sse-badge__reconnect-btn{background:none;border:none;padding:0;margin-left:4px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.sse-badge__reconnect-btn:hover{opacity:.8}@keyframes sse-pulse{0%,to{opacity:1}50%{opacity:.35}}max-width: 760px; margin: 0 auto; padding: 32px; } .loading,.empty-state{padding:32px;color:var(--fg-dim)}.empty-state h2{color:var(--fg);margin-top:0;margin-bottom:8px}.empty-state p{margin:0 0 12px}.empty-state-icon{font-size:28px;margin-bottom:8px;line-height:1}.empty-state-action{margin-top:4px}.auth-screen{min-height:100%;display:grid;place-items:center;padding:32px}.auth-card{width:min(420px,100%);background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;padding:24px}.auth-card h1{margin-top:0;margin-bottom:8px}.auth-card p{margin-top:0;margin-bottom:20px;color:var(--fg-dim)}.page{padding:24px 32px;overflow:auto;flex:1;min-width:0}.page-header{margin-bottom:16px}.page-header h1{margin:0 0 4px;font-size:22px}.error-notice{background:#f871711a;border:1px solid var(--danger);color:var(--danger);padding:12px 14px;border-radius:8px;margin-bottom:16px}.error-notice.inline{margin-bottom:12px}.error-notice-title{margin:0 0 4px;font-size:16px}.error-notice-message{margin:0}.error-notice-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.error-details{background:var(--bg-elev-2);padding:12px;border-radius:8px;overflow:auto}.loading-state{display:flex;align-items:flex-start;gap:12px;padding:16px 0;color:var(--fg-dim)}.loading-state.inline{padding:10px 0}.loading-state.centered{justify-content:center;padding:48px 32px}.loading-spinner{width:18px;height:18px;border:2px solid rgba(59,130,246,.25);border-top-color:var(--accent);border-radius:999px;flex:none;margin-top:2px;animation:spin .9s linear infinite}.loading-state-label{color:var(--fg);font-weight:500}.loading-state-detail{margin-top:2px}.btn{background:var(--bg-elev-2);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer}.btn:hover{background:var(--bg-elev)}.btn.primary{background:var(--accent);color:var(--accent-fg);border-color:var(--accent)}.btn.primary:hover{filter:brightness(1.1)}.btn.danger{border-color:var(--danger);color:var(--danger)}.btn.cancel{background:var(--danger);border-color:var(--danger);color:var(--accent-fg)}.chat{display:flex;flex-direction:column;height:100%}.chat-scroll{flex:1;overflow:auto;padding:24px 32px 0}.chat-log{display:flex;flex-direction:column}.turn-wrapper{margin-bottom:18px}.turn-wrapper+.turn-wrapper:not(.has-separator) .bubble{border-top:1px solid var(--border);padding-top:14px}.turn-header{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:12px}.turn-header--user{justify-content:flex-end}.turn-actor-badge{display:inline-flex;align-items:center;gap:4px;background:var(--bg-elev);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:11px;font-weight:500;-webkit-user-select:none;user-select:none}.turn-actor-icon{font-size:11px;line-height:1}.turn-ts{font-size:11px;color:var(--fg-dim);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;cursor:default;-webkit-user-select:none;user-select:none}.time-separator{display:flex;align-items:center;gap:10px;margin:16px 0 10px;color:var(--fg-dim);font-size:11px;font-style:italic;-webkit-user-select:none;user-select:none}.time-separator-line{flex:1;height:1px;background:var(--border);opacity:.5}.time-separator-label{white-space:nowrap;opacity:.7;letter-spacing:.02em}.bubble{max-width:800px}.bubble.user{margin-left:auto;text-align:right}.bubble--agent-activity{opacity:.88;border-left:2px solid var(--accent, #6366f1);padding-left:10px}.bubble.user .user-text{display:inline-block;background:var(--user-bubble);border:1px solid var(--border);padding:8px 14px;border-radius:14px;white-space:pre-wrap;text-align:left;margin:0}.route-tag{font-size:11px;color:var(--fg-dim);margin-top:4px}.copy-btn-wrap{position:relative}.copy-btn{position:absolute;top:6px;right:6px;display:flex;align-items:center;justify-content:center;padding:4px;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg-dim);cursor:pointer;z-index:1;line-height:0;transition:color .15s,background .15s}.copy-btn:hover{background:var(--bg-elev-2);color:var(--fg)}.copy-btn--copied{color:#4ade80;border-color:#4ade80}@media (hover: hover){.copy-btn{opacity:0;pointer-events:none;transition:opacity .15s,color .15s,background .15s}.copy-btn-wrap:hover .copy-btn,.copy-btn-wrap:focus-within .copy-btn{opacity:1;pointer-events:auto}}.copy-btn--code{top:8px;right:8px}.activity-heartbeat{display:flex;align-items:center;gap:6px;padding:4px 8px;margin:0 0 6px;border-radius:6px;border:1px solid rgba(59,130,246,.35);background:var(--bg-elev-2);font-size:12px;color:var(--fg);transition:opacity .3s}.activity-heartbeat.stale{opacity:.5;border-color:var(--border);color:var(--fg-dim)}.heartbeat-spinner{font-family:ui-monospace,monospace;font-size:11px;color:var(--accent);display:inline-block;animation:spin 1.4s linear infinite}.activity-heartbeat.stale .heartbeat-spinner{animation:none;color:var(--fg-dim)}.heartbeat-agent{font-weight:600}.heartbeat-sep{color:var(--fg-dim)}.heartbeat-action{color:var(--fg-dim);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.heartbeat-elapsed{font-family:ui-monospace,monospace;font-size:11px;color:var(--fg-dim);white-space:nowrap}.activity-strip{margin:0 0 8px;font-size:12px}.activity-summary{display:flex;flex-wrap:wrap;gap:6px}.activity-pill{display:inline-flex;align-items:center;gap:6px;background:var(--bg-elev);border:1px solid var(--border);color:var(--fg-dim);padding:3px 10px;border-radius:999px;cursor:pointer;font-size:12px}.activity-pill:hover{background:var(--bg-elev-2)}.activity-pill.running{color:var(--accent);border-color:#3b82f673}.activity-pill .glyph{font-family:ui-monospace,monospace;font-size:11px}.activity-pill.running .glyph{display:inline-block;animation:spin 1s linear infinite}.activity-pill .caret{color:var(--fg-dim);font-size:10px}.activity-headlines{display:flex;flex-direction:column;gap:2px;margin-top:6px}.activity-headline{display:inline-flex;align-items:center;gap:6px;padding:2px 4px;color:var(--fg-dim)}.activity-headline.status-running{color:var(--accent)}.activity-headline.status-failed{color:var(--danger)}.activity-headline .glyph{font-family:ui-monospace,monospace;font-size:11px;width:12px;text-align:center}.activity-headline.status-running .glyph{animation:spin 1s linear infinite}.agent-tag{font-size:10px;text-transform:lowercase;background:#3b82f629;color:#93c5fd;border:1px solid rgba(59,130,246,.35);padding:1px 6px;border-radius:4px;letter-spacing:.02em}.activity-thinking,.activity-details{margin-top:8px;padding:10px 12px;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px}.activity-details{display:flex;flex-direction:column;gap:6px}.thinking-block{margin:0;padding:8px;background:var(--bg-elev-2);border-radius:4px;white-space:pre-wrap;font-size:12px;line-height:1.5;max-height:280px;overflow:auto}.activity-row{border:1px solid var(--border);border-radius:6px;background:var(--bg-elev-2)}.activity-row.status-running{border-color:#3b82f673}.activity-row.status-failed{border-color:var(--danger)}.activity-row-head{width:100%;display:flex;align-items:center;gap:8px;background:transparent;border:0;color:var(--fg);text-align:left;padding:6px 10px;cursor:pointer;font-size:12px}.activity-row.status-running .activity-row-head .glyph{animation:spin 1s linear infinite;color:var(--accent)}.activity-row.status-failed .activity-row-head .glyph{color:var(--danger)}.activity-row .glyph{font-family:ui-monospace,monospace;width:12px;text-align:center}.activity-row .caret{margin-left:auto;color:var(--fg-dim)}.activity-row-body{padding:0 10px 10px;display:flex;flex-direction:column;gap:6px}.row-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--fg-dim)}.composer{border-top:1px solid var(--border);background:var(--bg-elev);padding:14px 32px;display:flex;flex-direction:column;gap:8px}.composer textarea{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--fg);padding:10px;resize:vertical}.composer-help{margin-top:-2px}.dreaming-indicator{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--fg-dim);padding:4px 0;animation:pulse 2s ease-in-out infinite}.dreaming-indicator-glyph{color:#c4b5fd}.composer-actions{display:flex;justify-content:flex-end;gap:6px}.md{line-height:1.55}.md p:first-child{margin-top:0}.md p:last-child{margin-bottom:0}.md pre{background:var(--bg-elev-2);border-radius:6px;padding:12px;overflow:auto}.md pre code{background:transparent;padding:0}.md table{border-collapse:collapse;margin:1em 0}.md th,.md td{border:1px solid var(--border);padding:6px 10px}.workers-layout{display:grid;grid-template-columns:320px 1fr;gap:18px;align-items:start}.workers-list{display:flex;flex-direction:column;gap:6px}.worker-row{text-align:left;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;color:var(--fg)}.worker-row.selected,.worker-row:hover{background:var(--bg-elev-2)}.worker-row-head{display:flex;justify-content:space-between;align-items:center}.worker-status{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;text-transform:uppercase;letter-spacing:.04em}.worker-status--running{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}.worker-status--completed{background:color-mix(in srgb,#4caf50 15%,transparent);color:#4caf50}.worker-status--error{background:color-mix(in srgb,#f44336 15%,transparent);color:#f44336}.worker-row-desc{margin-top:4px;font-size:13px;color:var(--fg)}.workers-detail{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:18px}.worker-detail-description{margin:4px 0 8px;font-size:15px}.worker-detail-slug,.worker-detail-taskid{font-size:.75em;font-family:var(--font-mono, monospace)}.worker-detail-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:8px}.worker-events{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto;background:var(--bg-elev-2);border-radius:6px;padding:10px 12px;margin-bottom:12px;font-size:12px;font-family:var(--font-mono, monospace)}.worker-event{display:flex;gap:8px;align-items:baseline;line-height:1.5}.worker-event-ts{color:var(--text-dim, #888);flex-shrink:0;font-size:11px}.worker-event-body{display:flex;gap:4px;align-items:baseline;flex-wrap:wrap;overflow:hidden}.worker-event-icon{flex-shrink:0}.worker-event--tool_complete .worker-event-icon{opacity:.7}.msg-queued-indicator{font-size:.8em;opacity:.6;vertical-align:middle;-webkit-user-select:none;user-select:none}.msg-queued-backend-indicator{display:inline-block;font-size:.78em;opacity:.75;margin-top:4px;color:var(--fg-dim);-webkit-user-select:none;user-select:none}.output{background:var(--bg-elev-2);padding:12px;border-radius:6px;overflow:auto;white-space:pre-wrap;font-size:13px}.projects-toolbar{margin-bottom:16px}.projects-register-form{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.projects-path-input{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:13px;padding:6px 10px;width:380px;max-width:100%}.projects-path-input:focus{outline:none;border-color:var(--accent)}.projects-register-error{font-size:12px}.projects-disabled{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:18px}.projects-empty{padding:24px 0}.projects-list{display:flex;flex-direction:column;gap:8px}.project-row{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px}.project-row-info{display:flex;flex-direction:column;gap:4px;min-width:0}.project-root{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-meta{display:flex;gap:12px;font-size:12px;flex-wrap:wrap}.project-badge{background:var(--bg-elev-2);border:1px solid var(--border);border-radius:10px;padding:1px 8px;font-size:11px;color:var(--fg)}.project-row-actions{display:flex;gap:6px;flex-shrink:0}.project-context-banner{display:flex;align-items:center;gap:8px;padding:6px 16px;background:color-mix(in srgb,var(--accent) 10%,var(--bg-elev));border-bottom:1px solid color-mix(in srgb,var(--accent) 25%,var(--border));font-size:12px;color:var(--fg-dim);flex-shrink:0}.project-context-icon{font-size:13px;flex-shrink:0}.project-context-name{font-weight:600;color:var(--fg);flex-shrink:0}.project-context-path{color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.project-context-clear{background:none;border:none;color:var(--fg-dim);cursor:pointer;font-size:16px;line-height:1;padding:0 2px;flex-shrink:0;border-radius:4px}.project-context-clear:hover{color:var(--fg);background:var(--bg-hover)}.project-chat-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 16px;background:color-mix(in srgb,var(--accent) 8%,var(--bg-elev));border-bottom:1px solid color-mix(in srgb,var(--accent) 20%,var(--border));flex-shrink:0}.project-chat-header-identity{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden}.project-chat-icon{font-size:16px;flex-shrink:0}.project-chat-title{font-size:14px;white-space:nowrap;flex-shrink:0}.project-chat-path{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.wiki{display:flex;flex-direction:column;min-height:100%}.wiki-layout{display:grid;grid-template-columns:minmax(320px,360px) minmax(0,1fr);gap:20px;flex:1;min-height:0}.wiki-sidebar,.wiki-main{min-height:0}.wiki-sidebar{display:flex;flex-direction:column;background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden}.wiki-sidebar-header{position:sticky;top:0;z-index:1;display:flex;flex-direction:column;gap:14px;padding:16px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,var(--bg-elev) 0%,rgba(22,27,34,.98) 100%)}.wiki-sidebar-header-row{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}.wiki-sidebar-header-row h2{margin:0 0 4px;font-size:16px}.wiki-sidebar-header-row p{margin:0}.wiki-search{display:flex;flex-direction:column;gap:12px}.wiki-search-field input,.wiki-filter select{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:9px 10px;border-radius:8px}.wiki-filter{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--fg-dim)}.wiki-search-meta,.wiki-shortcuts,.wiki-scope-legend{color:var(--fg-dim)}.wiki-scope-header-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.wiki-scope-header-row h1{margin:0}.wiki-shortcuts{border-top:1px solid var(--border);padding-top:12px}.wiki-scope-legend{display:flex;flex-wrap:wrap;gap:8px}.wiki-scope-legend>span{display:inline-flex;align-items:center;gap:4px}.wiki-sidebar-body{flex:1;min-height:0;overflow:auto;padding:12px}.wiki-tree,.wiki-tree-children{list-style:none;margin:0;padding:0}.wiki-tree-children{margin-top:4px}.wiki-node{margin:2px 0}.wiki-node-button{width:100%;display:flex;align-items:center;gap:8px;padding:7px 10px;background:transparent;border:1px solid transparent;border-radius:8px;color:var(--fg);text-align:left;cursor:pointer}.wiki-node-folder-button{color:var(--fg-dim)}.wiki-node-folder-button:hover,.wiki-node-folder-button.expanded,.wiki-node-page-button:hover{background:var(--bg-elev-2);border-color:var(--border);color:var(--fg)}.wiki-node-page-button{align-items:flex-start}.wiki-node-page-button.selected{background:#3b82f61f;border-color:#3b82f659;box-shadow:inset 2px 0 0 var(--accent)}.wiki-node-icon{width:14px;flex:none;text-align:center;color:var(--fg-dim)}.wiki-node-page-button.selected .wiki-node-icon{color:#93c5fd}.wiki-node-page-button.selected .wiki-node-scope-icon-personal{color:#ddd6fe}.wiki-node-page-button.selected .wiki-node-scope-icon-team{color:#a7f3d0}.wiki-node-content{min-width:0;display:flex;flex:1;flex-direction:column;gap:4px}.wiki-node-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wiki-node-meta{display:flex;flex-wrap:wrap;gap:6px;font-size:11px}.wiki-node-count{margin-left:auto;border:1px solid var(--border);border-radius:999px;padding:0 6px;font-size:11px;color:var(--fg-dim)}.wiki-main{min-width:0;display:flex;background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden}.wiki-main>.wiki-empty-state{width:100%}.wiki-document{width:100%;min-height:0;display:flex;flex-direction:column}.wiki-page-header{position:sticky;top:0;z-index:1;padding:18px 22px 16px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,var(--bg-elev) 0%,rgba(22,27,34,.98) 100%)}.wiki-page-header-main{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}.wiki-page-title-block h2{margin:0;font-size:28px;line-height:1.2}.wiki-page-summary{margin:8px 0 0;max-width:72ch;color:var(--fg-dim)}.wiki-page-actions{display:flex;gap:8px;flex:none}.wiki-breadcrumbs ol{display:flex;flex-wrap:wrap;gap:8px;list-style:none;margin:0 0 12px;padding:0}.wiki-breadcrumbs li{display:flex;align-items:center}.wiki-breadcrumbs li+li:before{content:"/";margin-right:8px;color:var(--fg-dim)}.wiki-breadcrumb-button{padding:0;border:0;background:transparent;color:var(--fg-dim);cursor:pointer}.wiki-breadcrumb-button:hover{color:var(--fg);text-decoration:underline}.wiki-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;font-size:12px;color:var(--fg-dim)}.wiki-badge,.wiki-tag,.wiki-meta-item{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:999px;padding:3px 8px;background:var(--bg-elev-2)}.wiki-badge{color:#93c5fd;border-color:#3b82f659}.wiki-scope-badge{display:inline-flex;align-items:center;gap:4px}.wiki-scope-badge-personal{color:#c4b5fd;border-color:#c4b5fd59;background:#c4b5fd14}.wiki-scope-badge-team{color:#6ee7b7;border-color:#6ee7b759;background:#6ee7b714}.wiki-node-scope-icon-personal{color:#c4b5fd}.wiki-node-scope-icon-team{color:#6ee7b7}.wiki-tag{color:var(--fg)}.wiki-meta-path{max-width:100%;overflow:auto;white-space:nowrap}.wiki-document-body{flex:1;min-height:0;overflow:auto}.wiki-article{max-width:76ch;padding:24px 22px 32px}.wiki-empty-state{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:12px;margin:auto;max-width:56ch;padding:32px}.wiki-empty-state.compact{margin:0;max-width:none;padding:20px 12px}.wiki-empty-state h2{margin:0;font-size:20px}.wiki-empty-state p{margin:0;color:var(--fg-dim)}.wiki-empty-state-actions{display:flex;flex-wrap:wrap;gap:8px}@media (max-width: 960px){.wiki-layout{grid-template-columns:1fr}.wiki-sidebar{max-height:50vh}.wiki-page-header-main,.wiki-sidebar-header-row,.wiki-scope-legend{flex-direction:column}.wiki-page-actions{width:100%}.wiki-page-actions .btn{flex:1}}.wiki-edit .row{display:flex;gap:12px;margin-bottom:12px}.wiki-edit input[type=text]{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--fg);padding:8px;border-radius:6px}.wiki-edit label{display:block;width:100%;font-size:12px;color:var(--fg-dim)}.wiki-editor{margin-bottom:16px}.skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}.skill-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px}.skill-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.tag{font-size:10px;text-transform:uppercase;padding:2px 6px;border-radius:4px;letter-spacing:.05em;background:var(--bg-elev-2);color:var(--fg-dim)}.tag-bundled{color:#93c5fd}.tag-local{color:#86efac}.tag-global{color:#fcd34d}.history-list{list-style:none;padding:0}.history-list li{padding:6px 0;border-bottom:1px solid var(--border)}.settings section{margin-bottom:28px}.settings-field{display:flex;flex-direction:column;gap:6px}.settings-field-label{font-size:12px;color:var(--fg-dim)}.settings select{background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:6px 10px}.row{display:flex;align-items:center;gap:8px}.settings-row{align-items:flex-end}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{opacity:.4}50%{opacity:1}}.ralph{max-width:760px}.ralph-section{margin-top:24px}.ralph-section h2{font-size:15px;font-weight:600;margin-bottom:10px;color:var(--fg)}.ralph-status-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;display:flex;flex-direction:column;gap:8px}.ralph-status-row{display:flex;align-items:center;gap:10px;font-size:14px}.ralph-status-label{min-width:90px;color:var(--text-dim, #888);font-size:12px;text-transform:uppercase;letter-spacing:.04em}.ralph-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:10px;text-transform:uppercase;letter-spacing:.04em}.ralph-badge--running{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent)}.ralph-badge--stopped{background:color-mix(in srgb,#888 15%,transparent);color:#888}.ralph-mono{font-family:var(--font-mono, monospace);font-size:13px}.ralph-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px}.ralph-controls{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:14px 16px;display:flex;flex-direction:column;gap:12px}.ralph-form-row{display:flex;align-items:center;gap:10px;font-size:14px}.ralph-form-row label{min-width:140px;color:var(--text-dim, #888);font-size:12px;text-transform:uppercase;letter-spacing:.04em}.ralph-select,.ralph-input{background:var(--bg-elev-2, var(--bg-elev));border:1px solid var(--border);border-radius:5px;color:var(--fg);font-size:13px;padding:5px 8px;min-width:240px}.ralph-input--narrow{min-width:80px;width:80px}.ralph-select:focus,.ralph-input:focus{outline:2px solid var(--accent);outline-offset:1px}.btn{border:none;border-radius:6px;font-size:13px;font-weight:600;padding:6px 14px;cursor:pointer;transition:opacity .1s;align-self:flex-start}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){opacity:.85}.btn-danger{background:#f44336;color:#fff}.btn-danger:hover:not(:disabled){opacity:.85}.ralph-queue-count{font-weight:400;color:var(--text-dim, #888);font-size:13px}.ralph-queue-source{margin-bottom:8px}.ralph-queue{display:flex;flex-direction:column;gap:6px}.ralph-issue{display:block;text-decoration:none;color:inherit;background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:10px 12px;transition:background .1s}.ralph-issue:hover{background:var(--bg-elev-2)}.ralph-issue-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}.ralph-issue-number{font-family:var(--font-mono, monospace);font-size:12px;color:var(--text-dim, #888);flex-shrink:0}.ralph-issue-title{font-size:14px;font-weight:500}.ralph-issue-meta{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.ralph-labels{display:flex;flex-wrap:wrap;gap:4px}.ralph-label{font-size:11px;padding:1px 6px;border-radius:8px;background:color-mix(in srgb,#888 12%,transparent);color:var(--fg)}.ralph-label--agent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent)}.ralph-label--triage{background:color-mix(in srgb,#9c27b0 12%,transparent);color:#9c27b0}.ralph-label--urgent{background:color-mix(in srgb,#f44336 12%,transparent);color:#f44336}.link-btn{background:none;border:none;color:var(--accent);font-size:inherit;cursor:pointer;padding:0;text-decoration:underline}