@xhulz/nina 0.72.0 → 0.73.0

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 (137) hide show
  1. package/package.json +1 -1
  2. package/releases/0.73.0/core/GAPS.md +125 -0
  3. package/releases/0.73.0/core/answered.json +104 -0
  4. package/releases/0.73.0/core/defaults/tree/.claude/agents/architect.md +1 -0
  5. package/releases/0.73.0/core/defaults/tree/.claude/agents/dba.md +3 -0
  6. package/releases/0.73.0/core/defaults/tree/.claude/agents/devops.md +1 -0
  7. package/releases/0.73.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
  8. package/releases/0.73.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
  9. package/releases/0.73.0/core/defaults/tree/.claude/agents/qa.md +7 -0
  10. package/releases/0.73.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
  11. package/releases/0.73.0/core/defaults/tree/.claude/agents/secops.md +3 -0
  12. package/releases/0.73.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
  13. package/releases/0.73.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
  14. package/releases/0.73.0/core/tree/.claude/agents/architect.md +135 -0
  15. package/releases/0.73.0/core/tree/.claude/agents/dba.md +119 -0
  16. package/releases/0.73.0/core/tree/.claude/agents/design-reviewer.md +78 -0
  17. package/releases/0.73.0/core/tree/.claude/agents/devops.md +118 -0
  18. package/releases/0.73.0/core/tree/.claude/agents/implementer.md +184 -0
  19. package/releases/0.73.0/core/tree/.claude/agents/integration-tester.md +170 -0
  20. package/releases/0.73.0/core/tree/.claude/agents/planner.md +132 -0
  21. package/releases/0.73.0/core/tree/.claude/agents/qa.md +150 -0
  22. package/releases/0.73.0/core/tree/.claude/agents/reviewer.md +187 -0
  23. package/releases/0.73.0/core/tree/.claude/agents/secops.md +120 -0
  24. package/releases/0.73.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
  25. package/releases/0.73.0/core/tree/.claude/agents/solidity-dev.md +123 -0
  26. package/releases/0.73.0/core/tree/.claude/agents-overview.md +65 -0
  27. package/releases/0.73.0/core/tree/.claude/frontend.md +10 -0
  28. package/releases/0.73.0/core/tree/.claude/graph.md +79 -0
  29. package/releases/0.73.0/core/tree/.claude/patterns.md +256 -0
  30. package/releases/0.73.0/core/tree/.claude/pills/README.md +150 -0
  31. package/releases/0.73.0/core/tree/.claude/pipeline.md +101 -0
  32. package/releases/0.73.0/core/tree/.claude/retrieval.md +103 -0
  33. package/releases/0.73.0/core/tree/.claude/router.md +221 -0
  34. package/releases/0.73.0/core/tree/.claude/templates/integration.md +48 -0
  35. package/releases/0.73.0/core/tree/CLAUDE.md +167 -0
  36. package/releases/0.73.0/core/tree/scripts/cost-watch.mjs +27 -0
  37. package/releases/0.73.0/core/tree/scripts/edit-guard.mjs +24 -0
  38. package/releases/0.73.0/core/tree/scripts/harness-check.mjs +82 -0
  39. package/releases/0.73.0/core/tree/scripts/loop-gate.mjs +19 -0
  40. package/releases/0.73.0/core/vocabulary.json +15 -0
  41. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
  42. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
  43. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
  44. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
  45. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
  46. package/releases/0.73.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
  47. package/releases/0.73.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
  48. package/releases/0.73.0/surfaces/blockchain/tree/.claude/router.md +6 -0
  49. package/releases/0.73.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
  50. package/releases/0.73.0/surfaces/d1/tree/.claude/agents/dba.md +15 -0
  51. package/releases/0.73.0/surfaces/d1/tree/.claude/agents/devops.md +2 -0
  52. package/releases/0.73.0/surfaces/d1/tree/.claude/agents/implementer.md +2 -0
  53. package/releases/0.73.0/surfaces/d1/tree/.claude/patterns.md +8 -0
  54. package/releases/0.73.0/surfaces/d1/tree/CLAUDE.md +2 -0
  55. package/releases/0.73.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
  56. package/releases/0.73.0/surfaces/db/tree/.claude/agents/dba.md +54 -0
  57. package/releases/0.73.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
  58. package/releases/0.73.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
  59. package/releases/0.73.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
  60. package/releases/0.73.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
  61. package/releases/0.73.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
  62. package/releases/0.73.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
  63. package/releases/0.73.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
  64. package/releases/0.73.0/surfaces/db/tree/.claude/graph.md +8 -0
  65. package/releases/0.73.0/surfaces/db/tree/.claude/patterns.md +22 -0
  66. package/releases/0.73.0/surfaces/db/tree/.claude/pipeline.md +13 -0
  67. package/releases/0.73.0/surfaces/db/tree/.claude/retrieval.md +5 -0
  68. package/releases/0.73.0/surfaces/db/tree/.claude/router.md +4 -0
  69. package/releases/0.73.0/surfaces/db/tree/CLAUDE.md +17 -0
  70. package/releases/0.73.0/surfaces/drizzle/tree/.claude/agents/dba.md +9 -0
  71. package/releases/0.73.0/surfaces/drizzle/tree/.claude/agents/devops.md +5 -0
  72. package/releases/0.73.0/surfaces/drizzle/tree/.claude/agents/implementer.md +2 -0
  73. package/releases/0.73.0/surfaces/drizzle/tree/.claude/patterns.md +7 -0
  74. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
  75. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
  76. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/devops.md +29 -0
  77. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
  78. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
  79. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
  80. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
  81. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
  82. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
  83. package/releases/0.73.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
  84. package/releases/0.73.0/surfaces/edge-cf/tree/CLAUDE.md +29 -0
  85. package/releases/0.73.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
  86. package/releases/0.73.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
  87. package/releases/0.73.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
  88. package/releases/0.73.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
  89. package/releases/0.73.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
  90. package/releases/0.73.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
  91. package/releases/0.73.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
  92. package/releases/0.73.0/surfaces/frontend/tree/.claude/router.md +14 -0
  93. package/releases/0.73.0/surfaces/frontend/tree/CLAUDE.md +5 -0
  94. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
  95. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
  96. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
  97. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
  98. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
  99. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
  100. package/releases/0.73.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
  101. package/releases/0.73.0/surfaces/integrations/tree/.claude/graph.md +10 -0
  102. package/releases/0.73.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
  103. package/releases/0.73.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
  104. package/releases/0.73.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
  105. package/releases/0.73.0/surfaces/integrations/tree/.claude/router.md +6 -0
  106. package/releases/0.73.0/surfaces/integrations/tree/CLAUDE.md +17 -0
  107. package/releases/0.73.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
  108. package/releases/0.73.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
  109. package/releases/0.73.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
  110. package/releases/0.73.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
  111. package/releases/0.73.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
  112. package/releases/0.73.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
  113. package/releases/0.73.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
  114. package/releases/0.73.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
  115. package/releases/0.73.0/surfaces/money/tree/.claude/graph.md +2 -0
  116. package/releases/0.73.0/surfaces/money/tree/.claude/patterns.md +49 -0
  117. package/releases/0.73.0/surfaces/money/tree/.claude/pipeline.md +2 -0
  118. package/releases/0.73.0/surfaces/money/tree/.claude/retrieval.md +2 -0
  119. package/releases/0.73.0/surfaces/money/tree/CLAUDE.md +11 -0
  120. package/releases/0.73.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
  121. package/releases/0.73.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
  122. package/releases/0.73.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
  123. package/releases/0.73.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
  124. package/releases/0.73.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
  125. package/releases/0.73.0/surfaces/pii/tree/CLAUDE.md +5 -0
  126. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
  127. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/dba.md +42 -0
  128. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
  129. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
  130. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
  131. package/releases/0.73.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
  132. package/releases/0.73.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
  133. package/releases/0.73.0/surfaces/prisma/tree/CLAUDE.md +5 -0
  134. package/src/cost.mjs +3 -3
  135. package/src/office.html +52 -9
  136. package/src/office.mjs +155 -18
  137. package/src/transcripts.mjs +13 -1
