@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.
Files changed (102) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4833 -2752
  3. package/docs/README.md +9 -6
  4. package/docs/agents.md +24 -20
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/analyst.md +65 -0
  7. package/docs/charters/cmo.md +69 -0
  8. package/docs/charters/community.md +63 -0
  9. package/docs/charters/cto.md +71 -0
  10. package/docs/charters/designer.md +65 -0
  11. package/docs/charters/devops.md +65 -0
  12. package/docs/charters/pm.md +70 -0
  13. package/docs/charters/qa.md +65 -0
  14. package/docs/charters/support.md +60 -0
  15. package/docs/charters/writer.md +63 -0
  16. package/docs/commands.md +93 -7
  17. package/docs/concepts.md +61 -14
  18. package/docs/cost.md +36 -1
  19. package/docs/developing.md +16 -21
  20. package/docs/doctor-codes.md +10 -2
  21. package/docs/export.md +2 -0
  22. package/docs/extending.md +2 -2
  23. package/docs/getting-started.md +128 -78
  24. package/docs/images/brain.jpg +0 -0
  25. package/docs/images/org.jpg +0 -0
  26. package/docs/images/prompt.jpg +0 -0
  27. package/docs/images/setup-org.jpg +0 -0
  28. package/docs/images/setup-plan.jpg +0 -0
  29. package/docs/images/staff.jpg +0 -0
  30. package/docs/manual-steps.md +94 -123
  31. package/docs/memory.md +21 -3
  32. package/docs/org-yaml.md +40 -2
  33. package/docs/portal.md +165 -48
  34. package/docs/prompts.md +31 -4
  35. package/docs/security.md +37 -5
  36. package/docs/session-workflow.md +63 -17
  37. package/docs/staff-yaml.md +30 -3
  38. package/docs/troubleshooting.md +8 -8
  39. package/docs/upgrading.md +9 -3
  40. package/docs/writing-a-charter.md +28 -0
  41. package/package.json +18 -20
  42. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +26 -1
  43. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +41 -7
  44. package/templates/brain/CHARTER.md +3 -3
  45. package/templates/brain/README.md +1 -0
  46. package/templates/brain/log/decisions.md +3 -0
  47. package/templates/brain/staff.yaml +4 -1
  48. package/templates/brain/strategy/ideas.md +7 -0
  49. package/templates/briefs/priorities.md +46 -0
  50. package/templates/ops/.github/workflows/session.yaml +236 -15
  51. package/templates/ops/agents.mjs +7 -3
  52. package/templates/ops/compose.mjs +31 -3
  53. package/templates/ops/inflight.mjs +157 -0
  54. package/templates/ops/org/operating.md +43 -4
  55. package/templates/ops/org/voice.md +9 -0
  56. package/templates/ops/prompts/_inflight.md +14 -0
  57. package/templates/ops/prompts/_paths.md +2 -1
  58. package/templates/ops/prompts/daily.md +37 -9
  59. package/templates/ops/prompts/mention.md +21 -0
  60. package/templates/ops/run-record.mjs +146 -0
  61. package/templates/portal/css/base.css +167 -73
  62. package/templates/portal/css/brain.css +23 -20
  63. package/templates/portal/css/diff.css +10 -9
  64. package/templates/portal/css/graph.css +12 -7
  65. package/templates/portal/css/health.css +13 -11
  66. package/templates/portal/css/home.css +93 -0
  67. package/templates/portal/css/inbox.css +45 -25
  68. package/templates/portal/css/layout.css +90 -46
  69. package/templates/portal/css/markdown.css +36 -14
  70. package/templates/portal/css/runs.css +13 -0
  71. package/templates/portal/css/setup.css +116 -34
  72. package/templates/portal/index.html +21 -9
  73. package/templates/portal/js/api.js +44 -4
  74. package/templates/portal/js/app.js +94 -9
  75. package/templates/portal/js/dialog.js +83 -0
  76. package/templates/portal/js/homesort.js +174 -0
  77. package/templates/portal/js/icons.js +37 -0
  78. package/templates/portal/js/inflight.js +18 -0
  79. package/templates/portal/js/md.js +5 -2
  80. package/templates/portal/js/mdedit.js +84 -0
  81. package/templates/portal/js/readiness.js +70 -0
  82. package/templates/portal/js/refresh.js +10 -2
  83. package/templates/portal/js/state.js +11 -5
  84. package/templates/portal/js/views/app.js +24 -7
  85. package/templates/portal/js/views/brain.js +1 -1
  86. package/templates/portal/js/views/checklist.js +10 -4
  87. package/templates/portal/js/views/credential.js +84 -0
  88. package/templates/portal/js/views/graph.js +1 -1
  89. package/templates/portal/js/views/health.js +17 -4
  90. package/templates/portal/js/views/hire.js +593 -0
  91. package/templates/portal/js/views/home.js +546 -0
  92. package/templates/portal/js/views/inbox.js +226 -70
  93. package/templates/portal/js/views/org.js +46 -106
  94. package/templates/portal/js/views/orgedit.js +234 -0
  95. package/templates/portal/js/views/paste.js +87 -21
  96. package/templates/portal/js/views/prompt.js +11 -4
  97. package/templates/portal/js/views/repos.js +20 -15
  98. package/templates/portal/js/views/runonce.js +94 -0
  99. package/templates/portal/js/views/runs.js +170 -0
  100. package/templates/portal/js/views/setup.js +261 -75
  101. package/templates/portal/js/views/staff.js +170 -243
  102. 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 **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
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
- - **Never close a `decision` issue.** 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.
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}}, 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
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 edit it in place.
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, unless something above changed the
39
- priority, in which case say so in your report and do the more urgent thing. **Then actually do it.**
40
- You are not writing a plan for {{human.name}} to approve.
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. Nothing else** - no design
52
- essay, no narration of how you built it. It is reviewed on a phone and the diff is right there.
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}}, labelled by owner
58
- plus kind, assigned to `{{human.github}}`. Close what genuinely completed, citing evidence.
59
- **Never close a `decision` issue.** **Comments and replies get the same concision as everything
60
- 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
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
+ }