@nanocollective/roster 0.1.0-alpha.26 → 0.1.0-alpha.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ # Charter — Acme's Technical Writer
2
+
3
+ > **An example to adapt, not a template.** Acme is invented: a small company whose product is an
4
+ > open-source scheduling app, `acme/acme-web`, run by one founder, Sam. Replace every specific
5
+ > with your own. The shape is what has worked; the words have to be yours.
6
+
7
+ *Who I am and what only I do. The shared half lives in `roster-ops/org/`. This file is the
8
+ difference between me and the rest of the staff, and nothing else.*
9
+
10
+ ---
11
+
12
+ ## Who I am
13
+
14
+ Acme's Technical Writer. I own the docs in `acme-web/docs/`, the getting-started guide and the
15
+ changelog. I write for someone setting Acme up for the first time.
16
+
17
+ ## The mission
18
+
19
+ **The docs describe what the product does today, and every release has a changelog entry that
20
+ takes a minute to read.**
21
+
22
+ When they conflict, **a page that is wrong wins** over a page that is missing.
23
+
24
+ ## How I work, that others here do not
25
+
26
+ - **Merged changes drive the docs.** Every run starts with the PRs merged to `acme/acme-web` since
27
+ my last run. The CTO's PR descriptions are my source for what changed. When one is unclear, I
28
+ ask the CTO in a `from-writer` issue.
29
+ - **The changelog is `CHANGELOG.md`**, under an Unreleased heading, one line per change a user
30
+ would notice, each linking its PR. Refactors and internal changes are left out.
31
+ - **I run what I document.** Every command and code sample is checked by running it or reading the
32
+ code it describes. A sample I could not check is named in the PR.
33
+ - **The Head of Support fixes wrong answers** in the docs as they find them. I own the structure,
34
+ the guides and the reference, and I read their `strategy/themes.md` weekly for what is missing.
35
+ - **Release notes are drafted from the changelog** when Sam tags a release, as a `review` issue
36
+ with the exact text. The Community Manager builds the announcement from the approved notes.
37
+
38
+ ## Decision rights
39
+
40
+ | I do freely | I file an issue, then carry on |
41
+ |---|---|
42
+ | PRs to `docs/`, the README and `CHANGELOG.md` | Publishing release notes: a `review` issue with the text |
43
+ | Reorganising pages within the docs | Removing a page or changing a URL people link to |
44
+ | Questions to the CTO about a change | Documenting a feature that has not shipped |
45
+ | Anything in my own `writer/` repo | Paying for a docs tool or host |
46
+
47
+ ## Guardrails on top of the org's
48
+
49
+ 1. **The code decides.** If the docs and the code disagree, I document the code and send the
50
+ CTO a question if the behaviour looks wrong.
51
+ 2. **Nothing about future features** goes in the docs or the changelog.
52
+ 3. **Examples before explanation**, in the plain style set out in `style.md`.
53
+
54
+ ## Where the rest of it lives
55
+
56
+ | | |
57
+ |---|---|
58
+ | How I operate | `roster-ops/org/operating.md` |
59
+ | What matters this month | `roster-ops/org/priorities.md` |
60
+ | How the docs are written | `style.md` |
61
+ | What I know | `memory/INDEX.md` |
62
+ | What is outstanding | the pinned status issue |
63
+ | Why something was decided | `log/decisions.md` |
package/docs/commands.md CHANGED
@@ -214,7 +214,7 @@ amend <who> change what a staff member is told, with the whole prompt attach
214
214
  ```
215
215
  --kind <k> for amend: daily | mention (default: daily)
216
216
  --want <text> for amend: what you want changed
217
- --example <e> for charter: cto, cmo, support or none (default: matched to the role)
217
+ --example <e> for charter: an example's handle, or none (default: matched to the role)
218
218
  --ops <dir>
219
219
  ```
220
220
 
@@ -28,10 +28,10 @@ layer. That is the part that matters most, because without them the model writes
28
28
  of whoever it was shown. The brief then interviews you, drafts from your answers, and tells you
29
29
  what it cut and why.
30
30
 
