@nanocollective/roster 0.1.0-alpha.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/cli.js +5679 -0
  4. package/docs/README.md +99 -0
  5. package/docs/agents.md +163 -0
  6. package/docs/architecture.md +121 -0
  7. package/docs/commands.md +223 -0
  8. package/docs/concepts.md +112 -0
  9. package/docs/cost.md +61 -0
  10. package/docs/developing.md +147 -0
  11. package/docs/doctor-codes.md +74 -0
  12. package/docs/export.md +113 -0
  13. package/docs/extending.md +97 -0
  14. package/docs/getting-started.md +134 -0
  15. package/docs/hosting.md +72 -0
  16. package/docs/manual-steps.md +163 -0
  17. package/docs/memory.md +71 -0
  18. package/docs/org-yaml.md +143 -0
  19. package/docs/portal.md +342 -0
  20. package/docs/prompts.md +133 -0
  21. package/docs/security.md +122 -0
  22. package/docs/session-workflow.md +112 -0
  23. package/docs/staff-yaml.md +163 -0
  24. package/docs/troubleshooting.md +189 -0
  25. package/docs/upgrading.md +83 -0
  26. package/docs/writing-a-charter.md +83 -0
  27. package/package.json +60 -0
  28. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +33 -0
  29. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +65 -0
  30. package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +50 -0
  31. package/templates/brain/CHARTER.md +49 -0
  32. package/templates/brain/README.md +18 -0
  33. package/templates/brain/drafts/README.md +7 -0
  34. package/templates/brain/log/decisions.md +6 -0
  35. package/templates/brain/memory/INDEX.md +28 -0
  36. package/templates/brain/staff.yaml +44 -0
  37. package/templates/brain/strategy/README.md +7 -0
  38. package/templates/briefs/amend.md +60 -0
  39. package/templates/briefs/charter.md +47 -0
  40. package/templates/briefs/discover.md +61 -0
  41. package/templates/briefs/voice.md +53 -0
  42. package/templates/ops/.github/workflows/session.yaml +333 -0
  43. package/templates/ops/agents.mjs +143 -0
  44. package/templates/ops/compose.mjs +333 -0
  45. package/templates/ops/org/guardrails.md +14 -0
  46. package/templates/ops/org/operating.md +82 -0
  47. package/templates/ops/org/voice.md +40 -0
  48. package/templates/ops/prompts/_identity.md +14 -0
  49. package/templates/ops/prompts/_paths.md +15 -0
  50. package/templates/ops/prompts/daily.md +82 -0
  51. package/templates/ops/prompts/mention.md +53 -0
  52. package/templates/ops/prompts/pr-mention.md +57 -0
  53. package/templates/ops/runner-plan.mjs +65 -0
  54. package/templates/portal/css/base.css +104 -0
  55. package/templates/portal/css/brain.css +106 -0
  56. package/templates/portal/css/diff.css +28 -0
  57. package/templates/portal/css/graph.css +34 -0
  58. package/templates/portal/css/health.css +41 -0
  59. package/templates/portal/css/inbox.css +79 -0
  60. package/templates/portal/css/layout.css +98 -0
  61. package/templates/portal/css/markdown.css +54 -0
  62. package/templates/portal/css/setup.css +106 -0
  63. package/templates/portal/index.html +55 -0
  64. package/templates/portal/js/api.js +74 -0
  65. package/templates/portal/js/app.js +282 -0
  66. package/templates/portal/js/dialog.js +70 -0
  67. package/templates/portal/js/dom.js +106 -0
  68. package/templates/portal/js/icons.js +94 -0
  69. package/templates/portal/js/md.js +386 -0
  70. package/templates/portal/js/refresh.js +59 -0
  71. package/templates/portal/js/router.js +20 -0
  72. package/templates/portal/js/state.js +160 -0
  73. package/templates/portal/js/textdiff.js +96 -0
  74. package/templates/portal/js/views/app.js +128 -0
  75. package/templates/portal/js/views/brain.js +260 -0
  76. package/templates/portal/js/views/changed.js +157 -0
  77. package/templates/portal/js/views/checklist.js +87 -0
  78. package/templates/portal/js/views/docs.js +84 -0
  79. package/templates/portal/js/views/files.js +95 -0
  80. package/templates/portal/js/views/graph.js +436 -0
  81. package/templates/portal/js/views/health.js +158 -0
  82. package/templates/portal/js/views/inbox.js +549 -0
  83. package/templates/portal/js/views/memory.js +135 -0
  84. package/templates/portal/js/views/org.js +175 -0
  85. package/templates/portal/js/views/paste.js +142 -0
  86. package/templates/portal/js/views/prompt.js +412 -0
  87. package/templates/portal/js/views/repos.js +92 -0
  88. package/templates/portal/js/views/setup.js +344 -0
  89. package/templates/portal/js/views/staff.js +290 -0
