@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 +95 -9
- package/docs/export.md +1 -0
- package/docs/getting-started.md +34 -22
- package/docs/manual-steps.md +3 -2
- package/docs/portal.md +37 -10
- package/package.json +1 -1
- package/templates/portal/css/setup.css +24 -0
- package/templates/portal/js/api.js +11 -2
- package/templates/portal/js/app.js +1 -0
- package/templates/portal/js/state.js +2 -0
- package/templates/portal/js/views/app.js +1 -2
- package/templates/portal/js/views/hire.js +526 -0
- package/templates/portal/js/views/orgedit.js +24 -12
- package/templates/portal/js/views/paste.js +4 -3
- package/templates/portal/js/views/setup.js +2 -41
- package/templates/portal/js/views/staff.js +81 -241
- package/templates/portal/js/views/todo.js +62 -0
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
|
-
"
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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/` |
|
package/docs/getting-started.md
CHANGED
|
@@ -71,49 +71,61 @@ See [concepts](concepts.md#priorities).
|
|
|
71
71
|
|
|
72
72
|

|
|
73
73
|
|
|
74
|
-
**Staff
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
**
|
|
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.
|
|
107
|
+
## 5. Write the charter
|
|
97
108
|
|
|
98
|
-
**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
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
|
package/docs/manual-steps.md
CHANGED
|
@@ -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,
|
|
49
|
-
peer's tracker, and the product repos. Check the
|
|
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**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
@@ -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
|
|
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
|
|