eve-hub 0.16.31 → 0.16.34

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/fanout/SKILL.md CHANGED
@@ -56,4 +56,4 @@ Never try a rung a second time. If two workers given the same task return substa
56
56
 
57
57
  ## 5. The cap
58
58
 
59
- Run at most 3 workers in parallel.
59
+ Run at most 3 workers in parallel, unless a skill names its own count for read-only workers that each write only their own file; then that skill's count is the cap.
package/kit-modules.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "memory/workspace",
12
12
  "schedules/prune",
13
13
  "schedules/wake",
14
+ "skills/deep-dive",
14
15
  "skills/fanout",
15
16
  "skills/forms",
16
17
  "skills/local-onboarding",
@@ -10,6 +10,8 @@ Main runs onboarding on Vercel, where it can't see this Mac, so it hands you thi
10
10
 
11
11
  Two scripts gather the facts. They live in `{{scripts}}`, read only, and print metadata: repos, branches, counts, times, the agents' own session titles and program names. Work only from what they print. Don't open the agents' session files yourself: they hold the owner's prompts and code. Delete the JSON files when you are done.
12
12
 
13
+ Every script run goes to an `agent` copy, so you stay free to talk with the owner while they work. When main hands this half over, start a copy for each section it asks for in one step: `sessions-facts.mjs` for section 1, `agent-usage-facts.mjs` for section 2, and the four sources of "LLM usage". Each runs its script, reads the JSON and returns what its post needs. You judge, write and send the posts. Wait with `eve__task_wait`, which also returns when the owner writes: answer them right away, then wait again. The copies never post, never call `git__gh` and never open the agents' session files.
14
+
13
15
  ## 1. Live work to port
14
16
 
15
17
  ```sh
@@ -87,3 +89,44 @@ node {{scripts}}/prepare-edit.mjs "$TMPDIR/prepare.json" agents/<bot>/agent/sand
87
89
  Commit, push and open the PR with `gh pr create` (a hosted bot runs `git` and `gh` through its `git__gh` tool). The body lists the programs, why each one is there, and the usage counts behind them. Never name the owner's other repos.
88
90
 
89
91
  The PR's Vercel build runs `prepare`, so a step that fails, fails the build. Wait for its result with `gh pr checks --watch --fail-fast`. If it fails, fix the step in `prepare.json`, restore the file from the branch you cut from (`git checkout origin/HEAD -- <file>`), rerun the script and push. Post the PR link and its build result in the thread, then delete the scratch clone.
92
+
93
+ ## LLM usage, when main asks for it
94
+
95
+ Step 0 of main's onboarding-profile run hands this part over on its own. It reuses `agent-usage-facts.mjs`, one source at a time, so the four agents are read in parallel. Start four `agent` copies in one step, one per source, and stay free to answer the owner while they run:
96
+
97
+ ```sh
98
+ node {{scripts}}/agent-usage-facts.mjs --source claude-code --usage-only > "$TMPDIR/usage-claude-code.json"
99
+ node {{scripts}}/agent-usage-facts.mjs --source codex --usage-only > "$TMPDIR/usage-codex.json"
100
+ node {{scripts}}/agent-usage-facts.mjs --source opencode --usage-only > "$TMPDIR/usage-opencode.json"
101
+ node {{scripts}}/agent-usage-facts.mjs --source cursor --usage-only > "$TMPDIR/usage-cursor.json"
102
+ ```
103
+
104
+ `--usage-only` keeps only `timeZone` (this Mac's), `usage` and `unreadable`: no programs, repos, env names or MCP servers. Each file has `usage.<source>` for the last 180 days (`windowDays`), with times in this Mac's time zone:
105
+ - `models`: one row per model id with `turns` (model calls), `inputTokens`, `outputTokens`, `cacheReadTokens` and `cacheWriteTokens`, most turns first. A model id the script couldn't vouch for is counted as `(unrecognized model id)`;
106
+ - `sessionsPerDay` (`date`, `sessions`), `activeHours` (24 turn counts by local hour) and `weekdays` (7 turn counts, Sunday first);
107
+ - `sessions`, and `subagents`: how many of those sessions a subagent ran.
108
+
109
+ `usage.<source>` of `null` means the history couldn't be read: Cursor keeps its chat history opaque, so it is always `null`. That is unknown, not zero. `unreadable` names what couldn't be read.
110
+
111
+ Each copy writes a short section for its source from its own file only:
112
+ - the models and each one's share of the source's tokens (input, output and cache together);
113
+ - sessions a week, from `sessionsPerDay`;
114
+ - the active hours, named as local times in the Mac's time zone, and the busiest weekdays;
115
+ - the subagent share, `subagents` out of `sessions`;
116
+ - a "Could not tell" line for each source that is `null` or listed in `unreadable`.
117
+
118
+ Then post one message in the thread. The first bubble is two sentences: which agent and model carry most of the work, and when the owner works. Below a `---` line, one table per your forms rules:
119
+
120
+ ```md
121
+ | Agent | Model | Turns | Tokens | Sessions a week | Peak hours |
122
+ |---|---|---|---|---|---|
123
+ | Claude Code | claude-opus-4-5 | 4,210 | 61M | 38 | 10:00 to 12:00, 15:00 to 18:00 |
124
+ ```
125
+
126
+ Add one line per "Could not tell". Then one last line that links the four files, so main can cite them in the hosted profile:
127
+
128
+ ```md
129
+ The facts: [Claude Code]($TMPDIR/usage-claude-code.json), [Codex]($TMPDIR/usage-codex.json), [OpenCode]($TMPDIR/usage-opencode.json), [Cursor]($TMPDIR/usage-cursor.json).
130
+ ```
131
+
132
+ Write the real `$TMPDIR` path in each link: posting uploads the file and the link becomes a `hub://upload` link that main fetches. The table has no permalinks, so these files are the only thing the hosted profile can cite for this section. Never write a path, a prompt or anything else from the sessions in the text; the files hold names and counts only. Delete the four files once the post is up.
@@ -3,7 +3,9 @@
3
3
  // (Claude Code, Codex, OpenCode, Cursor, Claude Desktop) have actually used, as names and counts.