package/src/office.mjs CHANGED
@@ -4,11 +4,14 @@
4
4
  * Each stage of the project's composed graph has a desk, and each agent the loop gate saw launched is someone
5
5
  * sitting at one. The gate's ledger is what is read, and it holds metadata only, so the office shows who worked,
6
6
  * what they answered, the ids of the issues they named and the few words the orchestrator gave each dispatch —
7
- * never what an agent wrote or reported. Beside the room, a timeline of the session's tasks (`officeTimeline`), cut
8
- * and named as `nina runs` cuts and names them:
7
+ * never what an agent reported. Of a run's own transcript it reads when it was last written, how much context it
8
+ * holds and what it is doing — the description it gave its shell command, or a tool and a file's name (`actionOf`).
9
+ * Beside the room, a timeline of the session's tasks (`officeTimeline`), cut and named as `nina runs` cuts and names
10
+ * them, each open one with its agents at work:
9
11
  *
10
12
  * a dispatch someone sits down to work, the folder carried over by whoever handed it on
11
13
  * a verdict a check over their head, or a cross when it sends work back
14
+ * a report nothing follows after `HANDOFF_MS`, its agent goes home
12
15
  * a verdict the graph sends to the a question mark over the one who asked, until the owner speaks
13
16
  * owner, or the orchestrator asking