31
- **So is a worked example, when one fits.** A staff member whose handle or role reads as a CTO, a
32
- CMO or support gets the matching [example](#worked-examples) inside the brief, labelled as a
31
+ **So is a worked example, when one fits.** A staff member whose handle or role reads as one of
32
+ the ten roles below gets the matching [example](#worked-examples) inside the brief, labelled as a
33
33
  model for the shape and not content to copy. The copy-a-prompt panel has a picker to choose
34
- another or none; in a terminal it is `--example cto|cmo|support|none`. It is still a brief you
34
+ another or none; in a terminal it is `--example <handle>` or `--example none`. It is still a brief you
35
35
  answer: the interview comes first, and nothing in the charter should come from the example
36
36
  rather than from you.
37
37
 
@@ -72,9 +72,22 @@ status issue, and the surfaces the manifest declares.
72
72
 
73
73
  ## Worked examples
74
74
 
75
- Three, for an invented company called Acme: a [CTO](charters/cto.md), a [CMO](charters/cmo.md)
76
- and a [Head of Support](charters/support.md). They are examples to adapt, not templates to fill
77
- in. Read them for what a finished charter covers and how specific it gets, then write your own
75
+ Ten, for an invented company called Acme. The handle in brackets is the one `--example` takes.
76
+
77
+ | Example | What they do |
78
+ |---|---|
79
+ | [CTO](charters/cto.md) (`cto`) | Builds the product: fixes, features and tests, as pull requests. |
80
+ | [CMO](charters/cmo.md) (`cmo`) | Posts, SEO, copy and launch plans, as drafts to approve. |
81
+ | [Head of Support](charters/support.md) (`support`) | Answers issues, writes help docs, and turns user reports into bugs. |
82
+ | [Product Manager](charters/pm.md) (`pm`) | Turns ideas and user feedback into clear specs and a ranked backlog. |
83
+ | [Designer](charters/designer.md) (`designer`) | Improves the product's look and usability, with accessibility fixes, as pull requests. |
84
+ | [QA Engineer](charters/qa.md) (`qa`) | Tests the product, finds bugs, and writes clear reproductions and tests. |
85
+ | [DevOps Engineer](charters/devops.md) (`devops`) | Keeps CI, deploys and dependencies healthy, and patches security updates. |
86
+ | [Technical Writer](charters/writer.md) (`writer`) | Writes and keeps up the docs, guides and changelog. |
87
+ | [Data Analyst](charters/analyst.md) (`analyst`) | Reads the numbers and writes a short weekly report on what changed. |
88
+ | [Community Manager](charters/community.md) (`community`) | Answers discussions, welcomes contributors, and drafts release announcements. |
89
+
90
+ They are examples to adapt, not templates to fill in. Read them for what a finished charter covers and how specific it gets, then write your own
78
91
  about your business. A charter copied from one of these describes Acme. The brief carries the matching one for
79
92
  you; these links are for reading them first.
80
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.26",
3
+ "version": "0.1.0-alpha.28",
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": {
@@ -285,3 +285,7 @@ dialog.sheet[open]{animation:dlgin .18s var(--ease)}
285
285
  dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
286
286
  .sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
287
287
  .sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
288
+ .sheetloading{display:grid;place-items:center;min-height:260px}
289
+ .spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--accent);
290
+ animation:spin .7s linear infinite}
291
+ @keyframes spin{to{transform:rotate(360deg)}}
@@ -148,3 +148,12 @@ main{padding:36px 44px 96px;max-width:1220px;overflow-x:hidden}
148
148
  .side{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line)}
149
149
  .sidefoot{margin-top:0}
150
150
  }
151
+
152
+ /* A staff member who cannot run yet (readiness.js). */
153
+ .readydot{width:7px;height:7px;border-radius:50%;background:var(--warn);flex:none;margin-left:4px}
154
+ .staffcard.notready{border-color:color-mix(in srgb,var(--warn) 45%,var(--line))}
155
+ .hirecard{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:120px;
156
+ border:1.5px dashed var(--border-strong);border-radius:var(--r-lg);background:none;color:var(--ink-dim);
157
+ font:600 14px var(--sans);cursor:pointer}
158
+ .hirecard:hover{color:var(--ink);border-color:var(--accent)}
159
+ .hirecard::before{content:"+";font-size:22px;line-height:1}
@@ -182,3 +182,12 @@
182
182
  .pastenotes summary{cursor:pointer;font:600 12.5px var(--sans);color:var(--ink-dim);margin-bottom:6px}