4
4
  // Prints one JSON document on stdout; progress goes to stderr. Never writes, never emits a
5
5
  // command's arguments, a prompt, a path inside a repo, or any value: every string that leaves this
6
- // file went through `name()` or `normalizeOrigin()` (or is a source path under the agents' own dirs).
6
+ // file went through `name()`, `modelName()` or `normalizeOrigin()` (or is a source path under the agents' own dirs).
7
+ // `--source <claude-code|codex|opencode|cursor>` runs one agent's reader (and its MCP config) only, so the four can run in parallel.
8
+ // `--usage-only` prints only the LLM usage (`usage`, `unreadable` and the Mac's `timeZone`): the file the Mac bot uploads for the hosted profile.
7
9
  import { createReadStream, existsSync, readFileSync, globSync } from 'node:fs';
8
10
  import { execFileSync } from 'node:child_process';
9
11
  import { createInterface } from 'node:readline';
@@ -49,6 +51,61 @@ const mcp = new Map(); // name -> { transports, endpoints, agents, sources }
49
51
 
50
52
  const name = (s) => (typeof s === 'string' && NAME.test(s) ? s : null);
51
53
 
54
+ // ---- LLM usage per agent, last 180 days, bucketed by the Mac's local time ----
55
+ const SOURCES = ['claude-code', 'codex', 'opencode', 'cursor'];
56
+ const USAGE_DAYS = 180;
57
+ const since = Date.now() - USAGE_DAYS * 86400000;
58
+ const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:\/@-]{0,79}$/;
59
+ const UNNAMED_MODEL = '(unrecognized model id)';
60
+ const modelName = (s) => (typeof s === 'string' && MODEL.test(s) ? s : UNNAMED_MODEL);
61
+ const usage = {}; // agent -> accumulator, or null when the agent's history can't be read
62
+ const pad = (n) => String(n).padStart(2, '0');
63
+ const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
64
+ const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
65
+ function usageFor(agent) {
66
+ usage[agent] ??= { models: new Map(), days: new Map(), activeHours: Array(24).fill(0), weekdays: Array(7).fill(0), sessions: 0, subagents: 0 };
67
+ return usage[agent];
68
+ }
69
+ // One session's turns: `turn` counts a model call in the window; the session counts on the local date of its first one.
70
+ function sessionUsage(agent) {
71
+ const u = usageFor(agent);
72
+ let first = null;
73
+ return {
74
+ turn(time, model, t) {
75
+ const d = new Date(time);
76
+ if (Number.isNaN(d.getTime()) || d.getTime() < since) return false;
77
+ first ??= d;
78
+ const m = modelName(model);
79
+ const row = u.models.get(m) ?? { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
80
+ row.turns++;
81
+ row.inputTokens += num(t.input);
82
+ row.outputTokens += num(t.output);
83
+ row.cacheReadTokens += num(t.cacheRead);
84
+ row.cacheWriteTokens += num(t.cacheWrite);
85
+ u.models.set(m, row);
86
+ u.activeHours[d.getHours()]++;
87
+ u.weekdays[d.getDay()]++;
88
+ return true;
89
+ },
90
+ end(subagent) {
91
+ if (!first) return;
92
+ u.sessions++;
93
+ if (subagent) u.subagents++;
94
+ const day = localDate(first);
95
+ u.days.set(day, (u.days.get(day) ?? 0) + 1);
96
+ },
97
+ };
98
+ }
99
+ const usageOut = (u) => (u === null ? null : {
100
+ windowDays: USAGE_DAYS,
101
+ models: [...u.models].map(([model, r]) => ({ model, ...r })).sort((a, b) => b.turns - a.turns || a.model.localeCompare(b.model)),
102
+ sessionsPerDay: [...u.days].sort(([a], [b]) => a.localeCompare(b)).map(([date, sessions]) => ({ date, sessions })),
103
+ activeHours: u.activeHours,
104
+ weekdays: u.weekdays, // index 0 is Sunday
105
+ sessions: u.sessions,
106
+ subagents: u.subagents,
107
+ });
108
+
52
109
  function addEnvName(n, source, uses = 0, shellAssigned = 0) {
53
110
  if (!ENV_NAME.test(n)) return;
54
111
  const row = envNames.get(n) ?? { count: 0, shellAssigned: 0, sources: new Set() };
@@ -216,14 +273,29 @@ async function claudeCode() {
216
273
  }
217
274
  const transcripts = files(join(HOME, '.claude/projects'), '**/*.jsonl');
218
275
  scanned[agent].records = transcripts.length;
276
+ usageFor(agent);
277
+ const messageIds = new Set(); // one API message is written as several lines (one per content block), and resumed sessions copy them
219
278
  for (const f of transcripts) {
220
279
  let cwd = null;
221
280
  let seen = 0;
281
+ let sidechain = f.slice(join(HOME, '.claude/projects').length).split('/').includes('subagents');
282
+ const session = sessionUsage(agent);
222
283
  for await (const line of lines(f)) {
223
284
  if (!cwd && seen++ < 50) cwd = line.match(/"cwd":"([^"]+)"/)?.[1] ?? null;
224
- if (!line.includes('"type":"tool_use"')) continue;
285
+ const tools = line.includes('"type":"tool_use"');
286
+ const turn = line.includes('"usage"') && line.includes('"type":"assistant"');
287
+ if (!tools && !turn) continue;
225
288
  let msg;
226
289
  try { msg = JSON.parse(line); } catch { continue; }
290
+ if (turn && msg.type === 'assistant' && msg.message?.usage && msg.message.model !== '<synthetic>') {
291
+ const id = msg.message.id;
292
+ if (!id || !messageIds.has(id)) {
293
+ if (id) messageIds.add(id);
294
+ const t = msg.message.usage;
295
+ if (session.turn(msg.timestamp, msg.message.model, { input: t.input_tokens, output: t.output_tokens, cacheRead: t.cache_read_input_tokens, cacheWrite: t.cache_creation_input_tokens })) sidechain ||= msg.isSidechain === true;
296
+ }
297
+ }
298
+ if (!tools) continue;
227
299
  for (const b of msg.message?.content ?? []) {
228
300
  if (b.type !== 'tool_use') continue;
229
301
  if (b.name === 'Bash') commandFacts(String(b.input?.command ?? ''), agent);
@@ -231,6 +303,7 @@ async function claudeCode() {
231
303
  }
232
304
  }
233
305
  if (cwd) addRepo(cwd, null, agent);
306
+ session.end(sidechain);
234
307
  }
235
308
  log(`claude-code: ${transcripts.length} transcripts`);
236
309
  }
@@ -255,14 +328,37 @@ async function codex() {
255
328
  });
