@nanocollective/roster 0.1.0-alpha.6 → 0.1.0-alpha.7
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/README.md +65 -84
- package/dist/cli.js +4153 -2708
- package/docs/README.md +9 -6
- package/docs/agents.md +24 -20
- package/docs/architecture.md +13 -5
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/support.md +60 -0
- package/docs/commands.md +81 -5
- package/docs/concepts.md +48 -14
- package/docs/cost.md +36 -1
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +8 -2
- package/docs/extending.md +2 -2
- package/docs/getting-started.md +100 -77
- package/docs/manual-steps.md +93 -123
- package/docs/memory.md +21 -3
- package/docs/org-yaml.md +37 -2
- package/docs/portal.md +59 -33
- package/docs/prompts.md +25 -4
- package/docs/security.md +29 -5
- package/docs/session-workflow.md +49 -17
- package/docs/staff-yaml.md +13 -2
- package/docs/troubleshooting.md +8 -8
- package/docs/upgrading.md +6 -0
- package/docs/writing-a-charter.md +15 -0
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
- package/templates/brain/CHARTER.md +3 -3
- package/templates/brain/README.md +1 -0
- package/templates/brain/log/decisions.md +3 -0
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/ops/.github/workflows/session.yaml +108 -14
- package/templates/ops/agents.mjs +7 -3
- package/templates/ops/compose.mjs +16 -3
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +21 -1
- package/templates/ops/org/voice.md +9 -0
- package/templates/ops/prompts/_inflight.md +14 -0
- package/templates/ops/prompts/_paths.md +2 -1
- package/templates/ops/prompts/daily.md +16 -7
- package/templates/ops/prompts/mention.md +2 -0
- package/templates/ops/run-record.mjs +144 -0
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/index.html +4 -0
- package/templates/portal/js/api.js +29 -3
- package/templates/portal/js/app.js +4 -1
- package/templates/portal/js/state.js +3 -1
- package/templates/portal/js/views/app.js +23 -5
- package/templates/portal/js/views/credential.js +93 -0
- package/templates/portal/js/views/health.js +15 -3
- package/templates/portal/js/views/org.js +2 -0
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/runonce.js +88 -0
- package/templates/portal/js/views/runs.js +165 -0
- package/templates/portal/js/views/setup.js +67 -26
- package/templates/portal/js/views/staff.js +47 -10
package/docs/prompts.md
CHANGED
|
@@ -58,12 +58,12 @@ ROSTER_CONTEXT='{"issue_number":"1","comment_id":"1","repo":"o/r"}' \
|
|
|
58
58
|
`comment_id`; a mention typed into the body of a *new* issue does not, and there is no comment
|
|
59
59
|
for the agent to fetch. So `compose.mjs` derives `event.no_comment` from the absence, and
|
|
60
60
|
`prompts/mention.md` branches on it: one side points at the comment, the other at the issue
|
|
61
|
-
body. Without that, the first instruction in the prompt
|
|
61
|
+
body. Without that, the first instruction in the prompt would be a `gh api .../issues/comments/`
|
|
62
62
|
call with no id on the end, which 404s.
|
|
63
63
|
|
|
64
|
-
The second route is the busier one
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
The second route is the busier one. It is what the portal produces when you [ask for a
|
|
65
|
+
change](portal.md#asking-for-a-change), and those issues often carry a pull request that lives
|
|
66
|
+
somewhere else and say to answer there instead.
|
|
67
67
|
|
|
68
68
|
## Syntax
|
|
69
69
|
|
|
@@ -108,10 +108,31 @@ than a hang.
|
|
|
108
108
|
| `event` | trigger context, from `ROSTER_CONTEXT` |
|
|
109
109
|
| `event.issue_number`, `event.comment_id`, `event.repo`, `event.actor` | |
|
|
110
110
|
| `event.no_comment` | true when the ask is the issue body rather than a comment. There is no `{{#unless}}`, so the absence is a value |
|
|
111
|
+
| `inflight` | the pull requests people have open on the product repos, as a markdown list. **Empty outside a run**, and when there are none. See below |
|
|
111
112
|
|
|
112
113
|
Anything else in a manifest is reachable under `staff.`, so `staff.status_issue` and
|
|
113
114
|
`staff.public_token_env` work without being listed here.
|
|
114
115
|
|
|
116
|
+
## Human work in flight
|
|
117
|
+
|
|
118
|
+
Before composing, a run looks at the staff member's product repos (their `works_in`, or every
|
|
119
|
+
`role: product` repo) for open pull requests opened by people rather than by any App. Each one
|
|
120
|
+
goes in as a line: title, author, age, branch, and the files it touches, capped at twenty with
|
|
121
|
+
the top directories named when there are more.
|
|
122
|
+
|
|
123
|
+
`prompts/_inflight.md` carries that list under a short instruction, inside `{{#if inflight}}`,
|
|
124
|
+
and both kinds include it: do not open competing work on files a human branch is changing, and
|
|
125
|
+
raise anything about it on that pull request instead.
|
|
126
|
+
|
|
127
|
+
It exists because nothing else tells them. Without it, a person's long-running branch is
|
|
128
|
+
invisible, and the staff open pull requests and issues chasing the same files, which the branch
|
|
129
|
+
then overtakes.
|
|
130
|
+
|
|
131
|
+
The list is a value, never a template. Titles are a person's words, and a `{{` in one must not
|
|
132
|
+
be able to break composition, so `compose.mjs` reads `.roster-run/inflight.md` and substitutes it
|
|
133
|
+
as it stands. Locally there is no such file, so `roster prompt` leaves the section out; pass
|
|
134
|
+
`--inflight` to fetch the real list through your own `gh`.
|
|
135
|
+
|
|
115
136
|
## Guarding
|
|
116
137
|
|
|
117
138
|
`{{staff.product}}` is null for a staff member with an empty `works_in`, and an unresolved
|
package/docs/security.md
CHANGED
|
@@ -45,6 +45,31 @@ whole organisation can reach anything in it.
|
|
|
45
45
|
Install narrowly. The Staff card's **GitHub App** panel prints the list it actually needs, and
|
|
46
46
|
so does `roster app`.
|
|
47
47
|
|
|
48
|
+
## The review gate
|
|
49
|
+
|
|
50
|
+
"Nothing goes out unread" is enforced by GitHub, not by the prompt. An App with
|
|
51
|
+
`contents: write` on a product repo can push to its default branch, and with
|
|
52
|
+
`pull_requests: write` it can merge its own PR, unless a rule on the branch says otherwise.
|
|
53
|
+
|
|
54
|
+
The rule: **the default branch of every product repo requires a pull request with at least one
|
|
55
|
+
approving review, and no staff App is on the list of who may bypass it.** A ruleset or classic
|
|
56
|
+
branch protection both count.
|
|
57
|
+
|
|
58
|
+
`roster doctor` reads it for every `role: product` repo in `org.yaml` and every repo a staff
|
|
59
|
+
member `works_in`, as `review-gate`. It fails when nothing requires a PR or a staff App can
|
|
60
|
+
bypass the rule, and warns when a PR is required with no approval, since then whoever opened
|
|
61
|
+
it can merge it.
|
|
62
|
+
|
|
63
|
+
`roster hire --apply` adds a ruleset named `roster: review before merge` to each product repo
|
|
64
|
+
that does not already require an approving review, and leaves anything stricter alone. Pass
|
|
65
|
+
`--no-review-gate` to skip it. The ruleset lets repository admins bypass it only by merging a
|
|
66
|
+
pull request, so your own merge is still the approval (GitHub will not let you approve your
|
|
67
|
+
own PR) and an App, which is never an admin, cannot merge at all.
|
|
68
|
+
|
|
69
|
+
To set it by hand: repo **Settings -> Rules -> Rulesets -> New branch ruleset**, target the
|
|
70
|
+
default branch, tick **Require a pull request before merging** with one required approval, and
|
|
71
|
+
keep the staff Apps off the bypass list.
|
|
72
|
+
|
|
48
73
|
## Permissions a new App asks for
|
|
49
74
|
|
|
50
75
|
```
|
|
@@ -106,13 +131,12 @@ into a world-readable log.
|
|
|
106
131
|
|
|
107
132
|
So a mention on a public pull request is decoration. It posts, it renders as a chip, and
|
|
108
133
|
nothing happens, which is safe and also invisible. The portal is what makes it visible and what
|
|
109
|
-
gets you out of it: replying with an `@handle` where nothing listens says so, and
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
gets you out of it: replying with an `@handle` where nothing listens says so, and offers to
|
|
135
|
+
open the request on that person's own private tracker as well, carrying the pull request and
|
|
136
|
+
the hunk. It writes through your own `gh`, as you, so no credential lives on the
|
|
112
137
|
public repo and nothing is dispatched across a boundary.
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
see [concepts](concepts.md#kinds-of-run). If you reinstate one, its author gate is
|
|
139
|
+
If you add a workflow to a product repo that bridges this automatically, its author gate is
|
|
116
140
|
load-bearing. Do not relax it.
|
|
117
141
|
|
|
118
142
|
## The loop guard
|
package/docs/session-workflow.md
CHANGED
|
@@ -14,8 +14,8 @@ A caller is about forty lines and does nothing but pass arguments.
|
|
|
14
14
|
## Why it lives in the tenant
|
|
15
15
|
|
|
16
16
|
A reusable workflow in a **private** repo can only be called from inside its own organisation.
|
|
17
|
-
A tenant therefore cannot call the framework's copy. That constraint
|
|
18
|
-
design, and it
|
|
17
|
+
A tenant therefore cannot call the framework's copy. That constraint shapes the whole
|
|
18
|
+
design, and it is the better one anyway: the framework is never a runtime dependency, so nothing
|
|
19
19
|
breaks if it moves, goes private, or is deleted.
|
|
20
20
|
|
|
21
21
|
This is also why `roster-ops` needs **Settings -> Actions -> General -> accessible from
|
|
@@ -28,7 +28,7 @@ repositories in this organisation**. Without it, callers fail with "workflow not
|
|
|
28
28
|
| `staff` | string | required | Handle, as in `org.yaml`. |
|
|
29
29
|
| `kind` | string | `daily` | `daily` or `mention`. |
|
|
30
30
|
| `ops_repo` | string | required | `owner/name` of the ops repo. |
|
|
31
|
-
| `model` | string | `claude-opus-5` | Passed to the agent, unless the agent resolves its own. |
|
|
31
|
+
| `model` | string | `claude-opus-5-5` | Passed to the agent, unless the agent resolves its own. |
|
|
32
32
|
| `timeout_minutes` | number | `90` | Job ceiling. |
|
|
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. |
|
|
@@ -51,23 +51,54 @@ authentication error forty lines into a log.
|
|
|
51
51
|
|
|
52
52
|
## What it does, in order
|
|
53
53
|
|
|
54
|
-
1. **
|
|
55
|
-
2. **Mint the
|
|
56
|
-
3. **
|
|
54
|
+
1. **Start the clock**, for the run record.
|
|
55
|
+
2. **Mint the private-tracker token** from the staff member's App.
|
|
56
|
+
3. **Mint the public-repo token**, if a public App was passed.
|
|
57
|
+
4. **React to the request** with eyes, on a `mention` only. Before any checkout, so it lands in
|
|
57
58
|
seconds. `continue-on-error`: a missing reaction must never cost the answer.
|
|
58
|
-
|
|
59
|
+
5. **Check out the ops repo.** It is the only thing that can be cloned without having read a
|
|
59
60
|
manifest, so it goes first and then says what else to clone.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
10. **
|
|
66
|
-
11. **
|
|
67
|
-
12. **
|
|
68
|
-
13. **
|
|
61
|
+
6. **Work out what to check out**, by running `runner-plan.mjs`.
|
|
62
|
+
7. **Check out the brain**, full history. The agent reads its own past.
|
|
63
|
+
8. **Check out peers and product repos**, per the plan.
|
|
64
|
+
9. **Gather human work in flight**: open pull requests people have on the product repos, via
|
|
65
|
+
`inflight.mjs`, for the prompt. Never fatal. See [prompts](prompts.md#human-work-in-flight).
|
|
66
|
+
10. **Set git identity** to the App.
|
|
67
|
+
11. **Set up Node and pnpm**, if the plan found a `package.json`.
|
|
68
|
+
12. **Compose the prompt**, to a step output and to `.roster-prompt.txt`.
|
|
69
|
+
13. **Check the agent has a credential.**
|
|
70
|
+
14. **Work out which agent runs this**, by running `agents.mjs`.
|
|
71
|
+
15. **Run the session**, by one of two steps: the Action-based reference runner, or the generic
|
|
69
72
|
CLI one. See [choosing a coding agent](agents.md).
|
|
70
|
-
|
|
73
|
+
16. **Write down the run**, whatever happened: staff, kind, outcome, duration, and turns, cost
|
|
74
|
+
and tokens where the agent reports them. Into the job summary, and kept as an artifact
|
|
75
|
+
called `roster-run`. Never fatal. See [cost](cost.md#what-each-run-cost).
|
|
76
|
+
17. **Say so if the run did not finish.** A comment on the status issue, or on the issue that
|
|
77
|
+
woke a mention, linking the run.
|
|
78
|
+
|
|
79
|
+
## When the failure is the token
|
|
80
|
+
|
|
81
|
+
The failure notice cannot rely on anything that might be what failed. A renamed repo, a rotated
|
|
82
|
+
key or an uninstalled App breaks the App token first, and an alert that posts with that token
|
|
83
|
+
says nothing at exactly the moment it is needed. So the notice uses the App token when there is
|
|
84
|
+
one, and falls back to the job's own `github.token` when there is not or it is refused. It then
|
|
85
|
+
posts as `github-actions`, and says to check the App.
|
|
86
|
+
|
|
87
|
+
That needs `issues: write` on the job token. `session.yaml` asks for it, but a called workflow
|
|
88
|
+
can only narrow what its caller grants, so both callers grant it too:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
jobs:
|
|
92
|
+
session:
|
|
93
|
+
permissions:
|
|
94
|
+
contents: read
|
|
95
|
+
issues: write
|
|
96
|
+
uses: acme/roster-ops/.github/workflows/session.yaml@main
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Callers generated before this lack it, and their fallback cannot post. `roster upgrade --apply`
|
|
100
|
+
regenerates them. `roster doctor` separately reports any other workflow in the ops or brain
|
|
101
|
+
repos that has failed run after run, as `workflows.failing`.
|
|
71
102
|
|
|
72
103
|
## What `runner-plan.mjs` emits
|
|
73
104
|
|
|
@@ -92,6 +123,7 @@ Consumed by later steps as `steps.plan.outputs.*`.
|
|
|
92
123
|
| `GH_TOKEN` | private-tracker token, already authenticated |
|
|
93
124
|
| `PUBLIC_TOKEN` | public product repo token |
|
|
94
125
|
| `AGENT_PROMPT_FILE` | absolute path to the composed prompt |
|
|
126
|
+
| `AGENT_RESULT_FILE` | where to write the agent's own result JSON, if it has one. Optional; it is how cost gets into the run record |
|
|
95
127
|
| `AGENT_MODEL` | resolved model |
|
|
96
128
|
| `AGENT_TOOLS` | the `allowed_tools` string |
|
|
97
129
|
| *the agent's own* | its credential, under whatever name it declares |
|
package/docs/staff-yaml.md
CHANGED
|
@@ -22,13 +22,13 @@ brain: acme/technology
|
|
|
22
22
|
status_issue: 15
|
|
23
23
|
|
|
24
24
|
schedule: "0 7 * * 1-5"
|
|
25
|
-
model: claude-opus-5
|
|
25
|
+
model: claude-opus-5-5
|
|
26
26
|
timeout_minutes: 90
|
|
27
27
|
mention_timeout_minutes: 90
|
|
28
28
|
|
|
29
29
|
bot: acme-cto[bot]
|
|
30
30
|
public_bot: acme-robot[bot]
|
|
31
|
-
public_token_env:
|
|
31
|
+
public_token_env: PUBLIC_TOKEN
|
|
32
32
|
agent_secret: CLAUDE_CODE_OAUTH_TOKEN
|
|
33
33
|
|
|
34
34
|
identities:
|
|
@@ -154,6 +154,17 @@ Labels this staff member expects to exist on its own tracker, grouped for readab
|
|
|
154
154
|
value across every group is checked by `roster doctor`. An agent applying a label that does not
|
|
155
155
|
exist gets an API error mid-run.
|
|
156
156
|
|
|
157
|
+
## `memory`
|
|
158
|
+
|
|
159
|
+
This staff member's own memory budgets, overriding the org's. Same three fields as
|
|
160
|
+
[`memory` in org.yaml](org-yaml.md#memory); any left out fall back to the org, then to the
|
|
161
|
+
defaults.
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
memory:
|
|
165
|
+
max_index_kb: 32
|
|
166
|
+
```
|
|
167
|
+
|
|
157
168
|
## What `roster upgrade` does to this file
|
|
158
169
|
|
|
159
170
|
Nothing. It is `scaffold` class: written once by `roster hire`, and yours from that moment.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -22,8 +22,10 @@ a coding agent, split into what an agent can fix and what only a person can. `ro
|
|
|
22
22
|
|
|
23
23
|
**Is:** the ops repo's Actions access is not set to organisation-wide.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
`roster init --apply` sets it, and so does *Set it for me* on the setup screen. If GitHub
|
|
26
|
+
refused (it needs admin on the ops repo), set it by hand: Settings -> Actions -> General ->
|
|
27
|
+
Access on `roster-ops`, "accessible from repositories in the organisation". Health and `roster
|
|
28
|
+
doctor` both check it explicitly.
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
@@ -90,11 +92,9 @@ constrains it is not a thing anybody wants.
|
|
|
90
92
|
**Is:** you edited a framework-owned file. `compose.mjs`, `agents.mjs`, `runner-plan.mjs` and
|
|
91
93
|
`session.yaml` are generated. The next `roster upgrade` reconciles them against the template.
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`roster upgrade` now reports an edit to a framework-owned file whether or not anything has
|
|
97
|
-
collided, and `roster upgrade --check` fails on it. Move the change upstream.
|
|
95
|
+
Nothing notices until the framework next touches that file. `roster upgrade` reports an edit
|
|
96
|
+
to a framework-owned file whether or not anything has collided, and `roster upgrade --check`
|
|
97
|
+
fails on it. Move the change upstream.
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -181,7 +181,7 @@ landed on GitHub thirty seconds ago and the checkout is behind, that is what you
|
|
|
181
181
|
|
|
182
182
|
## `roster upgrade` says a file has no base
|
|
183
183
|
|
|
184
|
-
A tenant
|
|
184
|
+
A tenant with no recorded merge base has nothing to merge against. Reconstruct
|
|
185
185
|
one from the framework's history:
|
|
186
186
|
|
|
187
187
|
```bash
|
package/docs/upgrading.md
CHANGED
|
@@ -53,6 +53,12 @@ The diff is printed either way, so nothing goes quietly.
|
|
|
53
53
|
If you want a caller to differ, change the thing it is generated from. Timeouts, schedule,
|
|
54
54
|
model and identities all live in `staff.yaml`.
|
|
55
55
|
|
|
56
|
+
**A caller the framework no longer generates is removed.** It would still dispatch into
|
|
57
|
+
`session.yaml` with a kind that no longer composes, and fail at run time. The plan lists it.
|
|
58
|
+
The one that has gone so far is `<handle>-pr-mention.yaml`; if your tenant is old enough to
|
|
59
|
+
have one, a forwarding workflow in the product repo went with it, and that one is yours to
|
|
60
|
+
delete, because `roster upgrade` never writes into product repos.
|
|
61
|
+
|
|
56
62
|
## Conflicts
|
|
57
63
|
|
|
58
64
|
A conflict is never written into a live file. Agents read `org/voice.md` at every boot, and
|
|
@@ -28,6 +28,13 @@ layer. That is the part that matters most, because without them the model writes
|
|
|
28
28
|
of whoever it was shown. The brief then interviews you, drafts from your answers, and tells you
|
|
29
29
|
what it cut and why.
|
|
30
30
|
|
|
31
|
+
**So is a worked example, when one fits.** A staff member whose handle or role reads as a CTO, a
|
|
32
|
+
CMO or support gets the matching [example](#worked-examples) inside the brief, labelled as a
|
|
33
|
+
model for the shape and not content to copy. The copy-a-prompt panel has a picker to choose
|
|
34
|
+
another or none; in a terminal it is `--example cto|cmo|support|none`. It is still a brief you
|
|
35
|
+
answer: the interview comes first, and nothing in the charter should come from the example
|
|
36
|
+
rather than from you.
|
|
37
|
+
|
|
31
38
|
From a terminal, the same brief:
|
|
32
39
|
|
|
33
40
|
```bash
|
|
@@ -63,6 +70,14 @@ touches. Be specific. A vague boundary is one that gets crossed at 07:00 with no
|
|
|
63
70
|
**Where the rest of it lives.** Point at `memory/INDEX.md`, `log/decisions.md`, the pinned
|
|
64
71
|
status issue, and the surfaces the manifest declares.
|
|
65
72
|
|
|
73
|
+
## Worked examples
|
|
74
|
+
|
|
75
|
+
Three, for an invented company called Acme: a [CTO](charters/cto.md), a [CMO](charters/cmo.md)
|
|
76
|
+
and a [Head of Support](charters/support.md). They are examples to adapt, not templates to fill
|
|
77
|
+
in. Read them for what a finished charter covers and how specific it gets, then write your own
|
|
78
|
+
about your business. A charter copied from one of these describes Acme. The brief carries the matching one for
|
|
79
|
+
you; these links are for reading them first.
|
|
80
|
+
|
|
66
81
|
## Things worth being concrete about
|
|
67
82
|
|
|
68
83
|
- **Escalation.** Name the label and the mechanism, not the sentiment. "Open an issue labelled
|
package/package.json
CHANGED
|
@@ -18,6 +18,12 @@ concurrency:
|
|
|
18
18
|
|
|
19
19
|
jobs:
|
|
20
20
|
session:
|
|
21
|
+
# The ceiling on what session.yaml's job token may do: reading the checkout, and the one
|
|
22
|
+
# comment that says a run failed when the App that would normally say so is what broke.
|
|
23
|
+
# A called workflow cannot raise these, so they have to be granted here.
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
issues: write
|
|
21
27
|
uses: %%OPS_REPO%%/.github/workflows/session.yaml@main
|
|
22
28
|
with:
|
|
23
29
|
staff: %%STAFF%%
|
|
@@ -53,6 +53,12 @@ jobs:
|
|
|
53
53
|
(github.event_name == 'issues' &&
|
|
54
54
|
contains(github.event.issue.body, '%%MENTION%%'))
|
|
55
55
|
)
|
|
56
|
+
# The ceiling on what session.yaml's job token may do: reading the checkout, and the one
|
|
57
|
+
# comment that says a run failed when the App that would normally say so is what broke.
|
|
58
|
+
# A called workflow cannot raise these, so they have to be granted here.
|
|
59
|
+
permissions:
|
|
60
|
+
contents: read
|
|
61
|
+
issues: write
|
|
56
62
|
uses: %%OPS_REPO%%/.github/workflows/session.yaml@main
|
|
57
63
|
with:
|
|
58
64
|
staff: %%STAFF%%
|
|
@@ -11,10 +11,10 @@ between %%MENTION%% and everyone else.
|
|
|
11
11
|
Write it before the first unattended run. A generated charter would produce a generic agent,
|
|
12
12
|
which is the failure this whole arrangement exists to avoid.
|
|
13
13
|
|
|
14
|
-
Write it with your own AI
|
|
14
|
+
Write it with your own AI. This prints a brief to paste into whichever agent you use (in
|
|
15
|
+
Claude Code it is also /charter, from inside this repo):
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
/charter
|
|
17
|
+
roster brief charter %%STAFF%%
|
|
18
18
|
|
|
19
19
|
Or write it by hand. The headings below are the shape that has worked; the words are yours.
|
|
20
20
|
|
|
@@ -10,6 +10,7 @@ on, and has decided.
|
|
|
10
10
|
| `memory/INDEX.md` | One line per fact, read at every boot. |
|
|
11
11
|
| `memory/notes/` | The argument behind a fact, read on demand. |
|
|
12
12
|
| `log/decisions.md` | Why things were decided. Not boot context. |
|
|
13
|
+
| `strategy/` | Longer role documents, and `ideas.md`: ideas parked here rather than filed as issues. |
|
|
13
14
|
| `.github/workflows/` | Three callers. The body lives in `%%OPS_REPO%%`. |
|
|
14
15
|
|
|
15
16
|
Scheduled runs and mentions are wired up by roster. To see what this staff member is actually
|
|
@@ -4,3 +4,6 @@ Why things were decided, newest first. Not boot context: this is read when a dec
|
|
|
4
4
|
being revisited, not every morning.
|
|
5
5
|
|
|
6
6
|
One entry per decision. What was decided, why, and what would change it back.
|
|
7
|
+
|
|
8
|
+
This file holds the current month. Move anything older into `log/decisions/<YYYY-MM>.md`:
|
|
9
|
+
`roster lint` warns past 24KB.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Ideas
|
|
2
|
+
|
|
3
|
+
Speculative ideas, one line each, parked here rather than filed as issues. An issue is for
|
|
4
|
+
something that needs a ruling; an idea that does not yet is noise on the tracker.
|
|
5
|
+
|
|
6
|
+
Promote one to an issue when it serves a priority in `org/priorities.md` and needs a ruling.
|
|
7
|
+
Delete one when it stops being interesting.
|
|
@@ -25,7 +25,7 @@ on:
|
|
|
25
25
|
type: string
|
|
26
26
|
model:
|
|
27
27
|
required: false
|
|
28
|
-
default: claude-opus-5
|
|
28
|
+
default: claude-opus-5-5
|
|
29
29
|
type: string
|
|
30
30
|
timeout_minutes:
|
|
31
31
|
required: false
|
|
@@ -69,11 +69,22 @@ jobs:
|
|
|
69
69
|
runs-on: ubuntu-latest
|
|
70
70
|
timeout-minutes: ${{ inputs.timeout_minutes }}
|
|
71
71
|
|
|
72
|
+
# `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. A called workflow cannot raise what its caller granted, so
|
|
74
|
+
# the callers ask for the same.
|
|
75
|
+
permissions:
|
|
76
|
+
contents: read
|
|
77
|
+
issues: write
|
|
78
|
+
|
|
72
79
|
# secrets are not usable in a step-level `if`, so the presence check is hoisted here.
|
|
73
80
|
env:
|
|
74
81
|
HAS_PUBLIC_APP: ${{ secrets.PUBLIC_APP_ID != '' }}
|
|
75
82
|
|
|
76
83
|
steps:
|
|
84
|
+
# For the run record at the end. A job has no start time of its own to read back.
|
|
85
|
+
- name: Start the clock
|
|
86
|
+
run: echo "ROSTER_STARTED=$(date +%s)" >> "$GITHUB_ENV"
|
|
87
|
+
|
|
77
88
|
- name: Mint the private-tracker token
|
|
78
89
|
id: private
|
|
79
90
|
uses: actions/create-github-app-token@v2
|
|
@@ -161,6 +172,36 @@ jobs:
|
|
|
161
172
|
echo "product: $repo -> $dir"
|
|
162
173
|
done
|
|
163
174
|
|
|
175
|
+
# What people have open on the product repos, so the agent does not open competing work
|
|
176
|
+
# on files a human branch is rewriting. Read by "Compose the prompt" below; never
|
|
177
|
+
# fatal, because without it the prompt just has no section about it.
|
|
178
|
+
- name: Gather human work in flight
|
|
179
|
+
if: steps.plan.outputs.products != ''
|
|
180
|
+
continue-on-error: true
|
|
181
|
+
env:
|
|
182
|
+
GH_TOKEN: ${{ steps.public.outputs.token || steps.private.outputs.token }}
|
|
183
|
+
run: node roster-ops/inflight.mjs --staff "${{ inputs.staff }}" --ops roster-ops --brains . --out .roster-run/inflight.md
|
|
184
|
+
|
|
185
|
+
# staff.yaml names the variable its prompts use for the public token (`public_token_env`),
|
|
186
|
+
# and an `env:` key cannot be an expression, so the name is exported here for every later
|
|
187
|
+
# step. PUBLIC_TOKEN is always set as well; this only adds the name the manifest chose.
|
|
188
|
+
- name: Export the public token under its manifest name
|
|
189
|
+
if: steps.public.outputs.token != ''
|
|
190
|
+
env:
|
|
191
|
+
BRAIN_DIR: ${{ steps.plan.outputs.brain_dir }}
|
|
192
|
+
TOKEN: ${{ steps.public.outputs.token }}
|
|
193
|
+
run: |
|
|
194
|
+
set -euo pipefail
|
|
195
|
+
name=$(sed -n 's/^public_token_env:[[:space:]]*//p' "$BRAIN_DIR/staff.yaml" | head -1 | tr -d "\"' ")
|
|
196
|
+
case "$name" in
|
|
197
|
+
""|PUBLIC_TOKEN|GH_TOKEN|GITHUB_TOKEN) exit 0 ;;
|
|
198
|
+
esac
|
|
199
|
+
if ! [[ "$name" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then
|
|
200
|
+
echo "::warning::public_token_env '$name' is not a variable name; only PUBLIC_TOKEN is set"
|
|
201
|
+
exit 0
|
|
202
|
+
fi
|
|
203
|
+
echo "$name=$TOKEN" >> "$GITHUB_ENV"
|
|
204
|
+
|
|
164
205
|
# Commits read as the bot, not as a human, so the git history stays legible.
|
|
165
206
|
- name: Set git identity
|
|
166
207
|
env:
|
|
@@ -236,13 +277,12 @@ jobs:
|
|
|
236
277
|
# `uses:` cannot be an expression, so an Action-based runner has to be written out
|
|
237
278
|
# literally. This is the reference one; every other agent goes through the step below.
|
|
238
279
|
- name: Run the session
|
|
280
|
+
id: session_action
|
|
239
281
|
if: steps.agent.outputs.kind == 'action'
|
|
240
282
|
uses: anthropics/claude-code-action@v1
|
|
241
283
|
env:
|
|
242
284
|
GH_TOKEN: ${{ steps.private.outputs.token }}
|
|
243
285
|
PUBLIC_TOKEN: ${{ steps.public.outputs.token }}
|
|
244
|
-
# Kept as an alias while charters and memory still name it. Retire once they do not.
|
|
245
|
-
PIPWEB_TOKEN: ${{ steps.public.outputs.token }}
|
|
246
286
|
with:
|
|
247
287
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN || secrets.AGENT_TOKEN }}
|
|
248
288
|
# These repos are private and single-user, so the usual reason to hide a run's output does
|
|
@@ -264,11 +304,11 @@ jobs:
|
|
|
264
304
|
# argument: it is thousands of words containing quotes and backticks, and argv limits and
|
|
265
305
|
# shell quoting fail at 07:00 rather than in review.
|
|
266
306
|
- name: Run the session
|
|
307
|
+
id: session_cli
|
|
267
308
|
if: steps.agent.outputs.kind == 'cli'
|
|
268
309
|
env:
|
|
269
310
|
GH_TOKEN: ${{ steps.private.outputs.token }}
|
|
270
311
|
PUBLIC_TOKEN: ${{ steps.public.outputs.token }}
|
|
271
|
-
PIPWEB_TOKEN: ${{ steps.public.outputs.token }}
|
|
272
312
|
AGENT_MODEL: ${{ steps.agent.outputs.model || inputs.model }}
|
|
273
313
|
AGENT_FLAGS: ${{ steps.agent.outputs.flags }}
|
|
274
314
|
# Kept for a custom `run` written before permissions existed.
|
|
@@ -289,6 +329,10 @@ jobs:
|
|
|
289
329
|
# an `env:` key cannot be an expression.
|
|
290
330
|
export "$TOKEN_ENV=$TOKEN"
|
|
291
331
|
export AGENT_PROMPT_FILE="$PWD/.roster-prompt.txt"
|
|
332
|
+
# Where an agent that can report its own turns and cost writes them. Optional: the
|
|
333
|
+
# run record reads it if it is there and records what it cannot know as unknown.
|
|
334
|
+
mkdir -p .roster-run
|
|
335
|
+
export AGENT_RESULT_FILE="$PWD/.roster-run/agent-result.json"
|
|
292
336
|
|
|
293
337
|
if [ -n "$INSTALL" ]; then
|
|
294
338
|
echo "::group::install ${{ steps.agent.outputs.id }}"
|
|
@@ -297,20 +341,70 @@ jobs:
|
|
|
297
341
|
fi
|
|
298
342
|
eval "$RUN"
|
|
299
343
|
|
|
300
|
-
#
|
|
301
|
-
#
|
|
344
|
+
# What this run was and what it cost, in the job summary and as an artifact with a stable
|
|
345
|
+
# name, which is what the portal's Runs screen and `roster doctor` read back. Never fatal:
|
|
346
|
+
# a missing record costs a row in a table, and failing the job over it would cost the run.
|
|
347
|
+
- name: Write down the run
|
|
348
|
+
if: always()
|
|
349
|
+
continue-on-error: true
|
|
350
|
+
env:
|
|
351
|
+
STAFF: ${{ inputs.staff }}
|
|
352
|
+
KIND: ${{ inputs.kind }}
|
|
353
|
+
AGENT_ID: ${{ steps.agent.outputs.id }}
|
|
354
|
+
MODEL: ${{ steps.agent.outputs.model || inputs.model }}
|
|
355
|
+
AGENT_OUTCOME: ${{ steps.session_action.outcome != 'skipped' && steps.session_action.outcome || steps.session_cli.outcome }}
|
|
356
|
+
JOB_STATUS: ${{ job.status }}
|
|
357
|
+
RESULT_FILE: ${{ steps.session_action.outputs.execution_file || format('{0}/.roster-run/agent-result.json', github.workspace) }}
|
|
358
|
+
run: node roster-ops/run-record.mjs --out .roster-run/run.json
|
|
359
|
+
|
|
360
|
+
- name: Keep the run record
|
|
361
|
+
if: always()
|
|
362
|
+
continue-on-error: true
|
|
363
|
+
uses: actions/upload-artifact@v6
|
|
364
|
+
with:
|
|
365
|
+
name: roster-run
|
|
366
|
+
path: .roster-run/run.json
|
|
367
|
+
if-no-files-found: ignore
|
|
368
|
+
|
|
369
|
+
# A failed unattended run is otherwise a red X in a tab nobody opens, so every failure path
|
|
370
|
+
# has to end somewhere a human reads.
|
|
371
|
+
#
|
|
372
|
+
# It cannot lean on anything that might be what failed. The App token is the first thing a
|
|
373
|
+
# renamed repo, a rotated key or an uninstalled App breaks, and a canary sat red for twelve
|
|
374
|
+
# days because its alert used exactly that token. So: the App token when there is one, and
|
|
375
|
+
# this job's own token when there is not or it is refused. The plan may not have run
|
|
376
|
+
# either, so the brain repo falls back to the caller's, which is the same repo, and
|
|
377
|
+
# staff.yaml is read over the API when it was never checked out.
|
|
302
378
|
- name: Say so if the run did not finish
|
|
303
379
|
if: failure() || cancelled()
|
|
304
380
|
env:
|
|
305
|
-
|
|
381
|
+
APP_TOKEN: ${{ steps.private.outputs.token }}
|
|
382
|
+
JOB_TOKEN: ${{ github.token }}
|
|
306
383
|
BRAIN_DIR: ${{ steps.plan.outputs.brain_dir }}
|
|
307
|
-
BRAIN_REPO: ${{ steps.plan.outputs.brain_repo }}
|
|
384
|
+
BRAIN_REPO: ${{ steps.plan.outputs.brain_repo || github.repository }}
|
|
385
|
+
ISSUE: ${{ inputs.issue_number }}
|
|
386
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
308
387
|
run: |
|
|
309
|
-
set -
|
|
310
|
-
issue="$
|
|
388
|
+
set -uo pipefail
|
|
389
|
+
issue="$ISSUE"
|
|
390
|
+
if [ -z "$issue" ] && [ -n "$BRAIN_DIR" ] && [ -f "$BRAIN_DIR/staff.yaml" ]; then
|
|
391
|
+
issue=$(sed -n 's/^status_issue:[[:space:]]*//p' "$BRAIN_DIR/staff.yaml" | head -1)
|
|
392
|
+
fi
|
|
393
|
+
if [ -z "$issue" ]; then
|
|
394
|
+
issue=$(GH_TOKEN="${APP_TOKEN:-$JOB_TOKEN}" gh api "repos/$BRAIN_REPO/contents/staff.yaml" \
|
|
395
|
+
-H "Accept: application/vnd.github.raw" 2>/dev/null \
|
|
396
|
+
| sed -n 's/^status_issue:[[:space:]]*//p' | head -1)
|
|
397
|
+
fi
|
|
398
|
+
issue="${issue%%[[:space:]#]*}"
|
|
311
399
|
if [ -z "$issue" ]; then
|
|
312
|
-
|
|
400
|
+
echo "::error::no status_issue in staff.yaml and no issue in the trigger, so nobody was told"
|
|
401
|
+
exit 1
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
body="This run did not finish, so there is no answer coming. [Run ${GITHUB_RUN_ID}]($RUN_URL) has the error."
|
|
405
|
+
if [ -n "$APP_TOKEN" ] && GH_TOKEN="$APP_TOKEN" gh issue comment "$issue" --repo "$BRAIN_REPO" --body "$body"; then
|
|
406
|
+
exit 0
|
|
313
407
|
fi
|
|
314
|
-
|
|
315
|
-
gh issue comment "$issue" --repo "$BRAIN_REPO" --body \
|
|
316
|
-
"
|
|
408
|
+
# Posted as github-actions rather than as the staff member, which is itself the clue.
|
|
409
|
+
GH_TOKEN="$JOB_TOKEN" gh issue comment "$issue" --repo "$BRAIN_REPO" --body \
|
|
410
|
+
"$body The staff member's own App could not post it, so check the App is installed on this repo and its APP_ID and APP_PRIVATE_KEY secrets are current."
|
package/templates/ops/agents.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export const PRESETS = {
|
|
|
43
43
|
"claude-code-action": {
|
|
44
44
|
kind: "action",
|
|
45
45
|
token_env: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
46
|
-
model: "claude-opus-5",
|
|
46
|
+
model: "claude-opus-5-5",
|
|
47
47
|
permissions: CLAUDE_TOOLS,
|
|
48
48
|
option: (k, v) => `--${k} ${shellArg(v)}`,
|
|
49
49
|
},
|
|
@@ -52,9 +52,13 @@ export const PRESETS = {
|
|
|
52
52
|
claude: {
|
|
53
53
|
kind: "cli",
|
|
54
54
|
install: "npm install -g @anthropic-ai/claude-code",
|
|
55
|
-
run
|
|
55
|
+
// JSON rather than text so the run record can read turns and cost off it. The log still
|
|
56
|
+
// carries the answer, inside the `result` field.
|
|
57
|
+
run:
|
|
58
|
+
'claude -p --model "$AGENT_MODEL" $AGENT_FLAGS --output-format json < "$AGENT_PROMPT_FILE"' +
|
|
59
|
+
' | tee "$AGENT_RESULT_FILE"',
|
|
56
60
|
token_env: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
57
|
-
model: "claude-opus-5",
|
|
61
|
+
model: "claude-opus-5-5",
|
|
58
62
|
permissions: CLAUDE_TOOLS,
|
|
59
63
|
option: (k, v) => `--${k} ${shellArg(v)}`,
|
|
60
64
|
},
|