183
183
  .pastenotes .md{border-left:3px solid var(--line);padding-left:12px}
184
184
  .pastefile .diff{white-space:pre-wrap;overflow-wrap:anywhere}
185
+ .warnline{margin:10px 0 0;padding:8px 11px;border-radius:var(--r);background:color-mix(in srgb,var(--warn) 12%,transparent);color:var(--ink);font-size:13px}
186
+
187
+ /* A single-line text box, matching the question fields. */
188
+ .textfield{width:100%;background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
189
+ border-radius:var(--r);padding:8px 10px;font:13px var(--sans);outline:none}
190
+ .textfield:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
191
+ .credform{display:grid;gap:10px}
192
+ .credform .row{margin:0}
193
+ .okline{margin:0 0 8px;color:var(--accent);font:600 13px var(--sans)}
@@ -24,6 +24,7 @@ import { viewInbox, viewPrs } from "./views/inbox.js";
24
24
  import { viewOrg } from "./views/org.js";
25
25
  import { viewPrompt } from "./views/prompt.js";
26
26
  import { viewRuns } from "./views/runs.js";
27
+ import { sentence, whatsLeft } from "./readiness.js";
27
28
  import { paintSetupNav, viewGettingStarted, viewSetup } from "./views/setup.js";
28
29
  import { viewStaff } from "./views/staff.js";
29
30
 
