@nanocollective/roster 0.1.0-alpha.5 → 0.1.0-alpha.50
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 +70 -84
- package/dist/cli.js +4833 -2752
- package/docs/README.md +9 -6
- package/docs/agents.md +24 -20
- package/docs/architecture.md +13 -5
- package/docs/charters/analyst.md +65 -0
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/community.md +63 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/designer.md +65 -0
- package/docs/charters/devops.md +65 -0
- package/docs/charters/pm.md +70 -0
- package/docs/charters/qa.md +65 -0
- package/docs/charters/support.md +60 -0
- package/docs/charters/writer.md +63 -0
- package/docs/commands.md +93 -7
- package/docs/concepts.md +61 -14
- package/docs/cost.md +36 -1
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +10 -2
- package/docs/export.md +2 -0
- package/docs/extending.md +2 -2
- package/docs/getting-started.md +128 -78
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +94 -123
- package/docs/memory.md +21 -3
- package/docs/org-yaml.md +40 -2
- package/docs/portal.md +165 -48
- package/docs/prompts.md +31 -4
- package/docs/security.md +37 -5
- package/docs/session-workflow.md +63 -17
- package/docs/staff-yaml.md +30 -3
- package/docs/troubleshooting.md +8 -8
- package/docs/upgrading.md +9 -3
- package/docs/writing-a-charter.md +28 -0
- package/package.json +18 -20
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +26 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +41 -7
- 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/staff.yaml +4 -1
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/briefs/priorities.md +46 -0
- package/templates/ops/.github/workflows/session.yaml +236 -15
- package/templates/ops/agents.mjs +7 -3
- package/templates/ops/compose.mjs +31 -3
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +43 -4
- 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 +37 -9
- package/templates/ops/prompts/mention.md +21 -0
- package/templates/ops/run-record.mjs +146 -0
- package/templates/portal/css/base.css +167 -73
- package/templates/portal/css/brain.css +23 -20
- package/templates/portal/css/diff.css +10 -9
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +13 -11
- package/templates/portal/css/home.css +93 -0
- package/templates/portal/css/inbox.css +45 -25
- package/templates/portal/css/layout.css +90 -46
- package/templates/portal/css/markdown.css +36 -14
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +116 -34
- package/templates/portal/index.html +21 -9
- package/templates/portal/js/api.js +44 -4
- package/templates/portal/js/app.js +94 -9
- package/templates/portal/js/dialog.js +83 -0
- package/templates/portal/js/homesort.js +174 -0
- package/templates/portal/js/icons.js +37 -0
- package/templates/portal/js/inflight.js +18 -0
- package/templates/portal/js/md.js +5 -2
- package/templates/portal/js/mdedit.js +84 -0
- package/templates/portal/js/readiness.js +70 -0
- package/templates/portal/js/refresh.js +10 -2
- package/templates/portal/js/state.js +11 -5
- package/templates/portal/js/views/app.js +24 -7
- package/templates/portal/js/views/brain.js +1 -1
- package/templates/portal/js/views/checklist.js +10 -4
- package/templates/portal/js/views/credential.js +84 -0
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/health.js +17 -4
- package/templates/portal/js/views/hire.js +593 -0
- package/templates/portal/js/views/home.js +546 -0
- package/templates/portal/js/views/inbox.js +226 -70
- package/templates/portal/js/views/org.js +46 -106
- package/templates/portal/js/views/orgedit.js +234 -0
- package/templates/portal/js/views/paste.js +87 -21
- package/templates/portal/js/views/prompt.js +11 -4
- package/templates/portal/js/views/repos.js +20 -15
- package/templates/portal/js/views/runonce.js +94 -0
- package/templates/portal/js/views/runs.js +170 -0
- package/templates/portal/js/views/setup.js +261 -75
- package/templates/portal/js/views/staff.js +170 -243
- package/templates/portal/js/views/todo.js +62 -0
|
@@ -9,11 +9,45 @@ 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
|
|
16
17
|
unblocked thing you can find and say so in the report.
|
|
18
|
+
- **Finish inside the run.** Once you stop, the run is over: nothing will wake you, and anything
|
|
19
|
+
left running dies with it. Never end a turn to wait for a command; wait for it in the same call.
|
|
20
|
+
**Push your branch before a slow check** such as a full test suite, so the work survives if the
|
|
21
|
+
run is cut short, and push again once it passes.
|
|
22
|
+
|
|
23
|
+
## Choosing work
|
|
24
|
+
|
|
25
|
+
- **Serve the priorities.** Work that serves none of the ranked priorities in `org/priorities.md`
|
|
26
|
+
waits, unless something is broken. Say which priority a PR serves.
|
|
27
|
+
- **Product before process.** Guards, checks, claim-policing and measuring your own output earn a
|
|
28
|
+
run when they protect something that has shipped. Most runs should move the product forward; if
|
|
29
|
+
your last few went on meta-work, this one does not.
|
|
30
|
+
|
|
31
|
+
## Keeping your tracker clean
|
|
32
|
+
|
|
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.
|
|
48
|
+
- **Ideas live in your brain, not on the tracker.** Park a speculative idea as one line in
|
|
49
|
+
`strategy/ideas.md`. Open an `IDEA:` issue only when it needs a ruling, and never more than one
|
|
50
|
+
at a time.
|
|
17
51
|
|
|
18
52
|
## What you may not do
|
|
19
53
|
|
|
@@ -21,7 +55,9 @@ question instead of doing work has wasted its slot.
|
|
|
21
55
|
mechanical rather than a promise: protected branches mean you open a PR and their merge is the
|
|
22
56
|
approval. **Do not look for a way around it.** Being unable to ship unreviewed is what earns the
|
|
23
57
|
autonomy.
|
|
24
|
-
- **
|
|
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.
|
|
25
61
|
- **Never `git add -A` in another staff member's repo, or in a repo where a human may have work in
|
|
26
62
|
flight.** Stage explicit paths. Doing otherwise has swept someone else's uncommitted work into an
|
|
27
63
|
unrelated commit.
|
|
@@ -45,6 +81,8 @@ overhead.**
|
|
|
45
81
|
`log/decisions.md`. A memory that only grows is a memory nobody reads.
|
|
46
82
|
- Mark every fact with where it came from: `[{{human.marker}}]` for a ruling, `[measured]` for
|
|
47
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.
|
|
48
86
|
|
|
49
87
|
`log/decisions.md` is **not** boot context. It is the audit trail: read it when you need to know why
|
|
50
88
|
something was decided, or before reversing a call somebody already made.
|
|
@@ -56,9 +94,10 @@ Other staff members are peers, not subordinates and not tools. **Write to them f
|
|
|
56
94
|
team updated is always fine, and over-communicating is the right default.
|
|
57
95
|
|
|
58
96
|
**Comms are issues, not file drops:** open an issue on their tracker labelled `from-{{staff.handle}}`.
|
|
59
|
-
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
|
|
60
98
|
read the whole conversation in one place. Genuinely long-form output can still be a file, with the
|
|
61
|
-
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.
|
|
62
101
|
|
|
63
102
|
**An ask of a peer stays an ask.** They own their own priorities. Anything that needs
|
|
64
103
|
{{human.name}}'s money or public sign-off still goes through them.
|
|
@@ -12,6 +12,15 @@ bodies, run reports, briefs to other staff, and how you talk to them in a sessio
|
|
|
12
12
|
- **An issue title is the ask, not the topic.**
|
|
13
13
|
- **Say the default** on anything needing a ruling: what you do if they say nothing.
|
|
14
14
|
|
|
15
|
+
**Length ceilings.** Lead with the outcome or the ask, then stop at:
|
|
16
|
+
|
|
17
|
+
- **A comment or reply: 100 words.** Most need three lines.
|
|
18
|
+
- **An issue or PR body: 200 words.**
|
|
19
|
+
- **The pinned status issue: 300 words**, readable on one phone screen.
|
|
20
|
+
|
|
21
|
+
Past the ceiling, the detail goes in a file in your brain and the comment links to it in one
|
|
22
|
+
line. A status {{human.name}} has to rewrite before they can use it has cost more than it saved.
|
|
23
|
+
|
|
15
24
|
**Cut on sight:**
|
|
16
25
|
|
|
17
26
|
- Context they already have. They founded this; it does not need explaining to them.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{{#if inflight}}
|
|
2
|
+
## Human work in flight
|
|
3
|
+
|
|
4
|
+
People have these pull requests open on the product repos, and each one is theirs. A long-lived
|
|
5
|
+
branch is usually rewriting what you would otherwise be about to change.
|
|
6
|
+
|
|
7
|
+
- **Do not open competing work on files they touch**: no pull request, and no issue asking for a
|
|
8
|
+
change there. It gets overtaken when their branch lands, and it costs them a review first.
|
|
9
|
+
- **If you have something to say about that work, say it on their pull request**, briefly, and
|
|
10
|
+
leave the decision to them.
|
|
11
|
+
- If today's task sits on those files, say so in your report and take the next thing.
|
|
12
|
+
|
|
13
|
+
{{inflight}}
|
|
14
|
+
{{/if}}
|
|
@@ -5,7 +5,8 @@ repos sit side by side inside it:
|
|
|
5
5
|
|
|
6
6
|
- `{{staff.dir}}/` - your brain. **Start by reading it.**
|
|
7
7
|
- `{{ops.dir}}/` - the org's shared brain: `org/operating.md`, `org/voice.md`, `org/guardrails.md`,
|
|
8
|
-
`org/business.md`. **Read-only to you.** Propose a change as a PR; do not
|
|
8
|
+
`org/business.md`, `org/priorities.md`. **Read-only to you.** Propose a change as a PR; do not
|
|
9
|
+
edit it in place.
|
|
9
10
|
{{#if peers}}
|
|
10
11
|
{{peer_list}}
|
|
11
12
|
{{/if}}
|
|
@@ -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
|
|
@@ -33,11 +38,15 @@ your brain. Reconstitute yourself, do a day's work, hand off.
|
|
|
33
38
|
**Do not read `log/decisions.md` at boot**; it is the audit trail, for when you need to know why
|
|
34
39
|
something was decided.
|
|
35
40
|
|
|
41
|
+
{{> prompts/_inflight.md}}
|
|
42
|
+
{{>? org/priorities.md}}
|
|
43
|
+
|
|
36
44
|
## Then work. Autonomously.
|
|
37
45
|
|
|
38
|
-
Take the top item off #{{staff.status_issue}}'s ordered list
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
Take the top item off #{{staff.status_issue}}'s ordered list that serves the org's priorities
|
|
47
|
+
(`org/priorities.md`, where there is one), unless something above changed the priority, in which
|
|
48
|
+
case say so in your report and do the more urgent thing. **Then actually do it.** You are not
|
|
49
|
+
writing a plan for {{human.name}} to approve.
|
|
41
50
|
|
|
42
51
|
{{> org/operating.md}}
|
|
43
52
|
|
|
@@ -48,22 +57,29 @@ You are not writing a plan for {{human.name}} to approve.
|
|
|
48
57
|
{{#if staff.product}}
|
|
49
58
|
1. **Open the PR** on `{{staff.product.repo}}` if you produced anything there, from a branch:
|
|
50
59
|
`GH_TOKEN=${{staff.public_token_env}} gh pr create --repo {{staff.product.repo}} ...`
|
|
51
|
-
**Body: what it does, what the gate covered, what it did not cover.
|
|
52
|
-
essay, no narration of how you built it. It is reviewed on a phone
|
|
60
|
+
**Body: which priority it serves, what it does, what the gate covered, what it did not cover.
|
|
61
|
+
Nothing else** - no design essay, no narration of how you built it. It is reviewed on a phone
|
|
62
|
+
and the diff is right there.
|
|
53
63
|
{{/if}}
|
|
54
64
|
2. **Rewrite pinned issue #{{staff.status_issue}} "Where we are"**: the situation in a line, what
|
|
55
65
|
this run did, what the next run picks up in priority order. **It is a handover for the next run,
|
|
56
66
|
not a diary.** Rewrite it, do not append, and cut anything the next run can find for itself.
|
|
57
|
-
3. **Reconcile the tracker.** Open issues for anything new needing {{human.name}}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
73
|
is not worth the notification.
|
|
62
74
|
4. **Update `{{staff.dir}}/memory/` only if a fact or watch-out changed.** A new fact is **one line**
|
|
63
75
|
in `INDEX.md` saying what it changes; a corrected fact is **edited in place**, never appended to
|
|
64
76
|
with "updated:". If it needs an argument, that goes in `memory/notes/<slug>.md` and the line stays
|
|
65
77
|
one line. **Delete any line that no longer changes a decision** and say so in the decision log.
|
|
66
78
|
If the run was purely work, touch nothing.
|
|
79
|
+
**Then check the budget:** `wc -c {{staff.dir}}/memory/INDEX.md {{staff.dir}}/log/decisions.md`.
|
|
80
|
+
Over 24KB either, or any fact over 400 characters (unless a `memory:` block in `staff.yaml` or
|
|
81
|
+
`org.yaml` sets other limits), and pruning is this run's job: delete, shorten, move arguments
|
|
82
|
+
to notes, and roll older decisions into `log/decisions/<YYYY-MM>.md`.
|
|
67
83
|
5. **Log real decisions** in `{{staff.dir}}/log/decisions.md`, dated, newest at top, with the why.
|
|
68
84
|
{{#if peers}}
|
|
69
85
|
6. **Write to the other staff** if anything shipped, changed or broke that touches their patch.
|
|
@@ -76,6 +92,18 @@ You are not writing a plan for {{human.name}} to approve.
|
|
|
76
92
|
`@{{human.github}}`. **Three lines: what you did, what is now on them (issue numbers and the ask,
|
|
77
93
|
nothing more), what you would do next.** No preamble, no closing line, no headers. This is the
|
|
78
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}}
|
|
79
107
|
|
|
80
108
|
{{> org/guardrails.md}}
|
|
81
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.
|
|
@@ -35,6 +46,8 @@ An issue opened this way often carries a pull request somewhere else, and says w
|
|
|
35
46
|
|
|
36
47
|
`gh issue view --comments` is broken; use `gh api` as above.
|
|
37
48
|
|
|
49
|
+
{{> prompts/_inflight.md}}
|
|
50
|
+
|
|
38
51
|
## Do the work
|
|
39
52
|
|
|
40
53
|
- **Read `{{staff.dir}}/CHARTER.md` and `{{staff.dir}}/memory/INDEX.md` before acting.** They are
|
|
@@ -57,6 +70,14 @@ Answer where the request came from, so the conversation stays readable. **Do not
|
|
|
57
70
|
for the answer** - they are already reading this one. **Do not @-mention them**; they are subscribed
|
|
58
71
|
to a thread they are in.
|
|
59
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
|
+
|
|
60
81
|
{{> org/guardrails.md}}
|
|
61
82
|
|
|
62
83
|
{{> org/voice.md}}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Writes down what one run was and what it cost, after the agent has finished or failed.
|
|
3
|
+
//
|
|
4
|
+
// Vendored alongside compose.mjs for the same reason: it runs on the runner, and must not depend
|
|
5
|
+
// on npm or on an org the tenant does not control.
|
|
6
|
+
//
|
|
7
|
+
// The record is small and deliberately incomplete. Staff, kind, outcome and duration are always
|
|
8
|
+
// known. Turns, cost and tokens are known only when the agent says so: claude-code-action writes
|
|
9
|
+
// an execution file, the `claude` CLI writes JSON when asked, and anything else may say nothing.
|
|
10
|
+
// An unknown is null, never a guess, because a total built from guesses is worse than none.
|
|
11
|
+
//
|
|
12
|
+
// Usage: node roster-ops/run-record.mjs --out .roster-run/run.json
|
|
13
|
+
// (everything else arrives in the environment; see session.yaml)
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The agent's own account of the run, from whatever it wrote.
|
|
21
|
+
*
|
|
22
|
+
* Three shapes turn up: an array of messages whose last `result` is the summary (the Action's
|
|
23
|
+
* execution file), a single result object (`claude -p --output-format json`), and one message
|
|
24
|
+
* per line (`stream-json`). All three end in the same object, so find that.
|
|
25
|
+
*/
|
|
26
|
+
export function readResult(text) {
|
|
27
|
+
if (!text || !text.trim()) return null;
|
|
28
|
+
let items;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(text);
|
|
31
|
+
items = Array.isArray(parsed) ? parsed : [parsed];
|
|
32
|
+
} catch {
|
|
33
|
+
items = [];
|
|
34
|
+
for (const line of text.split("\n")) {
|
|
35
|
+
try {
|
|
36
|
+
items.push(JSON.parse(line));
|
|
37
|
+
} catch {
|
|
38
|
+
// A line of log output between the JSON is not ours to fail on.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const results = items.filter(
|
|
43
|
+
(m) => m && typeof m === "object" && (m.type === "result" || "total_cost_usd" in m),
|
|
44
|
+
);
|
|
45
|
+
const last = results[results.length - 1];
|
|
46
|
+
if (!last) return null;
|
|
47
|
+
|
|
48
|
+
const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
49
|
+
const u = last.usage ?? {};
|
|
50
|
+
const tokens = {
|
|
51
|
+
input: n(u.input_tokens),
|
|
52
|
+
output: n(u.output_tokens),
|
|
53
|
+
cache_read: n(u.cache_read_input_tokens),
|
|
54
|
+
cache_write: n(u.cache_creation_input_tokens),
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
turns: n(last.num_turns),
|
|
58
|
+
cost_usd: n(last.total_cost_usd ?? last.cost_usd),
|
|
59
|
+
tokens: Object.values(tokens).some((v) => v !== null) ? tokens : null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What happened, in one word.
|
|
65
|
+
*
|
|
66
|
+
* The agent step's own outcome when it ran. When it never ran, the job's status says whether
|
|
67
|
+
* that was a cancel (a timeout is one) or a failure somewhere in the setup before it. A mention
|
|
68
|
+
* the agent exited from cleanly without answering is not a success, whatever the agent says.
|
|
69
|
+
*/
|
|
70
|
+
export function outcomeOf(agentOutcome, jobStatus, unanswered = false) {
|
|
71
|
+
if (agentOutcome === "success" && unanswered) return "unanswered";
|
|
72
|
+
if (["success", "failure", "cancelled"].includes(agentOutcome)) return agentOutcome;
|
|
73
|
+
if (jobStatus === "cancelled") return "cancelled";
|
|
74
|
+
return "setup-failure";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildRecord(env, resultText, now = Date.now()) {
|
|
78
|
+
const started = Number(env.ROSTER_STARTED) || null;
|
|
79
|
+
const result = readResult(resultText);
|
|
80
|
+
return {
|
|
81
|
+
v: 1,
|
|
82
|
+
staff: env.STAFF ?? "",
|
|
83
|
+
kind: env.KIND ?? "",
|
|
84
|
+
outcome: outcomeOf(env.AGENT_OUTCOME, env.JOB_STATUS, env.UNANSWERED === "true"),
|
|
85
|
+
started: started ? new Date(started * 1000).toISOString() : null,
|
|
86
|
+
duration_s: started ? Math.max(0, Math.round(now / 1000 - started)) : null,
|
|
87
|
+
agent: env.AGENT_ID || null,
|
|
88
|
+
model: env.MODEL || null,
|
|
89
|
+
turns: result?.turns ?? null,
|
|
90
|
+
cost_usd: result?.cost_usd ?? null,
|
|
91
|
+
tokens: result?.tokens ?? null,
|
|
92
|
+
run_id: env.GITHUB_RUN_ID ?? null,
|
|
93
|
+
run_url:
|
|
94
|
+
env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID
|
|
95
|
+
? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`
|
|
96
|
+
: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The same record as the job summary shows it: one table, unknowns as a dash. */
|
|
101
|
+
export function summary(record) {
|
|
102
|
+
const dash = (v) => (v === null || v === undefined ? "—" : String(v));
|
|
103
|
+
const mins = record.duration_s === null ? null : `${Math.round(record.duration_s / 60)}m`;
|
|
104
|
+
const cost = record.cost_usd === null ? null : `$${record.cost_usd.toFixed(2)}`;
|
|
105
|
+
const t = record.tokens;
|
|
106
|
+
const tokens = t
|
|
107
|
+
? [
|
|
108
|
+
t.input !== null ? `${t.input} in` : "",
|
|
109
|
+
t.output !== null ? `${t.output} out` : "",
|
|
110
|
+
t.cache_read !== null ? `${t.cache_read} cached` : "",
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join(", ")
|
|
114
|
+
: null;
|
|
115
|
+
return [
|
|
116
|
+
`### ${record.staff} · ${record.kind} · ${record.outcome}`,
|
|
117
|
+
"",
|
|
118
|
+
"| duration | turns | cost | tokens | agent |",
|
|
119
|
+
"|---|---|---|---|---|",
|
|
120
|
+
`| ${dash(mins)} | ${dash(record.turns)} | ${dash(cost)} | ${dash(tokens)} | ${dash(record.agent)} |`,
|
|
121
|
+
"",
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function main(argv) {
|
|
126
|
+
const args = {};
|
|
127
|
+
for (let i = 0; i < argv.length; i += 2) args[argv[i].replace(/^--/, "")] = argv[i + 1];
|
|
128
|
+
const out = resolve(args.out ?? ".roster-run/run.json");
|
|
129
|
+
const file = process.env.RESULT_FILE;
|
|
130
|
+
const text = file && existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
131
|
+
const record = buildRecord(process.env, text);
|
|
132
|
+
|
|
133
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
134
|
+
writeFileSync(out, JSON.stringify(record, null, 2) + "\n");
|
|
135
|
+
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary(record));
|
|
136
|
+
process.stdout.write(JSON.stringify(record) + "\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
140
|
+
try {
|
|
141
|
+
main(process.argv.slice(2));
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`run-record: ${err.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|