@nanocollective/roster 0.1.0-alpha.23 → 0.1.0-alpha.25

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.
package/dist/cli.js CHANGED
@@ -140,7 +140,7 @@ async function createApp(spec2, opts = {}) {
140
140
  res.end(
141
141
  page(
142
142
  `${app.slug} created`,
143
- "The private key went straight into the repository's secrets and was never written to disk. You can close this tab; the terminal has the next step."
143
+ "Its keys are saved in the repository's secrets. You can close this tab; the terminal has the next step."
144
144
  )
145
145
  );
146
146
  finish(() => resolve6(app));
@@ -210,7 +210,8 @@ async function open(url) {
210
210
  // src/lib/install.ts
211
211
  import { existsSync, readFileSync } from "fs";
212
212
  import { join } from "path";
213
- function installTargets(ws, org, parseYaml, handle, spec2) {
213
+ function installTargets(ws, org, parseYaml, handle, spec2, scope = "private") {
214
+ if (scope === "public") return [...new Set(spec2.worksIn)];
214
215
  const peers = [];
215
216
  for (const s of org.staff ?? []) {
216
217
  if (s.handle === handle) continue;
@@ -222,7 +223,8 @@ function installTargets(ws, org, parseYaml, handle, spec2) {
222
223
  } catch {
223
224
  }
224
225
  }
225
- return [.../* @__PURE__ */ new Set([spec2.brain, ...spec2.worksIn, ...peers])];
226
+ const ops = org.org ? [`${org.org}/${ws.opsName}`] : [];
227
+ return [.../* @__PURE__ */ new Set([spec2.brain, ...ops, ...spec2.worksIn, ...peers])];
226
228
  }
227
229
  function installUrl(slug, targetId, repoIds = []) {
228
230
  const base2 = `https://github.com/apps/${encodeURIComponent(slug)}/installations/new`;
@@ -538,7 +540,14 @@ async function appCommand(argv) {
538
540
  `);
539
541
  return 2;
540
542
  }
541
- const targets = installTargets(ws, org, parseYaml, handle, spec2);
543
+ const targets = installTargets(
544
+ ws,
545
+ org,
546
+ parseYaml,
547
+ handle,
548
+ spec2,
549
+ opts.public ? "public" : "private"
550
+ );
542
551
  const existing = await api(`/apps/${name}`);
543
552
  if (existing.ok) {
544
553
  const install2 = await preselectedInstall(existing.data?.slug ?? name, org.org, targets);
@@ -808,6 +817,11 @@ function exampleSection(choice) {
808
817
  ""
809
818
  ].join("\n");
810
819
  }
820
+ function charterStarter(choice, business) {
821
+ const path = join8(docsDir(), "charters", `${choice}.md`);
822
+ if (!existsSync8(path)) throw new Error(`there is no example charter called "${choice}"`);
823
+ return readFileSync7(path, "utf8").replace(/^> \*\*An example to adapt[\s\S]*?\n\n/m, "").replace(/\bAcme\b/g, business);
824
+ }
811
825
  function exampleOffer(handle) {
812
826
  return [
813
827
  "## Worked examples",
@@ -4260,6 +4274,7 @@ function readRig(root, manifest, commits) {
4260
4274
  return {
4261
4275
  workflows: existsSync17(wfDir) ? readdirSync7(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [],
4262
4276
  hasCharter: existsSync17(join19(root, "CHARTER.md")),
4277
+ charterStub: !existsSync17(join19(root, "CHARTER.md")) || looksUnwritten(readFileSync16(join19(root, "CHARTER.md"), "utf8")),
4263
4278
  hasManifest: existsSync17(join19(root, "staff.yaml")),
4264
4279
  missingSurfaces: declared.filter((s) => s?.path && !existsSync17(join19(root, s.path))).map((s) => s.path),
4265
4280
  memoryBytes: bytesOf(join19(root, "memory", "INDEX.md")),
@@ -6422,6 +6437,7 @@ async function portalCommand(argv) {
6422
6437
  const TTL = 45e3;
6423
6438
  const labelCache = /* @__PURE__ */ new Map();
6424
6439
  let runsCache = null;
6440
+ const progressCache = /* @__PURE__ */ new Map();
6425
6441
  const LABEL_TTL = 3e5;
6426
6442
  const brainDirs = () => (readOrg(ws.opsDir, parseYaml).staff ?? []).map((s) => s.dir ?? s.handle);
6427
6443
  const knownRepos = () => {
@@ -6429,7 +6445,9 @@ async function portalCommand(argv) {
6429
6445
  return (org.repos ?? []).map((r) => ({
6430
6446
  name: r.name,
6431
6447
  owner: org.org,
6432
- role: r.role ?? "repo"
6448
+ role: r.role ?? "repo",
6449
+ // As org.yaml has it. Absent on a hand-written entry, which hire treats as public.
6450
+ visibility: r.visibility
6433
6451
  }));
6434
6452
  };
6435
6453
  const writeAllowed = (req) => {
@@ -6471,7 +6489,14 @@ async function portalCommand(argv) {
6471
6489
  res.end(JSON.stringify({ error: `staff.yaml declares no ${payload.scope} app` }));
6472
6490
  return;
6473
6491
  }
6474
- const targets = installTargets(ws, org, parseYaml, entry.handle, spec2);
6492
+ const targets = installTargets(
6493
+ ws,
6494
+ org,
6495
+ parseYaml,
6496
+ entry.handle,
6497
+ spec2,
6498
+ isPublic ? "public" : "private"
6499
+ );
6475
6500
  const existing = await api(`/apps/${name}`);
6476
6501
  if (existing.ok) {
6477
6502
  const install = await preselectedInstall(existing.data?.slug ?? name, org.org, targets);
@@ -6808,7 +6833,7 @@ async function portalCommand(argv) {
6808
6833
  });
6809
6834
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
6810
6835
  res.end(
6811
- `<body style="font:15px/1.6 system-ui;max-width:34rem;margin:14vh auto;padding:0 1.4rem"><h1 style="font-size:1.3rem">${app.slug} created</h1><p>Its id and private key went straight into ${pending.brain}'s secrets. The key was never written to disk.</p><p><b>It still has to be installed</b>, and granted every tracker this staff member writes to \u2014 not just their own.</p><p>Close this tab; the portal has the link.</p></body>`
6836
+ `<body style="font:15px/1.6 system-ui;max-width:34rem;margin:14vh auto;padding:0 1.4rem"><h1 style="font-size:1.3rem">${app.slug} created</h1><p>Its keys are saved in ${pending.brain}'s secrets.</p><p><b>Next: install it.</b> Go back to the portal and press Install.</p><p>You can close this tab.</p></body>`
6812
6837
  );
6813
6838
  }).catch((err) => {
6814
6839
  appResults.set(state, { ok: false, error: err.message });
@@ -6894,6 +6919,40 @@ async function portalCommand(argv) {
6894
6919
  }
6895
6920
  return;
6896
6921
  }
6922
+ if (url.pathname === "/api/charter-example") {
6923
+ const org = readOrg(w.opsDir, parseYaml);
6924
+ const handle = url.searchParams.get("staff") ?? "";
6925
+ const entry = (org.staff ?? []).find((s) => s.handle === handle);
6926
+ const asked = url.searchParams.get("kind") ?? matchExample(handle, entry?.name ?? "");
6927
+ if (!asked || !CHARTER_EXAMPLES.includes(asked)) {
6928
+ json(res, { kind: null, text: "" });
6929
+ return;
6930
+ }
6931
+ json(res, { kind: asked, text: charterStarter(asked, String(org.name ?? org.org)) });
6932
+ return;
6933
+ }
6934
+ if (url.pathname === "/api/staff/progress") {
6935
+ const handle = url.searchParams.get("staff") ?? "";
6936
+ const hit = progressCache.get(handle);
6937
+ if (url.searchParams.get("fresh") !== "1" && hit && Date.now() - hit.at < 6e4) {
6938
+ json(res, hit.body);
6939
+ return;
6940
+ }
6941
+ const org = readOrg(w.opsDir, parseYaml);
6942
+ const entry = (org.staff ?? []).find((s) => s.handle === handle);
6943
+ if (!entry) {
6944
+ res.writeHead(400, { "content-type": "application/json" });
6945
+ res.end(JSON.stringify({ error: `no staff member "${handle}"` }));
6946
+ return;
6947
+ }
6948
+ const dir = entry.dir ?? entry.handle;
6949
+ const spec2 = specFromManifest(readManifest2(join28(w.root, dir), parseYaml), dir);
6950
+ staffProgress(spec2, dailyWorkflow(entry.handle)).then((body2) => {
6951
+ progressCache.set(handle, { at: Date.now(), body: body2 });
6952
+ json(res, body2);
6953
+ }).catch(() => json(res, { app: null, publicApp: null, ran: null }));
6954
+ return;
6955
+ }
6897
6956
  if (url.pathname === "/api/staff/plan") {
6898
6957
  const handle = url.searchParams.get("handle") ?? "";
6899
6958
  const action = url.searchParams.get("action") === "retire" ? "retire" : "hire";
@@ -7009,7 +7068,8 @@ async function portalCommand(argv) {
7009
7068
  parseYaml,
7010
7069
  kind,
7011
7070
  handle,
7012
- url.searchParams.get("example") ?? void 0
7071
+ url.searchParams.get("example") ?? void 0,
7072
+ url.searchParams.get("about") ?? void 0
7013
7073
  );
7014
7074
  json(res, {
7015
7075
  kind,
@@ -7485,7 +7545,7 @@ ${warn}
7485
7545
  });
7486
7546
  });
7487
7547
  }
7488
- function buildPasteBrief(ws, parseYaml, kind, handle, example) {
7548
+ function buildPasteBrief(ws, parseYaml, kind, handle, example, about) {
7489
7549
  if (!pasteable(kind)) throw new Error(`there is no paste-mode brief for "${kind}"`);
7490
7550
  const org = readOrg(ws.opsDir, parseYaml);
7491
7551
  const staff = org.staff ?? [];
@@ -7510,6 +7570,14 @@ function buildPasteBrief(ws, parseYaml, kind, handle, example) {
7510
7570
  let rendered = renderBrief2(briefTemplate(kind), tokens);
7511
7571
  const chosen = kind === "charter" ? example ?? matchExample(handle, tokens.NAME ?? "") ?? "none" : void 0;
7512
7572
  if (chosen) rendered = withExample(rendered, handle, tokens.NAME ?? "", chosen);
7573
+ if (kind === "charter" && about?.trim()) {
7574
+ rendered = `${rendered.trimEnd()}
7575
+
7576
+ ## What the owner said this role does
7577
+
7578
+ ${about.trim()}
7579
+ `;
7580
+ }
7513
7581
  return { ...pasteBrief(ws, kind, rendered, { dir, peers }), example: chosen };
7514
7582
  }
7515
7583
  function renderBrief2(text, tokens) {
@@ -7527,6 +7595,24 @@ function json(res, data, advice = false) {
7527
7595
  const text = JSON.stringify(data);
7528
7596
  res.end(advice ? withBin(text) : text);
7529
7597
  }
7598
+ async function staffProgress(spec2, workflow) {
7599
+ const [repo, shared, runs] = await Promise.all([
7600
+ api(`repos/${spec2.brain}/actions/secrets`),
7601
+ api(`repos/${spec2.brain}/actions/organization-secrets`),
7602
+ api(
7603
+ `repos/${spec2.brain}/actions/workflows/${workflow}/runs?status=success&per_page=1`
7604
+ )
7605
+ ]);
7606
+ const names = repo.ok ? /* @__PURE__ */ new Set([
7607
+ ...repo.data.secrets.map((x) => x.name),
7608
+ ...shared.ok ? shared.data.secrets.map((x) => x.name) : []
7609
+ ]) : null;
7610
+ return {
7611
+ app: names ? names.has(`${spec2.secretPrefix}_APP_ID`) : null,
7612
+ publicApp: names ? names.has(`${spec2.publicSecretPrefix}_APP_ID`) : null,
7613
+ ran: runs.ok ? (runs.data?.total_count ?? 0) > 0 : null
7614
+ };
7615
+ }
7530
7616
  function hireFlags(params) {
7531
7617
  const num = (k) => params.get(k) ? Number(params.get(k)) : void 0;
7532
7618
  return {
package/docs/export.md CHANGED
@@ -99,6 +99,7 @@ Only surfaces that exist on disk appear. A declared surface that is missing show
99
99
  |---|---|
100
100
  | `workflows[]` | filenames under `.github/workflows/` |
101
101
  | `hasCharter`, `hasManifest` | |
102
+ | `charterStub` | `CHARTER.md` is absent or still the scaffold, the same test as doctor's `charter.stub` |
102
103
  | `missingSurfaces[]` | declared, not on disk |
103
104
  | `memoryBytes` | size of `INDEX.md` |
104
105
  | `notesBytes` | total size of `memory/notes/` |
@@ -71,49 +71,61 @@ See [concepts](concepts.md#priorities).
71
71
 
72
72
  ![The Staff screen, with a card per staff member and Hire someone underneath](images/staff.jpg)
73
73
 
74
- **Staff → Hire someone.** Only the handle is required. The plan shows the repo it creates, the
75
- schedule it chose, and the commits it will make **as you** in repos that already exist: each
76
- peer's `staff.yaml`, and `org.yaml`. Nothing is left uncommitted on disk.
74
+ **Staff**, then pick a role: CTO, CMO, Support, or **Something else** with a name and a sentence
75
+ about what they do. With nobody hired yet the Staff screen opens on the picker. The role fills
76
+ in the handle, name and repo; they are under **Advanced** if you want to change them.
77
77
 
78
- For the first hire there is nobody to copy an App name from, so the form asks for two: this
79
- staff member's App, and the shared public App. Names are unique across GitHub, so prefix them
80
- with the org. The public one only matters if a product repo is public; leave it empty when they
81
- are all private.
78
+ The role opens a numbered list: hire, create their GitHub App, write their charter, add your
79
+ agent credential, run once now. The steps after the hire say *Hire first* until it is done.
80
+
81
+ Step 1 says in one sentence what the hire creates and when they run. **Show details** has the
82
+ full plan: the repo, the schedule and why, and the commits it will make **as you** in repos that
83
+ already exist: each peer's `staff.yaml`, and `org.yaml`. Nothing is left uncommitted on disk.
84
+
85
+ For the first hire there is nobody to copy an App name from, so **Advanced** also has two:
86
+ this staff member's App, and the shared public App, filled in as `<org>-<handle>` and
87
+ `<org>-robot`. Names are unique across GitHub, so keep the org prefix. The public one only
88
+ matters if a product repo is public.
82
89
 
83
90
  Product repos come from `org.yaml`. Mark one on the setup screen, or later with **Org → Add a
84
91
  product repo**.
85
92
 
93
+ After **Hire** the list stays on the same staff member, now on step 2. Leave and come back later
94
+ with **Finish setting up** on their card.
95
+
86
96
  ## 4. Create the App, and confirm the install
87
97
 
88
- **GitHub App**, on the new card. GitHub has no API that creates an App, so a tab opens and you
98
+ **Create the App**, in step 2. GitHub has no API that creates an App, so a tab opens and you
89
99
  confirm. The App's id and private key go straight into the repo's secrets and never touch disk.
100
+ The public App is offered too when a product repo is public.
90
101
 
91
102
  Then **Install it**. The install page opens with the organisation and every repo this staff
92
103
  member needs already ticked: its brain, the trackers of its peers, and the product repos. Check
93
104
  the list and confirm. That confirmation is yours by design: installing grants access, and GitHub
94
105
  asks a person.
95
106
 
96
- ## 5. Store the agent credential, once
107
+ ## 5. Write the charter
97
108
 
98
- **Agent credential**, on the card or on the setup screen. Paste the token from `claude
99
- setup-token` (or your agent's key; the box says where to get one). It is stored as one
100
- organisation secret, shared with the brain repos, and each later hire is added to it. It is not
101
- asked for again.
109
+ Step 3. **Let an AI interview you** is the same copy-a-prompt loop as step 2 of this guide, aimed
110
+ at `CHARTER.md`, carrying the org layer, the peers' charters and, where the role matches one, a
111
+ [worked example](writing-a-charter.md#worked-examples) to model the shape on. **Start from the
112
+ template** opens that example in an editor with your business's name in it; edit it so it fits
113
+ before saving. **Edit the file** is the file as it is. roster never generates a charter, because
114
+ a generated one makes exactly the generic agent this whole arrangement exists to avoid.
102
115
 
103
- On GitHub Free an org secret does not reach private repos, so there it goes on each brain repo
104
- instead, and the page says so. It is still one paste now; a later hire needs it once more.
116
+ ## 6. Store the agent credential, once
105
117
 
106
- ## 6. Write the charter
118
+ Step 4, shown only while no credential is stored. It is also on the card and on the setup
119
+ screen. Paste the token from `claude setup-token` (or your agent's key; the box says where to
120
+ get one). It is stored as one organisation secret, shared with the brain repos, and each later
121
+ hire is added to it. It is not asked for again.
107
122
 
108
- **Write the charter**, on the card. The same copy-a-prompt loop as step 2, aimed at
109
- `CHARTER.md`, carrying the org layer, the peers' charters and, where the role matches one, a
110
- [worked example](writing-a-charter.md#worked-examples) to model the shape on. The brief
111
- interviews you; the charter is yours. roster never generates one, because a generated charter
112
- makes exactly the generic agent this whole arrangement exists to avoid.
123
+ On GitHub Free an org secret does not reach private repos, so there it goes on each brain repo
124
+ instead, and the page says so. It is still one paste now; a later hire needs it once more.
113
125
 
114
126
  ## 7. Run it once
115
127
 
116
- **Run once now**, on the card or on **Health**. It starts the daily workflow, follows it, and
128
+ **Run once now**, step 5, also on the card and on **Health**. It starts the daily workflow, follows it, and
117
129
  tells you how it ended, with the log.
118
130
 
119
131
  **A workflow that has never run has proved nothing**: not that the App is installed on the right
@@ -45,8 +45,9 @@ hand. roster says so if it happens.
45
45
  ## 3. Confirm the App's install
46
46
 
47
47
  **Do:** press **Install it** in the portal, or open the link `roster app` prints. The page opens
48
- with the organisation and the repos this staff member needs already selected: its brain, each
49
- peer's tracker, and the product repos. Check the list and confirm.
48
+ with the organisation and the repos this staff member needs already selected: its brain, the
49
+ ops repo (every run checks it out first), each peer's tracker, and the product repos. Check the
50
+ list and confirm. Choosing **All repositories** instead also works.
50
51
 
51
52
  **Why not automated:** installing is a grant of access to specific repositories, and GitHub asks
52
53
  a person to confirm it. That is correct and should not be worked around.
package/docs/portal.md CHANGED
@@ -262,21 +262,48 @@ have more than one human, and a card that names one of two reads as the only one
262
262
 
263
263
  Everyone on the roster, and the things you would otherwise do from a terminal.
264
264
 
265
- **Hiring** runs the same `buildPlan` and `applyPlan` that `roster hire` does, on the server.
266
- Only the handle is required; everything else is copied from whoever is already here. The first
267
- hire has nobody to copy App names from, so its form also asks for the App's name and the shared
268
- public App's, which are `--app` and `--public-app`. The public one only matters when a product
269
- repo is public; on private ones the session uses the staff member's own App. You see
270
- the plan first, listing every file, every label, the schedule it chose and why, the commits it
271
- will make as you in repos that already exist (each peer's `staff.yaml`, `org.yaml`), whether the
272
- new brain joins the credential's org secret, and what is left for you. Nothing happens until you
273
- apply. What the terminal would have printed is shown when it finishes.
265
+ **Hiring** starts from a role. With nobody hired the Staff screen opens on the role picker;
266
+ after that it is behind **Hire someone**. The cards are CTO, CMO and Support, each with a line
267
+ on what they do, and **Something else**, which asks for the role's name and a sentence about it.
268
+ A role that is already hired is not offered. Picking one fills in the handle, name and repo
269
+ (`cto`, `cmo`, `support`, or a handle made from the name) and leaves the schedule empty, so the
270
+ server picks a free slot. All four are under **Advanced**, still editable. The first hire has
271
+ nobody to copy App names from, so Advanced also holds the App's name and the shared public
272
+ App's, which are `--app` and `--public-app`, filled in as `<org>-<handle>` and `<org>-robot`.
273
+ The public one only matters when a product repo is public; on private ones the session uses the
274
+ staff member's own App.
275
+
276
+ The role opens a numbered list on the same screen:
277
+
278
+ 1. **Hire.** One sentence from the plan: the repo it creates and when it runs. **Show details**
279
+ has the full plan, from the same `buildPlan` and `applyPlan` that `roster hire` runs on the
280
+ server: every file, every label, the schedule and why, the commits it makes as you in repos
281
+ that already exist (each peer's `staff.yaml`, `org.yaml`), and whether the new brain joins
282
+ the credential's org secret. **Hire** asks before it acts.
283
+ 2. **Create their GitHub App.** The private App, and the shared public one only when a product
284
+ repo in `org.yaml` is public (or has no visibility written, which hire treats as public).
285
+ 3. **Write their charter.** Three tabs: an AI interview (the default), the matching worked
286
+ example with your business's name in place of Acme's, and the file itself. A role that
287
+ matches no example has no template tab.
288
+ 4. **Add your agent credential.** Only while none is stored.
289
+ 5. **Run once now.**
290
+
291
+ Steps 2 to 5 say *Hire first* until the hire is done. Each step's Done comes from real data:
292
+ the hire from the staff member appearing in `org.yaml`, the charter from `CHARTER.md` no longer
293
+ being the stub (the same test as doctor's `charter.stub`), the App from its `_APP_ID` secret on
294
+ the brain repo, the credential from the org secret, and the run from a successful daily run.
295
+ After **Hire** the screen reloads the org and stays on the same staff member, on step 2.
296
+
297
+ A card whose setup is not finished (a stub charter, no App secrets, or no successful run yet)
298
+ shows **Finish setting up**, which opens the same list for them. The App secrets and the run
299
+ are read from GitHub, so offline only the charter counts.
274
300
 
275
301
  **Writing the charter** is the copy-a-prompt loop below, aimed at `CHARTER.md`. `hire`
276
302
  deliberately does not write it, because a generated charter produces exactly the generic agent
277
303
  this whole arrangement exists to avoid. It is the same brief as `roster brief charter
278
304
  <handle>`, with somewhere to put the answer, and a picker for the worked example it carries as a
279
- model: matched to the role, or another, or none.
305
+ model: matched to the role, or another, or none. For a role added with **Something else**, the
306
+ sentence you typed goes into the brief.
280
307
 
281
308
  **The GitHub App** is `roster app`, on this server rather than a second one. There is no API that
282
309
  creates an App: the only route is the manifest flow, where you post a manifest to a settings page,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.25",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -137,6 +137,30 @@
137
137
  .todohint{margin:8px 0 0 35px}
138
138
  .todobody{margin:12px 0 0 35px}
139
139
  .todobody .row{margin-top:10px}
140
+ .todo.locked{background:transparent;border-style:dashed}
141
+ .todo.locked h3{color:var(--ink-dim)}
142
+ .todo.locked .todopill{background:none;border:1px solid var(--line);color:var(--ink-faint)}
143
+
144
+ /* ---------- hiring on the Staff screen ---------- */
145
+
146
+ .hirepane > h3{margin:0 0 10px;font:600 14.5px var(--sans)}
147
+ .roles{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;max-width:920px}
148
+ .role{display:grid;gap:5px;align-content:start;text-align:left;cursor:pointer;
149
+ background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);padding:14px 16px;
150
+ color:var(--ink);font:inherit;
151
+ transition:border-color .15s var(--ease),box-shadow .15s var(--ease),transform .15s var(--ease)}
152
+ .role:hover{border-color:var(--accent-dim);box-shadow:var(--shadow-sm);transform:translateY(-1px)}
153
+ .role:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
154
+ .role b{font:600 14px var(--sans);letter-spacing:-.01em}
155
+ .role span{color:var(--ink-dim);font:12.5px/1.5 var(--sans)}
156
+ .roleform{max-width:560px;margin:14px 0 0}
157
+ .roleform textarea{width:100%}
158
+ .hireflow{max-width:920px}
159
+ .hireflowhead{justify-content:space-between;margin:0 0 12px}
160
+ .hireflowhead h2{margin:0;font:600 17px var(--sans);letter-spacing:-.015em}
161
+ .hiresummary{margin:0;font:13.5px/1.55 var(--sans);color:var(--ink)}
162
+ .hiresummary.err{color:var(--err)}
163
+ .hireadvanced{margin-top:12px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--r);max-width:560px}
140
164
  .qfield{display:grid;gap:5px;margin:0 0 12px}
141
165
  .qfield span{font:600 12.5px var(--sans)}
142
166
  .qfield input{background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
@@ -86,13 +86,22 @@ export const planTenant = (params) =>
86
86
  export const createTenant = (params) => post(params, "/api/setup/apply");
87
87
 
88
88
  /** The copyable prompt, with every file it refers to carried inside it. */
89
- export const getBrief = (kind, staff, example) =>
89
+ export const getBrief = (kind, staff, example, about) =>
90
90
  json(
91
91
  "/api/brief?kind=" + encodeURIComponent(kind) +
92
92
  (staff ? "&staff=" + encodeURIComponent(staff) : "") +
93
- (example ? "&example=" + encodeURIComponent(example) : ""),
93
+ (example ? "&example=" + encodeURIComponent(example) : "") +
94
+ (about ? "&about=" + encodeURIComponent(about) : ""),
94
95
  );
95
96
 
97
+ /** The worked charter that matches a staff member, with the business's name in it. */
98
+ export const getCharterExample = (staff) =>
99
+ json("/api/charter-example?staff=" + encodeURIComponent(staff));
100
+
101
+ /** Whether a staff member's App secrets are set and a daily run has succeeded. Online only. */
102
+ export const getStaffProgress = (staff, fresh) =>
103
+ json("/api/staff/progress?staff=" + encodeURIComponent(staff) + (fresh ? "&fresh=1" : ""));
104
+
96
105
  /** Parse what came back from the model. Reads only: saving is a second, deliberate step. */
97
106
  export const parsePaste = (kind, staff, answer) =>
98
107
  fetch("/api/paste", {
@@ -111,6 +111,7 @@ export async function boot() {
111
111
 
112
112
  S.data = first;
113
113
  S.loadedAt = new Date();
114
+ S.staffOpen = null; // an open hire list survives a refresh, not a page load
114
115
  S.staffHandle = S.data.staff[0]?.handle ?? null;
115
116
  $("#orgname").textContent = S.data.name + " · " + S.data.staff.length + " staff";
116
117
 
@@ -36,6 +36,8 @@ export const S = {
36
36
  runs: null,
37
37
  sync: null,
38
38
  loadedAt: null,
39
+ /** The role whose hire list is open on the Staff screen, kept across a refresh. */
40
+ staffOpen: null,
39
41
 
40
42
  staffHandle: null,
41
43
  /* What is waiting on you, not whose brain you read last. The org-wide screens are where a
@@ -27,8 +27,7 @@ export function appPanel(opts) {
27
27
  }),
28
28
  el("p", {
29
29
  textContent:
30
- "A tab opens, GitHub asks you to confirm, and the App's id and private key go straight " +
31
- "into the repo's secrets. The key is held in memory and never written to disk.",
30
+ "A GitHub tab opens. Confirm there, then come back here to install it.",
32
31
  }),
33
32
  );
34
33