@nanocollective/roster 0.1.0-alpha.43 → 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.
@@ -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).
@@ -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, setup, build, blocked]
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.43",
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. It is a task, not a session: no boot ritual, no handoff, and the reply
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.event_name == 'issue_comment' &&
51
- contains(fromJSON('%%HUMAN_LOGINS%%'), github.event.comment.user.login) &&
52
- contains(github.event.comment.body, '%%MENTION%%')) ||
53
- (github.event_name == 'issues' &&
54
- contains(github.event.issue.body, '%%MENTION%%'))
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, setup, build, blocked]
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. A called workflow cannot raise what its caller granted, so
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 **the default if they say nothing**. They are ruling from a phone.
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 your own issues** when the work is done or superseded, with one line naming what closed
35
- it: the PR, the commit, or the issue that replaced it. Do not leave one open "in case".
36
- - **Sweep them on every daily run.** Read every open issue you opened; close what is finished or
37
- stale, and fold duplicates into one.
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
- - **Never close a `decision` issue**, even in a sweep. Those are {{human.name}}'s rulings to close.
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}}, and he needs to be able to
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}}, labelled by owner
63
- plus kind, assigned to `{{human.github}}`. Sweep every open issue you opened: close what is done
64
- or superseded, citing what closed it. **Comments and replies get the same concision as everything
65
- else:** what changed and what it means for them. A comment that only says an issue is still open
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
 
@@ -1,5 +1,16 @@
1
+ {{#if event.from_human}}
1
2
  You are **{{staff.name}}** at {{org.name}}. {{human.name}} has asked you something directly, on
2
3
  your tracker. **Your reply in that thread is the only thing they will see.**
4
+ {{/if}}
5
+ {{#if event.from_peer}}
6
+ You are **{{staff.name}}** at {{org.name}}. Another staff member has filed something on your
7
+ tracker, and it woke you. **Reply in that thread**; they read it on their next run, and
8
+ {{human.name}} can see the chain.
9
+
10
+ **Do not file anything on another staff member's tracker in this run.** A peer's ask wakes them,
11
+ so two staff could keep waking each other. Anything you need from someone else goes in your reply
12
+ or in your status issue for your next daily run.
13
+ {{/if}}
3
14
 
4
15
  **This is not a session.** No boot ritual, no handoff, no rewriting #{{staff.status_issue}}. Answer
5
16
  the question or do the small thing asked, reply, stop.
@@ -59,6 +70,14 @@ Answer where the request came from, so the conversation stays readable. **Do not
59
70
  for the answer** - they are already reading this one. **Do not @-mention them**; they are subscribed
60
71
  to a thread they are in.
61
72
 
73
+ **Then close the issue if nothing is left to do on it:** the ask is done, or it needed nothing and
74
+ your reply says why. Leave it open when your reply asks them something, when it is labelled
75
+ `keep-open`, or when it is your pinned status issue #{{staff.status_issue}}.
76
+
77
+ ```
78
+ gh issue close {{event.issue_number}} --repo {{event.repo}}
79
+ ```
80
+
62
81
  {{> org/guardrails.md}}
63
82
 
64
83
  {{> org/voice.md}}
@@ -0,0 +1,57 @@
1
+ /* Home: what needs you, who is working, your requests, closed today. */
2
+
3
+ .home{max-width:820px}
4
+ .home h2{font:600 13px var(--sans);color:var(--ink-dim);text-transform:none;letter-spacing:0;
5
+ margin:26px 0 10px;display:flex;align-items:center;gap:8px}
6
+ .home h2 .pill{font-size:10.5px}
7
+ .homeempty{color:var(--ink-faint);font-size:13px;margin:0 0 8px}
8
+
9
+ .homeaskcard{display:flex;flex-direction:column;gap:10px}
10
+ .homeask,.homeanswer{width:100%;box-sizing:border-box;resize:vertical;min-height:44px;
11
+ font:13px/1.45 var(--sans);color:var(--ink);background:var(--surface-2);
12
+ border:1px solid transparent;border-radius:8px;padding:8px 10px}
13
+ .homeask:focus,.homeanswer:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
14
+ .homeanswer{margin-top:10px}
15
+ .homewho{font:500 12.5px var(--sans);color:var(--ink);background:var(--surface-2);border:0;
16
+ border-radius:8px;padding:5px 8px}
17
+
18
+ .homecard{padding:14px 16px}
19
+ .homehead{display:flex;align-items:baseline;gap:8px}
20
+ .homehead .chip{flex:none}
21
+ .homeopen{all:unset;cursor:pointer;font:600 14px/1.35 var(--sans);color:var(--ink);letter-spacing:-.01em}
22
+ .homeopen:hover{text-decoration:underline}
23
+ .homeopen:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
24
+ .homecard > .meta{margin-top:4px}
25
+ .homedue{margin:8px 0 0;font-size:12.5px;color:var(--ink-dim)}
26
+ .homedue.late{color:var(--warn)}
27
+ .homeacts{margin-top:10px}
28
+
29
+ .homeworking{display:flex;flex-direction:column;gap:10px;padding:12px 16px}
30
+ .homerun{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:13px}
31
+ .homewho2{display:inline-flex;align-items:center;gap:6px;min-width:190px}
32
+ .homewho2 .ic{--ic:14px;color:var(--ink-dim)}
33
+ .homelive{color:var(--accent);text-decoration:none;font-size:12.5px}
34
+ .homelive::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
35
+ background:var(--accent);margin-right:6px;animation:homepulse 1.6s var(--ease) infinite}
36
+ .homelive:hover{text-decoration:underline}
37
+ @keyframes homepulse{50%{opacity:.35}}
38
+ @media (prefers-reduced-motion:reduce){.homelive::before{animation:none}}
39
+
40
+ .homerow{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:8px 2px;
41
+ border-bottom:1px solid var(--line);font-size:13px}
42
+ .homerow .homeopen{font-weight:500;font-size:13px}
43
+ .homeclosed{display:grid;grid-template-columns:1fr auto;align-items:center;gap:4px 14px}
44
+ .homeclosed .meta{margin-top:3px;line-height:1.5;font-family:var(--sans);font-size:12px}
45
+ .homerest{margin-top:26px}
46
+
47
+ .homereports{display:flex;flex-direction:column;gap:14px;padding:14px 16px}
48
+ .homereport{display:grid;grid-template-columns:190px 1fr;gap:12px;font-size:13px;line-height:1.5}
49
+ .homereport p{margin:0 0 3px;color:var(--ink-dim)}
50
+ .homereport p:first-child{color:var(--ink)}
51
+ .homereport .homewho2{align-items:baseline;flex-wrap:wrap;min-width:0}
52
+ @media (max-width:640px){.homereport{grid-template-columns:1fr;gap:4px}}
53
+
54
+ @media (max-width:640px){
55
+ .homewho2{min-width:0}
56
+ .homehead{flex-wrap:wrap}
57
+ }