256
329
  const rollouts = [...files(root, 'sessions/**/*.jsonl'), ...files(root, 'archived_sessions/**/*.jsonl')];
257
330
  scanned[agent].records = rollouts.length;
331
+ usageFor(agent);
258
332
  for (const f of rollouts) {
333
+ const session = sessionUsage(agent);
334
+ let model = null;
335
+ let subagent = false;
336
+ let lastTotal = null;
259
337
  for await (const line of lines(f)) {
338
+ // Codex repeats a token_count event with unchanged totals (rate-limit refreshes); a call is a change in `total_token_usage`.
339
+ if (line.includes('"token_count"') || line.includes('"turn_context"') || line.includes('"session_meta"')) {
340
+ let l;
341
+ try { l = JSON.parse(line); } catch { continue; }
342
+ const p = l.payload ?? {};
343
+ if (l.type === 'session_meta') subagent = !!(p.source && typeof p.source === 'object' && 'subagent' in p.source);
344
+ else if (l.type === 'turn_context') model = p.model ?? model;
345
+ else if (l.type === 'event_msg' && p.type === 'token_count' && p.info?.last_token_usage) {
346
+ const total = JSON.stringify(p.info.total_token_usage ?? null);
347
+ if (total !== 'null' && total === lastTotal) continue;
348
+ lastTotal = total;
349
+ const t = p.info.last_token_usage;
350
+ const cached = num(t.cached_input_tokens);
351
+ session.turn(l.timestamp, model, { input: num(t.input_tokens) - cached, output: t.output_tokens, cacheRead: cached, cacheWrite: 0 });
352
+ }
353
+ continue;
354
+ }
260
355
  if (!line.includes('"exec_command"')) continue;
261
356
  let p;
262
357
  try { p = JSON.parse(line).payload; } catch { continue; }
263
358
  if (p?.type !== 'function_call' || p.name !== 'exec_command') continue;
264
359
  try { commandFacts(String(JSON.parse(p.arguments).cmd ?? ''), agent); } catch { /* arguments not JSON */ }
265
360
  }
361
+ session.end(subagent);
266
362
  }
267
363
  log(`codex: ${rollouts.length} rollouts`);
268
364
  }
