chamba 0.6.1 → 0.7.0

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.
Files changed (35) hide show
  1. package/README.md +13 -6
  2. package/dist/lib/agent-context.js +33 -7
  3. package/dist/lib/dockerfile-builder.js +2 -1
  4. package/dist/lib/safe-rm.js +13 -3
  5. package/package.json +3 -3
  6. package/templates/Dockerfile +20 -1
  7. package/templates/pane-apps/client/assets/specs-B1970L17.css +1 -0
  8. package/templates/pane-apps/client/assets/specs-cEee_SPn.js +23 -0
  9. package/templates/pane-apps/client/specs/index.html +13 -0
  10. package/templates/pane-apps/server/specs.mjs +1588 -0
  11. package/templates/skills/dx-spec/SKILL.md +365 -0
  12. package/templates/skills/dx-spec/references/imagination-guide.md +140 -0
  13. package/templates/skills/dx-spec/references/review-guide.md +173 -0
  14. package/templates/skills/dx-spec/references/spec-guide.md +125 -0
  15. package/templates/skills/dx-spec/references/stages.md +399 -0
  16. package/templates/skills/dx-spec-config/SKILL.md +313 -0
  17. package/templates/skills/dx-spec-config/references/principles-template.md +12 -0
  18. package/templates/skills/dx-spec-execute/SKILL.md +324 -0
  19. package/templates/specs.sh +106 -0
  20. package/templates/webterm/README.md +42 -6
  21. package/templates/webterm/config.js +43 -0
  22. package/templates/webterm/public/app/composer.js +4 -1
  23. package/templates/webterm/public/app/dom.js +13 -5
  24. package/templates/webterm/public/app/frames.js +7 -0
  25. package/templates/webterm/public/app/main.js +7 -1
  26. package/templates/webterm/public/app/pane-shell.js +315 -0
  27. package/templates/webterm/public/app/pane.js +58 -183
  28. package/templates/webterm/public/app/specs-host.js +222 -0
  29. package/templates/webterm/public/app/terminal.js +8 -0
  30. package/templates/webterm/public/index.html +51 -27
  31. package/templates/webterm/public/styles.css +144 -30
  32. package/templates/webterm/server.js +273 -0
  33. package/templates/webterm/specs.js +358 -0
  34. package/templates/webterm/tool-document.js +67 -0
  35. package/templates/webterm/typed-line.js +85 -0
package/README.md CHANGED
@@ -36,6 +36,7 @@ The optional fields - `shadow_paths`, `env`, `ports`, `profiles` - are all descr
36
36
  - **Hidden paths.** Shadow paths (`node_modules`, `.env*` by default) are overlaid with empty container-local copies, so an agent never reads the real thing.
37
37
  - **The current CLIs.** claude, opencode and codex are kept on their latest versions, each with its config directory bind-mounted to the workspace cache so conversations and memory survive rebuilds.
38
38
  - **A web interface.** The browser front-end relays the real agent TUI over a websocket - every session on one page, several agents at once, images and dictation. It starts with the container. Claude sessions also get a status strip above the composer: model, context used, quota left.
39
+ - **A pane of tools beside the terminal.** Pages, where an agent publishes a page for you to read or a form for you to answer, and Specs, where a piece of work is read, annotated and steered. An agent reaches Specs with the `specs` helper on its path, and the `dx-spec` skills teach it the craft.
39
40
 
40
41
  The menu has two doors into that container.
41
42
  **Web interface** brings it up, opens the interface in your browser, and hands your terminal straight back - the work happens in the tab.
@@ -43,15 +44,21 @@ The menu has two doors into that container.
43
44
 
44
45
  ## The web pane
45
46
 
46
- Beside the terminal is a pane of pages, there from the first second of a session: until an agent has published anything it is a spine at the edge of the window, and opening it says what the pane is for and what to ask for.
47
- An agent publishes a standalone HTML file into it - `webpane report.html --title "Port rework"` - and it shows up as a chip above the pane: a plan, a comparison table, a long answer, anything worth reading at your own pace instead of scrolling back for.
48
-
49
- A page can also ask.
50
- A form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
47
+ Beside the terminal is a pane, there from the first second of a session, and the pane holds tools.
48
+ One tab each, and the width controls beside them: drag the divider, take the whole window, or fold the pane into the spine at its edge.
51
49
 