@@ -112,6 +113,7 @@ export async function boot() {
112
113
  S.data = first;
113
114
  S.loadedAt = new Date();
114
115
  S.staffOpen = null; // an open hire list survives a refresh, not a page load
116
+ S.readiness = null;
115
117
  S.staffHandle = S.data.staff[0]?.handle ?? null;
116
118
  $("#orgname").textContent = S.data.name + " · " + S.data.staff.length + " staff";
117
119
 
@@ -262,11 +264,19 @@ function paintSidebar() {
262
264
  const head = el("button", { className: "nav staffrow" });
263
265
  head.dataset.staff = s.handle;
264
266
  head.setAttribute("aria-expanded", String(open));
267
+ const dot = el("span", { className: "readydot", hidden: true });
265
268
  head.append(
266
269
  icon("chevron", "caret"),
267
270
  el("span", { className: "t", textContent: s.name }),
271
+ dot,
268
272
  el("span", { className: "hh", textContent: s.handle }),
269
273
  );
274
+ whatsLeft(s).then((left) => {
275
+ if (!left.length) return;
276
+ dot.hidden = false;
277
+ head.title = "Not ready to run: " + sentence(left);
278
+ paintSetupNav();
279
+ });
270
280
 
271
281
  const views = el("div", { className: "staffviews" });
272
282
  views.hidden = !open;
@@ -0,0 +1,18 @@
1
+ /* How many requests the page has open. The setup dialog waits for this to reach zero before it
2
+ * shows itself, because the list it draws is only final once every answer is back. */
3
+
4
+ let open = 0;
5
+
6
+ if (typeof globalThis.fetch === "function" && !globalThis.fetch.counted) {
7
+ const real = globalThis.fetch.bind(globalThis);
8
+ const counted = (...args) => {
9
+ open++;
10
+ return real(...args).finally(() => {
11
+ open--;
12
+ });
13
+ };
14
+ counted.counted = true;
15
+ globalThis.fetch = counted;
16
+ }
17
+
18
+ export const pending = () => open;
@@ -0,0 +1,35 @@
1
+ /* Whether each staff member can actually run yet.
2
+ *
3
+ * A hire that had its App created but not installed, and no credential, looked finished: the
4
+ * card said so and Getting started went away. This is the one answer every screen asks. */
5
+
6
+ import { getStaffProgress } from "./api.js";
7
+ import { S } from "./state.js";
8
+
9
+ /** What is left for one staff member, as short instructions. Empty when they are ready. */
10
+ export async function whatsLeft(s, fresh = false) {
11
+ const left = [];
12
+ if (s.rig?.charterStub) left.push("write the charter");
13
+ const p = await getStaffProgress(s.handle, fresh).catch(() => ({}));
14
+ if (p.installed === false) left.push(p.app ? "install the GitHub App" : "create the GitHub App");
15
+ if (p.credential === false) left.push("add the agent credential");
16
+ if (p.ran === false) left.push("run once");
17
+ S.readiness = { ...(S.readiness ?? {}), [s.handle]: left };
18
+ return left;
19
+ }
20
+
21
+ /** Every staff member's, then `then` once all have answered. */
22
+ export function checkAll(then) {
23
+ return Promise.all((S.data?.staff ?? []).map((s) => whatsLeft(s))).then(() => then?.());
24
+ }
25
+
26
+ /** The staff with anything left, from what has been checked so far. */
27
+ export function notReady() {
28
+ return (S.data?.staff ?? []).filter((s) => (S.readiness?.[s.handle] ?? []).length);
29
+ }
30
+
31
+ /** "install the GitHub App, add the agent credential and run once" */
32
+ export function sentence(left) {
33
+ if (left.length < 2) return left.join("");
34
+ return left.slice(0, -1).join(", ") + " and " + left.at(-1);
35
+ }
@@ -38,6 +38,8 @@ export const S = {
38
38
  loadedAt: null,
39
39
  /** The role whose hire list is open on the Staff screen, kept across a refresh. */
40
40
  staffOpen: null,
41
+ /** Per staff member, what is left before they can run. See readiness.js. */
42
+ readiness: null,
41
43
 
42
44
  staffHandle: null,
43
45
  /* What is waiting on you, not whose brain you read last. The org-wide screens are where a
@@ -12,11 +12,11 @@ export function credentialPanel(opts = {}) {
12
12
  if (!opts.bare) box.append(el("b", { textContent: "Agent credential" }));
13
13
  const body = el("div");
14
14
  box.append(body);
15
- body.append(el("p", { className: "sub", textContent: "Looking at where it would go…" }));
15
+ body.append(el("p", { className: "sub", textContent: "Checking…" }));
16
16
 
17
17
  getCredential()
18
18
  .then((c) => {
19
- opts.onStatus?.(Boolean(c.orgSecret));
19
+ opts.onStatus?.(Boolean(c.stored ?? c.orgSecret));
20
20
  draw(body, c, opts);
21
21
  })
22
22
  .catch((err) => body.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) })));
@@ -25,74 +25,60 @@ export function credentialPanel(opts = {}) {
25
25
 
26
26
  function draw(body, c, opts = {}) {
27
27
  body.replaceChildren();
28
- if (c.howTo) body.append(el("p", { textContent: "Where to get one: " + c.howTo }));
29
28
 
30
29
  if (!(c.brains ?? []).length) {
31
30
  // Inside a step, the step's own hint already says when this opens.
32
- if (opts.bare) return;
33
- body.append(
34
- el("p", {
35
- className: "sub",
36
- textContent:
37
- "Available after your first hire. You only add it once.",
38
- }),
39
- );
31
+ if (!opts.bare) body.append(el("p", { className: "sub", textContent: "Available after your first hire. You only add it once." }));
40
32
  return;
41
33
  }
42
34
 
43
- if (c.orgSecret) {
44
- body.append(
45
- el("p", {
46
- className: "sub",
47
- textContent:
48
- c.name + " is already set. Paste a new one only to replace it.",
49
- }),
50
- );
51
- }
52
-
53
- body.append(
54
- el("p", {
55
- className: "sub",
56
- textContent:
57
- (c.plan.mode === "org"
58
- ? "Stored as one org secret, " + c.name + ", shared with " + c.brains.join(", ") + ". "
59
- : "Stored as " + c.name + " on each of " + c.brains.join(", ") + ", because " + c.plan.reason + ". ") +
60
- "It's never written to disk.",
61
- }),
35
+ const form = el("div", { className: "credform" });
36
+ const input = el("input", { type: "password", placeholder: "Paste it here", autocomplete: "off", className: "textfield" });
37
+ const go = el("button", { className: "btn primary", textContent: "Save" });
38
+ const out = el("div");
39
+ form.append(
40
+ ...(c.howTo ? [el("p", { className: "sub", textContent: "To get one, " + c.howTo })] : []),
41
+ input,
42
+ el("div", { className: "row" }, [go]),
43
+ out,
62
44
  );
63
45
 
64
- const input = el("input", { type: "password", placeholder: c.name, autocomplete: "off" });
65
- input.style.width = "100%";
66
- const go = el("button", { className: "btn primary", textContent: "Store it" });
67
- const out = el("div", { style: "margin-top:9px" });
68
- body.append(input, el("div", { className: "row", style: "margin-top:9px" }, [go]), out);
46
+ // Stored already, on the org or on every brain repo: say so, and keep replacing it one click away.
47
+ if (c.stored ?? c.orgSecret) {
48
+ const replace = el("button", { className: "ghbtn", textContent: "Replace it" });
49
+ replace.onclick = () => {
50
+ replace.remove();
51
+ form.hidden = false;
52
+ input.focus?.();
53
+ };
54
+ form.hidden = true;
55
+ body.append(el("p", { className: "okline", textContent: "✓ Stored as " + c.name + "." }), replace, form);
56
+ } else {
57
+ body.append(form);
58
+ }
69
59
 
70
60
  go.onclick = async () => {
71
- const value = input.value.trim();
61
+ const value = String(input.value ?? "").trim();
72
62
  if (!value) {
73
- input.focus();
63
+ input.focus?.();
74
64
  return;
75
65
  }
76
66
  go.disabled = true;
77
- out.replaceChildren(el("p", { className: "sub", textContent: "Storing…" }));
67
+ go.textContent = "Saving…";
78
68
  try {
79
69
  const r = await storeCredential(value, false);
80
70
  input.value = "";
81
71
  if (!r.ok) throw new Error(r.error);
82
72
  opts.onStatus?.(true);
83
73
  out.replaceChildren(
84
- ...(r.fellBack ? [el("p", { className: "err", textContent: "The org secret was refused: " + r.fellBack })] : []),
85
- el("p", {
86
- textContent:
87
- r.mode === "org"
88
- ? "Saved."
89
- : "Saved on " + r.repos.join(", ") + ".",
90
- }),
74
+ ...(r.fellBack ? [el("p", { className: "sub", textContent: "Saved on each repo instead: " + r.fellBack })] : []),
75
+ el("p", { className: "okline", textContent: "✓ Saved." }),
91
76
  );
92
77
  } catch (err) {
93
78
  out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
94
79
  } finally {
95
80
  go.disabled = false;
81
+ go.textContent = "Save";
96
82
  }
97
83
  };
98
84
  }
@@ -39,10 +39,52 @@ const ROLES = [
39
39
  name: "Head of Support",
40
40
  about: "Answers issues, writes help docs, and turns user reports into bugs.",
41
41
  },
42
+ {
43
+ handle: "pm",
44
+ label: "Product manager",
45
+ name: "Product Manager",
46
+ about: "Turns ideas and user feedback into clear specs and a ranked backlog.",
47
+ },
48
+ {
49
+ handle: "designer",
50
+ label: "Designer",
51
+ name: "Designer",
52
+ about: "Improves the product's look and usability, with accessibility fixes, as pull requests.",
53
+ },
54
+ {
55
+ handle: "qa",
56
+ label: "QA",
57
+ name: "QA Engineer",
58
+ about: "Tests the product, finds bugs, and writes clear reproductions and tests.",
59
+ },
60
+ {
61
+ handle: "devops",
62
+ label: "DevOps",
63
+ name: "DevOps Engineer",
64
+ about: "Keeps CI, deploys and dependencies healthy, and patches security updates.",
65
+ },
66
+ {
67
+ handle: "writer",
68
+ label: "Writer",
69
+ name: "Technical Writer",
70
+ about: "Writes and keeps up the docs, guides and changelog.",
71
+ },
72
+ {
73
+ handle: "analyst",
74
+ label: "Analyst",
75
+ name: "Data Analyst",
76
+ about: "Reads the numbers and writes a short weekly report on what changed.",
77
+ },
78
+ {
79
+ handle: "community",
80
+ label: "Community",
81
+ name: "Community Manager",
82
+ about: "Answers discussions, welcomes contributors, and drafts release announcements.",
83
+ },
42
84
  ];
43
85
 
44
86
  /**
45
- * The roles as cards. The three with worked examples are offered until they are hired;
87
+ * The roles as cards. Each has a worked example, and is offered until it is hired;
46
88
  * Something else asks for a name and a sentence.
47
89
  * @param {{onPick: (role: {handle: string, name: string, dir: string, about?: string}) => void}} o
48
90
  */
@@ -141,7 +183,7 @@ export function hireFlow(o) {
141
183
  box.append(list);
142
184
 
143
185
  const hire = todo(1, "Hire", hired);
144
- const app = todo(2, "Create their GitHub App", false);
186
+ const app = todo(2, "Create and install their GitHub App", false);
145
187
  const charter = todo(3, "Write their charter", hired && !s.rig?.charterStub);
146
188
  const cred = todo(4, "Add your agent credential", false);
147
189
  const run = todo(5, "Run once now", false);
@@ -161,7 +203,7 @@ export function hireFlow(o) {
161
203
  hireStep(hire.body, role, o.onHired);
162
204
  for (const t of [app, charter, cred, run]) t.setLocked("Hire first");
163
205
  getCredential()
164
- .then((c) => credStatus(Boolean(c.orgSecret)))
206
+ .then((c) => credStatus(Boolean(c.stored ?? c.orgSecret)))
165
207
  .catch(() => {});
166
208
  return box;
167
209
  }
@@ -182,14 +224,17 @@ export function hireFlow(o) {
182
224
 
183
225
  /* 2 · the App. The public identity only matters when a product repo is public, and hire
184
226
  treats one with no visibility written as public, so this does too. */
227
+ /* Done means installed, not created: an App with its keys saved and no install looked
228
+ finished, and every run would have failed. */
185
229
  let needPublic = false;
186
230
  const made = { private: false, public: false };
187
231
  const appDone = () => app.setDone(made.private && (!needPublic || made.public));
188
232
  const panels = el("div");
189
- app.body.append(panels);
190
- panels.append(
191
- appPanel({ staff: s.handle, name: s.name, scope: "private", onDone: () => ((made.private = true), appDone()) }),
192
- );
233
+ const installNote = el("p", { className: "warnline", hidden: true });
234
+ const recheck = el("button", { className: "ghbtn", textContent: "Check again" });
235
+ recheck.onclick = () => checkProgress();
236
+ app.body.append(panels, installNote, el("div", { className: "row" }, [recheck]));
237
+ panels.append(appPanel({ staff: s.handle, name: s.name, scope: "private", onDone: () => checkProgress() }));
193
238
  getRepos()
194
239
  .then((r) => {
195
240
  needPublic = (r.repos ?? []).some(
@@ -198,7 +243,7 @@ export function hireFlow(o) {
198
243
  if (needPublic) {
199
244
  app.hint("One of your product repos is public. Public work uses a second App, shared by all staff.");
200
245
  panels.append(
201
- appPanel({ staff: s.handle, name: s.name, scope: "public", onDone: () => ((made.public = true), appDone()) }),
246
+ appPanel({ staff: s.handle, name: s.name, scope: "public", onDone: () => checkProgress() }),
202
247
  );
203
248
  }
204
249
  appDone();
@@ -225,15 +270,27 @@ export function hireFlow(o) {
225
270
  }),
226
271
  );
227
272
 
228
- /* What only GitHub knows: the App's secrets on the repo, and whether a run has succeeded. */
229
- getStaffProgress(s.handle, true)
230
- .then((p) => {
231
- if (p.app) made.private = true;
232
- if (p.publicApp) made.public = true;
233
- appDone();
234
- if (p.ran) run.setDone(true);
235
- })
236
- .catch(() => {});
273
+ /* What only GitHub knows: whether the App is installed, whether the credential is stored,
274
+ and whether a run has succeeded. Asked again by Check again, after a trip to GitHub. */
275
+ function checkProgress() {
276
+ return getStaffProgress(s.handle, true)
277
+ .then((p) => {
278
+ made.private = p.installed === true;
279
+ made.public = p.publicInstalled === true;
280
+ appDone();
281
+ installNote.hidden = !(p.app && p.installed === false);
282
+ installNote.textContent =
283
+ "Created, but not installed yet. Press Install, and choose All repositories or include roster-ops.";
284
+ if (p.installed && p.installSelection === "selected") {
285
+ installNote.hidden = false;
286
+ installNote.textContent =
287
+ "Installed on selected repos. Make sure roster-ops is one of them, or runs will fail.";
288
+ }
289
+ if (p.ran) run.setDone(true);
290
+ })
291
+ .catch(() => {});
292
+ }
293
+ checkProgress();
237
294
 
238
295
  return box;
239
296
  }
@@ -49,7 +49,19 @@ export function paste(opts) {
49
49
 
50
50
  let brief = null;
51
51
 
52
- const NAMES = { cto: "the CTO example", cmo: "the CMO example", support: "the support example", none: "no example" };
52
+ const NAMES = {
53
+ cto: "the CTO example",
54
+ cmo: "the CMO example",
55
+ support: "the support example",
56
+ pm: "the product manager example",
57
+ designer: "the designer example",
58
+ qa: "the QA example",
59
+ devops: "the DevOps example",
60
+ writer: "the writer example",
61
+ analyst: "the analyst example",
62
+ community: "the community example",
63
+ none: "no example",
64
+ };
53
65
  model.onchange = () => {
54
66
  copy.disabled = true;
55
67
  getBrief(opts.kind, opts.staff, model.value, opts.about)
@@ -18,6 +18,7 @@ import {
18
18
  import { $, el, esc, toClipboard } from "../dom.js";
19
19
  import { go } from "../router.js";
20
20
  import { S as App } from "../state.js";
21
+ import { checkAll, notReady, sentence } from "../readiness.js";
21
22
  import { checklist } from "./checklist.js";
22
23
  import { businessForm, prioritiesForm } from "./orgedit.js";
23
24
  import { credentialPanel } from "./credential.js";
@@ -60,8 +61,24 @@ export async function viewGettingStarted(main) {
60
61
  S = await getSetup();
61
62
  // Somebody may have clicked elsewhere while GitHub was answering.
62
63
  if (App.view !== "setup") return;
64
+ await checkAll();
65
+ if (App.view !== "setup") return;
63
66
  sub.textContent = "Work through these in order.";
64
- main.append(afterCreate());
67
+ const list = afterCreate();
68
+
69
+ // Somebody hired but not ready to run is the most important thing left, so it comes first.
70
+ for (const s of notReady().reverse()) {
71
+ const t = todo("!", "Finish setting up " + s.name, false);
72
+ t.hint("Still to do: " + sentence(App.readiness?.[s.handle] ?? []) + ".");
73
+ const cont = el("button", { className: "btn primary", textContent: "Continue" });
74
+ cont.onclick = () => {
75
+ App.staffOpen = { handle: s.handle, name: s.name, dir: s.dir };
76
+ go({ view: "staff" });
77
+ };
78
+ t.body.append(el("div", { className: "row" }, [cont]));
79
+ list.prepend(t.card);
80
+ }
81
+ main.append(list);
65
82
 
66
83
  const health = step("!", "Other problems", false);
67
84
  healthHost = el("div", { style: "margin-top:12px" });
@@ -75,7 +92,7 @@ export async function viewGettingStarted(main) {
75
92
  export function paintSetupNav() {
76
93
  const nav = $("#setupnav");
77
94
  if (!nav) return;
78
- nav.hidden = !App.data?.unfinished?.length && App.view !== "setup";
95
+ nav.hidden = !App.data?.unfinished?.length && !notReady().length && App.view !== "setup";
79
96
  }
80
97
 
81
98
  /* After a save: the checklist and the step list are both derived from disk, so both are asked