@@ -272,7 +368,8 @@ function openCode() {
272
368
  for (const f of ['opencode.json', 'opencode.jsonc']) {
273
369
  for (const [n, c] of Object.entries(readJson(join(HOME, '.config/opencode', f))?.mcp ?? {})) addMcp(n, agent, `~/.config/opencode/${f}#mcp`, c);
274
370
  }
275
- withDb(join(HOME, '.local/share/opencode/opencode.db'), (d) => {
371
+ const db = join(HOME, '.local/share/opencode/opencode.db');
372
+ withDb(db, (d) => {
276
373
  for (const r of d.prepare('select directory, count(*) as n from session group by directory').all()) addRepo(r.directory, null, agent, r.n);
277
374
  const q = d.prepare(`select json_extract(data,'$.tool') as tool, json_extract(data,'$.state.input.command') as cmd,
278
375
  json_extract(data,'$.state.input.filePath') as file from part where json_extract(data,'$.type')='tool'`);
@@ -285,11 +382,34 @@ function openCode() {
285
382
  scanned[agent].records = tools;
286
383
  log(`opencode: ${tools} tool parts`);
287
384
  });
385
+ withDb(db, (d) => {
386
+ const cols = (t) => new Set(d.prepare(`pragma table_info(${t})`).all().map((c) => c.name));
387
+ const messageCols = cols('message');
388
+ const sessionCols = cols('session');
389
+ if (!messageCols.has('data')) return; // no message table: an older OpenCode that kept no usage here
390
+ usageFor(agent);
391
+ const sessionId = messageCols.has('session_id') ? 'session_id' : "json_extract(data,'$.sessionID')";
392
+ const parent = sessionCols.has('parent_id') ? 'parent_id' : sessionCols.has('data') ? "json_extract(data,'$.parentID')" : 'null';
393
+ const parents = new Map(d.prepare(`select id, ${parent} as parent from session`).all().map((r) => [r.id, r.parent != null]));
394
+ const q = d.prepare(`select ${sessionId} as session, json_extract(data,'$.modelID') as model, json_extract(data,'$.time.created') as created,
395
+ json_extract(data,'$.tokens.input') as input, coalesce(json_extract(data,'$.tokens.output'),0) + coalesce(json_extract(data,'$.tokens.reasoning'),0) as output,
396
+ json_extract(data,'$.tokens.cache.read') as cacheRead, json_extract(data,'$.tokens.cache.write') as cacheWrite
397
+ from message where json_extract(data,'$.role')='assistant' order by session, created`);
398
+ let current;
399
+ let session = null;
400
+ for (const r of q.iterate()) {
401
+ if (r.session !== current) { session?.end(parents.get(current)); current = r.session; session = sessionUsage(agent); }
402
+ session.turn(num(r.created), r.model, r);
403
+ }
404
+ session?.end(parents.get(current));
405
+ });
288
406
  }
289
407
 
290
- // Cursor's chat history is opaque blobs; its MCP config is all that is readable.
408
+ // Cursor's chat history is opaque blobs; its MCP config is all that is readable, so its usage is unknown, not zero.
291
409
  function cursor() {
292
410
  for (const [n, c] of Object.entries(readJson(join(HOME, '.cursor/mcp.json'))?.mcpServers ?? {})) addMcp(n, 'cursor', '~/.cursor/mcp.json#mcpServers', c);
411
+ usage.cursor = null;
412
+ if (existsSync(join(HOME, '.cursor')) || existsSync(join(HOME, 'Library/Application Support/Cursor'))) unreadable.add('cursor chat history (opaque)');
293
413
  }
294
414
 
295
415
  function claudeDesktop() {
@@ -297,13 +417,17 @@ function claudeDesktop() {
297
417
  for (const [n, c] of Object.entries(readJson(f)?.mcpServers ?? {})) addMcp(n, 'claude-desktop', 'claude_desktop_config.json#mcpServers', c);
298
418
  }
299
419
 
300
- export async function gather() {
301
- claudeDesktop();
302
- cursor();
303
- openCode();
304
- await Promise.all([claudeCode(), codex()]);
420
+ const READERS = { 'claude-code': claudeCode, codex, opencode: openCode, cursor };
421
+
422
+ // `source` (one of SOURCES) runs only that agent's reader; without it every reader runs, Claude Desktop's MCP config included.
423
+ export async function gather({ source } = {}) {
424
+ if (source !== undefined && !SOURCES.includes(source)) throw new Error(`--source must be one of ${SOURCES.join(', ')}`);
425
+ const run = source ? [source] : SOURCES;
426
+ if (!source) claudeDesktop();
427
+ await Promise.all(run.map((s) => READERS[s]()));
305
428
  return {
306
- scanned,
429
+ scanned: Object.fromEntries(Object.entries(scanned).filter(([a]) => run.includes(a))),
430
+ usage: Object.fromEntries(run.map((a) => [a, usageOut(usage[a] ?? null)])),
307
431
  unreadable: [...unreadable].sort(),
308
432
  programs: rows(programs, 'name'),
309
433
  mcpServers: [...mcp].map(([n, r]) => ({ name: n, transport: [...r.transports].sort(), endpoints: [...r.endpoints].sort(), agents: [...r.agents].sort(), sources: [...r.sources].sort() }))
@@ -316,7 +440,11 @@ export async function gather() {
316
440
 
317
441
  if (import.meta.url === pathToFileURL(process.argv[1]).href) {
318
442
  const t = Date.now();
319
- const facts = await gather();
320
- process.stdout.write(`${JSON.stringify(facts)}\n`);
443
+ const i = process.argv.indexOf('--source');
444
+ const facts = await gather(i === -1 ? {} : { source: process.argv[i + 1] ?? '' });
445
+ const out = process.argv.includes('--usage-only')
446
+ ? { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, usage: facts.usage, unreadable: facts.unreadable }
447
+ : facts;
448
+ process.stdout.write(`${JSON.stringify(out)}\n`);
321
449
  log(`done in ${((Date.now() - t) / 1000).toFixed(1)}s`);
322
450
  }
@@ -1,78 +1,180 @@
1
1
  ---
2
2
  name: onboarding-profile
3
- description: "Right after sign-in: ask for the Kit access you need, then work out who the user is and what to focus on from their recent Slack and GitHub, and draft schedules that keep them on track. Reads only; the one write is a pull request the user approves by merging."
3
+ description: "Right after sign-in: ask for the Kit access you need, then build a cited profile of who the user is (team, manager, sibling teams, leadership priorities, where their time goes, which models they use) by fanning out over their Slack, Notion, GitHub and LLM usage, and draft schedules that keep them on track. Reads only; the one write is a pull request the user approves by merging."
4
4
  license: MIT
5
5
  ---
6
6
 
7
- # Profile the user from Slack and GitHub
7
+ # Profile the user from Slack, Notion, GitHub and their LLM usage
8
8
 
9
- Two scripts gather facts (GitHub in your sandbox through `git__gh`, Slack inside Kit); you write the profile and the schedule drafts from those facts alone. Nothing is posted to Slack and nothing is committed; the only write is the pull request in step 6, which the user approves by merging. The user sees one short message from you at the end (step 7), not one per step. If a step cannot finish, that message says which one and what the user must do; never write the profile as if every source had been read.
9
+ The profile is built from many small reads run at the same time, not one pass. You gather the raw facts (step 1), split them into slices that each read one source or one piece of a source (step 2, wave 1), merge the slices into three areas (step 3, wave 2), and write the profile from the areas (step 4). Every claim cites a URL that is in the facts, and everything you could not read is said, never guessed.
10
10
 
11
- ## 0. Hand the Mac half to the bot on the Mac
11
+ Nothing is posted to Slack and nothing is committed; the only write is the pull request in step 6, which the user approves by merging. The user sees short walkthrough messages while the subagents work (below), one message with every Kit approve link if access is pending (step 1), and one short message with the result at the end (step 7). If a step cannot finish, the final message says which and what the user must do; never write the profile as if every source had been read.
12
12
 
13
- You run on Vercel and can't see the user's Mac. If `list_agents` shows a bot that runs `local`, post one message in this thread before step 1 that mentions it (the first one, if there are several) and asks it to run its local-onboarding skill. Name the workspace repo for its sandbox PR, or say there is none. Then go on with step 1 without waiting: its half arrives in this thread as its own posts.
13
+ The whole run is capped at 25 minutes. Every directory below is under `/tmp/onboarding-skill/`.
14
14
 
15
- With no bot that runs `local`, skip this step and say nothing about a Mac.
15
+ ## The profile answers these ten questions
16
16
 
17
- ## 1. Ask for Kit access, once
17
+ Each gets an answer with a citation or a "Could not tell" line saying what was missing.
18
18
 
19
- Kit's tools are `request_access`, `request_status` and `execute`. Hosted, you reach them through the `vercel-kit` connection: `eve__tool({ name: "vercel-kit__request_access", input: { ... } })`, and the same shape for the others. Everywhere below, "call `request_access`" means that.
19
+ 1. Which team the user is on, with their title and start date.
20
+ 2. Who they most likely report to, and who leads the team. Say "inferred" unless a message says it.
21
+ 3. What the team is working on now.
22
+ 4. The sibling teams, and how the team rolls up to leadership.
23
+ 5. What leadership says the priorities are.
24
+ 6. Where their time goes, by repo and by week, over 180 days.
25
+ 7. Their cadence: active days, weekday and hour pattern in their Slack time zone, authoring versus reviewing.
26
+ 8. The themes of their PRs, per repo.
27
+ 9. Which models and coding agents they use, how much, and when.
28
+ 10. How much of their shipped work is agent-assisted.
20
29
 
21
- Slack goes through the user's Kit connection. Before any fetch, ask for everything the run reads in one request:
30
+ Plus what today's profile already answers: what they shipped and own, what people tag them for, what they missed (unanswered mentions, requested reviews, stale or conflicted PRs), and which open work-streams to close or focus on.
22
31
 
23
- ```
24
- request_access({ service: "slack", reads: true, reason: "Read your recent channel messages, mentions and threads to write your onboarding profile" })
25
- ```
32
+ ## Subagents do the work, and you talk with the user
26
33
 
27
- - `approved`: go on.
28
- - `pending` (or `connection_required`): tell the user, in one sentence, that Slack access is waiting, with the `approveUrl`; then `request_status({ request })` until they decide. Do not start step 3 before it is approved. If they decline, the final message says the Slack half was not read.
34
+ Every collection and synthesis task runs as a subagent: the Slack base facts, every slice, every area and the profile itself. You run none of the collection scripts yourself, so you stay free to talk with the user the whole time.
29
35
 
30
- GitHub does not use Kit: it uses your `git__gh` tool (step 2). If that command fails, the final message says GitHub was not read.
36
+ You do only what a subagent can't or shouldn't:
31
37
 
32
- ## 2. GitHub facts
38
+ - the `request_access` and `request_status` calls, and the post with their approve links, because the user answers them;
39
+ - the `onboarding_scripts` tool, once, before anything starts;
40
+ - the one `gh onboarding-facts` command, because subagents never call `git__gh`;
41
+ - planning the slices (step 2), writing each reader's report to its file, and running `check-report.mjs`;
42
+ - the pull request (step 6) and every post.
33
43
 
34
- Call the `onboarding_scripts` tool once: it writes the skill's scripts into your sandbox at `/tmp/onboarding-skill/` and installs the `gh onboarding-facts` extension, which runs the GitHub facts script. Plain `gh` in your shell holds no credential, so run the extension with your `git__gh` tool, one command, with no `repository`:
44
+ ### Walk the user through the team while they wait
35
45
 
36
- ```
37
- gh onboarding-facts
38
- ```
46
+ The user has nothing to do for about ten minutes, so use that time to show them how to work here. Each time you start subagents or a result comes back, post one short walkthrough message, then wait again. Take the topics in this order, and skip one that doesn't apply:
47
+
48
+ 1. Who is on the team: each bot, its role, and whether it runs on Vercel or on their Mac (a Mac bot answers only while the Hub app is open).
49
+ 2. How a piece of work moves: they ask, the coordinator routes it, the planner posts a brief, they say yes, the builder opens a PR, the reviewer gives a verdict, and they approve the merge. Load the forms skill and draw it as a `flow`.
50
+ 3. What waits on them: the yes on a brief, merge approval, and anything that needs their eyes. Everything else, a bot does.
51
+ 4. How to talk to the team: one thread per piece of work, and an @-mention of the bot that should act next.
52
+ 5. Two or three standing decisions in effect, each in one sentence on what it means for them.
53
+
54
+ Rules for these messages:
55
+
56
+ - Take every fact from your `<team_architecture>` and standing decisions, or from `list_agents`. Never describe a feature, command or bot that isn't there.
57
+ - Each is one bubble, at most 700 characters, and follows your writing rules.
58
+ - Name bots in plain text. Never @-mention one: a mention wakes that bot.
59
+ - Say nothing about the profile's findings: those come in step 7.
60
+ - Stop once the profile subagent of step 5 returns. At most five messages in all.
61
+
62
+ When you wait, use `eve__task_wait`: it returns on a task result or on a new message from the user. If the user asks something, answer it in the thread right away, then wait again. If they ask for work, route it as you normally would; the onboarding run goes on.
63
+
64
+ ## 0. Hand the Mac half to the bot on the Mac
39
65
 
40
- It prints nothing and writes `/tmp/onboarding-skill/github-facts.json`. A failed command means GitHub was not read.
66
+ You run on Vercel and can't see the user's Mac. If `list_agents` shows a bot that runs `local`, post one message in this thread before step 1 that mentions it (the first one, if there are several) and asks it to run its local-onboarding skill, including its "LLM usage" section. Name the workspace repo for its sandbox PR, or say there is none. Go on without waiting: its half arrives in this thread as its own posts. Never poll for it.
41
67
 
42
- The file holds one JSON document: `counts` (merged PRs, open PRs, review requests, assigned issues, mentions never replied to, PRs reviewed), `repos` (merged, reviewed, open and lines changed per repo), item lists (`merged`, `open`, `reviewRequested`, `assignedIssues`, `unansweredMentions`; `total` is the full count, `items` the newest 150), `teams`, and `codeowners` lines naming the user or their teams. Open PRs carry `draft`, `reviewDecision`, `mergeable` and `checks`. `author:@me` includes PRs the user's bots open under their login. `unreadable` lists a source that could not be read.
68
+ Its LLM-usage post links one facts file per coding agent, written by `agent-usage-facts.mjs --usage-only`: names and counts, with no web permalinks. When it arrives, fetch each linked file with `fetch_upload` into `/tmp/onboarding-skill/facts/local/usage-<source>.json` (`claude-code`, `codex`, `opencode`, `cursor`). These files, not the post's table, are what the profile cites, with `fact:` references (step 5).
43
69
 
44
- ## 3. Slack facts
70
+ If the files arrive after the `llm` synthesizer of step 4 has started, its area did not read them, whether or not `facts/` still exists. Fetch them into `facts/local/` (make it if step 5 deleted `facts/`), start one `agent` copy as the `llm` synthesizer on them alone, and check its `areas/llm-local.md` with `node check-report.mjs --profile areas/llm-local.md --facts facts`. If `profile.md` is not written yet, write it from both LLM area files. If it is, put the new answer to question 9 into it in place of the old one, delete `facts/` again, and post only what changed.
45
71
 
46
- Run the script in "The Slack script" below with Kit's `execute`: pass it as the script, unchanged. It runs inside Kit, so no raw message reaches you; it returns one JSON document in about 10 seconds.
72
+ With no bot that runs `local`, skip this step and say nothing about a Mac; question 9 then rests on v0 alone, and "Could not tell" says the Mac's agent logs were not read.
47
73
 
48
- - A `ratelimited` error means Slack's search limit (about 20 a minute; the script makes at most 19). Wait 70 seconds (`sleep 70`) and run it again unchanged, up to five times.
49
- - A `suspended` result whose output says `consent_required` is a pause, not an error: tell the user in one sentence that the Slack read is waiting, with its `approveUrl`; `request_status({ request })` until they decide; then `execute({ run })` continues the same run (settled steps are not repeated).
50
- - An `output` with `refused` means Slack answered `missing_scope` (or another refusal) to a call, so no facts were read: `refused.needed` names the scope the user's Slack connection lacks (`refused.provided` what it has). Do not continue. Call `request_access({ service: "slack", capability: "search_messages", reason })`: if it returns an `approveUrl`, give it to the user; otherwise tell them to reconnect Slack in Kit allowing `refused.needed`. The final message says the Slack half was not read and names the scope.
74
+ ## 1. Access and raw facts, in one parallel step
75
+
76
+ Kit's tools are `request_access`, `request_status` and `execute`. Hosted, you reach them through the `vercel-kit` connection: `eve__tool({ name: "vercel-kit__request_access", input: { ... } })`, and the same shape for the others.
77
+
78
+ Make all of these calls in one step:
79
+
80
+ - `request_access({ service, reads: true, reason })` for each of `slack`, `notion` and `v0`, with a reason naming what the profile reads.
81
+ - The `onboarding_scripts` tool: it writes the skill's scripts into `/tmp/onboarding-skill/`, makes the `facts/`, `slices/` and `areas/` directories, and installs the `gh onboarding-facts` extension.
82
+
83
+ Then, in the next step together, start the Slack base subagent, run the GitHub command and post the first walkthrough message:
84
+
85
+ - **GitHub.** Run `gh onboarding-facts` with your `git__gh` tool, one command, no `repository`. Plain `gh` in your shell holds no credential. It reads 180 days and writes `facts/github/`: `base.json` (counts, per-repo totals, merged, open, review requests, assigned issues, unanswered mentions, teams, CODEOWNERS lines; open PRs carry `draft`, `reviewDecision`, `mergeable` and `checks`), `calendar.json` (contributions per day and week, and timestamped events), `reviews.json`, `issues.json`, and `repos/<owner>__<name>.json` for the top 5 repos by active days. `unreadable` in any file lists what could not be read. A failed command means GitHub was not read.
86
+ - **Slack base.** If Slack is `approved`, start one `agent` copy, `slack-base`, that reads `/tmp/onboarding-skill/slack-facts.callscript.js` and passes its text, unchanged, as `script` to `vercel-kit__execute`, handles the results listed below, saves the `output` whole as `facts/slack/base.json` (every list item, matching its `returned` count), and returns the time it finished (`date -u +%s`): the leadership slice's searches wait for it. Give it these rules: never call `git__gh`, never post, and on `consent_required` or `refused` return the `approveUrl`, `run` and `refused.needed` without retrying. You post the link, and once the user approves you continue the same task by its `taskId` with `execute({ run })`. It gets 8 turns.
87
+ - **One access post.** If any `request_access` came back `pending` or `connection_required`, post one message with every pending `approveUrl` and one line each on what it unlocks. Then `request_status({ request })` for each until it is decided. A declined service is a "Could not tell" line, never a retry.
88
+
89
+ Slack's search limit is about 20 a minute per user. Only two scripts in the run call `search_messages`: the base script (18 searches) and the leadership slice's `lead-posts.callscript.js` (at most 10, started at least 60 seconds after the base finished). No other script searches.
90
+
91
+ The base script's results, which the `slack-base` copy handles:
92
+
93
+ - A `ratelimited` error: wait 70 seconds (`sleep 70`) and run it again unchanged, up to five times.
94
+ - A `suspended` result whose output says `consent_required` is a pause: post its `approveUrl`, `request_status({ request })` until decided, then `execute({ run })` continues the same run.
95
+ - An `output` with `refused`: Slack answered `missing_scope` (or another refusal), so no facts were read. `refused.needed` names the scope. Call `request_access({ service: "slack", capability: "search_messages", reason })` and give the user its `approveUrl`, or tell them to reconnect Slack in Kit allowing `refused.needed`. The Slack slices don't start.
51
96
  - Any other error goes to the user in the final message. Never continue with fewer facts, and never retry around a refusal.
52
- - Save the run's `output` as `/tmp/onboarding-skill/slack-facts.json` (the checker reads it).
53
- - `window`: the last 30 days. Slack search allows 15 pages of 100 here, so a very active account is covered only back to `own.oldest` (`newestOnly: true`); state that window in the profile.
54
- - `own`: messages per channel, thread replies, recent snippets. `mentions`: `replied` means the user posted later in the same thread; `unanswered` means they did not. A mention older than what was read with no reply among it (`notJudged`) is unknown, not unanswered. `mentions.capped` means the search found more mentions (`searchTotal`) than the three pages read: say so in "Could not tell". Reactions are not read (the Slack grant has no `reactions:read`): say so in "Could not tell". `memberships`: channels the user is in.
55
- - DMs and group DMs are never searched. Every list item carries a permalink (`url`); a snippet that looked like a secret is `[withheld]`.
97
+ - What it holds: `me` (id, name, title, team, time zone, start date), `window` (30 days; `newestOnly: true` means a very active account is covered only back to `own.oldest`), `own` (messages per channel, thread replies, recent snippets), `mentions` (`replied`, `unanswered`, `notJudged`, `capped`, and `mentioners`: the user ids that mention them most). DMs and group DMs are never searched; reactions are not read.
98
+
99
+ ## 2. Plan the slices
100
+
101
+ From the files of step 1, pick the slices. A slice with no data isn't started, so the counts in the table are the most there can be.
102
+
103
+ - **Channel clusters.** Group `own.channels` into at most 3 clusters by what they are about (team, project, support, social), judged from names and snippets.
104
+ - **Team channels.** Up to 3 channels whose name or purpose matches the user's team as `me.title` names it (Slack's `team` field holds the workspace id, not a team), or where they post most. List them with `conversations_info` only if you can't tell from the name.
105
+ - **Leadership.** Any user ids the facts already show lead the team or org. The leadership slice picks its own channels (step 3).
106
+ - **People.** Up to 15 user ids from `mentions.mentioners`, most first.
107
+ - **Notion queries.** Up to 5: the team's name, the team plus "strategy" or "roadmap", and the org's name.
108
+ - **Repos.** The files in `facts/github/repos/`.
56
109
 
57
- ## 4. Write the profile and drafts
110
+ ## 3. Wave 1: one slice per source, all at once
58
111
 
59
- Write the profile to `/tmp/onboarding-skill/profile.md`. It names the user's repos and colleagues, so it is posted only in this thread and never committed.
112
+ Start every slice in one step, then wait. Each slice reads only its own inputs and writes `slices/<name>.md`: its findings as list items, each citing a URL from its facts, and a `## Could not tell` section. Tell each slice the user's login, Slack name, time zone and the window, the exact file paths it reads, the path it writes, and this format.
60
113
 
61
- Answer, in the user's terms:
114
+ | Area | Slice | Count | Reads | Runs as |
115
+ |---|---|---|---|---|
116
+ | Role | `own-<cluster>`: what the user writes about and does in that cluster | up to 3 | `facts/slack/base.json` | `reader` |
117
+ | Role | `mentions`: who asks the user for what, and what went unanswered | 1 | `facts/slack/base.json` | `reader` |
118
+ | Role | `team-<channel>`: what the team is doing, who posts, who leads | up to 3 | Kit, `team-channel.callscript.js` | `agent` |
119
+ | Role | `leadership`: what the leads and announcement channels say the priorities are | 1 | Kit, `leadership.callscript.js` then `lead-posts.callscript.js` | `agent` |
120
+ | Role | `people`: titles and teams of the 15 people the user works with most | 1 | Kit, `people.callscript.js` | `agent` |
121
+ | Role | `notion`: strategy and roadmap pages for the team and org | 1 | Kit, `notion.callscript.js` | `agent` |
122
+ | Work | `repo-<owner>__<name>`: active days, cadence, authoring versus reviewing, PR themes | up to 5 | `facts/github/repos/<owner>__<name>.json` | `reader` |
123
+ | Work | `reviews`: whose work the user reviews, where, how deeply | 1 | `facts/github/reviews.json` | `reader` |
124
+ | Work | `issues`: issues opened and joined | 1 | `facts/github/issues.json` | `reader` |
125
+ | Work | `calendar`: weekly volume, weekday and hour pattern in the user's time zone, gaps | 1 | `facts/github/calendar.json` | `reader` |
126
+ | LLM | `agent-assisted`: share of merged PRs and commits with an agent `Co-authored-by:` or a "Generated with" line | 1 | `facts/github/repos/` | `reader` |
127
+ | LLM | `v0`: v0 chats and usage over 180 days | 1 | Kit, `v0.callscript.js` | `agent` |
62
128
 
63
- - What they have shipped, what they own, what they are responsible for.
64
- - What people tag them for, and what they write about.
65
- - What they have missed: unanswered mentions, requested reviews, stale or conflicted PRs.
66
- - Which open GitHub work-streams to close (stale, conflicted, superseded) and which to focus on.
129
+ **Reader slices** (`reader` tool) are far cheaper and need no Kit. A reader never writes files, so ask it for its findings as `evidence` lines, each a claim with the URL it rests on, and its gaps in `open`. Write each report to `slices/<name>.md` yourself, unchanged: evidence lines become list items, `open` becomes the `## Could not tell` section. Give each a budget of 4 turns.
130
+
131
+ **Kit slices** (`agent` copies) each run one script: read `/tmp/onboarding-skill/<script>` and pass its text as `script` to `vercel-kit__execute`, with the slice's `input`:
132
+
133
+ - `/tmp/onboarding-skill/team-channel.callscript.js`: `{ channel, days: 30 }`, the channel by id. If Kit answers that the script read more than it can keep, run it once more with `days: 14` and say so in the slice.
134
+ - `/tmp/onboarding-skill/leadership.callscript.js`, run twice. First with `{}`: it returns the user's channel memberships, and the slice picks up to 3 announcement or leadership channels from them (names like `announcements`, `eng-leads`, the team's `-leads` channel); this output is not saved. Then with `{ channels, days: 90 }`, channels by id: it reads the channels and returns their 20 most frequent posters as `candidates`, with title, team, `isBot`, `deleted` and post count, and searches nothing. The slice saves it as `facts/kit/leadership.json`, then judges from the candidates and the channels' messages who leads (what they announce, their titles, who others answer to), up to 10, with the known leads from step 2 first. Posting often is not leading: a bot, a deleted user or someone who only relays reminders is not a lead. It writes one line per chosen lead on why.
135
+ - `/tmp/onboarding-skill/lead-posts.callscript.js`: `{ leads, days: 90 }`, the ids the slice chose. Before it runs, the slice sleeps until 60 seconds after the base script finished (give it that time as a Unix timestamp). It saves the output as `facts/kit/lead-posts.json`.
136
+ - `/tmp/onboarding-skill/people.callscript.js`: `{ users }`
137
+ - `/tmp/onboarding-skill/notion.callscript.js`: `{ queries }`
138
+ - `/tmp/onboarding-skill/v0.callscript.js`: `{ days: 180 }`
139
+
140
+ The slice saves the run's `output` as `facts/kit/<slice>.json` whole, every list item included: each list's length sits beside it in `returned`, and `check-report.mjs` fails a facts file under `facts/slack/` or `facts/kit/` whose list is shorter than its `returned` or has no `returned` count. The scripts keep their output to about 50 messages so a slice can copy all of it. It then writes `slices/<slice>.md` from it, and returns. Tell each Kit slice: never call `git__gh`, never post, never call `search_messages` outside its script; on `ratelimited` wait the `retry_after` once and run again, then record the gap; on `consent_required` or `access_denied` stop and return the `approveUrl` without retrying. Post that link, and restart the slice once after the user approves. Start a Kit slice only once its service is approved. Each gets 4 turns, and the leadership slice, which runs three scripts, gets 8.
141
+
142
+ This skill names its own count: up to 20 hosted slices run at once, overriding the fanout skill's cap of 3, because every slice only reads and writes its own file.
143
+
144
+ Wait with `eve__task_wait`. A slice still running 10 minutes after it started is cancelled with `eve__task_cancel`, and its area says it timed out.
145
+
146
+ Then check every slice against its facts and have any slice with problems fixed once:
147
+
148
+ ```sh
149
+ cd /tmp/onboarding-skill && node check-report.mjs --profile slices/*.md --facts facts
150
+ ```
151
+
152
+ ## 4. Wave 2: one synthesizer per area, all at once
153
+
154
+ Start three `agent` copies in one step. Each reads its area's slice files and writes `areas/<area>.md` with list items that keep the slices' citations, a `## Conflicts` section where slices disagree (state both, don't pick one quietly), and a `## Could not tell` section. Each gets 6 turns and is cancelled at 10 minutes.
155
+
156
+ - `role`: the role slices. Answers questions 1 to 5: team, title, start date, likely manager and team lead, what the team is doing, sibling teams, roll-up to leadership, leadership's priorities.
157
+ - `work`: the work slices and `facts/github/base.json`. Answers questions 6 to 8, plus what they shipped and own, review requests, and which open PRs to close (stale, conflicted, superseded) or focus on.
158
+ - `llm`: the LLM slices, and `facts/local/` if the Mac bot's files have arrived. Answers questions 9 and 10. A claim from a local file cites the value it rests on as `fact:local/usage-<source>.json#<JSON pointer>`, for example `fact:local/usage-codex.json#/usage/codex/models/0`; the times are in the file's `timeZone`.
159
+
160
+ ```sh
161
+ cd /tmp/onboarding-skill && node check-report.mjs --profile areas/*.md --facts facts
162
+ ```
163
+
164
+ ## 5. Write the profile and drafts
165
+
166
+ Start one `agent` copy, `profile`, that does this whole step and returns the path of `profile.md` and of each draft. Give it the rules below, these paths, and 8 turns; it never calls `git__gh` and never posts.
167
+
168
+ It writes `profile.md` from the three area files. It names the user's repos and colleagues, so it is posted only in this thread and never committed. Link facts across areas: the team from Slack with the repos from GitHub, the priorities from leadership with the PRs that serve them. Answer the ten questions and today's four, in the user's terms.
67
169
 
68
170
  Rules:
69
171
 
70
172
  - Judge; the scripts only counted. No scoring formula and no thresholds from code: say why in a sentence.
71
- - Every list item cites at least one permalink that appears in the facts, and no URL appears that isn't in the facts. State counts as the facts give them, with the window.
173
+ - Every list item cites at least one URL that is in the facts, or a `fact:` reference to a value in `facts/local/`, and no URL or reference appears that isn't. Never cite a web URL for a local count. State counts as the facts give them, with the window.
72
174
  - Don't quote messages beyond the snippets, and don't name people beyond what a claim needs.
73
- - Add a "Could not tell" section: every `unreadable` source, `notJudged` mentions, that reactions were not read, the Slack window, a Kit grant the user declined.
175
+ - A `## Could not tell` section: every `unreadable` source, `notJudged` mentions, that reactions were not read, the Slack window, a Kit grant the user declined, a slice that timed out, the Mac half if it hasn't reported.
74
176
 
75
- For each recurring need you found (a morning pass over unanswered mentions, a weekly stale-PR pass), write one draft to `/tmp/onboarding-skill/schedules/<bot>/<id>.md` for an existing bot of your workspace:
177
+ For each recurring need you found (a morning pass over unanswered mentions, a weekly stale-PR pass), write one draft to `schedules/<bot>/<id>.md` for an existing bot of your workspace:
76
178
 
77
179
  ```md
78
180
  ---
@@ -84,13 +186,11 @@ The prompt the bot runs.
84
186
 
85
187
  `cron` is five fields in UTC on a hosted deploy. Each draft says in its prompt which facts it reads and what it reports.
86
188
 
87
- ## 5. Check
88
-
89
189
  ```sh
90
- node /tmp/onboarding-skill/check-report.mjs --profile /tmp/onboarding-skill/profile.md --facts /tmp/onboarding-skill/github-facts.json /tmp/onboarding-skill/slack-facts.json
190
+ cd /tmp/onboarding-skill && node check-report.mjs --profile profile.md --facts facts
91
191
  ```
92
192
 
93
- Fix every line it prints and rerun until it prints nothing. Then delete the two facts files.
193
+ It fixes every line the checker prints and reruns until it prints nothing. When it returns, run the checker on `profile.md` yourself once more; it must print nothing. Keep `profile.md`, then delete `facts/`.
94
194
 
95
195
  ## 6. Open the pull request, if the workspace has a repo
96
196
 
@@ -101,13 +201,13 @@ When the workspace is backed by a GitHub repo, open one PR on it that adds only
101
201
  The user reads it cold, often only its first line (your writing rules). Write it to `/tmp/onboarding-skill/post.md` in this shape, load the forms skill for the tables, and check it:
102
202
 
103
203
  ```md
104
- One line on who they are and what they own, then the one thing to do first.
204
+ One line on who they are, their team and what they own, then the one thing to do first.
105
205
 
106
206
  ---
107
207
 
108
208
  | Act on first | Why | Link |
109
209
  |---|---|---|
110
- (three rows, each linking a permalink from the facts)
210
+ (three rows, each linking a URL from the facts)
111
211
 
112
212
  | Schedule | Bot | When | What it reports |
113
213
  |---|---|---|---|
@@ -131,7 +231,3 @@ Fix every line it prints and rerun until it prints nothing, then post the file's
131
231
  - Nothing else goes in: not the profile, the "Could not tell" section, counts, windows, or how the run went. Keep `profile.md`, and post it, or the part asked for, only when the user asks.
132
232
 
133
233
  Only the schedule files ever leave the thread.
134
-
135
- ## The Slack script
136
-
137
- <!-- script:slack-facts.callscript.js -->