50
+ **Pages** is where an agent publishes.
51
+ `webpane report.html --title "Port rework"` puts a standalone HTML file in the pane as a chip: a plan, a comparison table, a long answer, anything worth reading at your own pace instead of scrolling back for.
52
+ A page can also ask - a form marked `data-feedback` is wired up by the interface, and what you submit is written as JSON next to the page, with one line typed into the agent's terminal saying where to read it.
52
53
  Pages are kept, one directory per conversation, so resuming a conversation opens with them again.
53
54
  They live under `~/.chamba/workspaces/<id>/agents/webpane/` on the host, and nothing deletes them.
54
55
 
56
+ **Specs** is where a piece of work is read and steered.
57
+ It renders the specs directory of your repository: the work, its artifacts, and where each piece stands.
58
+ Read an artifact as one continuous document, select any part of it to comment on or ask for its removal, and answer what the run asks - a gate on a screen, a round of questions, a decision that needs your judgment - as a form beside the thing it is about.
59
+ Everything is a file in your repository, so two windows agree, a session that ends loses nothing, and git sees the work.
60
+ The agent's half is `specs` on its path inside the container, and the `dx-spec` skills carry the craft: `/dx-spec` builds a specification, `/dx-spec-execute` runs its plan, and `/dx-spec-config` keeps the settings beside them.
61
+
55
62
  ## Where state lives
56
63
 
57
64
  `chamba.yaml` lives in the workspace and can be committed.
@@ -62,7 +69,7 @@ Everything else is on your machine under `~/.chamba/`: one directory per workspa
62
69
  ```bash
63
70
  pnpm install
64
71
  pnpm start # run the CLI from source (host only - Docker is not available in the dev container)
65
- pnpm check # typecheck, lint, repo checks, tests, build
72
+ pnpm check # build, typecheck, lint, repo checks, tests
66
73
  pnpm release # check, bump, commit and publish to npm
67
74
  ```
68
75
 
@@ -8,7 +8,7 @@
8
8
  // Claude Code: https://docs.anthropic.com/en/docs/claude-code
9
9
  // OpenCode: https://github.com/opencode-ai/opencode
10
10
  // Codex: https://github.com/openai/codex
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
12
  import { dirname, join } from "node:path";
13
13
  import { AGENTS_DIR, CLAUDE_STATUSLINE_PATH, CONTAINER_HOME, GIT_MODE, PACKAGE_ROOT } from "./constants.js";
14
14
  export const AGENT_MOUNTS = [
@@ -145,8 +145,12 @@ export function ensureClaudeStatusLine(workspaceDir) {
145
145
  }
146
146
  }
147
147
  // --- Claude skills injection -------------------------------------------------------------------------------------------------------------