14
17
  * the gate asking at a cap the phone on the reception's counter rings, the owner's line
@@ -31,7 +34,8 @@ import { readLedger, sessionLedgers } from './gate.mjs';
31
34
  import { readProfile, slugFor } from './paths.mjs';
32
35
  import { projectStoreFile, readStoreFile } from './store.mjs';
33
36
  import { CYCLE_ENDS, IN_FLIGHT, cyclesOf } from './commands/stats.mjs';
34
- import { isLoopBack, transcriptsOf } from './transcripts.mjs';
37
+ import { contextSize, isLoopBack, transcriptsOf } from './transcripts.mjs';
38
+ import { gained } from './cost.mjs';
35
39
  import { shapeOf } from './commands/pipeline.mjs';
36
40
  import { listenHere, openInBrowser, refuseElsewhere, untilStopped } from './serve.mjs';
37
41
 
@@ -48,6 +52,14 @@ export const OFFICE_PORT = 6462;
48
52
  */
49
53
  export const PARTY_QUIET_MS = 20_000;
50
54
 
55
+ /**
56
+ * How long one whose report is in waits at its desk to hand its work on before it goes home. Any dispatch moves the
57
+ * reported on, but with tasks side by side the next could be a long while coming: of two projects' 1,266 reports, 346
58
+ * sat on past two minutes and 149 past ten, done and with nothing in hand, while Claude Code had already let them go.
59
+ * Of 544 hand-offs, 92.8% went out within two minutes, and 93.9% within three.
60
+ */
61
+ export const HANDOFF_MS = 2 * 60 * 1000;
62
+
51
63
  /** How far back, when a live client moves to a session already running, beats still play rather than set the room. */
52
64
  const SWITCH_GRACE_MS = 10_000;
53
65
 
@@ -77,14 +89,13 @@ export const SILENT_MS = 45 * 60 * 1000;
77
89
  const staleAt = (start, heard) => (heard === null ? start.at + STALE_MS : Math.max(start.at, heard) + SILENT_MS);
78
90
 
79
91
  /**
80
- * When each run of a session last wrote its own transcript, so that a run still writing is at work however long it
81
- * takes. A run is found by its agent's id once the gate knows it, and before that — a foreground run still going — by
82
- * the tool_use id that launched it, which its transcript's `.meta.json` keeps.
92
+ * Each run's own transcript in a session. A run is found by its agent's id once the gate knows it, and before that — a
93
+ * foreground run still going — by the tool_use id that launched it, which its transcript's `.meta.json` keeps.
83
94
  *
84
95
  * @param {string} target - The project directory.
85
- * @returns {(session: string|null) => (start: {agent: string|null, id: string|null}) => number|null}
96
+ * @returns {(session: string|null) => (start: {agent: string|null, id: string|null}) => string|null}
86
97
  */
