chapterhouse 0.2.0 → 0.3.1

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,189 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { execSync } from 'node:child_process';
5
+ import test from 'node:test';
6
+ // ---------------------------------------------------------------------------
7
+ // Test fixture: minimal git repo
8
+ // ---------------------------------------------------------------------------
9
+ const repoRoot = process.cwd();
10
+ const sandboxRoot = join(repoRoot, '.test-work', `worktree-${process.pid}`);
11
+ function initRepo(dir) {
12
+ mkdirSync(dir, { recursive: true });
13
+ // --initial-branch=main ensures the branch is named 'main' regardless of
14
+ // the runner's global init.defaultBranch setting (older git may default to 'master').
15
+ execSync('git init --initial-branch=main', { cwd: dir, stdio: 'pipe' });
16
+ execSync('git config user.email "test@example.com"', { cwd: dir, stdio: 'pipe' });
17
+ execSync('git config user.name "Test CI"', { cwd: dir, stdio: 'pipe' });
18
+ writeFileSync(join(dir, 'README.md'), '# test\n');
19
+ execSync('git add .', { cwd: dir, stdio: 'pipe' });
20
+ // An initial commit is required so that 'main' resolves as a ref.
21
+ // Without it, `git worktree add <path> -b <branch> main` fails with
22
+ // "fatal: invalid reference: main" because the branch has no commits.
23
+ execSync('git commit -m "chore: initial commit"', { cwd: dir, stdio: 'pipe' });
24
+ }
25
+ // We need a separate repo because the main repo's worktrees would interfere.
26
+ // Each test suite gets its own isolated sandbox git repo.
27
+ const testRepo = join(sandboxRoot, 'repo');
28
+ test.before(() => {
29
+ mkdirSync(sandboxRoot, { recursive: true });
30
+ initRepo(testRepo);
31
+ });
32
+ test.after(() => {
33
+ // Clean up any worktrees first (git worktree remove) then the sandbox
34
+ try {
35
+ execSync('git worktree prune', { cwd: testRepo });
36
+ }
37
+ catch { /* ignore */ }
38
+ rmSync(sandboxRoot, { recursive: true, force: true });
39
+ });
40
+ async function loadWorktree() {
41
+ return await import(new URL(`./worktree.js?v=${Date.now()}-${Math.random()}`, import.meta.url).href);
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // getWorktreePath
45
+ // ---------------------------------------------------------------------------
46
+ test('getWorktreePath returns correct path inside .worktrees/', async () => {
47
+ const m = await loadWorktree();
48
+ const result = m.getWorktreePath('/repo', 'kaylee', 42);
49
+ assert.equal(result, '/repo/.worktrees/kaylee-42');
50
+ });
51
+ test('getWorktreePath works with string issue number', async () => {
52
+ const m = await loadWorktree();
53
+ const result = m.getWorktreePath('/repo', 'wash', '17');
54
+ assert.equal(result, '/repo/.worktrees/wash-17');
55
+ });
56
+ // ---------------------------------------------------------------------------
57
+ // getBranchName
58
+ // ---------------------------------------------------------------------------
59
+ test('getBranchName without slug uses squad/{issue} format', async () => {
60
+ const m = await loadWorktree();
61
+ assert.equal(m.getBranchName(49), 'squad/49');
62
+ });
63
+ test('getBranchName with slug appends kebab-case slug', async () => {
64
+ const m = await loadWorktree();
65
+ assert.equal(m.getBranchName(49, 'git worktrees'), 'squad/49-git-worktrees');
66
+ });
67
+ test('getBranchName normalises special characters in slug', async () => {
68
+ const m = await loadWorktree();
69
+ assert.equal(m.getBranchName(10, 'Fix Login!!'), 'squad/10-fix-login');
70
+ });
71
+ // ---------------------------------------------------------------------------
72
+ // createWorktree
73
+ // ---------------------------------------------------------------------------
74
+ test('createWorktree creates a worktree at .worktrees/{agent}-{issue}/', async () => {
75
+ const m = await loadWorktree();
76
+ const { existsSync } = await import('node:fs');
77
+ const wt = m.createWorktree(testRepo, 'kaylee', 1);
78
+ assert.ok(existsSync(wt), `Worktree path should exist: ${wt}`);
79
+ assert.ok(wt.endsWith('.worktrees/kaylee-1'), `Path should end with .worktrees/kaylee-1, got: ${wt}`);
80
+ // Verify branch was created
81
+ const { spawnSync } = await import('node:child_process');
82
+ const r = spawnSync('git', ['branch', '--list', 'squad/1-kaylee'], { cwd: testRepo, encoding: 'utf-8' });
83
+ assert.ok(r.stdout.trim().length > 0, 'Branch squad/1-kaylee should exist');
84
+ });
85
+ test('createWorktree reuses existing worktree without error', async () => {
86
+ const m = await loadWorktree();
87
+ // create again (already exists from above)
88
+ assert.doesNotThrow(() => {
89
+ m.createWorktree(testRepo, 'kaylee', 1);
90
+ });
91
+ });
92
+ test('createWorktree respects --base option', async () => {
93
+ const m = await loadWorktree();
94
+ const { existsSync } = await import('node:fs');
95
+ // We guarantee 'main' exists via --initial-branch=main in initRepo
96
+ const wt = m.createWorktree(testRepo, 'wash', 2, { baseBranch: 'main' });
97
+ assert.ok(existsSync(wt), `Worktree should be created at ${wt}`);
98
+ });
99
+ test('createWorktree with slug uses slug in branch name', async () => {
100
+ const m = await loadWorktree();
101
+ const wt = m.createWorktree(testRepo, 'zoe', 3, { slug: 'test-isolation' });
102
+ const { spawnSync } = await import('node:child_process');
103
+ const r = spawnSync('git', ['branch', '--list', 'squad/3-test-isolation'], { cwd: testRepo, encoding: 'utf-8' });
104
+ assert.ok(r.stdout.trim().length > 0, 'Branch squad/3-test-isolation should exist');
105
+ void wt;
106
+ });
107
+ // ---------------------------------------------------------------------------
108
+ // listWorktrees
109
+ // ---------------------------------------------------------------------------
110
+ test('listWorktrees returns entries for squad worktrees', async () => {
111
+ const m = await loadWorktree();
112
+ const list = m.listWorktrees(testRepo);
113
+ // We created kaylee-1 above; at minimum it should appear
114
+ assert.ok(list.length >= 1, 'Should have at least 1 squad worktree');
115
+ const kaylee = list.find(w => w.agent === 'kaylee' && w.issueNum === '1');
116
+ assert.ok(kaylee, 'Should include kaylee-1 worktree');
117
+ assert.equal(kaylee?.branch, 'squad/1-kaylee');
118
+ assert.equal(typeof kaylee?.dirty, 'boolean');
119
+ });
120
+ test('listWorktrees does not include the main checkout', async () => {
121
+ const m = await loadWorktree();
122
+ const list = m.listWorktrees(testRepo);
123
+ const hasMain = list.some(w => !w.path.includes('/.worktrees/'));
124
+ assert.ok(!hasMain, 'Main checkout should not appear in squad worktree list');
125
+ });
126
+ // ---------------------------------------------------------------------------
127
+ // removeWorktree
128
+ // ---------------------------------------------------------------------------
129
+ test('removeWorktree removes a clean worktree', async () => {
130
+ const m = await loadWorktree();
131
+ // Create a fresh worktree to remove
132
+ m.createWorktree(testRepo, 'scribe', 5);
133
+ const wtPath = m.getWorktreePath(testRepo, 'scribe', 5);
134
+ const { existsSync } = await import('node:fs');
135
+ assert.ok(existsSync(wtPath));
136
+ m.removeWorktree(testRepo, 'scribe', 5);
137
+ assert.ok(!existsSync(wtPath), 'Worktree should be removed');
138
+ });
139
+ test('removeWorktree is a no-op for non-existent worktree', async () => {
140
+ const m = await loadWorktree();
141
+ assert.doesNotThrow(() => {
142
+ m.removeWorktree(testRepo, 'ghost', 999);
143
+ });
144
+ });
145
+ test('removeWorktree refuses to remove dirty worktree without --force', async () => {
146
+ const m = await loadWorktree();
147
+ const { writeFileSync } = await import('node:fs');
148
+ // Create worktree, then add an uncommitted file
149
+ m.createWorktree(testRepo, 'mal', 6);
150
+ const wtPath = m.getWorktreePath(testRepo, 'mal', 6);
151
+ writeFileSync(join(wtPath, 'uncommitted.txt'), 'dirty\n');
152
+ // Should set exitCode=1 but not throw
153
+ const prev = process.exitCode;
154
+ process.exitCode = 0;
155
+ m.removeWorktree(testRepo, 'mal', 6, { force: false });
156
+ const rejected = process.exitCode === 1;
157
+ process.exitCode = prev; // restore
158
+ assert.ok(rejected, 'Should set exitCode=1 for dirty worktree');
159
+ // cleanup with force
160
+ m.removeWorktree(testRepo, 'mal', 6, { force: true });
161
+ });
162
+ // ---------------------------------------------------------------------------
163
+ // pruneWorktrees
164
+ // ---------------------------------------------------------------------------
165
+ test('pruneWorktrees dry-run lists worktrees that would be removed', async () => {
166
+ const m = await loadWorktree();
167
+ // Capture stdout (basic test — just ensure it doesn't throw)
168
+ assert.doesNotThrow(() => {
169
+ m.pruneWorktrees(testRepo, { dryRun: true });
170
+ });
171
+ });
172
+ test('pruneWorktrees skips worktrees on unmerged branches', async () => {
173
+ const m = await loadWorktree();
174
+ const { existsSync, writeFileSync } = await import('node:fs');
175
+ const { spawnSync } = await import('node:child_process');
176
+ const { join } = await import('node:path');
177
+ // zoe-3 worktree should exist (created in earlier test)
178
+ const wt = m.getWorktreePath(testRepo, 'zoe', 3);
179
+ assert.ok(existsSync(wt), 'zoe-3 worktree should exist');
180
+ // Add a commit so the branch diverges from base — otherwise git considers
181
+ // a branch with no extra commits as already merged into its parent.
182
+ writeFileSync(join(wt, 'feature.txt'), 'in progress\n');
183
+ spawnSync('git', ['add', 'feature.txt'], { cwd: wt, stdio: 'pipe' });
184
+ spawnSync('git', ['commit', '-m', 'feat: in-progress work'], { cwd: wt, stdio: 'pipe' });
185
+ // We guarantee 'main' via --initial-branch=main in initRepo
186
+ m.pruneWorktrees(testRepo, { base: 'main' });
187
+ assert.ok(existsSync(wt), 'Unmerged worktree should NOT be pruned');
188
+ });
189
+ //# sourceMappingURL=worktree.test.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.2.0",
4
- "description": "Chapterhouse \u2014 a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
3
+ "version": "0.3.1",
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"
7
7
  },