148
- // Renders every templates/skills/<name>/SKILL.md into agents/claude/skills/<name>/SKILL.md. Drop a
149
- // new directory under templates/skills/ to ship a new skill - no other code changes needed.
148
+ // Copies every templates/skills/<name>/ into agents/claude/skills/<name>/, whole. Drop a new directory
149
+ // under templates/skills/ to ship a new skill - no other code changes needed.
150
+ //
151
+ // The directory rather than the one file, because a skill of any size keeps its craft in references/ and
152
+ // points at those files by name. A SKILL.md that reaches a session without them names files that are not
153
+ // there, which is worse than shipping nothing.
150
154
  export function injectClaudeSkills(workspaceDir) {
151
155
  const templatesSkillsDir = join(PACKAGE_ROOT, "templates", "skills");
152
156
  if (!existsSync(templatesSkillsDir))
@@ -158,11 +162,33 @@ export function injectClaudeSkills(workspaceDir) {
158
162
  for (const entry of readdirSync(templatesSkillsDir, { withFileTypes: true })) {
159
163
  if (!entry.isDirectory())
160
164
  continue;
161
- const sourcePath = join(templatesSkillsDir, entry.name, "SKILL.md");
162
- if (!existsSync(sourcePath))
165
+ if (!existsSync(join(templatesSkillsDir, entry.name, "SKILL.md")))
163
166
  continue;
164
- const rendered = renderTemplate(readFileSync(sourcePath, "utf8"), vars);
165
- writeFileEnsuringDir(join(targetSkillsDir, entry.name, "SKILL.md"), rendered);
167
+ copySkillDir(join(templatesSkillsDir, entry.name), join(targetSkillsDir, entry.name), vars);
168
+ }
169
+ }
170
+ // One skill's files, at whatever depth they sit. Markdown is rendered with the skill variables, since a
171
+ // reference states the same placeholders SKILL.md does; anything else is copied byte for byte.
172
+ function copySkillDir(from, to, vars) {
173
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
174
+ const source = join(from, entry.name);
175
+ const target = join(to, entry.name);
176
+ // A link is copied as what it points at. The container gets a directory rather than a tree of links,
177
+ // and a shared reference brought in as a link still ships - a skill naming a file this walk skipped
178
+ // is exactly the broken skill that copying the whole directory exists to prevent.
179
+ const kind = statSync(source);
180
+ if (kind.isDirectory()) {
181
+ copySkillDir(source, target, vars);
182
+ }
183
+ else if (kind.isFile()) {
184
+ if (entry.name.endsWith(".md")) {
185
+ writeFileEnsuringDir(target, renderTemplate(readFileSync(source, "utf8"), vars));
186
+ }
187
+ else {
188
+ mkdirSync(dirname(target), { recursive: true });
189
+ copyFileSync(source, target);
190
+ }
191
+ }
166
192
  }
167
193
  }
168
194
  // --- Inject agent context ----------------------------------------------------------------------------------------------------------------
