@nanocollective/roster 0.1.0-alpha.42 → 0.1.0-alpha.44
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 +283 -22
- package/docs/README.md +1 -1
- package/docs/commands.md +3 -2
- package/docs/concepts.md +33 -20
- package/docs/doctor-codes.md +2 -0
- package/docs/getting-started.md +5 -4
- package/docs/org-yaml.md +1 -0
- package/docs/portal.md +39 -6
- package/docs/session-workflow.md +14 -0
- package/docs/staff-yaml.md +16 -1
- package/package.json +18 -20
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +20 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +35 -7
- package/templates/brain/staff.yaml +4 -1
- package/templates/ops/.github/workflows/session.yaml +86 -3
- package/templates/ops/compose.mjs +15 -0
- package/templates/ops/org/operating.md +25 -10
- package/templates/ops/prompts/daily.md +23 -4
- package/templates/ops/prompts/mention.md +19 -0
- package/templates/portal/css/home.css +57 -0
- package/templates/portal/index.html +8 -8
- package/templates/portal/js/api.js +3 -0
- package/templates/portal/js/app.js +5 -3
- package/templates/portal/js/homesort.js +154 -0
- package/templates/portal/js/readiness.js +35 -0
- package/templates/portal/js/refresh.js +9 -2
- package/templates/portal/js/state.js +2 -6
- package/templates/portal/js/views/brain.js +1 -1
- package/templates/portal/js/views/hire.js +10 -0
- package/templates/portal/js/views/home.js +391 -0
- package/templates/portal/js/views/inbox.js +5 -4
- package/templates/portal/js/views/setup.js +6 -2
- package/templates/portal/js/views/staff.js +14 -2
package/docs/portal.md
CHANGED
|
@@ -21,8 +21,8 @@ Keep the repos checked out beside each other, in the same shape the runner uses.
|
|
|
21
21
|
Anything that asks before it acts (a merge, a push, a hire, a retire, a paid run) asks in the
|
|
22
22
|
page's own dialog, never the browser's `confirm()`, which blocks the whole tab.
|
|
23
23
|
|
|
24
|
-
**The counts are right on load.** The badges beside
|
|
25
|
-
boot, and the
|
|
24
|
+
**The counts are right on load.** The badges beside Home and Trackers are fetched once at
|
|
25
|
+
boot, and the screens share that request rather than making a second one. A sidebar that
|
|
26
26
|
says nothing until you look at it is not a sidebar.
|
|
27
27
|
|
|
28
28
|
While the first answer is outstanding the badge is a placeholder rather than blank, because an
|
|
@@ -78,12 +78,45 @@ staff member* first, then the Actions setting, the credential, the repo picker,
|
|
|
78
78
|
what doctor still says. A link to another screen still wins. When the list is empty the entry
|
|
79
79
|
goes away; the repo picker lives on under [Org](#org).
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## Home
|
|
82
|
+
|
|
83
|
+
Where the portal opens. An Ask box at the top, then these, in order.
|
|
84
|
+
|
|
85
|
+
- **Ask.** Pick a staff member, type what you want, **Send**. It opens an issue on their
|
|
86
|
+
tracker with their `@handle` in front, which wakes them in about a minute.
|
|
87
|
+
- **Latest reports.** Each staff member's run report from the last day: the three lines they
|
|
88
|
+
post on their status issue at the end of a daily run. Nothing shows until there is one.
|
|
89
|
+
- **Needs you.** Every ask a staff member has put on you, and every pull request ready to
|
|
90
|
+
merge, oldest first. Each card has its action on it:
|
|
91
|
+
- a `decision` has an answer box, and **Go with the default** when it carries one. The
|
|
92
|
+
default and its date are shown, and turn amber once the date has passed.
|
|
93
|
+
- a `review` has **Approve**.
|
|
94
|
+
- a `chore` has **Done**, which closes it.
|
|
95
|
+
- a pull request has **Merge**, unless its checks fail, are still running, or it conflicts.
|
|
96
|
+
|
|
97
|
+
Answers go out as you, with the staff member's `@handle`, so they act on them straight away.
|
|
98
|
+
When nothing is waiting, it says so.
|
|
99
|
+
- **Working now.** Each staff member: what they are running and what started it (the daily run,
|
|
100
|
+
a follow-on, answering an issue, a peer's ask), for how long, with a link to the log. When
|
|
101
|
+
they are idle, how their last run ended. Peer and follow-on runs today are counted against
|
|
102
|
+
`max_runs_per_day`. This asks GitHub every few seconds while a run is going and every half
|
|
103
|
+
minute otherwise, and only while Home is on screen.
|
|
104
|
+
- **Your requests.** What you asked for: waiting, being worked on (a run for it is going), or
|
|
105
|
+
answered (a staff member had the last word).
|
|
106
|
+
- **Closed today.** What the staff closed today, with the line they closed it with, and
|
|
107
|
+
**Reopen** beside each. Staff close finished issues themselves, so this is where you check.
|
|
108
|
+
|
|
109
|
+
**Nothing is missed.** Every open issue and pull request has exactly one place: on Home, or on
|
|
110
|
+
the staff member's own tracker (their status issue, a peer's ask, their own work), or elsewhere
|
|
111
|
+
(draft pull requests, contributor issues). The line at the bottom counts the last two, with a
|
|
112
|
+
link to Trackers. Clicking any title opens its thread there.
|
|
113
|
+
|
|
114
|
+
## Trackers
|
|
82
115
|
|
|
83
116
|
Everything open across the org, from one GraphQL call per repo. Bodies and full timelines come
|
|
84
117
|
down with the list, so opening a thread is a render rather than a request.
|
|
85
118
|
|
|
86
|
-
- **The links under
|
|
119
|
+
- **The links under Trackers in the sidebar filter it.** All; Unread; each staff member, which
|
|
87
120
|
lists the issues on their own tracker whoever filed them; and Issues, the product repos.
|
|
88
121
|
- **Unread comes from your GitHub notifications.** A thread with activity you have not read has
|
|
89
122
|
a bar on the left and a bold title, and Unread lists only those, with a count. Opening one
|
|
@@ -144,7 +177,7 @@ down with the list, so opening a thread is a render rather than a request.
|
|
|
144
177
|
|
|
145
178
|
## Pending work
|
|
146
179
|
|
|
147
|
-
The same screen, scoped to pull requests
|
|
180
|
+
The same screen, scoped to pull requests. It is where a pull request opens from Home. An inbox is what is
|
|
148
181
|
waiting on you; a pull request is work that is finished and waiting on a merge, and the count
|
|
149
182
|
that matters is not how many are open but how many are green and still sitting there.
|
|
150
183
|
|
|
@@ -339,7 +372,7 @@ already declared `memory/` as a surface.
|
|
|
339
372
|
The navigator has three boxes, because a parsed memory section and a file on disk are
|
|
340
373
|
different kinds of thing.
|
|
341
374
|
|
|
342
|
-
**
|
|
375
|
+
**What they know** is the fact sections, the notes behind them, and `INDEX.md` itself. A note is the
|
|
343
376
|
argument behind one fact, read only when that fact is in play, which is what keeps the index
|
|
344
377
|
cheap enough to read at every boot. Both live here rather than among the files: `INDEX.md` is
|
|
345
378
|
literally what "All facts" renders.
|
package/docs/session-workflow.md
CHANGED
|
@@ -33,6 +33,8 @@ repositories in this organisation**. Without it, callers fail with "workflow not
|
|
|
33
33
|
| `allowed_tools` | string | `Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch` | Tool permissions, for agents that take them. |
|
|
34
34
|
| `issue_number` | string | `""` | Trigger context. |
|
|
35
35
|
| `comment_id` | string | `""` | Trigger context. |
|
|
36
|
+
| `trigger` | string | `""` | What started the run: `daily`, `manual`, `follow-on`, `mention` or `peer`. Passed to the prompt. |
|
|
37
|
+
| `max_runs_per_day` | number | `6` | How many `peer` and `follow-on` runs may start in a UTC day. Mentions are never counted. |
|
|
36
38
|
|
|
37
39
|
## Secrets
|
|
38
40
|
|
|
@@ -51,6 +53,14 @@ authentication error forty lines into a log.
|
|
|
51
53
|
|
|
52
54
|
## What it does, in order
|
|
53
55
|
|
|
56
|
+
Before the session job, a small **budget** job. For a `peer` or `follow-on` run it counts this
|
|
57
|
+
brain's runs today with those names in their `run-name`, this one included and skipped ones
|
|
58
|
+
left out. Over `max_runs_per_day`, the session does not start and the issue that woke it gets a
|
|
59
|
+
comment saying so. If the runs cannot be read, the run does not start either: a run held back
|
|
60
|
+
costs a day, and a loop costs a bill. Every other trigger goes straight through.
|
|
61
|
+
|
|
62
|
+
Then the session:
|
|
63
|
+
|
|
54
64
|
1. **Start the clock**, for the run record.
|
|
55
65
|
2. **Mint the private-tracker token** from the staff member's App.
|
|
56
66
|
3. **Mint the public-repo token**, if a public App was passed.
|
|
@@ -70,6 +80,10 @@ authentication error forty lines into a log.
|
|
|
70
80
|
14. **Work out which agent runs this**, by running `agents.mjs`.
|
|
71
81
|
15. **Run the session**, by one of two steps: the Action-based reference runner, or the generic
|
|
72
82
|
CLI one. See [choosing a coding agent](agents.md).
|
|
83
|
+
Then, for a mention, **check the request was answered**: a reply in the thread, or the issue
|
|
84
|
+
closed. Neither fails the job, so the notice below tells the human.
|
|
85
|
+
Then, for a daily run that finished and wrote `.roster-run/continue`, **start a follow-on
|
|
86
|
+
run**: one more daily run with `trigger: follow-on`, which waits for this one to end.
|
|
73
87
|
16. **Write down the run**, whatever happened: staff, kind, outcome, duration, and turns, cost
|
|
74
88
|
and tokens where the agent reports them. Into the job summary, and kept as an artifact
|
|
75
89
|
called `roster-run`. Never fatal. See [cost](cost.md#what-each-run-cost).
|
package/docs/staff-yaml.md
CHANGED
|
@@ -25,6 +25,7 @@ schedule: "0 7 * * 1-5"
|
|
|
25
25
|
model: claude-opus-5-5
|
|
26
26
|
timeout_minutes: 90
|
|
27
27
|
mention_timeout_minutes: 90
|
|
28
|
+
max_runs_per_day: 6
|
|
28
29
|
|
|
29
30
|
bot: acme-cto[bot]
|
|
30
31
|
public_bot: acme-robot[bot]
|
|
@@ -49,7 +50,7 @@ surfaces:
|
|
|
49
50
|
|
|
50
51
|
labels:
|
|
51
52
|
owner: [will, cto, cmo]
|
|
52
|
-
kind: [decision,
|
|
53
|
+
kind: [decision, review, chore, keep-open, build, blocked]
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
## Identity
|
|
@@ -71,6 +72,7 @@ labels:
|
|
|
71
72
|
| `model` | org default | Model id. |
|
|
72
73
|
| `timeout_minutes` | 90 | Ceiling on the daily session. |
|
|
73
74
|
| `mention_timeout_minutes` | 90 | Ceiling on a mention run. |
|
|
75
|
+
| `max_runs_per_day` | 6 | Runs a UTC day that start without a person asking: a peer's ask, or a follow-on to a daily run. Mentions never count. Rendered into the callers, so change it and run `roster upgrade`. |
|
|
74
76
|
|
|
75
77
|
The three ceilings are separate on purpose. Raising the daily one because sessions have grown
|
|
76
78
|
should not double the budget for a PR amendment. A job killed by a ceiling is reported by
|
|
@@ -155,6 +157,19 @@ Labels this staff member expects to exist on its own tracker, grouped for readab
|
|
|
155
157
|
value across every group is checked by `roster doctor`. An agent applying a label that does not
|
|
156
158
|
exist gets an API error mid-run.
|
|
157
159
|
|
|
160
|
+
Four labels are roster's own and are checked on every tracker whatever this lists:
|
|
161
|
+
|
|
162
|
+
| Label | Means |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `decision` | An ask for the human to rule on. It carries a default and a date; past the date, the staff member acts on the default and closes it. |
|
|
165
|
+
| `review` | An ask for the human to read or approve something. |
|
|
166
|
+
| `chore` | Something only the human can do: a setting, an account, a key. |
|
|
167
|
+
| `keep-open` | A standing thread. A sweep never closes it. `roster hire` puts it on the status issue. |
|
|
168
|
+
|
|
169
|
+
Every ask on the human carries exactly one of the first three, which is how the portal sorts
|
|
170
|
+
what needs them. Staff close any issue on their tracker once nothing is left to do on it,
|
|
171
|
+
including the human's requests, with one line saying what closed it.
|
|
172
|
+
|
|
158
173
|
## `memory`
|
|
159
174
|
|
|
160
175
|
This staff member's own memory budgets, overriding the org's. Same three fields as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanocollective/roster",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.44",
|
|
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": {
|
|
@@ -16,23 +16,6 @@
|
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=20"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsup src/cli.ts --format esm --target node20 --clean",
|
|
21
|
-
"dev": "tsx src/cli.ts",
|
|
22
|
-
"test": "tsx --test test/*.test.ts",
|
|
23
|
-
"test:all": "pnpm test:format && pnpm test:lint && pnpm test:types && pnpm test:knip && pnpm test",
|
|
24
|
-
"test:ava:coverage": "c8 --reporter=text --reporter=json-summary tsx --test test/*.test.ts",
|
|
25
|
-
"test:format": "biome check --no-errors-on-unmatched .",
|
|
26
|
-
"test:lint": "biome lint .",
|
|
27
|
-
"test:lint:fix": "biome check --write .",
|
|
28
|
-
"test:types": "tsc --noEmit",
|
|
29
|
-
"test:knip": "knip",
|
|
30
|
-
"test:audit": "pnpm audit --audit-level=high",
|
|
31
|
-
"test:security": "semgrep scan --config auto --error",
|
|
32
|
-
"typecheck": "tsc --noEmit",
|
|
33
|
-
"format": "biome check --write .",
|
|
34
|
-
"prepublishOnly": "pnpm test:all && pnpm build"
|
|
35
|
-
},
|
|
36
19
|
"devDependencies": {
|
|
37
20
|
"@biomejs/biome": "^2.5.12",
|
|
38
21
|
"@types/node": "^22.10.2",
|
|
@@ -52,9 +35,24 @@
|
|
|
52
35
|
"url": "https://github.com/Nano-Collective/roster/issues"
|
|
53
36
|
},
|
|
54
37
|
"author": "Nano Collective",
|
|
55
|
-
"packageManager": "pnpm@11.0.9",
|
|
56
38
|
"publishConfig": {
|
|
57
39
|
"access": "public",
|
|
58
40
|
"tag": "latest"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup src/cli.ts --format esm --target node20 --clean",
|
|
44
|
+
"dev": "tsx src/cli.ts",
|
|
45
|
+
"test": "tsx --test test/*.test.ts",
|
|
46
|
+
"test:all": "pnpm test:format && pnpm test:lint && pnpm test:types && pnpm test:knip && pnpm test",
|
|
47
|
+
"test:ava:coverage": "c8 --reporter=text --reporter=json-summary tsx --test test/*.test.ts",
|
|
48
|
+
"test:format": "biome check --no-errors-on-unmatched .",
|
|
49
|
+
"test:lint": "biome lint .",
|
|
50
|
+
"test:lint:fix": "biome check --write .",
|
|
51
|
+
"test:types": "tsc --noEmit",
|
|
52
|
+
"test:knip": "knip",
|
|
53
|
+
"test:audit": "pnpm audit --audit-level=high",
|
|
54
|
+
"test:security": "semgrep scan --config auto --error",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"format": "biome check --write ."
|
|
59
57
|
}
|
|
60
|
-
}
|
|
58
|
+
}
|
|
@@ -6,12 +6,25 @@ name: %%STAFF_UPPER%% daily run
|
|
|
6
6
|
#
|
|
7
7
|
# NOTE: %%TOKENS%% are filled by `roster hire`; GitHub's own ${{ }} are left alone.
|
|
8
8
|
|
|
9
|
+
# The trigger and the date, as the Actions list shows it. The daily limit counts follow-on runs
|
|
10
|
+
# by this name.
|
|
11
|
+
run-name: "%%STAFF%% ${{ github.event_name == 'schedule' && 'daily' || inputs.trigger || 'manual' }}"
|
|
12
|
+
|
|
9
13
|
on:
|
|
10
14
|
schedule:
|
|
11
15
|
- cron: "%%SCHEDULE%%"
|
|
16
|
+
# By hand, from Run once now, or as a follow-on: a daily run that ended with the next step
|
|
17
|
+
# ready starts one more, up to max_runs_per_day.
|
|
12
18
|
workflow_dispatch:
|
|
19
|
+
inputs:
|
|
20
|
+
trigger:
|
|
21
|
+
description: "manual | follow-on"
|
|
22
|
+
required: false
|
|
23
|
+
default: manual
|
|
24
|
+
type: string
|
|
13
25
|
|
|
14
|
-
# A second run while one is in flight would fight it over the tracker and the branch state.
|
|
26
|
+
# A second run while one is in flight would fight it over the tracker and the branch state. A
|
|
27
|
+
# follow-on is started from inside the run before it, so it waits here until that one ends.
|
|
15
28
|
concurrency:
|
|
16
29
|
group: %%STAFF%%-session
|
|
17
30
|
cancel-in-progress: false
|
|
@@ -21,13 +34,19 @@ jobs:
|
|
|
21
34
|
# The ceiling on what session.yaml's job token may do: reading the checkout, and the one
|
|
22
35
|
# comment that says a run failed when the App that would normally say so is what broke.
|
|
23
36
|
# A called workflow cannot raise these, so they have to be granted here.
|
|
37
|
+
#
|
|
38
|
+
# `actions` is for the daily limit, which reads this repo's runs, and for starting a
|
|
39
|
+
# follow-on run, which the job token may do: a workflow_dispatch it sends still starts a run.
|
|
24
40
|
permissions:
|
|
25
41
|
contents: read
|
|
26
42
|
issues: write
|
|
43
|
+
actions: write
|
|
27
44
|
uses: %%OPS_REPO%%/.github/workflows/session.yaml@main
|
|
28
45
|
with:
|
|
29
46
|
staff: %%STAFF%%
|
|
30
47
|
kind: daily
|
|
48
|
+
trigger: ${{ github.event_name == 'schedule' && 'daily' || inputs.trigger || 'manual' }}
|
|
49
|
+
max_runs_per_day: %%MAX_RUNS%%
|
|
31
50
|
ops_repo: %%OPS_REPO%%
|
|
32
51
|
model: %%MODEL%%
|
|
33
52
|
timeout_minutes: %%TIMEOUT%%
|
|
@@ -3,11 +3,18 @@ name: %%STAFF_UPPER%% on a mention
|
|
|
3
3
|
# Generated by roster. The body lives in %%OPS_REPO%%/.github/workflows/session.yaml.
|
|
4
4
|
#
|
|
5
5
|
# "%%MENTION%% ..." in a comment on this tracker, or in the body of a new issue, wakes a focused
|
|
6
|
-
# run in about 30 seconds.
|
|
6
|
+
# run in about 30 seconds. So does another staff member filing an issue here with their
|
|
7
|
+
# `from-<handle>` label. It is a task, not a session: no boot ritual, no handoff, and the reply
|
|
7
8
|
# goes in the thread.
|
|
8
9
|
#
|
|
9
10
|
# NOTE: %%TOKENS%% are filled by `roster hire`. GitHub's own ${{ }} expressions are left alone.
|
|
10
11
|
|
|
12
|
+
# What the run is and what started it, as the Actions list shows it. The portal reads this back
|
|
13
|
+
# to link a live run to the request behind it, and the daily limit counts peer runs by it.
|
|
14
|
+
run-name: >-
|
|
15
|
+
%%STAFF%% ${{ contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) && 'mention' || 'peer' }}
|
|
16
|
+
#${{ github.event.issue.number }}
|
|
17
|
+
|
|
11
18
|
on:
|
|
12
19
|
# A mention in the body of a brand new issue, which is often the faster route: one box, rather
|
|
13
20
|
# than a "create, then comment" round trip. `edited` doubles as the way to re-ask without
|
|
@@ -44,25 +51,46 @@ jobs:
|
|
|
44
51
|
#
|
|
45
52
|
# One spelling of the mention is enough: GitHub's `contains` is documented as not case
|
|
46
53
|
# sensitive, so this already matches an uppercase mention at the start of a sentence.
|
|
54
|
+
#
|
|
55
|
+
# The second route is a peer's ask: a new issue opened by an App, never this staff member's
|
|
56
|
+
# own, carrying a `from-<handle>` label. Only `opened`, so the peer editing its own issue
|
|
57
|
+
# later wakes nobody. It names no peer, so a later hire needs no regenerated caller: the only
|
|
58
|
+
# accounts that can open an issue on a private tracker are the people and Apps it is
|
|
59
|
+
# installed for. A peer run may not file on another peer, and session.yaml holds peer runs
|
|
60
|
+
# to the day's limit, so two staff cannot wake each other forever.
|
|
47
61
|
if: >-
|
|
48
|
-
contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) &&
|
|
49
62
|
(
|
|
50
|
-
(github.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) &&
|
|
64
|
+
(
|
|
65
|
+
(github.event_name == 'issue_comment' &&
|
|
66
|
+
contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.comment.user.login) &&
|
|
67
|
+
contains(github.event.comment.body, '%%MENTION%%')) ||
|
|
68
|
+
(github.event_name == 'issues' &&
|
|
69
|
+
contains(github.event.issue.body, '%%MENTION%%'))
|
|
70
|
+
)
|
|
71
|
+
) || (
|
|
72
|
+
github.event_name == 'issues' && github.event.action == 'opened' &&
|
|
73
|
+
endsWith(github.event.sender.login, '[bot]') &&
|
|
74
|
+
github.event.sender.login != '%%APP%%[bot]' &&
|
|
75
|
+
contains(join(github.event.issue.labels.*.name, ','), 'from-')
|
|
55
76
|
)
|
|
56
77
|
# The ceiling on what session.yaml's job token may do: reading the checkout, and the one
|
|
57
78
|
# comment that says a run failed when the App that would normally say so is what broke.
|
|
58
79
|
# A called workflow cannot raise these, so they have to be granted here.
|
|
80
|
+
#
|
|
81
|
+
# `actions` is for the daily limit, which counts this repo's runs today. It is `write`
|
|
82
|
+
# because session.yaml's job asks for that (a daily run starts its follow-on with it), and a
|
|
83
|
+
# called job asking for more than its caller granted fails before it starts.
|
|
59
84
|
permissions:
|
|
60
85
|
contents: read
|
|
61
86
|
issues: write
|
|
87
|
+
actions: write
|
|
62
88
|
uses: %%OPS_REPO%%/.github/workflows/session.yaml@main
|
|
63
89
|
with:
|
|
64
90
|
staff: %%STAFF%%
|
|
65
91
|
kind: mention
|
|
92
|
+
trigger: ${{ contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.sender.login) && 'mention' || 'peer' }}
|
|
93
|
+
max_runs_per_day: %%MAX_RUNS%%
|
|
66
94
|
ops_repo: %%OPS_REPO%%
|
|
67
95
|
model: %%MODEL%%
|
|
68
96
|
timeout_minutes: %%MENTION_TIMEOUT%%
|
|
@@ -13,6 +13,9 @@ schedule: "%%SCHEDULE%%"
|
|
|
13
13
|
model: %%MODEL%%
|
|
14
14
|
timeout_minutes: %%TIMEOUT%%
|
|
15
15
|
mention_timeout_minutes: %%MENTION_TIMEOUT%%
|
|
16
|
+
# Runs a day that start without a person asking: a peer's ask, or a follow-on to a daily run.
|
|
17
|
+
# Mentions never count against it.
|
|
18
|
+
max_runs_per_day: %%MAX_RUNS%%
|
|
16
19
|
|
|
17
20
|
bot: %%APP%%[bot]
|
|
18
21
|
public_bot: %%PUBLIC_APP%%[bot]
|
|
@@ -40,4 +43,4 @@ surfaces:
|
|
|
40
43
|
|
|
41
44
|
labels:
|
|
42
45
|
owner: [%%HUMAN_MARKER%%, %%STAFF%%]
|
|
43
|
-
kind: [decision,
|
|
46
|
+
kind: [decision, review, chore, keep-open, build, blocked]
|
|
@@ -19,6 +19,16 @@ on:
|
|
|
19
19
|
required: false
|
|
20
20
|
default: daily
|
|
21
21
|
type: string
|
|
22
|
+
# What started the run: daily | manual | follow-on | mention | peer. Peer and follow-on
|
|
23
|
+
# runs start without a person asking, so they are the ones held to max_runs_per_day.
|
|
24
|
+
trigger:
|
|
25
|
+
required: false
|
|
26
|
+
default: ""
|
|
27
|
+
type: string
|
|
28
|
+
max_runs_per_day:
|
|
29
|
+
required: false
|
|
30
|
+
default: 6
|
|
31
|
+
type: number
|
|
22
32
|
ops_repo:
|
|
23
33
|
description: "owner/name of the ops repo holding org.yaml and the prompts"
|
|
24
34
|
required: true
|
|
@@ -65,16 +75,69 @@ permissions:
|
|
|
65
75
|
contents: read
|
|
66
76
|
|
|
67
77
|
jobs:
|
|
78
|
+
# Two staff can ask each other things, and a daily run can start a follow-on, so runs can start
|
|
79
|
+
# runs. This is what stops that at max_runs_per_day. A mention is a person asking and is never
|
|
80
|
+
# held back, so only peer and follow-on runs are counted, and only the ones that ran: a caller
|
|
81
|
+
# whose condition did not match leaves a skipped run behind under the same name.
|
|
82
|
+
budget:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
permissions:
|
|
85
|
+
actions: read
|
|
86
|
+
issues: write
|
|
87
|
+
outputs:
|
|
88
|
+
go: ${{ steps.count.outputs.go }}
|
|
89
|
+
steps:
|
|
90
|
+
- name: Count today's runs nobody asked for
|
|
91
|
+
id: count
|
|
92
|
+
env:
|
|
93
|
+
GH_TOKEN: ${{ github.token }}
|
|
94
|
+
TRIGGER: ${{ inputs.trigger }}
|
|
95
|
+
STAFF: ${{ inputs.staff }}
|
|
96
|
+
LIMIT: ${{ inputs.max_runs_per_day }}
|
|
97
|
+
ISSUE: ${{ inputs.issue_number }}
|
|
98
|
+
run: |
|
|
99
|
+
set -uo pipefail
|
|
100
|
+
case "$TRIGGER" in
|
|
101
|
+
peer|follow-on) ;;
|
|
102
|
+
*) echo "go=true" >> "$GITHUB_OUTPUT"; exit 0 ;;
|
|
103
|
+
esac
|
|
104
|
+
today=$(date -u +%F)
|
|
105
|
+
# Includes this run, which is in progress and named like the rest.
|
|
106
|
+
filter="[.workflow_runs[]
|
|
107
|
+
| select(.display_title | test(\"^$STAFF (peer|follow-on)( |\$)\"))
|
|
108
|
+
| select(.conclusion != \"skipped\")] | length"
|
|
109
|
+
# One count per page, summed. pipefail makes a failed read fail the whole line.
|
|
110
|
+
if ! n=$(gh api --paginate "repos/${{ github.repository }}/actions/runs?created=>=$today&per_page=100" \
|
|
111
|
+
--jq "$filter" | awk '{s += $1} END {print s + 0}'); then
|
|
112
|
+
# Unknown is not under the limit. A run held back costs a day; a loop costs a bill.
|
|
113
|
+
echo "::warning::could not count today's runs, so this one does not start"
|
|
114
|
+
n=999999
|
|
115
|
+
fi
|
|
116
|
+
echo "$n of $LIMIT runs today that nobody asked for, this one included"
|
|
117
|
+
if [ "$n" -le "$LIMIT" ]; then
|
|
118
|
+
echo "go=true" >> "$GITHUB_OUTPUT"
|
|
119
|
+
exit 0
|
|
120
|
+
fi
|
|
121
|
+
echo "go=false" >> "$GITHUB_OUTPUT"
|
|
122
|
+
msg="Not started: $STAFF has had $LIMIT runs today that nobody asked for, which is max_runs_per_day. The next daily run picks this up, or mention them to start one now."
|
|
123
|
+
echo "$msg" >> "$GITHUB_STEP_SUMMARY"
|
|
124
|
+
if [ -n "$ISSUE" ]; then
|
|
125
|
+
gh issue comment "$ISSUE" --repo "${{ github.repository }}" --body "$msg" || true
|
|
126
|
+
fi
|
|
127
|
+
|
|
68
128
|
session:
|
|
129
|
+
needs: budget
|
|
130
|
+
if: needs.budget.outputs.go == 'true'
|
|
69
131
|
runs-on: ubuntu-latest
|
|
70
132
|
timeout-minutes: ${{ inputs.timeout_minutes }}
|
|
71
133
|
|
|
72
134
|
# `issues: write` is for the failure notice alone, which falls back to this job's own token
|
|
73
|
-
# when the App's cannot be minted.
|
|
74
|
-
# the callers ask for the same.
|
|
135
|
+
# when the App's cannot be minted. `actions: write` starts a follow-on run. A called workflow
|
|
136
|
+
# cannot raise what its caller granted, so the callers ask for the same.
|
|
75
137
|
permissions:
|
|
76
138
|
contents: read
|
|
77
139
|
issues: write
|
|
140
|
+
actions: write
|
|
78
141
|
|
|
79
142
|
# secrets are not usable in a step-level `if`, so the presence check is hoisted here.
|
|
80
143
|
env:
|
|
@@ -256,7 +319,8 @@ jobs:
|
|
|
256
319
|
{"issue_number":"${{ inputs.issue_number }}",
|
|
257
320
|
"comment_id":"${{ inputs.comment_id }}",
|
|
258
321
|
"repo":"${{ github.repository }}",
|
|
259
|
-
"actor":"${{ github.actor }}"
|
|
322
|
+
"actor":"${{ github.actor }}",
|
|
323
|
+
"trigger":"${{ inputs.trigger }}"}
|
|
260
324
|
run: |
|
|
261
325
|
node roster-ops/compose.mjs --staff "${{ inputs.staff }}" --kind "${{ inputs.kind }}" --ops roster-ops --brains . > .roster-prompt.txt
|
|
262
326
|
{
|
|
@@ -380,6 +444,25 @@ jobs:
|
|
|
380
444
|
echo "::error::the session ended without replying on #$ISSUE or closing it"
|
|
381
445
|
exit 1
|
|
382
446
|
|
|
447
|
+
# A daily run that ended with the next step ready says so in .roster-run/continue, and one
|
|
448
|
+
# more run starts once this one ends: it waits on the caller's concurrency group. Only
|
|
449
|
+
# after a run that finished, and the budget job above holds the chain to the day's limit.
|
|
450
|
+
# The job token can do this: a workflow_dispatch it sends still starts a run.
|
|
451
|
+
- name: Start a follow-on run
|
|
452
|
+
if: >-
|
|
453
|
+
inputs.kind == 'daily' && success()
|
|
454
|
+
&& (steps.session_action.outcome == 'success' || steps.session_cli.outcome == 'success')
|
|
455
|
+
&& hashFiles('.roster-run/continue') != ''
|
|
456
|
+
continue-on-error: true
|
|
457
|
+
env:
|
|
458
|
+
GH_TOKEN: ${{ github.token }}
|
|
459
|
+
CALLER: ${{ github.workflow_ref }}
|
|
460
|
+
run: |
|
|
461
|
+
set -uo pipefail
|
|
462
|
+
file=$(basename "${CALLER%@*}")
|
|
463
|
+
gh workflow run "$file" --repo "${{ github.repository }}" -f trigger=follow-on
|
|
464
|
+
{ echo "Started a follow-on run:"; head -c 300 .roster-run/continue; echo; } >> "$GITHUB_STEP_SUMMARY"
|
|
465
|
+
|
|
383
466
|
# What this run was and what it cost, in the job summary and as an artifact with a stable
|
|
384
467
|
# name, which is what the portal's Runs screen and `roster doctor` read back. Never fatal:
|
|
385
468
|
# a missing record costs a row in a table, and failing the job over it would cost the run.
|
|
@@ -323,6 +323,16 @@ export function compose({ opsDir, brainsDir, staff, kind, runDir }) {
|
|
|
323
323
|
// sends it to a 404 before it has read anything. So the absence is a value of its own.
|
|
324
324
|
if (Object.keys(event).length) event = { ...event, no_comment: !event.comment_id };
|
|
325
325
|
|
|
326
|
+
// Who started this run, as flags: the prompt language has `{{#if}}` and nothing to compare
|
|
327
|
+
// with. A peer's ask reads differently from a person's, and a follow-on picks up mid-task.
|
|
328
|
+
const trigger = String(event.trigger ?? "");
|
|
329
|
+
event = {
|
|
330
|
+
...event,
|
|
331
|
+
from_peer: trigger === "peer",
|
|
332
|
+
from_human: trigger !== "peer",
|
|
333
|
+
follow_on: trigger === "follow-on",
|
|
334
|
+
};
|
|
335
|
+
|
|
326
336
|
// What people have open on the product repos, gathered by inflight.mjs just before this runs.
|
|
327
337
|
// Read as a value and never rendered as a template: it is PR titles, which are a person's
|
|
328
338
|
// words, and a `{{` in one must not be able to break composition.
|
|
@@ -345,6 +355,11 @@ export function compose({ opsDir, brainsDir, staff, kind, runDir }) {
|
|
|
345
355
|
// The repo this role contributes to but does not own. Named explicitly in the
|
|
346
356
|
// prompt because "a repo you do not own" is vaguer than an agent needs.
|
|
347
357
|
product: (self.works_in ?? [])[0] ?? null,
|
|
358
|
+
// Absent from manifests written before it existed; the callers fall back to the same.
|
|
359
|
+
max_runs_per_day: self.max_runs_per_day ?? 6,
|
|
360
|
+
// One staff member drafts next month's org/priorities.md, so there is one draft rather
|
|
361
|
+
// than one per person: whoever org.yaml lists first.
|
|
362
|
+
drafts_priorities: (org.staff ?? [])[0]?.handle === staff,
|
|
348
363
|
},
|
|
349
364
|
peers,
|
|
350
365
|
peer: peers[0] ?? null,
|
|
@@ -9,7 +9,8 @@ question instead of doing work has wasted its slot.
|
|
|
9
9
|
- **A question is an issue, never a stopped run.** When you hit something genuinely load-bearing,
|
|
10
10
|
open a `decision` issue on your own tracker, assign `{{human.github}}`, @-mention them, **then move
|
|
11
11
|
to the next item.** The body: the ask as the first line, your recommendation, the argument stripped
|
|
12
|
-
to what they need to rule, and
|
|
12
|
+
to what they need to rule, and the default with a date, as its own last line: **"If I hear nothing
|
|
13
|
+
by <YYYY-MM-DD>, I'll <do X>."** At least two working days out. They are ruling from a phone.
|
|
13
14
|
- **Filing an issue does not stop the run.** "This needs a human" means open the issue and carry on,
|
|
14
15
|
not stand still.
|
|
15
16
|
- **Never end a run blocked.** If everything on the list is genuinely blocked, do the most useful
|
|
@@ -29,12 +30,21 @@ question instead of doing work has wasted its slot.
|
|
|
29
30
|
|
|
30
31
|
## Keeping your tracker clean
|
|
31
32
|
|
|
32
|
-
{{human.name}} should never have to ask whether an issue can be closed.
|
|
33
|
-
|
|
34
|
-
- **Close
|
|
35
|
-
it: the PR, the commit,
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
{{human.name}} should never have to ask whether an issue can be closed, or close one themselves.
|
|
34
|
+
|
|
35
|
+
- **Close any issue on your tracker once nothing is left to do on it**, whoever opened it, including
|
|
36
|
+
{{human.name}}'s requests. One line naming what closed it: the PR, the commit, the answer, or the
|
|
37
|
+
issue that replaced it. If a request needs nothing, say why in that line and close it. Do not leave
|
|
38
|
+
one open "in case".
|
|
39
|
+
- **Sweep your tracker on every daily run.** Every open issue on it, not only the ones you opened:
|
|
40
|
+
close it, do it, or put it in your plan. Fold duplicates into one.
|
|
41
|
+
- **Never close an issue labelled `keep-open`, or your pinned status issue.** Those are standing
|
|
42
|
+
threads.
|
|
43
|
+
- **Every ask on {{human.name}} carries the `{{human.marker}}` label and exactly one kind**, and is
|
|
44
|
+
assigned to `{{human.github}}`:
|
|
45
|
+
- `decision`: they rule on something. Carries the default and date above.
|
|
46
|
+
- `review`: they read or approve something you made.
|
|
47
|
+
- `chore`: something only they can do, such as a setting, an account or a key.
|
|
38
48
|
- **Ideas live in your brain, not on the tracker.** Park a speculative idea as one line in
|
|
39
49
|
`strategy/ideas.md`. Open an `IDEA:` issue only when it needs a ruling, and never more than one
|
|
40
50
|
at a time.
|
|
@@ -45,7 +55,9 @@ question instead of doing work has wasted its slot.
|
|
|
45
55
|
mechanical rather than a promise: protected branches mean you open a PR and their merge is the
|
|
46
56
|
approval. **Do not look for a way around it.** Being unable to ship unreviewed is what earns the
|
|
47
57
|
autonomy.
|
|
48
|
-
- **
|
|
58
|
+
- **Close a `decision` only once it is settled.** Either {{human.name}} answered: act on it, then
|
|
59
|
+
close it quoting the ruling. Or the date passed with no answer: act on the default, then close it
|
|
60
|
+
saying you did. Never before one of those.
|
|
49
61
|
- **Never `git add -A` in another staff member's repo, or in a repo where a human may have work in
|
|
50
62
|
flight.** Stage explicit paths. Doing otherwise has swept someone else's uncommitted work into an
|
|
51
63
|
unrelated commit.
|
|
@@ -69,6 +81,8 @@ overhead.**
|
|
|
69
81
|
`log/decisions.md`. A memory that only grows is a memory nobody reads.
|
|
70
82
|
- Mark every fact with where it came from: `[{{human.marker}}]` for a ruling, `[measured]` for
|
|
71
83
|
something with an `n` and a date, `[derived]` for your own inference.
|
|
84
|
+
- **Another run of yours may be working at the same time**: a mention, or a peer's ask. If a push
|
|
85
|
+
is rejected, `git pull --rebase` and push again. Never force-push.
|
|
72
86
|
|
|
73
87
|
`log/decisions.md` is **not** boot context. It is the audit trail: read it when you need to know why
|
|
74
88
|
something was decided, or before reversing a call somebody already made.
|
|
@@ -80,9 +94,10 @@ Other staff members are peers, not subordinates and not tools. **Write to them f
|
|
|
80
94
|
team updated is always fine, and over-communicating is the right default.
|
|
81
95
|
|
|
82
96
|
**Comms are issues, not file drops:** open an issue on their tracker labelled `from-{{staff.handle}}`.
|
|
83
|
-
A file in their inbox works for them and is invisible to {{human.name}},
|
|
97
|
+
A file in their inbox works for them and is invisible to {{human.name}}, who needs to be able to
|
|
84
98
|
read the whole conversation in one place. Genuinely long-form output can still be a file, with the
|
|
85
|
-
issue linking to it.
|
|
99
|
+
issue linking to it. **Filing one wakes them within a minute**, so file what they can act on now
|
|
100
|
+
and put everything else in one issue rather than several.
|
|
86
101
|
|
|
87
102
|
**An ask of a peer stays an ask.** They own their own priorities. Anything that needs
|
|
88
103
|
{{human.name}}'s money or public sign-off still goes through them.
|
|
@@ -8,6 +8,11 @@ your brain. Reconstitute yourself, do a day's work, hand off.
|
|
|
8
8
|
|
|
9
9
|
{{>? staff:prompts/boot.md}}
|
|
10
10
|
|
|
11
|
+
{{#if event.follow_on}}
|
|
12
|
+
**This is a follow-on run.** The last run ended with the next step ready and started this one.
|
|
13
|
+
Boot as usual; #{{staff.status_issue}} says what it is.
|
|
14
|
+
{{/if}}
|
|
15
|
+
|
|
11
16
|
## Do this now, in order
|
|
12
17
|
|
|
13
18
|
1. **Check the real date:** `date +%F`. Do not infer it from a file. A run's output was once dated
|
|
@@ -59,10 +64,12 @@ writing a plan for {{human.name}} to approve.
|
|
|
59
64
|
2. **Rewrite pinned issue #{{staff.status_issue}} "Where we are"**: the situation in a line, what
|
|
60
65
|
this run did, what the next run picks up in priority order. **It is a handover for the next run,
|
|
61
66
|
not a diary.** Rewrite it, do not append, and cut anything the next run can find for itself.
|
|
62
|
-
3. **Reconcile the tracker.** Open issues for anything new needing {{human.name}}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
3. **Reconcile the tracker.** Open issues for anything new needing {{human.name}}: the
|
|
68
|
+
`{{human.marker}}` label plus one of `decision`, `review` or `chore`, assigned to
|
|
69
|
+
`{{human.github}}`. Sweep **every** open issue on your tracker, whoever opened it: close what is
|
|
70
|
+
done, superseded or needs nothing, citing what closed it. A `decision` whose date has passed with
|
|
71
|
+
no answer: act on the default and close it. **Comments and replies get the same concision as
|
|
72
|
+
everything else:** what changed and what it means for them. A comment that only says an issue is still open
|
|
66
73
|
is not worth the notification.
|
|
67
74
|
4. **Update `{{staff.dir}}/memory/` only if a fact or watch-out changed.** A new fact is **one line**
|
|
68
75
|
in `INDEX.md` saying what it changes; a corrected fact is **edited in place**, never appended to
|
|
@@ -85,6 +92,18 @@ writing a plan for {{human.name}} to approve.
|
|
|
85
92
|
`@{{human.github}}`. **Three lines: what you did, what is now on them (issue numbers and the ask,
|
|
86
93
|
nothing more), what you would do next.** No preamble, no closing line, no headers. This is the
|
|
87
94
|
only thing they read, and a long one does not get read.
|
|
95
|
+
9. **Start the next run now, if the next step is ready.** When it serves the priorities and waits on
|
|
96
|
+
nobody (not on {{human.name}}, not on a review, not on a peer), write one line saying what it is:
|
|
97
|
+
`mkdir -p "$GITHUB_WORKSPACE/.roster-run" && echo "<the step>" > "$GITHUB_WORKSPACE/.roster-run/continue"`.
|
|
98
|
+
Another run starts when this one ends, up to {{staff.max_runs_per_day}} a day that nobody asked
|
|
99
|
+
for. Leave it out when there is nothing that cannot wait until tomorrow.
|
|
100
|
+
{{#if staff.drafts_priorities}}
|
|
101
|
+
10. **In the last three days of a month, draft next month's priorities.** If there is no open pull
|
|
102
|
+
request on `{{ops.dir}}` changing `org/priorities.md` already, open one from a branch: at
|
|
103
|
+
most three ranked priorities and what is out of scope, built from this month's, what shipped,
|
|
104
|
+
and the other staff's status issues. The body says what changed from this month and why, in a
|
|
105
|
+
few lines. {{human.name}} edits and merges it; until then this month's stand.
|
|
106
|
+
{{/if}}
|
|
88
107
|
|
|
89
108
|
{{> org/guardrails.md}}
|
|
90
109
|
|