eve-hub 0.5.0 → 0.5.1-pre.1

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.
@@ -54,7 +54,8 @@ export async function publish({ type: typeName, key, source }, { sandbox, sessio
54
54
  const unique = [...new Map(files.map((f) => [f.sha, f])).values()];
55
55
  for (let i = 0; i < unique.length; i += UPLOADS_AT_ONCE) {
56
56
  await Promise.all(unique.slice(i, i + UPLOADS_AT_ONCE).map((f) =>
57
- hub(`${at}/files/${f.sha}`, { method: "PUT", headers: { "content-type": "application/octet-stream" }, body: f.data, signal: AbortSignal.timeout(120_000) }, fetchImpl),
57
+ // A file body is the bytes alone, so the session rides in the header.
58
+ hub(`${at}/files/${f.sha}`, { method: "PUT", headers: { "x-hub-session": sessionId, "content-type": "application/octet-stream" }, body: f.data, signal: AbortSignal.timeout(120_000) }, fetchImpl),
58
59
  ));
59
60
  }
60
61
  return hub(`${at}/v/${version}`, {
package/hub-path.mjs CHANGED
@@ -3,8 +3,7 @@
3
3
  // authored modules into the workspace's node_modules/.cache, where
4
4
  // import.meta.url and import.meta.dirname point into the cache; there it goes
5
5
  // through the workspace's link to the kit (node_modules/eve-hub, which
6
- // the daemon keeps). Used for hub skills (hub-skill.mjs) and the explainer
7
- // package that artifact builds shell out to (artifacts/types/change-explainer.mjs).
6
+ // the daemon keeps).
8
7
  import { existsSync, realpathSync } from "node:fs";
9
8
  import { dirname, join } from "node:path";
10
9
  import { fileURLToPath } from "node:url";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.5.0",
3
+ "version": "0.5.1-pre.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
6
6
  "type": "module",
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-explainer-assets.mjs from packages/explainer/skills/change-explainer/SKILL.md. Do not edit.
2
+ export default {"description":"Use when asked to explain a code change (a PR, a stack of PRs, or a base..head commit range) as a published, narrated explainer - a synthesis overview, a module figure, and one test-backed sequence walkthrough per change, every claim pinned to a file and line.","markdown":"# Change explainer\n\nYou turn a change (repo + base + head) into a **fixture**: three kinds of JSON files that `packages/explainer` (hub repo) validates, renders and publishes to a protected Vercel URL. The reader's goal is to *understand* the change, from a 30-second skim down to the exact line. Everything you write is checkable against code at a pinned commit; nothing is decoration.\n\nThe reader should finish able to answer: what changed and why it matters; how a request moves through the new code, module by module; what behaved differently before; which test proves it.\n\n## Ground rules\n\n- **Read-only on the change.** Never comment on, push to, rebase, review or otherwise touch the PRs or their branches. Never edit the author's checkouts. Read from a clone or `git worktree` you own (`git worktree add --detach <dir> <head>`), or from any repo object store with `git show <sha>:<path>` (that is what `build-data.mjs` does).\n- **Pin commits.** Full or 9+ char shas for base, head and every PR boundary. Never cite a branch name as evidence.\n- **Code is the only source of truth.** PR descriptions, commit messages and thread discussion tell you where to look; they are not evidence. If you cannot cite it at head (or base, for old behavior), don't claim it.\n- **No invented motivation.** Say what the code does. Say *why* only when a comment, test name, commit message or doc states it, and cite that.\n\n## Workflow\n\nWork in this order. Each step has an exit check; don't move on until it passes.\n\n### 1. Gather the stack and the diff\n\n```\ngit log --reverse --format='%h %s' <base>..<head>\ngit diff --stat -M <base> <head>\ngit diff -M <base> <head> -- <path> # per file, as you read\n```\n\n- **PRs:** list each PR's `from..to` range. For a single PR, one entry. For a stack, one per PR, contiguous (`pr[i].to == pr[i+1].from`). For a commit range with no PR, use the PR that carries the branch (or the closest one) and say so in `title`.\n- **Merges from main pollute a range.** If `git diff --stat` shows files the change never meant to touch, narrow head to before the first `Merge main into …` commit, or pick a range whose first-parent history has no merges. Record the choice in `fixture.json` `title`.\n- **Rebuilt history:** if commits were recreated (rebased with conflicts, squashed), don't explain by commit. Explain by behavior; commits are only labels.\n\n*Exit:* you can state base, head, each PR range, commit count and file count.\n\n### 2. Map the modules\n\nRead every changed file at head (and the old version of each modified or deleted file at base). Build a list:\n\n- module short name (`intake`, `reducer`, `queues`, `Forks`), path, status (added / changed / deleted / unchanged-but-central)\n- what it exports that the change uses, and who calls it (`git grep -n '<symbol>' <head> -- <dir>`)\n- edges: \"A calls B.fn\" with the call site's file:line\n\nKeep 6-12 nodes. Fold leaf helpers into their caller. Include an unchanged module only when the new flow runs through it.\n\n*Exit:* every changed source file belongs to a node, or you can say why it doesn't (tests, docs, fixtures). Read every file the diff touches, ordered by changed lines; don't narrow to one directory because it looks central. The files with the most changed lines are where the change actually is, and new or deleted modules (`git diff --name-status` A/D) are almost always overview material.\n\n### 3. Choose the 5-8 changes\n\nGroup by **theme** (a behavior or boundary), never by PR or commit. A good change answers: *what behaved one way before, and how does it behave now?* Order them along the flow of a request (entry → decision → effect → persistence → guard), so the overview reads top to bottom as one story.\n\nFor each: title (≤ 72 chars, a verb phrase), one-sentence gist, a one-line was→now (≤ 22 words per side), a longer summary/before/after for the Details disclosure, head refs, base refs (or `before` starting with \"New:\"), PRs, commits, files, modules.\n\nThen write the **thesis**: at most 3 sentences, ≤ 90 words, what the change does as a whole and the one or two consequences that matter most. Write it last.\n\n- **`before` is behavior, not imports.** \"Intake imported approval helpers\" says nothing; \"a typed `approve` from any principal settled the approval\" does. Read the base code until you can say what a caller observed.\n- **Commits** in `changes[].commits` are full 40-char shas (`git log --format=%H`).\n\n*Exit:* each change has a distinct before/after; no two changes describe the same code; `build-data` prints no `coverage:` warning naming a file with more than ~100 changed lines, and the uncovered share is under ~20% of changed source lines. Deleted and renamed modules count: if the change removes `coordinator.ts`, some change says what replaced it.\n\n### 4. Find a test for every change\n\nEach change gets a walkthrough built from **one real test at head**:\n\n```\ngit grep -n -E \"(test|it|describe)\\(\" <head> -- '*.test.*' | grep -i <keyword>\n```\n\nPrefer, in order: a test that drives the full path across modules (integration, e2e, store reload); a unit test of the deciding function. Read the test body. The walkthrough follows what the test *executes*, from its setup through the assertion.\n\n- Steps the test runs: `\"evidence\": \"test\"`.\n- Steps you need for the story but the test doesn't execute (the runtime caller, the effect host, the UI that renders the result): `\"evidence\": \"runtime\"`, with a `note` saying what is and isn't shown. The viewer draws these dotted and tags them \"runtime context\".\n- If more than half a flow is runtime context, look for a better test. If there is none, say so in the sequence `summary`.\n\n- **At least one trunk flow** starts at the runtime entry point (the request handler, the tool loop, the server route) and runs through the changed modules to the observable result, using an integration or e2e test if one exists. Rule-level unit tests explain decisions; they don't show how a request reaches them. If no test drives the trunk, write it with `evidence: \"runtime\"` steps and say so.\n\n*Exit:* every change has `provenance.test` (full name, `describe › it`) and a test ref with line range.\n\n### 5. Write the sequences\n\nOne file per flow in `sequences/<id>.json`. A sequence shows **calls between modules**, in execution order.\n\n- **Lanes:** ≤ 8 participants, each a module (the same short names as the figure) or an actor (`test`, `user`, `browser`). `short` ≤ 14 chars.\n- **Arrows:** `call` is the bare function or event name (`beforeStep`, `message.received`), ≤ 36 chars, no parentheses. Arguments go in `args` (`\"(view, checked)\"`); context goes in `note`.\n- **Kinds:** `call` (solid), `return` (dashed; the value that drives the next decision, e.g. `{ kind: \"stop\" }`), `self` (a loop; local work that matters).\n- **No self-call flows.** At most 40% self messages and at least 3 distinct cross-module hops. If a flow is mostly one module talking to itself, the lanes are wrong: split the module into the files or functions the calls cross, or pick a flow that crosses boundaries. (A flow of `effects → effects → effects` explains nothing.)\n- **Pin every step** to the line that performs it: the call site for a `call`, the `return` statement for a return, the body for a self step. Line ranges ≤ ~15 lines, so the highlight lands on something specific.\n- **Notes carry the concrete example:** real values from the test (`sender: BOB`, `text: \"approve\"`, the queued message's text), what state looks like entering and leaving, and the old behavior where it differs.\n- 6-16 messages. If a flow needs more, it's two flows.\n\nLink each change to its flow with `\"sequence\": { \"id\": \"<seq id>\", \"msg\": \"m1\" }`. A change may start mid-flow in another change's sequence only when it really is the same path; prefer a flow of its own.\n\n### 6. Write the module figure\n\n`overview.architecture`: `nodes` (`id`, short `label`, `group` (directory), `ref` (the file at its main export), `status`) and `edges` (`from`, `to`, short `label` = the function called, `ref` = the call site). Edges point caller → callee. Every module named in a change's `modules` must be a node. The layout (ELK, layered, orthogonal) is automatic; your job is fewer, truer edges.\n\n### 7. Validate, then fix, until clean\n\nCall `artifact_publish` with type `change-explainer` and source `{ fixture, repo, scip? }` (paths inside your sandbox). It runs the bundled `lint-fixture` and `build-data` scripts in your sandbox, then publishes the viewer. No hub checkout or dependency installation is needed. Fix reported errors and publish again.\n\n- `lint-fixture` checks the editorial rules above (thesis length, 5-8 changes, gist and was→now, tests, short calls, self-call share, evidence on every step). Zero errors required; read the warnings.\n- `build-data` checks every ref resolves at its side (head/base), line ranges are inside the file, PRs and commits are in range, modules exist, every change has a walkthrough. It exits 1 with the list.\n\nLoop until both pass. Don't silence a rule by weakening a claim's evidence; fix the claim.\n\n**Code intelligence (TypeScript, optional but recommended):** hover and go-to-definition come from a SCIP index of head:\n\n```\ngit worktree add --detach /tmp/<id>-head <head>\ncd /tmp/<id>-head/<package> && pnpm install --frozen-lockfile\nnpx @sourcegraph/scip-typescript index --output /tmp/<id>.scip\n```\n\nSet `fixture.json` `indexRoot` to `<package>` (the path the index is relative to). Skip it for non-TS changes; everything else works without it.\n\n### 8. Independent claim check\n\nHand the fixture to a **separate agent** that did not write it, with only: the repo, base, head, and the fixture files. Its job: for every sentence in thesis, gist, shift, summary, before, after, notes and every message `note`, open the cited lines and mark it **correct**, **wrong** (with the correct statement and ref) or **unsupported** (no ref proves it). It also checks each `evidence: \"test\"` step really runs in the named test. The first hand-built explainer had 18 wrong claims at this stage; expect some.\n\nIf you can't start a separate agent (some hosts don't allow nested agents), finish steps 1-7, publish, and say plainly in your report that the claim check hasn't run so the caller runs it. Never substitute a self-review and call it independent.\n\nFix every wrong and unsupported item (correct it, re-cite it, mark it runtime, or delete it), then re-run step 7.\n\n### 9. Publish\n\nPublish with the agent-kit tool, which also posts the live card in your thread:\n\n```\nartifact_publish({ type: \"change-explainer\", key: \"<id>\",\n source: { fixture: \"<abs fixture dir>\", repo: \"<abs git checkout>\", scip?: \"<abs .scip>\", branch?: \"<ref>\" } })\n```\n\nPaths are in your sandbox: the tool builds the site there, uploads its files to the hub, and the hub deploys it to the protected `hub-artifacts` project with the workspace's Vercel sign-in. It returns `url` (this version) and `alias` (stable, always the latest version). Report both.\n\n## Working practice\n\n- **Write as you go:** `fixture.json` first, then `overview.json`, then each sequence file as soon as it's drafted; lint after each. A run that's interrupted keeps its work. (Lint reports sequences you haven't written yet; that's expected until the end.)\n- **The explainer describes the change, not your process.** No sentences about what you ran, skipped or were told (\"claim checking was skipped\", \"not a report of tests executed\"). The claim check flags these as unsupported.\n- **Shared test state:** when a test depends on state from earlier cases or a shared `before`, put that setup in the sequence `summary` and cite it.\n- **Helpers in the test lane:** scenario builders and fakes belong to the `test` actor; name them in `args`/`note`, not as lanes.\n\n## Fixture format\n\n`fixtures/<id>/fixture.json`\n\n```json\n{ \"id\": \"fork-queue\", \"title\": \"Fork vs. queue for busy bots\",\n \"repo\": \"owner/name\", \"base\": \"a89e4b676\", \"head\": \"571cd6e5b\",\n \"indexRoot\": \"packages/eve\",\n \"prs\": [{ \"number\": 75, \"title\": \"…\", \"from\": \"a89e4b676\", \"to\": \"571cd6e5b\" }] }\n```\n\n`fixtures/<id>/overview.json`\n\n```json\n{\n \"thesis\": \"≤ 3 sentences.\",\n \"thesisRefs\": [{ \"label\": \"beforeStep\", \"path\": \"src/a.ts\", \"line\": 29, \"endLine\": 34, \"side\": \"head\" }],\n \"changes\": [{\n \"id\": \"c1-intake\", \"theme\": \"Intake\",\n \"title\": \"Keep live intake, replace its decision engine\",\n \"gist\": \"One sentence on what changed.\",\n \"shift\": { \"before\": \"Intake imported approval helpers\", \"after\": \"Intake runs beforeStep and applies its decision\" },\n \"summary\": \"2-3 sentences for Details.\",\n \"before\": \"Old behavior, concretely.\", \"after\": \"New behavior, concretely.\",\n \"refs\": [{ \"label\": \"apply decision\", \"path\": \"src/intake.ts\", \"line\": 257, \"endLine\": 303, \"side\": \"head\" }],\n \"beforeRefs\": [{ \"label\": \"old imports\", \"path\": \"src/intake.ts\", \"line\": 20, \"endLine\": 34, \"side\": \"base\" }],\n \"afterRefs\": [],\n \"tests\": [{ \"label\": \"full test name\", \"path\": \"src/a.test.ts\", \"line\": 444, \"side\": \"head\" }],\n \"prs\": [4343], \"commits\": [\"<full sha>\"],\n \"files\": [\"src/intake.ts\"], \"modules\": [\"intake\", \"decisions\"],\n \"sequence\": { \"id\": \"seq-intake-steering\", \"msg\": \"m1\" }\n }],\n \"architecture\": {\n \"nodes\": [{ \"id\": \"intake\", \"label\": \"intake\", \"group\": \"harness/hitl\", \"status\": \"changed\",\n \"ref\": { \"path\": \"src/intake.ts\", \"line\": 62, \"side\": \"head\" } }],\n \"edges\": [{ \"from\": \"intake\", \"to\": \"decisions\", \"label\": \"beforeStep\",\n \"ref\": { \"path\": \"src/intake.ts\", \"line\": 227, \"side\": \"head\" } }]\n },\n \"notes\": { \"text\": \"Optional: caveats the reader must know.\", \"refs\": [] }\n}\n```\n\n`fixtures/<id>/sequences/<seq id>.json`\n\n```json\n{\n \"id\": \"seq-typed-approve\",\n \"title\": \"Bob types \\\"approve\\\" on Alice's approval\",\n \"summary\": \"Setup, what happens, and what the test asserts, in 2-3 sentences.\",\n \"provenance\": { \"kind\": \"test\", \"test\": \"describe › it\",\n \"ref\": { \"path\": \"src/x.test.ts\", \"line\": 9, \"endLine\": 18 } },\n \"participants\": [\n { \"id\": \"test\", \"label\": \"Regression test\", \"short\": \"test\", \"kind\": \"actor\" },\n { \"id\": \"intake\", \"label\": \"hitl/intake.ts\", \"short\": \"intake\", \"ref\": { \"path\": \"src/intake.ts\", \"line\": 62 } }\n ],\n \"messages\": [\n { \"id\": \"m1\", \"from\": \"test\", \"to\": \"intake\", \"kind\": \"call\",\n \"call\": \"acceptHumanInput\", \"args\": \"(step, input)\", \"evidence\": \"test\",\n \"ref\": { \"path\": \"src/x.test.ts\", \"line\": 12, \"endLine\": 13 },\n \"note\": \"Concrete values, state in and out.\" },\n { \"id\": \"m2\", \"from\": \"intake\", \"to\": \"test\", \"kind\": \"return\",\n \"call\": \"stop\", \"args\": \"{ kind: \\\"stop\\\" }\", \"evidence\": \"test\",\n \"ref\": { \"path\": \"src/intake.ts\", \"line\": 129 } }\n ]\n}\n```\n\nAll `ref`s without `side` are head. `line`/`endLine` are 1-based, inclusive.\n\n## Writing\n\n- Short sentences, subject-verb-object, most under 28 words. Name the actor on every state change.\n- Concrete over abstract: real function names, real values from tests, real file:line.\n- Define a term once, reuse it exactly. Same names in the figure, the overview and the sequences.\n- State persistence, network and side effects at the step where they execute.\n- No rhetorical questions, metaphors for named constructs, \"just/simply/obviously\", or marketing tone.\n- The overview must be skimmable in 30 seconds: title + gist + was→now per change. Depth lives in Details and the walkthroughs.\n\n## Done\n\n- `lint-fixture` 0 errors, `build-data` passes.\n- The independent claim check found nothing left wrong or unsupported.\n- Published; you report the version URL, the alias, the base..head range, and any flows that are mostly runtime context.\n"};
@@ -1,6 +1,7 @@
1
1
  import { defineSkill } from "eve/skills";
2
- import { hubSkill } from "../hub-skill.mjs";
3
- // The change-explainer skill, from hub's
4
- // packages/explainer/skills/change-explainer/SKILL.md (see hub-skill.mjs). A
5
- // bot re-exports this as agent/skills/change-explainer.ts.
6
- export default defineSkill(hubSkill("packages/explainer/skills/change-explainer"));
2
+ import skill from "./change-explainer.gen.mjs";
3
+ // The change-explainer skill: packages/explainer/skills/change-explainer/SKILL.md,
4
+ // embedded at build time (scripts/build-explainer-assets.mjs), so it loads the
5
+ // same on the hub's machine and in a deployed bot. A bot re-exports this as
6
+ // agent/skills/change-explainer.ts.
7
+ export default defineSkill(skill);
package/hub-skill.mjs DELETED
@@ -1,29 +0,0 @@
1
- // Skills that live in the hub checkout (packages/*/skills/<name>/SKILL.md),
2
- // served to workspace bots through the kit so a bot never carries a copy that
3
- // drifts. A bot re-exports one as agent/skills/<name>.ts; eve evaluates that
4
- // module at compile time and stores the markdown in the bot's manifest.
5
- //
6
- // The checkout is found through hub-path.mjs (bundled kit code can't trust
7
- // import.meta.url).
8
- import { join } from "node:path";
9
- import { readFileSync } from "node:fs";
10
- import { hubPath } from "./hub-path.mjs";
11
-
12
- export const hubSkillFile = (rel, options) => join(hubPath(rel, { ...options, probe: "SKILL.md" }), "SKILL.md");
13
-
14
- // SKILL.md -> { description, markdown } for defineSkill. eve names the skill
15
- // after the stub's file name, so frontmatter `name` is dropped; `description`
16
- // is required (it is the routing hint eve advertises).
17
- export function parseSkillMarkdown(text, source = "SKILL.md") {
18
- const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
19
- if (!m) throw new Error(`${source} has no frontmatter`);
20
- const line = m[1].split(/\r?\n/).find((l) => /^description:\s*/.test(l));
21
- const description = line?.replace(/^description:\s*/, "").replace(/^(["'])(.*)\1$/, "$2").trim();
22
- if (!description) throw new Error(`${source} has no description in its frontmatter`);
23
- return { description, markdown: text.slice(m[0].length).trim() + "\n" };
24
- }
25
-
26
- export function hubSkill(rel, options) {
27
- const file = hubSkillFile(rel, options);
28
- return parseSkillMarkdown(readFileSync(file, "utf8"), file);
29
- }