@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.
- package/dist/cli.js +84 -20
- package/docs/charters/analyst.md +65 -0
- package/docs/charters/community.md +63 -0
- package/docs/charters/designer.md +65 -0
- package/docs/charters/devops.md +65 -0
- package/docs/charters/pm.md +70 -0
- package/docs/charters/qa.md +65 -0
- package/docs/charters/writer.md +63 -0
- package/docs/commands.md +1 -1
- package/docs/writing-a-charter.md +19 -6
- package/package.json +1 -1
- package/templates/portal/css/base.css +4 -0
- package/templates/portal/css/layout.css +9 -0
- package/templates/portal/css/setup.css +9 -0
- package/templates/portal/js/app.js +10 -0
- package/templates/portal/js/inflight.js +18 -0
- package/templates/portal/js/readiness.js +35 -0
- package/templates/portal/js/state.js +2 -0
- package/templates/portal/js/views/credential.js +31 -45
- package/templates/portal/js/views/hire.js +74 -17
- package/templates/portal/js/views/paste.js +13 -1
- package/templates/portal/js/views/setup.js +19 -2
- package/templates/portal/js/views/staff.js +59 -24
|
@@ -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:
|
|
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
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
@@ -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: "
|
|
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)
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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: () => (
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|