@@ -74,6 +74,7 @@ export const BAKED_TEMPLATE_FILES = [
74
74
  "npmrc",
75
75
  "pnpm-config.yaml",
76
76
  "runtime-constants.mjs",
77
+ "specs.sh",
77
78
  "startup-git-mode.mjs",
78
79
  "startup.mjs",
79
80
  "webpane.sh",
@@ -82,7 +83,7 @@ export const BAKED_TEMPLATE_FILES = [
82
83
  // Directories (relative to the templates dir) baked into the image via a directory COPY. Hashed
83
84
  // recursively by computeBuildHash so editing any file inside triggers a rebuild prompt, with
84
85
  // node_modules skipped - a local install there must never flip the hash (it is .dockerignore'd too).
85
- export const BAKED_TEMPLATE_DIRS = ["webterm"];
86
+ export const BAKED_TEMPLATE_DIRS = ["pane-apps", "webterm"];
86
87
  // Relative paths of every regular file under dir (sorted, node_modules subtrees skipped). Missing
87
88
  // dirs yield [] for the same reason computeBuildHash tolerates missing files: minimal test contexts.
88
89
  function walkTemplateDir(root, prefix = "") {
@@ -7,20 +7,30 @@
7
7
  import { rmSync } from "node:fs";
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { basename, join, resolve, sep } from "node:path";
10
- import { CHAMBA_DIR, CHAMBA_YAML, CONTAINER_NAME_PREFIX } from "./constants.js";
10
+ import { CHAMBA_DIR, CHAMBA_YAML, CONTAINER_NAME_PREFIX, PACKAGE_ROOT } from "./constants.js";
11
11
  const CHAMBA_HOME = resolve(join(homedir(), CHAMBA_DIR));
12
12
  const TEST_TMP_PREFIX = join(tmpdir(), `${CONTAINER_NAME_PREFIX}test-`);
13
+ // chamba's own build outputs, inside the package itself. The build stages the pane apps into the
14
+ // templates tree, and it clears the previous staging before it copies: every asset in that dist is
15
+ // named by its own content hash, so merging would keep every past build's files for ever, and the
16
+ // image build hash covers each of them.
17
+ const BUILD_OUTPUTS = [resolve(join(PACKAGE_ROOT, "dist")), resolve(join(PACKAGE_ROOT, "templates", "pane-apps"))];
13
18
  /**
14
19
  * Safe wrapper around rmSync. Throws if the path is outside a chamba-owned location:
15
20
  * - ~/.chamba/ (workspace caches, agents, shadows, global config)
16
21
  * - A file named chamba.yaml (workspace config file in any user workspace root)
17
22
  * - <tmpdir>/chamba-test-* (test temp directories)
23
+ * - chamba's own build outputs inside the package (dist/, templates/pane-apps/)
18
24
  */
19
25
  export function safeRmSync(path, options) {
20
26
  const r = resolve(path);
21
- const ok = r === CHAMBA_HOME || r.startsWith(CHAMBA_HOME + sep) || basename(r) === CHAMBA_YAML || r.startsWith(TEST_TMP_PREFIX);
27
+ const ok = r === CHAMBA_HOME ||
28
+ r.startsWith(CHAMBA_HOME + sep) ||
29
+ basename(r) === CHAMBA_YAML ||
30
+ r.startsWith(TEST_TMP_PREFIX) ||
31
+ BUILD_OUTPUTS.some((output) => r === output || r.startsWith(output + sep));
22
32
  if (!ok) {
23
- throw new Error(`safeRmSync: refusing to delete '${r}' - must be under ~/.chamba/, named chamba.yaml, or a test temp dir`);
33
+ throw new Error(`safeRmSync: refusing to delete '${r}' - must be under ~/.chamba/, named chamba.yaml, a test temp dir, or a build output of this package`);
24
34
  }
25
35
  rmSync(r, options);
26
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chamba",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Run AI coding agents in a container, from your browser",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "js-yaml": "^4.1.1"
14
14
  },
15
15
  "devDependencies": {
16
- "@biomejs/biome": "^2.4.10",
16
+ "@biomejs/biome": "2.5.5",
17
17
  "@types/js-yaml": "^4.0.9",
18
18
  "@types/node": "^25.4.0",
19
19
  "tsx": "^4.21.0",
@@ -30,7 +30,7 @@
30
30
  "test:docker": "node --import tsx/esm --test 'tests/docker/**/*.test.ts'",
31
31
  "test:all": "pnpm test && pnpm test:docker",
32
32
  "validate": "pnpm typecheck && pnpm lint && tsx scripts/check.ts && pnpm test",
33
- "check": "pnpm validate && pnpm re:build",
33
+ "check": "pnpm re:build && pnpm validate",
34
34
  "release": "tsx scripts/release.ts",
35
35
  "prepublishOnly": "pnpm re:build"
36
36
  },
@@ -157,7 +157,17 @@ RUN chmod +x /usr/local/share/chamba/webterm.sh && \
157
157
  ln -sf /usr/local/share/chamba/webterm.sh /usr/local/bin/webterm
158
158
 
159
159
  # ---------------------------------------------------------------------------
160
- # Layer 14 - Bake the web pane publish helper
160
+ # Layer 14 - Bake the pane apps (the tool UIs the pane hosts, and their servers)
161
+ # One built dist from the private pane-apps package: a client per tool and the
162
+ # module webterm mounts for it. Beside webterm rather than inside it, because
163
+ # webterm resolves it as a sibling and its own npm ci layer stays untouched by
164
+ # a UI change. Nothing to install - each server is bundled with no dependency
165
+ # outside node's own builtins.
166
+ # ---------------------------------------------------------------------------
167
+ COPY pane-apps/ /usr/local/share/chamba/pane-apps/
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Layer 15 - Bake the web pane publish helper
161
171
  # Publishes an HTML file as a page in the calling session's pane. On PATH via
162
172
  # the symlink, so any process inside a session can use it.
163
173
  # ---------------------------------------------------------------------------
@@ -165,6 +175,15 @@ COPY webpane.sh /usr/local/share/chamba/webpane.sh
165
175
  RUN chmod +x /usr/local/share/chamba/webpane.sh && \
166
176
  ln -sf /usr/local/share/chamba/webpane.sh /usr/local/bin/webpane
167
177
 
178
+ # ---------------------------------------------------------------------------
179
+ # Layer 16 - Bake the Specs helper
180
+ # The agent's half of the Specs channel: the verbs that move a piece of work.
181
+ # Gated like webpane, so only a process inside a session may use it.
182
+ # ---------------------------------------------------------------------------
183
+ COPY specs.sh /usr/local/share/chamba/specs.sh
184
+ RUN chmod +x /usr/local/share/chamba/specs.sh && \
185
+ ln -sf /usr/local/share/chamba/specs.sh /usr/local/bin/specs
186
+
168
187
  WORKDIR /workspace
169
188
 
170
189
  # ---------------------------------------------------------------------------
@@ -0,0 +1 @@
1
+ :root{--bg:#0d1117;--panel:#161b22;--fg:#e6edf3;--muted:#8b949e;--border:#30363d;--accent:#58a6ff;--ok:#3fb950;--warn:#d29922;--sunken:#0b0e13;--sel:#1f6feb33}:root[data-theme=light]{--bg:#fff;--panel:#f6f8fa;--fg:#1f2328;--muted:#656d76;--border:#d0d7de;--accent:#0969da;--ok:#1a7f37;--warn:#9a6700;--sunken:#f6f8fa;--sel:#ddf4ff}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--fg);margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.6}@media (prefers-reduced-motion:no-preference){html{scroll-behavior:smooth}}.workspace{grid-template-columns:minmax(180px,220px) minmax(0,1fr) minmax(220px,280px);height:100%;display:grid}.nav,.rail{background:var(--panel);font-size:12.5px;overflow:auto}.nav{border-right:1px solid var(--border);padding:10px 0 24px}.rail{border-left:1px solid var(--border);flex-direction:column;gap:18px;padding:12px 14px 24px;display:flex}.nav-head,.rail-head{letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:12px 0 5px;padding:0 14px;font-size:10.5px;font-weight:700}.rail-head{padding:0}.nav-list{margin:0;padding:0;list-style:none}.nav-list button{text-align:left;width:100%;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-left:2px solid #0000;padding:3px 14px;display:block}.nav-list button:hover{color:var(--accent)}.nav-list button[aria-current]{background:var(--sel);border-left-color:var(--accent)}.nav-list button.nav-artifact{padding-left:22px}.nav-list button.nav-spec{align-items:baseline;gap:6px;display:flex}.nav-spec-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.nav-spec-status{color:var(--muted);letter-spacing:.04em;text-transform:uppercase;flex:none;font-size:10px}.nav-spec-awaiting{background:var(--accent);min-width:15px;color:var(--bg);text-align:center;border-radius:7px;flex:none;padding:0 4px;font-size:10px;font-weight:700}.nav-dir{color:var(--muted);letter-spacing:.04em;margin:8px 0 2px;padding:0 14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10.5px}.nav-sections{margin:0 0 4px;padding:0;font-size:12px;list-style:none}.nav-sections button{color:var(--muted);padding-left:32px}.nav-sections button[aria-current]{color:var(--fg);font-weight:600}.nav-sections button.is-read{opacity:.55}.nav-changed{color:var(--warn);vertical-align:middle;margin-left:6px;font-size:9px}.doc{padding:20px 40px 96px;overflow:auto}.doc-facts{color:var(--muted);align-items:center;gap:10px;margin-bottom:14px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;display:flex}.doc-section{scroll-margin-top:12px;position:relative}.doc-section.is-read{opacity:.62}.doc-section h2{margin:22px 0 8px;font-size:1.15em}.sec-mark{position:absolute;top:24px;left:-26px}.mark{border:1px solid var(--border);background:var(--bg);color:#0000;cursor:pointer;border-radius:5px;width:20px;height:20px;padding:0;font-size:11px;line-height:1}.doc-section:hover .mark,.mark:focus-visible{color:var(--muted)}.mark[aria-pressed=true]{color:var(--ok);border-color:var(--ok)}.sec-changed{color:var(--warn);margin-left:10px;font-size:11px;font-weight:400}.art-frame{border:1px solid var(--border);background:var(--bg);border-radius:8px;width:100%;height:calc(100vh - 90px)}.art-image img{max-width:100%}.rail-top{justify-content:flex-end;display:flex}.rail-fact{margin:0 0 2px}.rail-changed{color:var(--warn)}.theme-toggle{border:1px solid var(--border);color:var(--muted);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 12px;font-size:11px}.notice{color:var(--muted);padding:20px 24px}.notice-failure{border-left:3px solid var(--warn);color:var(--fg)}.muted{color:var(--muted)}.visually-hidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.plain{white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;overflow-x:auto}.prose{overflow-wrap:anywhere;font-size:13.5px}.prose h1{margin:18px 0 8px;font-size:1.4em}.prose h2{margin:18px 0 8px;font-size:1.2em}.prose h3{margin:16px 0 6px;font-size:1.08em}.prose h4,.prose h5,.prose h6{color:var(--muted);margin:14px 0 6px;font-size:1em}.prose p{margin:0 0 10px}.prose ul,.prose ol{margin:0 0 10px;padding-left:22px}.prose li{margin:2px 0}.prose li.task{margin-left:-20px;list-style:none}.task-box{border:1px solid var(--muted);vertical-align:baseline;border-radius:3px;width:11px;height:11px;margin-right:8px;display:inline-block}.task-box.is-done{border-color:var(--ok);background:var(--ok)}.prose a{color:var(--accent)}.prose code{background:var(--sunken);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}.prose blockquote{border-left:3px solid var(--border);color:var(--muted);margin:0 0 12px;padding:2px 14px}.prose blockquote>:last-child{margin-bottom:0}.prose hr{border:0;border-top:1px solid var(--border);margin:16px 0}.prose img{max-width:100%}.prose table{border-collapse:collapse;margin:0 0 12px;font-size:.95em;display:block;overflow-x:auto}.prose th,.prose td{border:1px solid var(--border);text-align:left;padding:4px 10px}.prose th{background:var(--sunken)}.prose .cell-left{text-align:left}.prose .cell-center{text-align:center}.prose .cell-right{text-align:right}.prose pre{background:var(--sunken);border:1px solid var(--border);border-radius:8px;margin:0 0 12px;padding:12px;overflow-x:auto}.prose pre code{background:0 0;border:0;padding:0}.annotate{z-index:2;border:1px solid var(--border);background:var(--panel);border-radius:7px;gap:4px;padding:3px 6px;display:flex;position:absolute;box-shadow:0 4px 14px #00000059}.annotate button{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:5px;padding:2px 5px;font-size:13px;line-height:1}.annotate button:hover{background:var(--sel)}.annotate-whole{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:0 9px;font-size:10.5px}.queue{margin:0 0 8px;padding:0;list-style:none}.filed{margin:0;padding:0;list-style:none}.filed li{border-bottom:1px solid var(--border);align-items:baseline;gap:6px;padding:3px 0;font-size:11.5px;display:flex}.filed li:last-child{border-bottom:none}.filed-round{flex:none;font-weight:600}.filed-count{color:var(--muted);flex:1}.filed-when{color:var(--muted);flex:none;font-size:10.5px}.queued{border:1px solid var(--border);border-left:3px solid var(--accent);background:var(--bg);border-radius:7px;margin-bottom:7px;padding:7px 9px}.queued.is-remove{border-left-color:var(--warn)}.queued-target{color:var(--muted);margin:0;font-size:11px;font-style:italic}.queued-kind{color:var(--warn);margin:3px 0 0;font-size:11.5px}.queued textarea{background:var(--panel);width:100%;min-height:42px;color:inherit;border:1px solid var(--border);font:inherit;resize:vertical;border-radius:6px;margin-top:4px;padding:5px 7px;font-size:12px}.queued-acts{gap:10px;margin-top:4px;display:flex}.queued-acts button{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;padding:0;font-size:11px}.queued-acts button:disabled{color:var(--muted);cursor:default}.send{background:var(--accent);width:100%;color:var(--bg);font:inherit;cursor:pointer;border:none;border-radius:7px;padding:6px 0;font-size:12px}.send:disabled{background:var(--border);color:var(--muted);cursor:default}.rail-continue{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:7px;margin-top:6px;padding:3px 10px;font-size:11.5px}.dialog-shade{z-index:10;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.dialog{border:1px solid var(--border);background:var(--panel);border-radius:10px;max-width:340px;padding:16px 18px}.dialog p{margin:0 0 12px}.dialog-acts{justify-content:flex-end;gap:10px;display:flex}.dialog-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:4px 12px;font-size:12px}.dialog-acts .send{border-color:var(--accent);width:auto}.nav-top{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.nav-new{border:1px solid var(--border);color:var(--accent);font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:2px 9px;font-size:11px}.board-list{margin:0;padding:0;list-style:none}.board-stage{padding:2px 0;font-size:12px}.board-stage-name{color:var(--muted);margin:0}.board-stage.is-running .board-stage-name,.board-stage.is-done .board-stage-name{color:var(--text)}.board-mark{width:14px;color:var(--muted);display:inline-block}.board-stage.is-running .board-mark{color:var(--accent)}.board-artifacts{margin:0 0 4px 14px;padding:0;list-style:none}.board-artifacts button{color:var(--accent);font:inherit;cursor:pointer;text-align:left;background:0 0;border:none;padding:1px 0;font-size:11.5px}.board-activity{color:var(--accent);margin:6px 0 0;font-size:11.5px}.board-awaiting{gap:6px;margin:10px 0 0;padding:0;list-style:none;display:grid}.form{padding-bottom:40px}.form .lede{color:var(--muted)}.choices{gap:12px;margin:0;padding:0;list-style:none;display:grid}.choices li{border:1px solid var(--border);border-radius:8px;padding:10px 12px}.choices li.is-recommended{border-color:var(--accent)}.choices label{cursor:pointer;align-items:center;gap:8px;display:flex}.choice-title{font-weight:600}.choice-flag{color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-left:8px;font-size:10.5px;font-weight:400}.choice-about{color:var(--muted);margin:4px 0 0 22px;font-size:12.5px}.choice-why{margin:4px 0 0 22px;font-size:12.5px}.choice-mode{cursor:pointer;align-items:flex-start;gap:8px;display:flex}.field{margin-bottom:12px;display:block}.field>span{color:var(--muted);margin-bottom:4px;font-size:11.5px;display:block}.field input,.field-dump{box-sizing:border-box;background:var(--bg);border:1px solid var(--border);width:100%;color:inherit;font:inherit;resize:vertical;border-radius:7px;padding:7px 9px;font-size:13px}.field-dump{min-height:140px}.attached{gap:4px;margin:0 0 12px;padding:0;font-size:12px;list-style:none;display:grid}.attached li{justify-content:space-between;gap:8px;display:flex}.attached button{color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;font-size:11px}.form-acts{justify-content:flex-end;gap:10px;margin-top:20px;display:flex}.form-acts button{border:1px solid var(--border);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:7px;padding:5px 14px;font-size:12.5px}.form-acts .send{border-color:var(--accent);width:auto;color:var(--accent)}.dialog-wide{width:90%;max-width:560px}.doc-and-ask{flex-direction:column;min-width:0;display:flex;overflow-y:auto}.doc-and-ask .doc{flex:1 0 auto;overflow-y:visible}.ask{border:1px solid var(--accent);background:var(--panel);border-radius:10px;width:calc(100% - 80px);max-width:780px;margin:0 auto 40px;padding:16px 20px}.ask-head h2{margin:0;font-size:15px}.ask-about{color:var(--muted);margin:2px 0 10px;font-size:11.5px}.ask-evidence{margin:0 0 8px}.ask-tradeoff{color:var(--muted);margin:0 0 12px}.questions{gap:18px;margin:0 0 14px;padding-left:18px;display:grid}.question-ask{margin:0 0 8px;font-weight:600}.question-said{box-sizing:border-box;background:var(--bg);border:1px solid var(--border);width:100%;min-height:48px;color:inherit;font:inherit;resize:vertical;border-radius:7px;margin-top:8px;padding:6px 8px;font-size:12.5px}.board-review{padding:3px 0;font-size:12px}