@@ -22,8 +22,12 @@
22
22
  "dev:server": "tsx --watch src/daemon.ts",
23
23
  "dev:web": "npm --prefix web run dev",
24
24
  "dev": "tsx --watch src/daemon.ts",
25
+ "release:check": "if [ -n \"$(git status --porcelain)\" ]; then echo '❌ Working tree is not clean. Stage or stash changes before running npm version.'; git status --short; exit 1; fi",
26
+ "preversion": "npm run release:check",
27
+ "prepare": "husky",
25
28
  "test": "npm run clean && npm run build:server && node --experimental-test-module-mocks --import ./dist/test/setup-env.js --test 'dist/**/*.test.js'",
26
- "prepublishOnly": "npm run build"
29
+ "prepublishOnly": "npm run build",
30
+ "prepare": "husky"
27
31
  },
28
32
  "engines": {
29
33
  "node": ">=22.5.0"
@@ -62,11 +66,14 @@
62
66
  },
63
67
  "devDependencies": {
64
68
  "@bradygaster/squad-cli": "^0.9.4",
69
+ "@commitlint/cli": "^21.0.0",
70
+ "@commitlint/config-conventional": "^21.0.0",
65
71
  "@types/better-sqlite3": "^7.6.13",
66
72
  "@types/cors": "^2.8.19",
67
73
  "@types/express": "^5.0.6",
68
74
  "@types/jsonwebtoken": "^9.0.10",
69
75
  "@types/node": "^25.6.0",
76
+ "husky": "^9.1.7",
70
77
  "tsx": "^4.21.0",
71
78
  "typescript": "^5.9.3"
72
79
  }
@@ -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)}.page-shell{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}.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}.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}}