@nanocollective/roster 0.1.0-alpha.22 → 0.1.0-alpha.24
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 +77 -4
- package/docs/export.md +1 -0
- package/docs/getting-started.md +36 -23
- package/docs/portal.md +37 -10
- package/package.json +1 -1
- package/templates/briefs/priorities.md +46 -0
- 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/hire.js +526 -0
- package/templates/portal/js/views/orgedit.js +30 -13
- 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
|
@@ -808,6 +808,11 @@ function exampleSection(choice) {
|
|
|
808
808
|
""
|
|
809
809
|
].join("\n");
|
|
810
810
|
}
|
|
811
|
+
function charterStarter(choice, business) {
|
|
812
|
+
const path = join8(docsDir(), "charters", `${choice}.md`);
|
|
813
|
+
if (!existsSync8(path)) throw new Error(`there is no example charter called "${choice}"`);
|
|
814
|
+
return readFileSync7(path, "utf8").replace(/^> \*\*An example to adapt[\s\S]*?\n\n/m, "").replace(/\bAcme\b/g, business);
|
|
815
|
+
}
|
|
811
816
|
function exampleOffer(handle) {
|
|
812
817
|
return [
|
|
813
818
|
"## Worked examples",
|
|
@@ -1029,6 +1034,7 @@ roster brief <kind> [handle]
|
|
|
1029
1034
|
discover write org/business.md, which every prompt is composed on top of
|
|
1030
1035
|
charter <who> write a staff member's CHARTER.md
|
|
1031
1036
|
voice revise org/voice.md, the house style every surface inherits
|
|
1037
|
+
priorities write org/priorities.md, this month's priorities
|
|
1032
1038
|
amend <who> change what a staff member is told, with the whole prompt attached
|
|
1033
1039
|
|
|
1034
1040
|
\`charter\` carries one of the worked examples in docs/charters as a model for the shape,
|
|
@@ -4259,6 +4265,7 @@ function readRig(root, manifest, commits) {
|
|
|
4259
4265
|
return {
|
|
4260
4266
|
workflows: existsSync17(wfDir) ? readdirSync7(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [],
|
|
4261
4267
|
hasCharter: existsSync17(join19(root, "CHARTER.md")),
|
|
4268
|
+
charterStub: !existsSync17(join19(root, "CHARTER.md")) || looksUnwritten(readFileSync16(join19(root, "CHARTER.md"), "utf8")),
|
|
4262
4269
|
hasManifest: existsSync17(join19(root, "staff.yaml")),
|
|
4263
4270
|
missingSurfaces: declared.filter((s) => s?.path && !existsSync17(join19(root, s.path))).map((s) => s.path),
|
|
4264
4271
|
memoryBytes: bytesOf(join19(root, "memory", "INDEX.md")),
|
|
@@ -4890,7 +4897,7 @@ async function initFiles(o) {
|
|
|
4890
4897
|
}
|
|
4891
4898
|
files.set("org/business.md", businessStub(o.name, o.org));
|
|
4892
4899
|
files.set("org/priorities.md", prioritiesStub());
|
|
4893
|
-
for (const [rel, text] of briefCommands(["discover", "voice"], {
|
|
4900
|
+
for (const [rel, text] of briefCommands(["discover", "priorities", "voice"], {
|
|
4894
4901
|
ORG: o.org,
|
|
4895
4902
|
ORG_NAME: o.name,
|
|
4896
4903
|
OPS_REPO: `${o.org}/${o.opsName}`,
|
|
@@ -5861,6 +5868,7 @@ import { join as join24 } from "path";
|
|
|
5861
5868
|
var WRITES = {
|
|
5862
5869
|
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
5863
5870
|
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
5871
|
+
priorities: (c) => [`${c.opsName}/org/priorities.md`],
|
|
5864
5872
|
charter: (c) => [`${c.dir}/CHARTER.md`]
|
|
5865
5873
|
};
|
|
5866
5874
|
var READS = {
|
|
@@ -5870,6 +5878,7 @@ var READS = {
|
|
|
5870
5878
|
`${c.opsName}/org/guardrails.md`
|
|
5871
5879
|
],
|
|
5872
5880
|
voice: (c) => [`${c.opsName}/org/business.md`, `${c.opsName}/org/guardrails.md`],
|
|
5881
|
+
priorities: (c) => [`${c.opsName}/org/business.md`],
|
|
5873
5882
|
/* A charter is the difference between this staff member and everyone else, so the peers'
|
|
5874
5883
|
charters are the most useful thing in the room: without them the model writes a second
|
|
5875
5884
|
copy of whoever it was shown. */
|
|
@@ -6419,6 +6428,7 @@ async function portalCommand(argv) {
|
|
|
6419
6428
|
const TTL = 45e3;
|
|
6420
6429
|
const labelCache = /* @__PURE__ */ new Map();
|
|
6421
6430
|
let runsCache = null;
|
|
6431
|
+
const progressCache = /* @__PURE__ */ new Map();
|
|
6422
6432
|
const LABEL_TTL = 3e5;
|
|
6423
6433
|
const brainDirs = () => (readOrg(ws.opsDir, parseYaml).staff ?? []).map((s) => s.dir ?? s.handle);
|
|
6424
6434
|
const knownRepos = () => {
|
|
@@ -6426,7 +6436,9 @@ async function portalCommand(argv) {
|
|
|
6426
6436
|
return (org.repos ?? []).map((r) => ({
|
|
6427
6437
|
name: r.name,
|
|
6428
6438
|
owner: org.org,
|
|
6429
|
-
role: r.role ?? "repo"
|
|
6439
|
+
role: r.role ?? "repo",
|
|
6440
|
+
// As org.yaml has it. Absent on a hand-written entry, which hire treats as public.
|
|
6441
|
+
visibility: r.visibility
|
|
6430
6442
|
}));
|
|
6431
6443
|
};
|
|
6432
6444
|
const writeAllowed = (req) => {
|
|
@@ -6891,6 +6903,40 @@ async function portalCommand(argv) {
|
|
|
6891
6903
|
}
|
|
6892
6904
|
return;
|
|
6893
6905
|
}
|
|
6906
|
+
if (url.pathname === "/api/charter-example") {
|
|
6907
|
+
const org = readOrg(w.opsDir, parseYaml);
|
|
6908
|
+
const handle = url.searchParams.get("staff") ?? "";
|
|
6909
|
+
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
6910
|
+
const asked = url.searchParams.get("kind") ?? matchExample(handle, entry?.name ?? "");
|
|
6911
|
+
if (!asked || !CHARTER_EXAMPLES.includes(asked)) {
|
|
6912
|
+
json(res, { kind: null, text: "" });
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
json(res, { kind: asked, text: charterStarter(asked, String(org.name ?? org.org)) });
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
if (url.pathname === "/api/staff/progress") {
|
|
6919
|
+
const handle = url.searchParams.get("staff") ?? "";
|
|
6920
|
+
const hit = progressCache.get(handle);
|
|
6921
|
+
if (url.searchParams.get("fresh") !== "1" && hit && Date.now() - hit.at < 6e4) {
|
|
6922
|
+
json(res, hit.body);
|
|
6923
|
+
return;
|
|
6924
|
+
}
|
|
6925
|
+
const org = readOrg(w.opsDir, parseYaml);
|
|
6926
|
+
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
6927
|
+
if (!entry) {
|
|
6928
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
6929
|
+
res.end(JSON.stringify({ error: `no staff member "${handle}"` }));
|
|
6930
|
+
return;
|
|
6931
|
+
}
|
|
6932
|
+
const dir = entry.dir ?? entry.handle;
|
|
6933
|
+
const spec2 = specFromManifest(readManifest2(join28(w.root, dir), parseYaml), dir);
|
|
6934
|
+
staffProgress(spec2, dailyWorkflow(entry.handle)).then((body2) => {
|
|
6935
|
+
progressCache.set(handle, { at: Date.now(), body: body2 });
|
|
6936
|
+
json(res, body2);
|
|
6937
|
+
}).catch(() => json(res, { app: null, publicApp: null, ran: null }));
|
|
6938
|
+
return;
|
|
6939
|
+
}
|
|
6894
6940
|
if (url.pathname === "/api/staff/plan") {
|
|
6895
6941
|
const handle = url.searchParams.get("handle") ?? "";
|
|
6896
6942
|
const action = url.searchParams.get("action") === "retire" ? "retire" : "hire";
|
|
@@ -7006,7 +7052,8 @@ async function portalCommand(argv) {
|
|
|
7006
7052
|
parseYaml,
|
|
7007
7053
|
kind,
|
|
7008
7054
|
handle,
|
|
7009
|
-
url.searchParams.get("example") ?? void 0
|
|
7055
|
+
url.searchParams.get("example") ?? void 0,
|
|
7056
|
+
url.searchParams.get("about") ?? void 0
|
|
7010
7057
|
);
|
|
7011
7058
|
json(res, {
|
|
7012
7059
|
kind,
|
|
@@ -7482,7 +7529,7 @@ ${warn}
|
|
|
7482
7529
|
});
|
|
7483
7530
|
});
|
|
7484
7531
|
}
|
|
7485
|
-
function buildPasteBrief(ws, parseYaml, kind, handle, example) {
|
|
7532
|
+
function buildPasteBrief(ws, parseYaml, kind, handle, example, about) {
|
|
7486
7533
|
if (!pasteable(kind)) throw new Error(`there is no paste-mode brief for "${kind}"`);
|
|
7487
7534
|
const org = readOrg(ws.opsDir, parseYaml);
|
|
7488
7535
|
const staff = org.staff ?? [];
|
|
@@ -7507,6 +7554,14 @@ function buildPasteBrief(ws, parseYaml, kind, handle, example) {
|
|
|
7507
7554
|
let rendered = renderBrief2(briefTemplate(kind), tokens);
|
|
7508
7555
|
const chosen = kind === "charter" ? example ?? matchExample(handle, tokens.NAME ?? "") ?? "none" : void 0;
|
|
7509
7556
|
if (chosen) rendered = withExample(rendered, handle, tokens.NAME ?? "", chosen);
|
|
7557
|
+
if (kind === "charter" && about?.trim()) {
|
|
7558
|
+
rendered = `${rendered.trimEnd()}
|
|
7559
|
+
|
|
7560
|
+
## What the owner said this role does
|
|
7561
|
+
|
|
7562
|
+
${about.trim()}
|
|
7563
|
+
`;
|
|
7564
|
+
}
|
|
7510
7565
|
return { ...pasteBrief(ws, kind, rendered, { dir, peers }), example: chosen };
|
|
7511
7566
|
}
|
|
7512
7567
|
function renderBrief2(text, tokens) {
|
|
@@ -7524,6 +7579,24 @@ function json(res, data, advice = false) {
|
|
|
7524
7579
|
const text = JSON.stringify(data);
|
|
7525
7580
|
res.end(advice ? withBin(text) : text);
|
|
7526
7581
|
}
|
|
7582
|
+
async function staffProgress(spec2, workflow) {
|
|
7583
|
+
const [repo, shared, runs] = await Promise.all([
|
|
7584
|
+
api(`repos/${spec2.brain}/actions/secrets`),
|
|
7585
|
+
api(`repos/${spec2.brain}/actions/organization-secrets`),
|
|
7586
|
+
api(
|
|
7587
|
+
`repos/${spec2.brain}/actions/workflows/${workflow}/runs?status=success&per_page=1`
|
|
7588
|
+
)
|
|
7589
|
+
]);
|
|
7590
|
+
const names = repo.ok ? /* @__PURE__ */ new Set([
|
|
7591
|
+
...repo.data.secrets.map((x) => x.name),
|
|
7592
|
+
...shared.ok ? shared.data.secrets.map((x) => x.name) : []
|
|
7593
|
+
]) : null;
|
|
7594
|
+
return {
|
|
7595
|
+
app: names ? names.has(`${spec2.secretPrefix}_APP_ID`) : null,
|
|
7596
|
+
publicApp: names ? names.has(`${spec2.publicSecretPrefix}_APP_ID`) : null,
|
|
7597
|
+
ran: runs.ok ? (runs.data?.total_count ?? 0) > 0 : null
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7527
7600
|
function hireFlags(params) {
|
|
7528
7601
|
const num = (k) => params.get(k) ? Number(params.get(k)) : void 0;
|
|
7529
7602
|
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
|
|
@@ -149,7 +161,8 @@ The same road, command by command. Each prints its plan and changes nothing with
|
|
|
149
161
|
|
|
150
162
|
```bash
|
|
151
163
|
roster init --org acme --apply # the ops repo, and its Actions access
|
|
152
|
-
roster brief discover # a brief for org/business.md
|
|
164
|
+
roster brief discover # a brief for org/business.md
|
|
165
|
+
roster brief priorities # a brief for org/priorities.md
|
|
153
166
|
roster hire cto --apply # the brain, the wiring, the commits as you
|
|
154
167
|
roster app cto --apply # the App, its secrets, and a pre-ticked install link
|
|
155
168
|
roster credential --apply # the agent credential, once for the org
|
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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Write `org/priorities.md` for %%ORG_NAME%%.
|
|
2
|
+
|
|
3
|
+
You are helping %%HUMAN%% decide what their AI staff should work on this month. Every staff
|
|
4
|
+
member reads this file at the start of every run and picks work that serves it, so a vague
|
|
5
|
+
priority produces scattered work.
|
|
6
|
+
|
|
7
|
+
## Where you are
|
|
8
|
+
|
|
9
|
+
- `%%OPS_REPO_DIR%%/org/business.md`: what the business is. **Read it first**, and don't ask
|
|
10
|
+
anything it already answers.
|
|
11
|
+
- `%%OPS_REPO_DIR%%/org/priorities.md`: the current file, which you are replacing.
|
|
12
|
+
|
|
13
|
+
If you cannot read files where you are running, ask %%HUMAN%% to paste `org/business.md`.
|
|
14
|
+
|
|
15
|
+
## Interview
|
|
16
|
+
|
|
17
|
+
Ask one question at a time. You are after:
|
|
18
|
+
|
|
19
|
+
- **The one outcome that matters most this month**, and how they will know it happened.
|
|
20
|
+
- **At most two more**, in order. Push back on a fourth: past three it is a wish list.
|
|
21
|
+
- **What is out of scope this month**: work that is tempting but not now. Naming it is what
|
|
22
|
+
stops the staff doing it.
|
|
23
|
+
|
|
24
|
+
Prefer outcomes ("a stranger pays for it") to activities ("improve the landing page"). If an
|
|
25
|
+
answer is an activity, ask what it is for.
|
|
26
|
+
|
|
27
|
+
## Write it
|
|
28
|
+
|
|
29
|
+
Use exactly this shape:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
## What matters this month
|
|
33
|
+
|
|
34
|
+
### Priorities, in order
|
|
35
|
+
|
|
36
|
+
1. The first outcome, and how you will know it happened.
|
|
37
|
+
2. The second.
|
|
38
|
+
3. The third.
|
|
39
|
+
|
|
40
|
+
### Out of scope this month
|
|
41
|
+
|
|
42
|
+
- One thing per line.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Keep each priority to a sentence or two. Don't add anything %%HUMAN%% didn't say without
|
|
46
|
+
listing it after the file so they can check it.
|
|
@@ -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
|