87
- export function runClock(target) {
98
+ export function runFiles(target) {
88
99
  // Named as the gate names the project's ledgers, by where it really is: reached through a link, it is the same one.
89
100
  let real = target;
90
101
  try {
@@ -121,9 +132,23 @@ export function runClock(target) {
121
132
  }
122
133
  if (launchedBy.has(meta)) byLaunch.set(launchedBy.get(meta), file);
123
134
  }
135
+ return (start) => (start.agent && byAgent.get(start.agent)) || (start.id && byLaunch.get(start.id)) || null;
136
+ };
137
+ }
138
+
139
+ /**
140
+ * When each run of a session last wrote its own transcript, so that a run still writing is at work however long it
141
+ * takes.
142
+ *
143
+ * @param {(session: string|null) => (start: object) => string|null} files - Each run's transcript (`runFiles`).
144
+ * @returns {(session: string|null) => (start: {agent: string|null, id: string|null}) => number|null}
145
+ */
146
+ export function runClock(files) {
147
+ return (session) => {
148
+ const fileOf = files(session);
124
149
  const written = new Map();
125
150
  return (start) => {
126
- const file = (start.agent && byAgent.get(start.agent)) || (start.id && byLaunch.get(start.id)) || null;
151
+ const file = fileOf(start);
127
152
  if (!file) return null;
128
153
  if (!written.has(file)) {
129
154
  try {
@@ -137,6 +162,89 @@ export function runClock(target) {
137
162
  };
138
163
  }
139
164
 
165
+ /** The most of a run's own words the timeline shows for what it is doing. */
166
+ const ACTION_MAX = 120;
167
+
168
+ /**
169
+ * What a tool call is doing, in the few words a list beside the room has: the description the run gave a shell
170
+ * command, which the run writes for a person to read, or the tool and the name of the file it works on. Never the
171
+ * command, a path, a pattern, another tool's input or what came back.
172
+ *
173
+ * @param {object} block - A `tool_use` block of an assistant message.
174
+ * @returns {string|null}
175
+ */
176
+ export function actionOf(block) {
177
+ if (block?.type !== 'tool_use' || typeof block.name !== 'string') return null;
178
+ const input = block.input && typeof block.input === 'object' ? block.input : {};
179
+ // An MCP tool is named by its server too: the tool's own name says what it does.
180
+ const name = block.name.startsWith('mcp__') ? (block.name.split('__').filter(Boolean).at(-1) ?? block.name) : block.name;
181
+ const said = block.name === 'Bash' && typeof input.description === 'string' ? input.description.replace(/\s+/g, ' ').trim() : '';
182
+ if (said) return said.length > ACTION_MAX ? `${said.slice(0, ACTION_MAX - 1)}…` : said;
183
+ const path = [input.file_path, input.notebook_path].find((x) => typeof x === 'string' && x.trim());
184
+ // A transcript written on Windows names its files with backslashes, which a path read here does not split on.
185
+ return path ? `${name} ${basename(path.trim().replace(/\\/g, '/'))}` : name;
186
+ }
187
+
188
+ /**
189
+ * Each run of a session as its own transcript tells it now: how much context it holds (`contextSize` of its newest
190
+ * message) and what it is doing (`actionOf` its newest tool call) — what Claude Code shows beside each of its running
191
+ * agents, but for a line it writes as it watches, which it keeps nowhere. Read once a look and only what the
192
+ * transcript gained since the last, keeping those and nothing else the run wrote; each with when its message was
193
+ * written, so a replay is told the run as it stood at a beat.
194
+ *
195
+ * @param {(session: string|null) => (start: object) => string|null} files - Each run's transcript (`runFiles`).
196
+ * @returns {(session: string|null) => (start: {agent: string|null, id: string|null}, moment?: number) => {tokens: number|null, doing: string|null}|null}
197
+ */
198
+ export function runState(files) {
199
+ /** By transcript, in the session last looked at: how far it was read, its last message, the counts and actions by when. */
200
+ let read = new Map();
201
+ let looking = null;
202
+ return (session) => {
203
+ // The office follows one session at a time: one it moved away from is read again if it comes back.
204
+ if (session !== looking) read = new Map();
205
+ looking = session;
206
+ const fileOf = files(session);
207
+ const looked = new Set();
208
+ return (start, moment = Number.POSITIVE_INFINITY) => {
209
+ const file = fileOf(start);
210
+ if (!file) return null;
211
+ if (!read.has(file)) read.set(file, { offset: 0, last: null, counts: [], actions: [] });
212
+ const kept = read.get(file);
213
+ if (!looked.has(file)) {
214
+ looked.add(file);
215
+ try {
216
+ const { text, to } = gained(file, kept.offset);
217
+ // A transcript shorter than what was read of it is another one: read again from its start.
218
+ if (to < kept.offset) Object.assign(kept, { last: null, counts: [], actions: [] });
219
+ kept.offset = to;
220
+ for (const line of text.split('\n')) {
221
+ if (!line.includes('"cache_read_input_tokens"')) continue;
222
+ try {
223
+ const row = JSON.parse(line);
224
+ const at = Date.parse(row?.timestamp);
225
+ if (!row?.message?.usage || !row.message.id || !Number.isFinite(at)) continue;
226
+ // A message is written once for each of its blocks, with the same usage: its first is counted, and
227
+ // each of its tool calls is a step.
228
+ if (row.message.id !== kept.last) kept.counts.push([at, contextSize(row.message.usage)]);
229
+ kept.last = row.message.id;
230
+ for (const block of Array.isArray(row.message.content) ? row.message.content : []) {
231
+ const action = actionOf(block);
232
+ if (action) kept.actions.push([at, action]);
233
+ }
234
+ } catch {
235
+ // A torn line loses one message's count; the next one has its own.
236
+ }
237
+ }
238
+ } catch {
239
+ // A transcript gone between the listing and the read tells what it had.
240
+ }
241
+ }
242
+ const by = (list) => list.findLast(([at]) => at <= moment)?.[1] ?? null;
243
+ return { tokens: by(kept.counts), doing: by(kept.actions) };
244
+ };
245
+ };
246
+ }
247
+
140
248
  /** A wait between two of the office's looks this much longer than it asks for is the machine having slept. */
141
249
  const SLEPT_MS = 60_000;
142
250
 
@@ -256,11 +364,12 @@ function readStarts(ledger) {
256
364
  * verdict that ends a cycle of the timeline, qa's `PASS` — is a party at once, whoever else is at work. Any other pass
257
365
  * ends the work — a party — when nothing since the last dispatch was sent back, something since then could route to
258
366
  * `done`, nobody is still at work, and no dispatch follows within `PARTY_QUIET_MS`; at once when the pass routes only
259
- * to a terminal. A party that waits on the quiet is only beaten once `now` has passed it.
367
+ * to a terminal. A party that waits on the quiet is only beaten once `now` has passed it. So is the beat that sends one
368
+ * whose report is in home (`home`): `HANDOFF_MS` after its verdict, when no dispatch and no party came between.
260
369
  *
261
370
  * @param {object[]} entries - The ledger, oldest first.
262
371
  * @param {{edges: {from: string, to: string, token: string}[]}} graph - The project's parsed graph.
263
- * @param {number} [now] - The time a waiting party is judged against; `Infinity` for a session that has ended.
372
+ * @param {number} [now] - The time a waiting party or a going home is judged against; `Infinity` for a session that has ended.
264
373
  * @param {(start: object) => number|null} [heard] - When a run last wrote its transcript (`runClock`), or null.
265
374
  * @returns {object[]} Beats, each `{id, at, type, ...}`; the id is stable as the ledger grows.
266
375
  */
@@ -345,6 +454,19 @@ export function officeBeats(ledger, graph, now = Date.now(), heard = () => null)
345
454
  // A reset written for the model's own question, by ledgers older than the `question` entry, is not the owner.
346
455
  else if (e.k === 'reset' && e.why !== 'ask') beats.push({ id, at, type: 'owner' });
347
456
  });
457
+ // One whose report is in goes home once `HANDOFF_MS` passes with no dispatch and no party, either of which sends the
458
+ // reported on anyway. Its work, handed on later, the orchestrator carries. One that asked the owner stays with its
459
+ // question until the work moves on. "After" is the ledger's order, which the beats keep until they are sorted: a
460
+ // qa's party falls at its verdict's own moment.
461
+ const moves = beats.flatMap((b, i) => ((b.type === 'work' && !b.message) || b.type === 'party' ? [{ i, at: b.at }] : []));
462
+ const home = [];
463
+ beats.forEach((v, i) => {
464
+ if (v.type !== 'verdict' || v.asks) return;
465
+ const moment = v.at + HANDOFF_MS;
466
+ if (now < moment || moves.some((m) => m.i > i && m.at <= moment)) return;
467
+ home.push({ id: `home:${v.id}`, at: moment, type: 'home', role: v.role, agent: v.agent });
468
+ });
469
+ beats.push(...home);
348
470
  // Stable: beats of one moment keep the ledger's order. A beat that waited — a party, a refusal — sorts where it fell.
349
471
  return beats.sort((a, b) => a.at - b.at);
350
472
  }
@@ -359,13 +481,18 @@ export function officeBeats(ledger, graph, now = Date.now(), heard = () => null)
359
481
  * grows — a run still at work lets nothing through until its verdict, a dispatch is known refused only seconds later
360
482
  * — and sent piece by piece, a task that a later fact merged away stayed on the page for good.
361
483
  *
484
+ * A task still open lists its agents at work, oldest first, each with its stage, the words its own dispatch was given,
485
+ * when it started, how much context it holds and what it is doing (`runState`) — the detail Claude Code shows of its
486
+ * own running agents. Every task says when the last of its runs started or ended, which an idle one counts up to.
487
+ *
362
488
  * @param {object[]} ledger - The ledger, oldest first.
363
489
  * @param {number} [now] - The moment it is read at: a run with no verdict that has not gone quiet is still at work.
364
490
  * @param {Map<string, string>} [names] - Descriptions by dispatch id, for entries that carry none.
365
491
  * @param {(start: object) => number|null} [heard] - When a run last wrote its transcript (`runClock`), or null.
366
- * @returns {{key: string, name: string|null, start: number, end: number|null, how: string|null, busy: number|null, who: [string, number][]}[]}
492
+ * @param {(start: object) => {tokens: number|null, doing: string|null}|null} [state] - A run as its transcript tells it (`runState`).
493
+ * @returns {{key: string, name: string|null, start: number, end: number|null, last: number, how: string|null, busy: number|null, who: [string, number][], agents: {role: string, desc: string|null, start: number, tokens: number|null, doing: string|null}[]}[]}
367
494
  */
368
- export function officeTimeline(ledger, now = Date.now(), names = new Map(), heard = () => null) {
495
+ export function officeTimeline(ledger, now = Date.now(), names = new Map(), heard = () => null, state = () => null) {
369
496
  const { entries, starts, verdictOf } = readStarts(ledger);
370
497
  // A run a message started — an agent resumed after it had reported — is named by the message's summary, or, in a
371
498
  // ledger from before the gate kept one, by the words the agent was launched with: it goes on with that work.
@@ -385,15 +512,22 @@ export function officeTimeline(ledger, now = Date.now(), names = new Map(), hear
385
512
  // A task its closing verdict ended while one of its runs is still at work stays open until that run reports: work
386
513
  // planned side by side — three architects sent together, one of whose specs went on to qa — fell inside the task
387
514
  // the first one's qa closed, and the room showed two architects at work beside a list with no task open.
388
- const open = cycle.end === 'open' || cycle.runs.some((r) => r.verdict === IN_FLIGHT && r.start.end == null);
515
+ const atWork = cycle.runs.filter((r) => r.verdict === IN_FLIGHT && r.start.end == null);
516
+ const open = cycle.end === 'open' || atWork.length > 0;
517
+ const last = Math.max(...cycle.runs.map((r) => r.start.end ?? r.start.at));
389
518
  return {
390
519
  key: first.key,
391
520
  name: first.desc ? String(first.desc) : null,
392
521
  start: first.start.at,
393
- end: open ? null : Math.max(...cycle.runs.map((r) => r.start.end ?? r.start.at)),
522
+ end: open ? null : last,
523
+ last,
394
524
  how: open ? null : cycle.end,
395
525
  busy: open ? null : busyFor(cycle.runs.map((r) => r.start)),
396
526
  who: [...who],
527
+ agents: atWork.map((r) => {
528
+ const told = state(r.start);
529
+ return { role: r.role, desc: r.desc ? String(r.desc) : null, start: r.start.at, tokens: told?.tokens ?? null, doing: told?.doing ?? null };
530
+ }),
397
531
  };
398
532
  });
399
533
  }
@@ -505,7 +639,9 @@ export function officeRoutes({ target, wanted, replay = false }) {
505
639
  const timers = new Set();
506
640
 
507
641
  const names = storeNames(target);
508
- const clock = runClock(target);
642
+ const files = runFiles(target);
643
+ const clock = runClock(files);
644
+ const looks = runState(files);
509
645
  const wake = wakeClock();
510
646
 
511
647
  /** The session to show, and its entries; read again only when its ledger changed. */
@@ -559,7 +695,7 @@ export function officeRoutes({ target, wanted, replay = false }) {
559
695
  return at === null ? null : Math.max(at, woke);
560
696
  };
561
697
  const beats = officeBeats(state.entries, graph, now, heard);
562
- const tasks = officeTimeline(state.entries, now, names(state.entries), heard);
698
+ const tasks = officeTimeline(state.entries, now, names(state.entries), heard, looks(state.session));
563
699
  for (const client of clients) {
564
700
  if (client.replay) continue;
565
701
  if (client.session !== state.session) greet(client, state);
@@ -580,7 +716,8 @@ export function officeRoutes({ target, wanted, replay = false }) {
580
716
  const beats = officeBeats(state.entries, graph, Number.POSITIVE_INFINITY, heard);
581
717
  // The timeline as it stood at each beat: cut from the ledger up to that moment, so a replay tells nothing early.
582
718
  const known = names(state.entries);
583
- const tasksAt = (moment) => officeTimeline(state.entries.filter((e) => Date.parse(e?.at) <= moment), moment, known, heard);
719
+ const look = looks(state.session);
720
+ const tasksAt = (moment) => officeTimeline(state.entries.filter((e) => Date.parse(e?.at) <= moment), moment, known, heard, (start) => look(start, moment));
584
721
  // What came before the first dispatch sets the room, at once: a session often opens on many of the owner's
585
722
  // messages, and played out they kept the office empty for a minute.
586
723
  let i = Math.max(0, beats.findIndex((b) => b.type === 'work'));
@@ -1114,7 +1114,7 @@ const REWRITE_FLOOR = 30_000;
1114
1114
  */
1115
1115
  export function contextOf(usage) {
1116
1116
  const n = (v) => (typeof v === 'number' ? v : 0);
1117
- const turns = [...usage.values()].map(({ usage: u }) => ({ size: n(u.input_tokens) + n(u.cache_read_input_tokens) + n(u.cache_creation_input_tokens), written: n(u.cache_creation_input_tokens) }));
1117
+ const turns = [...usage.values()].map(({ usage: u }) => ({ size: contextSize(u), written: n(u.cache_creation_input_tokens) }));
1118
1118
  const sizes = turns.map((t) => t.size);
1119
1119
  let rewrites = 0;
1120
1120
  let rewritten = 0;
@@ -1127,6 +1127,18 @@ export function contextOf(usage) {
1127
1127
  return { turns: sizes.length, start: sizes[0] ?? null, peak: sizes.length > 0 ? Math.max(...sizes) : null, rewrites, rewritten };
1128
1128
  }
1129
1129
 
1130
+ /**
1131
+ * The context a turn held: everything it read, from the cache or not. What `stats` measures a round's context by, the
1132
+ * cost watch a run's, and the office an agent at work.
1133
+ *
1134
+ * @param {object} usage - A message's usage.
1135
+ * @returns {number} Tokens.
1136
+ */
1137
+ export function contextSize(usage) {
1138
+ const n = (v) => (typeof v === 'number' ? v : 0);
1139
+ return n(usage?.input_tokens) + n(usage?.cache_read_input_tokens) + n(usage?.cache_creation_input_tokens);
1140
+ }
1141
+
1130
1142
  /** How the cost watch opens an output it clipped (`src/cost.mjs`), which is how a snapshot counts them. */
1131
1143
  export const CLIP_MARK = '[NINA clipped this output';
1132
1144