@@ -0,0 +1,57 @@
1
+ You are **{{staff.name}}** at {{org.name}}. {{human.name}} has asked for a change to an open pull
2
+ request, in a comment on the PR itself. They are not watching a terminal; **your reply on that PR is
3
+ the only thing they will see.**
4
+
5
+ **This is not a session.** No boot ritual, no handoff, no rewriting #{{staff.status_issue}}. Make the
6
+ change asked for, push it, reply, stop.
7
+
8
+ {{#if staff.public_bot}}
9
+ ## You are anonymous over there
10
+
11
+ The PR is on a **public** repo. You post as `{{staff.public_bot}}`, an ordinary automation account.
12
+ **Never sign as {{staff.name}}, never mention a charter, a tracker, a scheduled run, or that there is
13
+ a role behind the account.** Write like a maintainer's bot: short, factual, no personality.
14
+
15
+ Every `gh` call against that repo must carry the public identity: `GH_TOKEN=$PUBLIC_TOKEN gh ...`.
16
+ The bare `GH_TOKEN` is your private-tracker identity and using it there would sign your name to a
17
+ public repo.
18
+ {{/if}}
19
+
20
+ {{> prompts/_paths.md}}
21
+
22
+ ## The request
23
+
24
+ **PR #{{event.pr_number}}, comment `{{event.comment_id}}`.** Read it before touching anything: an
25
+ inline review comment carries `path`, `line` and `diff_hunk`, which say exactly which code is meant.
26
+ Do not guess from the prose.
27
+
28
+ ## Do the work
29
+
30
+ 1. **You are already on the PR branch** - the runner checked it out. Do not start a new one.
31
+ 2. **If the PR is from a fork you cannot push to**, do not try. Reply with the exact diff to apply,
32
+ and say why you could not push it.
33
+ 3. **Make only the change asked for.** A review comment is a narrow request. Fixing something else
34
+ makes the diff unreviewable and costs a second review. If you spot something real and separate,
35
+ mention it in the reply and leave the code alone.
36
+ 4. **Run the full gate before pushing.** If it goes red on the requested change, push nothing and
37
+ reply with what broke.
38
+ 5. **Pull with rebase before pushing** - there may be several comments, and another run may be on
39
+ the same branch. If the push is still rejected, pull and retry rather than forcing: a force push
40
+ on a branch being reviewed throws away their place in the diff.
41
+ 6. **You cannot verify anything UI-shaped** - there is no browser and no device here. Say so plainly
42
+ rather than implying a green gate covered it.
43
+
44
+ **Do not merge, and do not push to the default branch.**
45
+
46
+ {{> org/guardrails.md}}
47
+
48
+ ## Reply where they asked
49
+
50
+ Answer in the same place the request came from. **Do not @-mention them** - they are subscribed to
51
+ their own PR.
52
+
53
+ {{> org/voice.md}}
54
+
55
+ **For this reply specifically:** what changed, the SHA, gate status, what it did not cover.
56
+ **Nothing else.** No preamble, no restating the request, no headers, no sign-off, no narration of
57
+ the steps you took. If nothing changed, one line saying so and why.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Works out what a runner has to check out, before it has checked anything out.
3
+ //
4
+ // The ops repo is the only thing a workflow can clone without reading a manifest first, so it
5
+ // clones that, runs this, and gets back everything else: where the staff member's brain lands,
6
+ // which peers to bring along, which product repos to clone and how deep.
7
+ //
8
+ // Emits `key=value` lines for $GITHUB_OUTPUT. Vendored alongside compose.mjs for the same reason:
9
+ // a morning run must not depend on npm or on an org the tenant does not control.
10
+ //
11
+ // Usage: node roster-ops/runner-plan.mjs --staff cto --ops roster-ops >> "$GITHUB_OUTPUT"
12
+
13
+ import { readFileSync, existsSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { parseYaml } from "./compose.mjs";
16
+
17
+ function main(argv) {
18
+ const args = {};
19
+ for (let i = 0; i < argv.length; i += 2) args[argv[i].replace(/^--/, "")] = argv[i + 1];
20
+ if (!args.staff) throw new Error("--staff is required");
21
+
22
+ const opsDir = resolve(args.ops ?? ".");
23
+ const org = parseYaml(readFileSync(join(opsDir, "org.yaml"), "utf8"), "org.yaml");
24
+
25
+ const entry = (org.staff ?? []).find((s) => s.handle === args.staff);
26
+ if (!entry) throw new Error(`org.yaml has no staff member "${args.staff}"`);
27
+ const dir = entry.dir ?? entry.handle;
28
+
29
+ // The staff manifest may not be readable yet (its repo is cloned after this runs), so
30
+ // everything here comes from org.yaml, which is the registry for exactly this reason.
31
+ const peers = (org.staff ?? [])
32
+ .filter((s) => s.handle !== args.staff)
33
+ .map((s) => `${org.org}/${s.dir ?? s.handle}:${s.dir ?? s.handle}`);
34
+
35
+ const products = (org.repos ?? [])
36
+ .filter((r) => r.role === "product")
37
+ .map((r) => `${org.org}/${r.name}:${r.name}:${r.depth ?? 0}`);
38
+
39
+ const out = {
40
+ brain_dir: dir,
41
+ brain_repo: `${org.org}/${dir}`,
42
+ org: org.org,
43
+ peers: peers.join(" "),
44
+ products: products.join(" "),
45
+ // A product repo present means node is worth setting up; a triage-only run should not
46
+ // pay for an install it will not use.
47
+ needs_node: products.length > 0 ? "true" : "false",
48
+ product_dir: products.length ? products[0].split(":")[1] : "",
49
+ product_repo: products.length ? products[0].split(":")[0] : "",
50
+ package_json: products.length ? `${products[0].split(":")[1]}/package.json` : "",
51
+ };
52
+
53
+ return Object.entries(out)
54
+ .map(([k, v]) => `${k}=${v}`)
55
+ .join("\n");
56
+ }
57
+
58
+ try {
59
+ process.stdout.write(main(process.argv.slice(2)) + "\n");
60
+ } catch (err) {
61
+ console.error(`runner-plan: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ export { main };
@@ -0,0 +1,104 @@
1
+ /* Tokens, reset, and the primitives every screen uses: buttons, inputs, chips, meta text. */
2
+
3
+ :root{
4
+ --bg:#0e1013; --panel:#15181d; --panel-2:#1b1f26; --line:#252a33;
5
+ --ink:#e6e9ef; --ink-dim:#9aa3b2; --ink-faint:#6b7484;
6
+ --accent:#7cc7a4; --accent-dim:#3d6b56;
7
+ --warn:#d8a657; --err:#e07b7b;
8
+ --will:#7aa2f7; --measured:#7cc7a4; --derived:#c99bd6;
9
+ --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
10
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;
11
+ }
12
+ /* System preference wins unless the toggle has stamped data-theme on the root. */
13
+ @media (prefers-color-scheme:light){
14
+ :root:not([data-theme="dark"]){
15
+ --bg:#fbfbfa; --panel:#fff; --panel-2:#f4f5f7; --line:#e3e5ea;
16
+ --ink:#1b1f26; --ink-dim:#5b6472; --ink-faint:#8b95a5;
17
+ --accent:#2f7d5b; --accent-dim:#a8d5bf;
18
+ --warn:#a5701d; --err:#b34a4a;
19
+ --will:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
20
+ }
21
+ }
22
+ :root[data-theme="light"]{
23
+ --bg:#fbfbfa; --panel:#fff; --panel-2:#f4f5f7; --line:#e3e5ea;
24
+ --ink:#1b1f26; --ink-dim:#5b6472; --ink-faint:#8b95a5;
25
+ --accent:#2f7d5b; --accent-dim:#a8d5bf;
26
+ --warn:#a5701d; --err:#b34a4a;
27
+ --will:#3b5fa8; --measured:#2f7d5b; --derived:#8a52a8;
28
+ }
29
+
30
+ *{box-sizing:border-box}
31
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 var(--sans);
32
+ -webkit-font-smoothing:antialiased}
33
+ a{color:inherit}
34
+
35
+ h1{font:600 21px var(--sans);margin:0 0 4px;letter-spacing:-.015em}
36
+ .sub{color:var(--ink-dim);margin:0 0 24px;font-size:13px}
37
+ .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
38
+ .meta{color:var(--ink-faint);font:11.5px var(--mono)}
39
+ .empty{color:var(--ink-faint);padding:26px 0;text-align:center;font-size:13px}
40
+ .err{color:var(--err)} .warn{color:var(--warn)} .ok{color:var(--accent)}
41
+
42
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:15px 17px;margin-bottom:11px}
43
+ .card h3{margin:0 0 3px;font:600 14px var(--sans)}
44
+ .grid{display:grid;gap:11px;grid-template-columns:repeat(auto-fill,minmax(250px,1fr))}
45
+
46
+ /* ---------- controls ---------- */
47
+ input[type=search],select{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
48
+ border-radius:7px;padding:7px 11px;font:13px var(--sans);outline:none}
49
+ input[type=search]{width:280px}
50
+ input[type=search]:focus,select:focus{border-color:var(--accent-dim)}
51
+ textarea{width:100%;background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
52
+ border-radius:8px;padding:10px 12px;font:13px/1.55 var(--sans);outline:none;resize:vertical}
53
+ textarea:focus{border-color:var(--accent-dim)}
54
+
55
+ .ghbtn{display:inline-block;padding:5px 11px;border:1px solid var(--line);border-radius:7px;
56
+ font:12px var(--sans);color:var(--ink-dim);text-decoration:none;white-space:nowrap}
57
+ .ghbtn:hover{color:var(--ink);border-color:var(--accent-dim);background:var(--panel-2)}
58
+ button.ghbtn{cursor:pointer;font-family:var(--sans);background:transparent;
59
+ -webkit-appearance:none;appearance:none;font-size:12px;line-height:1.4}
60
+ button.ghbtn:hover{background:var(--panel-2)}
61
+ .ghbtn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg);font-weight:600}
62
+ .ghbtn.primary:hover{filter:brightness(1.08);background:var(--accent)}
63
+ .ghbtn:disabled{opacity:.5;cursor:default}
64
+ .iconbtn{background:var(--panel-2);border:1px solid var(--line);color:var(--ink-dim);
65
+ border-radius:7px;width:34px;height:34px;cursor:pointer;font-size:15px;line-height:1}
66
+ .iconbtn:hover{color:var(--ink);border-color:var(--accent-dim)}
67
+ .iconbtn{display:inline-flex;align-items:center;justify-content:center}
68
+ .iconbtn .sync{display:inline-flex;--ic:15px}
69
+ .iconbtn.spin .sync{animation:spin .9s linear infinite}
70
+ @keyframes spin{to{transform:rotate(360deg)}}
71
+
72
+ /* ---------- icons ----------
73
+ Lucide, vendored in js/icons.js. They take colour from `currentColor` and size from the
74
+ `--ic` custom property, so a component sets both in CSS and never touches the SVG. */
75
+ svg.ic{width:var(--ic,15px);height:var(--ic,15px);display:block;flex:none}
76
+ /* A slot that holds one, centred, wherever a text glyph used to sit on a baseline. */
77
+ .icslot{display:inline-flex;align-items:center;justify-content:center;flex:none}
78
+
79
+ .chip{font:10px var(--sans);letter-spacing:.02em;padding:1.5px 7px;border-radius:20px;
80
+ border:1px solid var(--line);color:var(--ink-faint);white-space:nowrap}
81
+ .chip.hot{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
82
+ .chip.warm{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
83
+ .chip.cool{color:var(--will);border-color:color-mix(in srgb,var(--will) 40%,transparent)}
84
+ .pill{font:11px var(--mono);color:var(--ink-faint);border:1px solid var(--line);
85
+ border-radius:20px;padding:2px 9px}
86
+ kbd{font:11px var(--mono);border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;
87
+ padding:1px 5px;color:var(--ink-dim)}
88
+
89
+ .notice{background:color-mix(in srgb,var(--warn) 12%,transparent);border:1px solid
90
+ color-mix(in srgb,var(--warn) 40%,transparent);color:var(--ink-dim);border-radius:8px;
91
+ padding:10px 13px;margin-bottom:16px;font-size:12.5px;line-height:1.6}
92
+ .notice b{color:var(--ink)}
93
+
94
+ /* ---------- a dialog that asks for more than a line ----------
95
+ Native <dialog>, so the backdrop, the focus trap and Escape are the browser's job. */
96
+ dialog.ask{border:1px solid var(--line);border-radius:11px;background:var(--panel);
97
+ color:var(--ink);padding:20px 22px;width:min(560px,92vw);
98
+ box-shadow:0 24px 60px rgba(0,0,0,.34)}
99
+ dialog.ask::backdrop{background:rgba(0,0,0,.45);backdrop-filter:blur(2px)}
100
+ dialog.ask h3{margin:0;font:600 15.5px var(--sans);letter-spacing:-.012em}
101
+ dialog.ask .askhint{margin:6px 0 12px;font:12.5px/1.55 var(--sans);color:var(--ink-faint)}
102
+ dialog.ask textarea{font:13.5px/1.6 var(--sans);min-height:96px;max-height:44vh}
103
+ dialog.ask .askrow{margin-top:12px;justify-content:flex-end}
104
+ dialog.ask .askrow .meta{margin-right:auto}
@@ -0,0 +1,106 @@
1
+ /* The brain screen: its navigator, and the memory cards in the right pane. */
2
+
3
+ /* ---------- navigator ----------
4
+ Memory and Files are two kinds of thing, so they are two boxes rather than one flat list
5
+ where a parsed section and a markdown file are told apart only by their typeface. */
6
+ .navgroup{border:1px solid var(--line);border-radius:7px;margin-bottom:8px;padding:4px 3px 5px}
7
+ .navgroup:last-child{margin-bottom:0}
8
+ .ghead{display:flex;align-items:baseline;gap:8px;padding:6px 8px 6px;
9
+ font:600 10.5px var(--sans);letter-spacing:.09em;text-transform:uppercase;color:var(--ink-faint)}
10
+ .ghead i{font-style:normal;font:10.5px var(--mono);letter-spacing:0;text-transform:none;
11
+ margin-left:auto;color:var(--ink-faint)}
12
+
13
+ /* A memory section is prose, not a path, so it gets the sans face and is allowed to wrap
14
+ onto a second line rather than being cut off mid-word. */
15
+ .tfile.tmem{align-items:flex-start}
16
+ .tfile.tmem span.t{font:13px/1.4 var(--sans);color:var(--ink-dim);white-space:normal;
17
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
18
+ .tfile.tmem[aria-current="true"] span.t{color:var(--accent)}
19
+ .tfile.tmem i{position:relative;top:1px}
20
+ /* The section a focused fact belongs to stays lit, so opening one fact never loses your
21
+ place in the list. */
22
+ .tfile[data-within="true"] span.t{color:var(--ink)}
23
+ .tfile[data-within="true"]{background:color-mix(in srgb,var(--panel-2) 60%,transparent)}
24
+
25
+ .tsurface{display:flex;align-items:center;gap:7px;width:100%;text-align:left;background:none;
26
+ border:0;color:var(--ink-dim);font:11.5px var(--mono);padding:5px 8px;border-radius:5px;
27
+ cursor:pointer}
28
+ .tsurface:hover{background:var(--panel-2);color:var(--ink)}
29
+ .tsurface .caret{color:var(--ink-faint);--ic:12px;flex:none;transition:transform .12s ease}
30
+ .tsurface[aria-expanded="false"] .caret{transform:rotate(-90deg)}
31
+ .tsurface .t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
32
+ .tsurface i{font-style:normal;font-size:10.5px;color:var(--ink-faint);flex:none;
33
+ font-variant-numeric:tabular-nums}
34
+ .tsfiles{padding-left:15px}
35
+ .tsfiles .tfile{font-size:12px}
36
+
37
+ /* ---------- memory cards ---------- */
38
+ .fact{border-bottom:1px solid var(--line);padding:13px 2px;scroll-margin-top:12px}
39
+ .fact:last-child{border-bottom:0}
40
+ .fact.lit{background:color-mix(in srgb,var(--warn) 10%,transparent);
41
+ box-shadow:inset 3px 0 0 var(--warn);border-radius:0 6px 6px 0;padding-left:11px}
42
+ .slug{font:12px var(--mono);color:var(--accent);cursor:pointer}
43
+ .slug:hover{text-decoration:underline}
44
+ .stmt{margin:3px 0 0}
45
+ .so{margin:4px 0 0;color:var(--ink-dim);font-size:13.2px}
46
+ .so b{color:var(--ink);font-weight:600}
47
+ .factsub{color:var(--ink-faint);font-size:12.5px;margin:0 0 14px}
48
+ .tag{display:inline-block;font:10.5px var(--mono);padding:1px 6px;border-radius:20px;
49
+ border:1px solid var(--line);color:var(--ink-faint);margin-left:6px;vertical-align:1px}
50
+ .tag.will{color:var(--will);border-color:var(--will)}
51
+ .tag.measured{color:var(--measured);border-color:var(--measured)}
52
+ .tag.derived{color:var(--derived);border-color:var(--derived)}
53
+ .notelink{font:11.5px var(--mono);color:var(--ink-faint);cursor:pointer;margin-left:6px}
54
+ .notelink:hover{color:var(--accent)}
55
+
56
+ /* Clicking a fact's name narrows the pane to that one fact. Without a crumb saying so and
57
+ a way out, that read as the page breaking. */
58
+ .focusbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 12px;
59
+ background:color-mix(in srgb,var(--warn) 10%,transparent);
60
+ border:1px solid color-mix(in srgb,var(--warn) 34%,transparent);
61
+ border-radius:7px;padding:7px 8px 7px 11px;font:12px var(--sans);color:var(--ink-dim)}
62
+ .focusbar .crumb{background:none;border:0;color:var(--ink-dim);cursor:pointer;
63
+ font:12px var(--sans);padding:0;text-align:left}
64
+ .focusbar .crumb:hover{color:var(--ink);text-decoration:underline}
65
+ .focusbar .sep{color:var(--ink-faint)}
66
+ .focusbar .here{font:12px var(--mono);color:var(--ink)}
67
+ .focusbar .x{margin-left:auto;background:none;border:1px solid var(--line);border-radius:6px;
68
+ color:var(--ink-dim);cursor:pointer;font:11px var(--sans);padding:3px 9px;white-space:nowrap}
69
+ .focusbar .x:hover{color:var(--ink);border-color:var(--ink-faint);background:var(--panel-2)}
70
+
71
+ /* ---------- the prompt screen ----------
72
+ A layer row carries two facts: the file, and which repo it came out of. The repo matters
73
+ because editing one in `roster-ops` reaches every staff member and one in a brain reaches
74
+ one, and that is the difference worth seeing before you click Edit. */
75
+ .tfile.tlayer{align-items:flex-start;padding:5px 8px}
76
+ .tfile.tlayer span.t{display:flex;flex-direction:column;gap:1px}
77
+ .tfile.tlayer .lname{font:12.5px var(--mono);overflow:hidden;text-overflow:ellipsis}
78
+ .tfile.tlayer .lrepo{font:10px/1.45 var(--sans);color:var(--ink-faint);letter-spacing:.02em;
79
+ white-space:normal;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;
80
+ overflow:hidden}
81
+ .tfile.tlayer[aria-current="true"] .lname{color:var(--accent)}
82
+ .tfile.tlayer.dim{opacity:.5}
83
+ .treenote{margin:0 8px 6px;font:11px/1.45 var(--sans);color:var(--ink-faint)}
84
+
85
+ /* The card already scrolls. A textarea that scrolls too gives you two scrollbars for one
86
+ document and neither of them is the one you want, so this one grows to fit its content
87
+ and never scrolls itself. Height is set in JS from scrollHeight; min-height is the floor
88
+ for a short file. */
89
+ .editor{font:12.5px/1.7 var(--mono);min-height:340px;overflow:hidden;resize:none}
90
+ .editnote{margin:8px 0 0;font:11.5px var(--sans);color:var(--ink-faint)}
91
+
92
+ /* A prompt audit finding. Not a tree row: it carries an explanation and two buttons, and
93
+ squeezing that into the navigator's row shape is how it would get ignored. */
94
+ .prob{border:1px solid var(--line);border-radius:7px;padding:9px 10px;margin:0 3px 6px}
95
+ .prob .ptitle{font:600 12.5px var(--sans);color:var(--ink)}
96
+ .prob .pdetail{margin-top:3px;font:11.5px/1.5 var(--sans);color:var(--ink-faint)}
97
+ .prob.error{border-color:color-mix(in srgb,var(--err) 45%,transparent)}
98
+ .prob.error .ptitle{color:var(--err)}
99
+ .prob.warning{border-color:color-mix(in srgb,var(--warn) 38%,transparent)}
100
+ .prob.warning .ptitle{color:var(--warn)}
101
+ .prob .ghbtn{font-size:11px;padding:3px 8px}
102
+
103
+ /* What an edit did to the composed prompt, which is the question you were actually asking. */
104
+ .effect{border:1px solid color-mix(in srgb,var(--accent) 35%,transparent);
105
+ background:color-mix(in srgb,var(--accent) 7%,transparent);
106
+ border-radius:8px;padding:10px 12px;margin-bottom:14px}
@@ -0,0 +1,28 @@
1
+ /* A unified diff, on the "what changed" screen. */
2
+
3
+ .dblock{border:1px solid var(--line);border-radius:8px;overflow:hidden;margin-bottom:14px}
4
+ .dblock:last-child{margin-bottom:0}
5
+ .diff{margin:0;font:12px/1.6 var(--mono);background:var(--panel-2);overflow-x:auto}
6
+ .dline{display:grid;grid-template-columns:42px 42px 1fr;white-space:pre;min-width:max-content}
7
+ .dline .ln{color:var(--ink-faint);text-align:right;padding:0 7px 0 5px;font-size:11px;
8
+ user-select:none;background:color-mix(in srgb,var(--line) 34%,transparent)}
9
+ .dline .tx{padding:0 14px 0 9px}
10
+ .dadd{background:color-mix(in srgb,var(--accent) 15%,transparent)}
11
+ .dadd .tx{color:var(--ink)}
12
+ .dadd .ln{background:color-mix(in srgb,var(--accent) 24%,transparent);color:var(--accent)}
13
+ .ddel{background:color-mix(in srgb,var(--err) 13%,transparent)}
14
+ .ddel .tx{color:var(--ink-dim)}
15
+ .ddel .ln{background:color-mix(in srgb,var(--err) 22%,transparent);color:var(--err)}
16
+ .dhunk{background:color-mix(in srgb,var(--will) 13%,transparent);
17
+ border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
18
+ .dhunk .tx{color:var(--will);font-size:11.5px}
19
+ .dmeta .tx{color:var(--ink-faint);font-size:11.5px}
20
+ .dfocus{box-shadow:inset 3px 0 0 var(--warn)}
21
+ .dfile{font:11.5px var(--mono);color:var(--ink-dim);background:var(--panel);padding:8px 12px;
22
+ border-bottom:1px solid var(--line);display:flex;gap:10px;align-items:baseline}
23
+ .dfile b{color:var(--ink);font-weight:600}
24
+ .dfile .p{color:var(--accent)} .dfile .m{color:var(--err)}
25
+ .dempty{padding:20px;color:var(--ink-faint);font-size:12.5px}
26
+
27
+ /* The learned/forgotten marker on a "what changed" row. */
28
+ .chg{display:inline-flex;flex:none;--ic:13px;position:relative;top:2px}
@@ -0,0 +1,34 @@
1
+ /* The link graph: canvas, its controls, and the sheet a selected fact opens. */
2
+
3
+ .graphwrap{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:9px;overflow:hidden}
4
+ #graph{display:block;width:100%;height:72vh;cursor:grab;touch-action:none}
5
+ #graph.grabbing{cursor:grabbing}
6
+ #graph.onnode{cursor:pointer}
7
+
8
+ .legend{position:absolute;left:12px;bottom:12px;font:11px var(--mono);color:var(--ink-dim);
9
+ background:color-mix(in srgb,var(--panel) 92%,transparent);padding:8px 11px;border-radius:7px;
10
+ border:1px solid var(--line);max-height:44%;overflow:auto}
11
+ .legend div{white-space:nowrap;line-height:1.75;cursor:pointer;opacity:.55}
12
+ .legend div.on{opacity:1}
13
+ .legend i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:7px}
14
+
15
+ .gtools{position:absolute;right:12px;top:12px;display:flex;gap:7px;align-items:center}
16
+ .gtools button{background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);
17
+ color:var(--ink-dim);border-radius:7px;padding:5px 10px;font:11.5px var(--mono);cursor:pointer}
18
+ .gtools button:hover{color:var(--ink);border-color:var(--accent-dim)}
19
+ .gtools input[type=search]{width:170px;padding:5px 10px;font:11.5px var(--mono)}
20
+
21
+ .sheet{position:absolute;right:0;top:0;bottom:0;width:370px;max-width:100%;
22
+ background:var(--panel);border-left:1px solid var(--line);padding:20px 22px;
23
+ overflow:auto;box-shadow:-14px 0 34px rgba(0,0,0,.16)}
24
+ .sheet .x{position:absolute;right:12px;top:12px;background:none;border:0;color:var(--ink-faint);
25
+ cursor:pointer;display:inline-flex;padding:4px;border-radius:6px;--ic:16px}
26
+ .sheet .x:hover{color:var(--ink);background:var(--panel-2)}
27
+ .sheet .skind{font:10.5px var(--mono);letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px}
28
+ .sheet h4{margin:0 0 10px;font:600 15px var(--mono);word-break:break-word;padding-right:22px;letter-spacing:-.01em}
29
+ .sheet p{margin:8px 0 0;font-size:13.5px;line-height:1.6}
30
+ .sheet .lk{display:block;width:100%;text-align:left;background:none;border:0;color:var(--ink-dim);
31
+ font:12px var(--mono);padding:4px 0;cursor:pointer;white-space:nowrap;overflow:hidden;
32
+ text-overflow:ellipsis}
33
+ .sheet .lk:hover{color:var(--accent)}
34
+ @media (max-width:760px){.sheet{width:100%}}
@@ -0,0 +1,41 @@
1
+ /* The health screen's fact strip and lint cards. */
2
+
3
+ .kv{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:12px 14px}
4
+ .kv .k{font:10.5px var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint)}
5
+ .kv .v{margin-top:5px;font:14px var(--sans);color:var(--ink);line-height:1.45}
6
+ .kv .v small{display:block;color:var(--ink-faint);font-size:11.5px;margin-top:2px}
7
+ .kv.bad{border-color:color-mix(in srgb,var(--err) 45%,transparent)}
8
+ .kv.bad .v{color:var(--err)}
9
+ .card .fix{margin-top:10px}
10
+ .card .fixed{color:var(--accent);font:11.5px var(--mono)}
11
+ .hsect{font:600 10.5px var(--sans);letter-spacing:.09em;text-transform:uppercase;
12
+ color:var(--ink-faint);margin:24px 0 10px}
13
+
14
+ /* ---------- the staff screen ---------- */
15
+ .staffcard .v small{margin-top:4px}
16
+
17
+ /* A plan, before anything happens. The two halves are deliberately equal in weight: "this is
18
+ what it keeps" is what somebody needs to believe before retiring anybody. */
19
+ .plan{border:1px solid var(--line);border-radius:9px;padding:14px 16px;margin-top:14px;
20
+ background:var(--panel)}
21
+ .plan h3{margin:0 0 10px;font:600 15px var(--sans)}
22
+ .planhead{font:600 10.5px var(--sans);letter-spacing:.09em;text-transform:uppercase;
23
+ color:var(--ink-faint);margin:14px 0 7px}
24
+ .planhead:first-of-type{margin-top:0}
25
+ .planhead.keep{color:var(--accent)}
26
+ .planline{display:flex;gap:9px;align-items:flex-start;font:12.5px/1.55 var(--sans);
27
+ color:var(--ink-dim);padding:2px 0}
28
+ .planline .ic{--ic:14px;flex:none;position:relative;top:3px}
29
+ .planline.ok .ic{color:var(--accent)}
30
+ .planline.stop .ic{color:var(--err)}
31
+ .planline.keep .ic{color:var(--accent)}
32
+ .planline.keep span{color:var(--ink)}
33
+ .planline.todo .ic{color:var(--warn)}
34
+ .planline.warn{color:var(--ink-faint)}
35
+ .planline.warn .ic{color:var(--warn)}
36
+ .planout{margin-top:14px;max-height:320px;overflow:auto;font-size:11.5px}
37
+
38
+ .ffield{margin-bottom:10px}
39
+ .ffield label{display:block;font:600 11px var(--sans);letter-spacing:.05em;
40
+ text-transform:uppercase;color:var(--ink-faint);margin-bottom:4px}
41
+ .ffield small{display:block;margin-top:3px;font:11px var(--sans);color:var(--ink-faint)}
@@ -0,0 +1,79 @@
1
+ /* The inbox list, and the thread beside it. */
2
+
3
+ .irow{display:block;width:100%;text-align:left;background:none;border:0;cursor:pointer;
4
+ padding:9px 10px;border-radius:7px;color:var(--ink-dim)}
5
+ .irow:hover{background:var(--panel-2)}
6
+ .irow:focus{outline:none}
7
+ .irow:focus-visible{outline:2px solid var(--accent-dim);outline-offset:-2px}
8
+ .irow[aria-current="true"]{background:var(--panel-2);box-shadow:inset 2px 0 0 var(--accent)}
9
+ .ititle{font:13.5px/1.4 var(--sans);color:var(--ink);display:-webkit-box;-webkit-line-clamp:2;
10
+ -webkit-box-orient:vertical;overflow:hidden}
11
+ .irow[aria-current="true"] .ititle{color:var(--accent)}
12
+ .dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--warn);
13
+ margin-right:6px;vertical-align:2px}
14
+ .imeta{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:5px}
15
+ .imeta .repo{font:10px var(--sans);letter-spacing:.03em;text-transform:uppercase;
16
+ color:var(--ink-faint);border-right:1px solid var(--line);padding-right:7px}
17
+ .imeta .num{font:10.5px var(--mono);color:var(--ink-faint)}
18
+ .imeta .when{font:10.5px var(--sans);color:var(--ink-faint);margin-left:auto}
19
+ .ck{display:inline-flex;--ic:13px}
20
+ /* Closed work reads as finished at a glance, so it can share a list with open work without
21
+ competing with it. */
22
+ .irow.shut .ititle{color:var(--ink-dim)}
23
+ .irow.shut[aria-current="true"] .ititle{color:var(--accent)}
24
+ .ist{display:inline-flex;--ic:12px}
25
+ .ist.closed{color:var(--derived)} .ist.merged{color:var(--derived)}
26
+ .ck.passing{color:var(--accent)} .ck.failing{color:var(--err)} .ck.pending{color:var(--warn)}
27
+
28
+ /* ---------- thread ---------- */
29
+ .thead{border-bottom:1px solid var(--line);padding-bottom:14px;margin-bottom:4px}
30
+ .trow{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
31
+ .thead h3{margin:8px 0 0;font:600 17px/1.35 var(--sans);letter-spacing:-.012em;color:var(--ink)}
32
+ .cmt{border-top:1px solid var(--line);padding:16px 0}
33
+ .cmt.first{border-top:0}
34
+ .reply{border-top:1px solid var(--line);margin-top:16px;padding-top:14px}
35
+
36
+ /* ---------- timeline events ----------
37
+ Not comments, so not comment-shaped: a marker, a line of prose, and whatever it points at.
38
+ These carry the half of a thread that is agents referring to each other's work. */
39
+ .tev{display:flex;gap:10px;align-items:flex-start;padding:9px 0 9px 2px;
40
+ font:12.5px/1.5 var(--sans);color:var(--ink-dim)}
41
+ .tev .tico{flex:none;display:inline-flex;align-items:center;justify-content:center;
42
+ width:16px;height:19px;color:var(--ink-faint);--ic:14px}
43
+ .tev .tbody{flex:1;min-width:0}
44
+ .tev b{color:var(--ink);font-weight:600}
45
+ .tev .tsub{display:block;margin-top:4px}
46
+ .tev .tcommit{font:11.5px var(--mono);color:var(--ink-dim);background:var(--panel-2);
47
+ text-decoration:none;
48
+ border:1px solid var(--line);border-radius:6px;padding:4px 9px;display:inline-block;
49
+ max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
50
+ .tev .tcommit:hover{color:var(--ink);border-color:var(--ink-faint)}
51
+ .tev .twhen{color:var(--ink-faint);font-size:11px;white-space:nowrap}
52
+ .tev.closed .tico{color:var(--err)}
53
+ .tev.reopened .tico,.tev.merged .tico{color:var(--accent)}
54
+ .tev.cross-referenced .tico{color:var(--will)}
55
+
56
+ /* An item this thread points at, as a row you can open here rather than a link that throws
57
+ you out to GitHub. Open and closed read at a glance, which is the whole reason to show it. */
58
+ .xref{display:flex;gap:8px;align-items:baseline;width:100%;text-align:left;background:none;
59
+ border:1px solid var(--line);border-radius:7px;padding:6px 10px;margin-top:5px;
60
+ cursor:pointer;color:var(--ink-dim);font:12.5px var(--sans)}
61
+ .xref:hover{border-color:var(--accent-dim);background:var(--panel-2);color:var(--ink)}
62
+ .xref .st{flex:none;display:inline-flex;--ic:14px;position:relative;top:2px}
63
+ .xref .st.open{color:var(--accent)} .xref .st.closed{color:var(--derived)}
64
+ .xref .st.merged{color:var(--derived)} .xref .st.draft{color:var(--ink-faint)}
65
+ .xref .t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
66
+ .xref .n{flex:none;font:11px var(--mono);color:var(--ink-faint)}
67
+
68
+ /* The bookkeeping — labels, assignees, renames — folded away by default. It is real history
69
+ and worth being able to see, but it is not what you opened the thread to read. */
70
+ .tevmore{display:flex;align-items:center;gap:7px;background:none;border:0;cursor:pointer;
71
+ color:var(--ink-faint);font:11.5px var(--sans);padding:8px 2px}
72
+ .tevmore:hover{color:var(--ink-dim)}
73
+ /* The chevron points down when open and right when closed, which is the other way round
74
+ from the "▸" character this used to be. */
75
+ .tevmore .caret{--ic:12px;display:inline-flex;transition:transform .12s ease;
76
+ transform:rotate(-90deg)}
77
+ .tevmore[aria-expanded="true"] .caret{transform:none}
78
+ .tevquiet{border-left:1px solid var(--line);margin-left:9px;padding-left:14px}
79
+ .tevquiet .tev{padding:5px 0;font-size:12px}
@@ -0,0 +1,98 @@
1
+ /* The page frame: sidebar, main column, and the tree-beside-viewer split every screen uses. */
2
+
3
+ .app{display:grid;grid-template-columns:246px 1fr;min-height:100vh}
4
+ /* ---------- sidebar ---------- */
5
+ .side{background:var(--panel);border-right:1px solid var(--line);padding:20px 0 14px;
6
+ position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column}
7
+ .brand{padding:0 20px 18px;font:600 15px var(--sans);letter-spacing:-.01em}
8
+ .brand small{display:block;color:var(--ink-faint);font-weight:400;font-size:11.5px;
9
+ letter-spacing:.02em;margin-top:2px}
10
+ .refreshall{display:flex;align-items:center;gap:8px;width:calc(100% - 24px);margin:0 12px 4px;
11
+ background:var(--panel-2);border:1px solid var(--line);color:var(--ink-faint);
12
+ border-radius:7px;padding:7px 10px;cursor:pointer;font:11px var(--sans);text-align:left}
13
+ .refreshall:hover{color:var(--ink);border-color:var(--accent-dim)}
14
+ .refreshall .sync{display:inline-flex;--ic:13px;flex:none}
15
+ .refreshall.spin .sync{animation:spin .9s linear infinite}
16
+
17
+ .sect{padding:14px 20px 6px;font:600 10.5px var(--sans);letter-spacing:.09em;
18
+ text-transform:uppercase;color:var(--ink-faint)}
19
+ /* Flex, not block with a float. The icons are block-level SVGs and the trailing count has to
20
+ sit hard right; `float` does neither of those inside a flex row, and did neither of them
21
+ outside one once the icons arrived. */
22
+ .nav{display:flex;align-items:center;width:100%;text-align:left;background:none;border:0;
23
+ color:var(--ink-dim);font:14px var(--sans);padding:7px 20px;cursor:pointer;
24
+ border-left:2px solid transparent}
25
+ .nav:hover{color:var(--ink);background:var(--panel-2)}
26
+ .nav[aria-current="true"]{color:var(--ink);border-left-color:var(--accent);background:var(--panel-2)}
27
+ .nav .ic{color:var(--ink-faint);margin-right:9px;--ic:15px;flex:none}
28
+ .nav .n{margin-left:auto;padding-left:10px;color:var(--ink-faint);font:11px var(--mono)}
29
+ .nav.top{margin-bottom:2px}
30
+
31
+ /* Staff are rows that unfold into their own views, rather than a dropdown plus a list you
32
+ have to connect for yourself. The nesting is the hierarchy, said out loud. */
33
+ .staffrow{display:flex;align-items:center;gap:7px;font:13.5px var(--sans);color:var(--ink);
34
+ padding-right:14px}
35
+ .staffrow .caret{color:var(--ink-faint);--ic:12px;flex:none;transition:transform .12s ease}
36
+ .staffrow[aria-expanded="false"] .caret{transform:rotate(-90deg)}
37
+ .staffrow .t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
38
+ .staffrow .hh{font:10.5px var(--mono);color:var(--ink-faint);flex:none}
39
+ .staffrow[aria-selected="true"]{color:var(--ink)}
40
+ .staffviews{padding-bottom:4px}
41
+ /* `subnav`, not `sub`: `.sub` is the page subtitle and carries a 24px bottom margin, which
42
+ these rows inherited as a gap you could park a car in. */
43
+ .nav.subnav{padding:5px 20px 5px 40px;font-size:13px}
44
+ .nav.subnav .n{font-size:10px}
45
+ .sidefoot{margin-top:auto;padding-top:14px}
46
+ .sidefoot .nav{font-size:12.5px;color:var(--ink-faint)}
47
+ .sidefoot .nav:hover{color:var(--ink-dim)}
48
+
49
+ /* ---------- main ---------- */
50
+ main{padding:28px 34px 80px;max-width:1180px;overflow-x:hidden}
51
+
52
+ /* ---------- tree beside viewer ---------- */
53
+ .split{display:grid;grid-template-columns:314px 1fr;gap:16px;align-items:start}
54
+ @media (max-width:900px){.split{grid-template-columns:1fr}}
55
+ .tree{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:9px;
56
+ max-height:74vh;overflow:auto}
57
+ .tdir{font:600 11px var(--mono);color:var(--ink-faint);padding:8px 8px 4px;letter-spacing:.04em}
58
+ .tdir.first{padding-top:2px}
59
+ .tfile{display:flex;align-items:baseline;gap:10px;width:100%;text-align:left;background:none;
60
+ border:0;color:var(--ink-dim);font:12.5px var(--mono);padding:4px 8px;border-radius:5px;
61
+ cursor:pointer}
62
+ .tfile:hover{background:var(--panel-2);color:var(--ink)}
63
+ .tfile[aria-current="true"]{background:var(--panel-2);color:var(--accent)}
64
+ .tfile span.t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
65
+ .tfile i{color:var(--ink-faint);font-style:normal;font-size:10.5px;flex:none;
66
+ font-variant-numeric:tabular-nums}
67
+ .tfile:focus{outline:none}
68
+ .tfile:focus-visible{outline:2px solid var(--accent-dim);outline-offset:-2px}
69
+
70
+ .viewer{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:20px 22px;
71
+ min-height:340px;max-height:74vh;overflow:auto}
72
+ .viewer pre{margin:0;white-space:pre-wrap;word-break:break-word;font:12.5px/1.65 var(--mono);color:var(--ink-dim)}
73
+ /* Source keeps its own line breaks and scrolls, rather than wrapping into soup. */
74
+ .viewer pre.code{white-space:pre;word-break:normal;overflow-x:auto}
75
+ .viewer img{max-width:100%;border-radius:6px;background:#fff}
76
+ .viewer table{border-collapse:collapse;font:12px var(--mono);width:100%}
77
+ .viewer td,.viewer th{border:1px solid var(--line);padding:4px 8px;text-align:left;white-space:nowrap}
78
+ .viewer th{position:sticky;top:0;background:var(--panel-2);color:var(--ink)}
79
+
80
+ .gallery{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
81
+ .gallery figure{margin:0}
82
+ .gallery img{width:100%;aspect-ratio:4/3;object-fit:contain;background:var(--panel-2);border-radius:6px;
83
+ border:1px solid var(--line);cursor:pointer}
84
+ .gallery figcaption{font:10.5px var(--mono);color:var(--ink-faint);margin-top:4px;
85
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
86
+
87
+ /* ---------- narrow ----------
88
+ Last in the file on purpose. This block used to sit next to `.app`, above the `.side` rule
89
+ that sets `height:100vh` — same specificity, later wins, so the override never applied and
90
+ the stacked sidebar filled the viewport with the page below the fold.
91
+
92
+ `align-content:start` is the other half: a grid with a min-height and auto rows stretches
93
+ them by default. */
94
+ @media (max-width:820px){
95
+ .app{grid-template-columns:1fr;align-content:start}
96
+ .side{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line)}
97
+ .sidefoot{margin-top:0}
98
+ }