chapterhouse 0.3.1 → 0.3.2

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.
@@ -0,0 +1,351 @@
1
+ /**
2
+ * src/squad/init.test.ts
3
+ *
4
+ * Tests for the Squad init scaffolding module.
5
+ *
6
+ * Coverage:
7
+ * - scaffoldSquad produces the expected .squad/ layout
8
+ * - isSquadInitialized correctly detects initialized/uninitialized dirs
9
+ * - Idempotence: scaffoldSquad returns null (does not clobber) on re-run
10
+ * - Dune universe character allocation
11
+ * - allocateCast uses the selected universe
12
+ * - resolveRoles clamps to valid presets
13
+ * - force flag overrides idempotence guard
14
+ */
15
+ import assert from 'node:assert/strict';
16
+ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { tmpdir as osTmpdir } from 'node:os';
19
+ import test from 'node:test';
20
+ import { scaffoldSquad, isSquadInitialized, allocateCast, resolveRoles, DUNE_CHARACTERS, UNIVERSE_CHARACTERS, DEFAULT_UNIVERSE, } from './init.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+ function makeTempDir() {
25
+ return mkdtempSync(join(osTmpdir(), 'ch-init-test-'));
26
+ }
27
+ function cleanup(dir) {
28
+ rmSync(dir, { recursive: true, force: true });
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // Unit tests — pure functions
32
+ // ---------------------------------------------------------------------------
33
+ test('DUNE_CHARACTERS has at least 5 entries', () => {
34
+ assert.ok(DUNE_CHARACTERS.length >= 5, `Expected ≥ 5 Dune characters, got ${DUNE_CHARACTERS.length}`);
35
+ });
36
+ test('UNIVERSE_CHARACTERS includes Dune', () => {
37
+ assert.ok('Dune' in UNIVERSE_CHARACTERS, 'UNIVERSE_CHARACTERS must include "Dune"');
38
+ assert.equal(UNIVERSE_CHARACTERS['Dune'], DUNE_CHARACTERS);
39
+ });
40
+ test('DEFAULT_UNIVERSE is Dune', () => {
41
+ assert.equal(DEFAULT_UNIVERSE, 'Dune');
42
+ });
43
+ test('resolveRoles(4) returns 4 roles', () => {
44
+ const roles = resolveRoles(4);
45
+ assert.equal(roles.length, 4);
46
+ });
47
+ test('resolveRoles(3) returns 3 roles', () => {
48
+ assert.equal(resolveRoles(3).length, 3);
49
+ });
50
+ test('resolveRoles(5) returns 5 roles', () => {
51
+ assert.equal(resolveRoles(5).length, 5);
52
+ });
53
+ test('resolveRoles clamps values below 3 to 3', () => {
54
+ assert.equal(resolveRoles(1).length, 3);
55
+ });
56
+ test('resolveRoles clamps values above 5 to 5', () => {
57
+ assert.equal(resolveRoles(10).length, 5);
58
+ });
59
+ test('allocateCast assigns Dune names in order', () => {
60
+ const roles = resolveRoles(4);
61
+ const cast = allocateCast(roles, 'Dune');
62
+ assert.equal(Object.keys(cast).length, 4);
63
+ // First role maps to first Dune character
64
+ const firstRoleSlug = roles[0].slug;
65
+ assert.equal(cast[firstRoleSlug], DUNE_CHARACTERS[0]);
66
+ });
67
+ test('allocateCast uses provided universe', () => {
68
+ const roles = resolveRoles(3);
69
+ const cast = allocateCast(roles, 'Firefly');
70
+ const firfly = UNIVERSE_CHARACTERS['Firefly'];
71
+ const firstRoleSlug = roles[0].slug;
72
+ assert.equal(cast[firstRoleSlug], firfly[0]);
73
+ });
74
+ test('allocateCast falls back to Dune for unknown universe', () => {
75
+ const roles = resolveRoles(3);
76
+ const cast = allocateCast(roles, 'UnknownUniverse');
77
+ const firstRoleSlug = roles[0].slug;
78
+ assert.equal(cast[firstRoleSlug], DUNE_CHARACTERS[0]);
79
+ });
80
+ // ---------------------------------------------------------------------------
81
+ // isSquadInitialized
82
+ // ---------------------------------------------------------------------------
83
+ test('isSquadInitialized returns false for empty dir', () => {
84
+ const dir = makeTempDir();
85
+ try {
86
+ assert.equal(isSquadInitialized(dir), false);
87
+ }
88
+ finally {
89
+ cleanup(dir);
90
+ }
91
+ });
92
+ test('isSquadInitialized returns false when team.md missing', () => {
93
+ const dir = makeTempDir();
94
+ try {
95
+ assert.equal(isSquadInitialized(dir), false);
96
+ }
97
+ finally {
98
+ cleanup(dir);
99
+ }
100
+ });
101
+ // ---------------------------------------------------------------------------
102
+ // scaffoldSquad integration
103
+ // ---------------------------------------------------------------------------
104
+ const BASE_CONFIG = {
105
+ projectName: 'TestApp',
106
+ stack: 'TypeScript, Node.js',
107
+ goal: 'A test application',
108
+ teamSize: 4,
109
+ universe: 'Dune',
110
+ humanName: 'Alice',
111
+ };
112
+ test('scaffoldSquad returns ScaffoldResult with 4 cast agents', () => {
113
+ const dir = makeTempDir();
114
+ try {
115
+ const result = scaffoldSquad(dir, BASE_CONFIG);
116
+ assert.ok(result !== null, 'scaffoldSquad should return a result');
117
+ assert.equal(result.agents.length, 4, 'Should cast 4 agents');
118
+ assert.equal(result.universe, 'Dune');
119
+ }
120
+ finally {
121
+ cleanup(dir);
122
+ }
123
+ });
124
+ test('scaffoldSquad creates .squad directory', () => {
125
+ const dir = makeTempDir();
126
+ try {
127
+ scaffoldSquad(dir, BASE_CONFIG);
128
+ assert.ok(existsSync(join(dir, '.squad')), '.squad/ must exist');
129
+ }
130
+ finally {
131
+ cleanup(dir);
132
+ }
133
+ });
134
+ test('scaffoldSquad creates team.md with ## Members section', () => {
135
+ const dir = makeTempDir();
136
+ try {
137
+ scaffoldSquad(dir, BASE_CONFIG);
138
+ const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
139
+ assert.ok(teamMd.includes('## Members'), 'team.md must contain ## Members');
140
+ assert.ok(teamMd.includes('Scribe'), 'team.md must list Scribe');
141
+ assert.ok(teamMd.includes('Ralph'), 'team.md must list Ralph');
142
+ }
143
+ finally {
144
+ cleanup(dir);
145
+ }
146
+ });
147
+ test('scaffoldSquad creates team.md with project info', () => {
148
+ const dir = makeTempDir();
149
+ try {
150
+ scaffoldSquad(dir, BASE_CONFIG);
151
+ const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
152
+ assert.ok(teamMd.includes('TestApp'), 'team.md must contain project name');
153
+ assert.ok(teamMd.includes('Dune'), 'team.md must contain universe');
154
+ assert.ok(teamMd.includes('Alice'), 'team.md must contain human name');
155
+ }
156
+ finally {
157
+ cleanup(dir);
158
+ }
159
+ });
160
+ test('scaffoldSquad creates routing.md', () => {
161
+ const dir = makeTempDir();
162
+ try {
163
+ scaffoldSquad(dir, BASE_CONFIG);
164
+ assert.ok(existsSync(join(dir, '.squad', 'routing.md')));
165
+ }
166
+ finally {
167
+ cleanup(dir);
168
+ }
169
+ });
170
+ test('scaffoldSquad creates ceremonies.md', () => {
171
+ const dir = makeTempDir();
172
+ try {
173
+ scaffoldSquad(dir, BASE_CONFIG);
174
+ assert.ok(existsSync(join(dir, '.squad', 'ceremonies.md')));
175
+ }
176
+ finally {
177
+ cleanup(dir);
178
+ }
179
+ });
180
+ test('scaffoldSquad creates decisions.md', () => {
181
+ const dir = makeTempDir();
182
+ try {
183
+ scaffoldSquad(dir, BASE_CONFIG);
184
+ assert.ok(existsSync(join(dir, '.squad', 'decisions.md')));
185
+ }
186
+ finally {
187
+ cleanup(dir);
188
+ }
189
+ });
190
+ test('scaffoldSquad creates agent charter.md for each cast agent', () => {
191
+ const dir = makeTempDir();
192
+ try {
193
+ const result = scaffoldSquad(dir, BASE_CONFIG);
194
+ for (const agent of result.agents) {
195
+ const charterPath = join(dir, '.squad', 'agents', agent.slug, 'charter.md');
196
+ assert.ok(existsSync(charterPath), `charter.md missing for ${agent.slug}`);
197
+ const content = readFileSync(charterPath, 'utf-8');
198
+ assert.ok(content.includes(agent.castName), `charter should mention ${agent.castName}`);
199
+ }
200
+ }
201
+ finally {
202
+ cleanup(dir);
203
+ }
204
+ });
205
+ test('scaffoldSquad creates agent history.md for each cast agent', () => {
206
+ const dir = makeTempDir();
207
+ try {
208
+ const result = scaffoldSquad(dir, BASE_CONFIG);
209
+ for (const agent of result.agents) {
210
+ const histPath = join(dir, '.squad', 'agents', agent.slug, 'history.md');
211
+ assert.ok(existsSync(histPath), `history.md missing for ${agent.slug}`);
212
+ }
213
+ }
214
+ finally {
215
+ cleanup(dir);
216
+ }
217
+ });
218
+ test('scaffoldSquad creates scribe and ralph agent dirs', () => {
219
+ const dir = makeTempDir();
220
+ try {
221
+ scaffoldSquad(dir, BASE_CONFIG);
222
+ assert.ok(existsSync(join(dir, '.squad', 'agents', 'scribe', 'charter.md')));
223
+ assert.ok(existsSync(join(dir, '.squad', 'agents', 'ralph', 'charter.md')));
224
+ }
225
+ finally {
226
+ cleanup(dir);
227
+ }
228
+ });
229
+ test('scaffoldSquad creates casting/policy.json', () => {
230
+ const dir = makeTempDir();
231
+ try {
232
+ scaffoldSquad(dir, BASE_CONFIG);
233
+ const policyPath = join(dir, '.squad', 'casting', 'policy.json');
234
+ assert.ok(existsSync(policyPath));
235
+ const policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
236
+ assert.ok(Array.isArray(policy.allowlist_universes));
237
+ assert.ok(policy.allowlist_universes.includes('Dune'));
238
+ }
239
+ finally {
240
+ cleanup(dir);
241
+ }
242
+ });
243
+ test('scaffoldSquad creates casting/registry.json with all agents', () => {
244
+ const dir = makeTempDir();
245
+ try {
246
+ scaffoldSquad(dir, BASE_CONFIG);
247
+ const reg = JSON.parse(readFileSync(join(dir, '.squad', 'casting', 'registry.json'), 'utf-8'));
248
+ assert.ok(reg.agents, 'registry must have agents key');
249
+ assert.ok('scribe' in reg.agents, 'scribe must be in registry');
250
+ assert.ok('ralph' in reg.agents, 'ralph must be in registry');
251
+ }
252
+ finally {
253
+ cleanup(dir);
254
+ }
255
+ });
256
+ test('scaffoldSquad creates casting/history.json with assignment snapshot', () => {
257
+ const dir = makeTempDir();
258
+ try {
259
+ const result = scaffoldSquad(dir, BASE_CONFIG);
260
+ const hist = JSON.parse(readFileSync(join(dir, '.squad', 'casting', 'history.json'), 'utf-8'));
261
+ assert.ok(Array.isArray(hist.universe_usage_history));
262
+ assert.equal(hist.universe_usage_history[0].universe, 'Dune');
263
+ assert.ok(result.assignmentId in hist.assignment_cast_snapshots);
264
+ }
265
+ finally {
266
+ cleanup(dir);
267
+ }
268
+ });
269
+ test('scaffoldSquad creates or updates .gitattributes', () => {
270
+ const dir = makeTempDir();
271
+ try {
272
+ scaffoldSquad(dir, BASE_CONFIG);
273
+ const ga = readFileSync(join(dir, '.gitattributes'), 'utf-8');
274
+ assert.ok(ga.includes('.squad/decisions.md merge=union'), '.gitattributes must include union merge for decisions.md');
275
+ assert.ok(ga.includes('.squad/agents/*/history.md merge=union'));
276
+ }
277
+ finally {
278
+ cleanup(dir);
279
+ }
280
+ });
281
+ test('scaffoldSquad is idempotent — returns null on second call', () => {
282
+ const dir = makeTempDir();
283
+ try {
284
+ const first = scaffoldSquad(dir, BASE_CONFIG);
285
+ assert.ok(first !== null, 'first call should succeed');
286
+ const second = scaffoldSquad(dir, BASE_CONFIG);
287
+ assert.equal(second, null, 'second call should return null (idempotent)');
288
+ }
289
+ finally {
290
+ cleanup(dir);
291
+ }
292
+ });
293
+ test('scaffoldSquad with force:true overwrites existing scaffold', () => {
294
+ const dir = makeTempDir();
295
+ try {
296
+ scaffoldSquad(dir, BASE_CONFIG);
297
+ const result = scaffoldSquad(dir, { ...BASE_CONFIG, projectName: 'Updated' }, { force: true });
298
+ assert.ok(result !== null, 'forced call should succeed');
299
+ const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
300
+ assert.ok(teamMd.includes('Updated'), 'team.md should reflect updated project name');
301
+ }
302
+ finally {
303
+ cleanup(dir);
304
+ }
305
+ });
306
+ test('isSquadInitialized returns true after scaffoldSquad', () => {
307
+ const dir = makeTempDir();
308
+ try {
309
+ scaffoldSquad(dir, BASE_CONFIG);
310
+ assert.equal(isSquadInitialized(dir), true);
311
+ }
312
+ finally {
313
+ cleanup(dir);
314
+ }
315
+ });
316
+ test('scaffoldSquad uses Dune names from the canonical pool', () => {
317
+ const dir = makeTempDir();
318
+ try {
319
+ const result = scaffoldSquad(dir, BASE_CONFIG);
320
+ for (const agent of result.agents) {
321
+ assert.ok(DUNE_CHARACTERS.includes(agent.castName), `${agent.castName} is not in the Dune character pool`);
322
+ }
323
+ }
324
+ finally {
325
+ cleanup(dir);
326
+ }
327
+ });
328
+ test('scaffoldSquad uses Firefly names when universe is Firefly', () => {
329
+ const dir = makeTempDir();
330
+ try {
331
+ const result = scaffoldSquad(dir, { ...BASE_CONFIG, universe: 'Firefly' });
332
+ const fireflyPool = UNIVERSE_CHARACTERS['Firefly'];
333
+ for (const agent of result.agents) {
334
+ assert.ok(fireflyPool.includes(agent.castName), `${agent.castName} is not in the Firefly pool`);
335
+ }
336
+ }
337
+ finally {
338
+ cleanup(dir);
339
+ }
340
+ });
341
+ test('scaffoldSquad creates decisions/inbox directory', () => {
342
+ const dir = makeTempDir();
343
+ try {
344
+ scaffoldSquad(dir, BASE_CONFIG);
345
+ assert.ok(existsSync(join(dir, '.squad', 'decisions', 'inbox')));
346
+ }
347
+ finally {
348
+ cleanup(dir);
349
+ }
350
+ });
351
+ //# sourceMappingURL=init.test.js.map
@@ -2,6 +2,8 @@ import { basename, join } from 'path';
2
2
  import { existsSync, readFileSync, readdirSync } from 'fs';
3
3
  import { readPage, writePage } from '../wiki/fs.js';
4
4
  import { ensureWikiStructure } from '../wiki/fs.js';
5
+ import { childLogger } from '../util/logger.js';
6
+ const log = childLogger('squad.mirror');
5
7
  /**
6
8
  * Returns the wiki-relative path for a project's squad decisions page.
7
9
  * e.g. "pages/projects/chapterhouse/decisions.md"
@@ -69,12 +71,12 @@ export async function syncDecisionsFileToWiki(projectRoot) {
69
71
  // Visibility check: warn if inbox has unmerged drops
70
72
  const inbox = await checkDecisionsInbox(projectRoot);
71
73
  if (inbox.count > 0) {
72
- console.warn(`[squad] Project at ${projectRoot} has ${inbox.count} unmerged decision drops in inbox: ${inbox.files.join(', ')}. Run Scribe to merge.`);
74
+ log.warn({ projectRoot, count: inbox.count, files: inbox.files }, 'unmerged decision drops in inbox run Scribe to merge');
73
75
  }
74
76
  return { entriesSynced, wikiPath };
75
77
  }
76
78
  catch (err) {
77
- console.error('[squad] syncDecisionsFileToWiki failed (non-fatal):', err instanceof Error ? err.message : err);
79
+ log.error({ err: err instanceof Error ? err.message : err }, 'syncDecisionsFileToWiki failed (non-fatal)');
78
80
  return null;
79
81
  }
80
82
  }
@@ -1,5 +1,7 @@
1
1
  import { getDb } from '../store/db.js';
2
2
  import { syncDecisionsFileToWiki } from './mirror.js';
3
+ import { childLogger } from '../util/logger.js';
4
+ const log = childLogger('squad.mirror-scheduler');
3
5
  export const DEFAULT_DECISIONS_SYNC_INTERVAL_MS = 300_000; // 5 minutes
4
6
  function readIntervalFromEnv() {
5
7
  const raw = process.env.CHAPTERHOUSE_DECISIONS_SYNC_INTERVAL_MS;
@@ -17,7 +19,7 @@ function defaultGetRegisteredProjectRoots() {
17
19
  return rows.map(r => r.project_root);
18
20
  }
19
21
  catch (err) {
20
- console.error('[squad] DecisionsSyncScheduler: failed to query registered projects (non-fatal):', err instanceof Error ? err.message : err);
22
+ log.error({ err: err instanceof Error ? err.message : err }, 'failed to query registered projects (non-fatal)');
21
23
  return [];
22
24
  }
23
25
  }
@@ -38,12 +40,12 @@ export class DecisionsSyncScheduler {
38
40
  /** Start the periodic sync. No-op if intervalMs <= 0. */
39
41
  start() {
40
42
  if (this.intervalMs <= 0) {
41
- console.log('[squad] DecisionsSyncScheduler: disabled (intervalMs <= 0)');
43
+ log.debug({ intervalMs: this.intervalMs }, 'disabled (intervalMs <= 0)');
42
44
  return;
43
45
  }
44
46
  if (this.handle !== undefined)
45
47
  return; // already running
46
- console.log(`[squad] DecisionsSyncScheduler: starting, interval=${this.intervalMs}ms`);
48
+ log.info({ intervalMs: this.intervalMs }, 'starting');
47
49
  this.handle = this.setIntervalImpl(() => { void this.tick(); }, this.intervalMs);
48
50
  // Allow Node.js to exit even if the interval is still active
49
51
  this.handle?.unref?.();
@@ -53,24 +55,24 @@ export class DecisionsSyncScheduler {
53
55
  if (this.handle !== undefined) {
54
56
  this.clearIntervalImpl(this.handle);
55
57
  this.handle = undefined;
56
- console.log('[squad] DecisionsSyncScheduler: stopped');
58
+ log.info('stopped');
57
59
  }
58
60
  }
59
61
  async tick() {
60
62
  const projects = this.getRegisteredProjectRoots();
61
63
  if (projects.length === 0)
62
64
  return;
63
- console.log(`[squad] DecisionsSyncScheduler: syncing ${projects.length} project(s)`);
65
+ log.debug({ projects: projects.length }, 'syncing');
64
66
  for (const root of projects) {
65
67
  try {
66
68
  const result = await this.syncFn(root);
67
69
  if (result) {
68
- console.log(`[squad] DecisionsSyncScheduler: synced ${result.entriesSynced} entries for ${root}`);
70
+ log.debug({ entriesSynced: result.entriesSynced, root }, 'synced');
69
71
  }
70
72
  }
71
73
  catch (err) {
72
74
  // syncDecisionsFileToWiki already swallows errors; belt-and-suspenders here
73
- console.error(`[squad] DecisionsSyncScheduler: unexpected error for ${root}:`, err instanceof Error ? err.message : err);
75
+ log.error({ root, err: err instanceof Error ? err.message : err }, 'unexpected sync error (non-fatal)');
74
76
  }
75
77
  }
76
78
  }
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
6
+ export const CHAPTERHOUSE_VERSION = pkg.version;
7
+ //# sourceMappingURL=version.js.map
@@ -4,6 +4,8 @@ import { config } from "../config.js";
4
4
  import { WIKI_DIR } from "../paths.js";
5
5
  import { assertPagePath, readPage, writePage, writeFileAtomic } from "./fs.js";
6
6
  import { addToIndex, buildIndexEntryForPage } from "./index-manager.js";
7
+ import { childLogger } from "../util/logger.js";
8
+ const log = childLogger("team-sync");
7
9
  export class TeamWikiSync {
8
10
  teamChapterhouseUrl;
9
11
  teamChapterhouseToken;
@@ -28,7 +30,7 @@ export class TeamWikiSync {
28
30
  this.cacheRoot = join(this.wikiDir, ".team-cache");
29
31
  this.manifestPath = join(this.cacheRoot, "manifest.json");
30
32
  this.fetchImpl = options.fetchImpl ?? fetch;
31
- this.warn = options.warn ?? ((message) => console.warn(message));
33
+ this.warn = options.warn ?? ((message) => log.warn(message));
32
34
  this.now = options.now ?? (() => new Date());
33
35
  }
34
36
  isEnabled() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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"
@@ -26,8 +26,7 @@
26
26
  "preversion": "npm run release:check",
27
27
  "prepare": "husky",
28
28
  "test": "npm run clean && npm run build:server && node --experimental-test-module-mocks --import ./dist/test/setup-env.js --test 'dist/**/*.test.js'",
29
- "prepublishOnly": "npm run build",
30
- "prepare": "husky"
29
+ "prepublishOnly": "npm run build"
31
30
  },
32
31
  "engines": {
33
32
  "node": ">=22.5.0"
@@ -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:disabled{opacity:.5;cursor:not-allowed}.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}.bubble{margin-bottom:18px;max-width:800px}.bubble.user{margin-left:auto;text-align:right}.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-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}.msg-queued-indicator{font-size:.8em;opacity:.6;vertical-align:middle;-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}}