From 96f811a136f3f2e3ad89c23dcb5c5bd060725a75 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 18:55:11 +0000 Subject: [PATCH 01/18] Build the agent pages from src/ modules docs/kernel-agent.html and docs/kernel-agent-mobile.html are now built by scripts/build.mjs from src/. Each is still one self-contained file. - src/agent/desktop.html and mobile.html are the page templates. "" lines pull in CSS, shared markup, and JavaScript. - The one large script is split along its existing section markers into 60 files under src/agent/js/{notebook,agent,app,mobile}/. They are concatenated in name order into the same single function scope as before. - The Python kernel harness is src/agent/python/harness.py, plain Python that the build escapes into the page. Its five \u escapes are now doubled so Python, rather than JavaScript, decodes them; the text is unchanged. - Mobile shares desktop's .prompt-acts wrap rule, so layout.css is one file. The split was verified mechanically: before the header line was added, the build reproduced both pages byte for byte. The only change to the built pages is that header, which says where to edit. build.mjs replaces sync_agent_builds.mjs: the parts both builds share now exist once in src/ instead of being copied from desktop into mobile. CI runs "node scripts/build.mjs --check". src/README.md maps the tree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S9PfSmbaV1CY9ZawtfbiEm --- .github/workflows/verify.yml | 2 +- AGENT-V2-SPEC.md | 3 +- README.md | 19 +- docs/kernel-agent-mobile.html | 13 +- docs/kernel-agent.html | 11 +- scripts/build.mjs | 77 ++++ scripts/sync_agent_builds.mjs | 105 ----- src/README.md | 25 + src/agent/css/agent.css | 104 +++++ src/agent/css/base.css | 431 ++++++++++++++++++ src/agent/css/desktop.css | 7 + src/agent/css/layout.css | 85 ++++ src/agent/css/mobile.css | 130 ++++++ src/agent/desktop.html | 225 +++++++++ src/agent/js/agent/010-overview.js | 52 +++ src/agent/js/agent/020-run-ledger.js | 66 +++ src/agent/js/agent/030-threads.js | 82 ++++ src/agent/js/agent/040-state-chip.js | 17 + src/agent/js/agent/050-tools.js | 33 ++ src/agent/js/agent/060-marshaling.js | 40 ++ src/agent/js/agent/070-kernel-helpers.js | 27 ++ src/agent/js/agent/080-approvals.js | 14 + src/agent/js/agent/090-executor.js | 176 +++++++ src/agent/js/agent/100-system-prompt.js | 20 + src/agent/js/agent/110-context-budget.js | 47 ++ src/agent/js/agent/120-wire-formats.js | 104 +++++ src/agent/js/agent/130-loop.js | 58 +++ src/agent/js/agent/140-settings.js | 28 ++ src/agent/js/agent/150-run-control.js | 26 ++ src/agent/js/agent/160-composer.js | 48 ++ src/agent/js/agent/170-forking.js | 52 +++ src/agent/js/agent/180-open-toggle.js | 12 + src/agent/js/app/010-live-theme.js | 11 + src/agent/js/app/020-init.js | 11 + src/agent/js/mobile/controller.js | 126 +++++ src/agent/js/mobile/pwa.js | 81 ++++ src/agent/js/notebook/010-constants.js | 5 + src/agent/js/notebook/020-harness.js | 5 + src/agent/js/notebook/030-icons.js | 9 + src/agent/js/notebook/040-state.js | 10 + src/agent/js/notebook/050-panels.js | 28 ++ src/agent/js/notebook/060-split-outputs.js | 129 ++++++ src/agent/js/notebook/065-ui-helpers.js | 29 ++ src/agent/js/notebook/070-highlight.js | 22 + src/agent/js/notebook/080-markdown.js | 60 +++ src/agent/js/notebook/090-cell-model.js | 139 ++++++ src/agent/js/notebook/100-collapse.js | 29 ++ src/agent/js/notebook/110-outputs.js | 90 ++++ src/agent/js/notebook/120-cell-lookup.js | 15 + src/agent/js/notebook/130-selection.js | 57 +++ src/agent/js/notebook/140-editor-keys.js | 48 ++ src/agent/js/notebook/150-structural-edits.js | 51 +++ src/agent/js/notebook/160-execution.js | 152 ++++++ src/agent/js/notebook/170-worker.js | 66 +++ src/agent/js/notebook/180-math-diagrams.js | 31 ++ src/agent/js/notebook/185-kernel-lifecycle.js | 57 +++ src/agent/js/notebook/190-ipynb.js | 96 ++++ src/agent/js/notebook/200-data-files.js | 94 ++++ src/agent/js/notebook/210-workspace.js | 152 ++++++ src/agent/js/notebook/220-zip.js | 97 ++++ src/agent/js/notebook/230-library.js | 217 +++++++++ src/agent/js/notebook/240-help.js | 4 + src/agent/js/notebook/250-keyboard.js | 42 ++ src/agent/js/notebook/260-wiring.js | 64 +++ src/agent/js/notebook/270-welcome.js | 29 ++ src/agent/js/notebook/280-drop-files.js | 40 ++ src/agent/js/notebook/290-autocomplete.js | 106 +++++ src/agent/js/notebook/300-inspector.js | 142 ++++++ src/agent/js/notebook/310-find-replace.js | 63 +++ src/agent/markup/agent-panel.html | 42 ++ src/agent/markup/modals.html | 59 +++ src/agent/mobile.html | 231 ++++++++++ src/agent/python/harness.py | 376 +++++++++++++++ src/suite/suite-menu.js | 48 ++ src/suite/suite.css | 40 ++ 75 files changed, 5219 insertions(+), 123 deletions(-) create mode 100644 scripts/build.mjs delete mode 100644 scripts/sync_agent_builds.mjs create mode 100644 src/README.md create mode 100644 src/agent/css/agent.css create mode 100644 src/agent/css/base.css create mode 100644 src/agent/css/desktop.css create mode 100644 src/agent/css/layout.css create mode 100644 src/agent/css/mobile.css create mode 100644 src/agent/desktop.html create mode 100644 src/agent/js/agent/010-overview.js create mode 100644 src/agent/js/agent/020-run-ledger.js create mode 100644 src/agent/js/agent/030-threads.js create mode 100644 src/agent/js/agent/040-state-chip.js create mode 100644 src/agent/js/agent/050-tools.js create mode 100644 src/agent/js/agent/060-marshaling.js create mode 100644 src/agent/js/agent/070-kernel-helpers.js create mode 100644 src/agent/js/agent/080-approvals.js create mode 100644 src/agent/js/agent/090-executor.js create mode 100644 src/agent/js/agent/100-system-prompt.js create mode 100644 src/agent/js/agent/110-context-budget.js create mode 100644 src/agent/js/agent/120-wire-formats.js create mode 100644 src/agent/js/agent/130-loop.js create mode 100644 src/agent/js/agent/140-settings.js create mode 100644 src/agent/js/agent/150-run-control.js create mode 100644 src/agent/js/agent/160-composer.js create mode 100644 src/agent/js/agent/170-forking.js create mode 100644 src/agent/js/agent/180-open-toggle.js create mode 100644 src/agent/js/app/010-live-theme.js create mode 100644 src/agent/js/app/020-init.js create mode 100644 src/agent/js/mobile/controller.js create mode 100644 src/agent/js/mobile/pwa.js create mode 100644 src/agent/js/notebook/010-constants.js create mode 100644 src/agent/js/notebook/020-harness.js create mode 100644 src/agent/js/notebook/030-icons.js create mode 100644 src/agent/js/notebook/040-state.js create mode 100644 src/agent/js/notebook/050-panels.js create mode 100644 src/agent/js/notebook/060-split-outputs.js create mode 100644 src/agent/js/notebook/065-ui-helpers.js create mode 100644 src/agent/js/notebook/070-highlight.js create mode 100644 src/agent/js/notebook/080-markdown.js create mode 100644 src/agent/js/notebook/090-cell-model.js create mode 100644 src/agent/js/notebook/100-collapse.js create mode 100644 src/agent/js/notebook/110-outputs.js create mode 100644 src/agent/js/notebook/120-cell-lookup.js create mode 100644 src/agent/js/notebook/130-selection.js create mode 100644 src/agent/js/notebook/140-editor-keys.js create mode 100644 src/agent/js/notebook/150-structural-edits.js create mode 100644 src/agent/js/notebook/160-execution.js create mode 100644 src/agent/js/notebook/170-worker.js create mode 100644 src/agent/js/notebook/180-math-diagrams.js create mode 100644 src/agent/js/notebook/185-kernel-lifecycle.js create mode 100644 src/agent/js/notebook/190-ipynb.js create mode 100644 src/agent/js/notebook/200-data-files.js create mode 100644 src/agent/js/notebook/210-workspace.js create mode 100644 src/agent/js/notebook/220-zip.js create mode 100644 src/agent/js/notebook/230-library.js create mode 100644 src/agent/js/notebook/240-help.js create mode 100644 src/agent/js/notebook/250-keyboard.js create mode 100644 src/agent/js/notebook/260-wiring.js create mode 100644 src/agent/js/notebook/270-welcome.js create mode 100644 src/agent/js/notebook/280-drop-files.js create mode 100644 src/agent/js/notebook/290-autocomplete.js create mode 100644 src/agent/js/notebook/300-inspector.js create mode 100644 src/agent/js/notebook/310-find-replace.js create mode 100644 src/agent/markup/agent-panel.html create mode 100644 src/agent/markup/modals.html create mode 100644 src/agent/mobile.html create mode 100644 src/agent/python/harness.py create mode 100644 src/suite/suite-menu.js create mode 100644 src/suite/suite.css diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 9c90c97..2b5a07b 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 - - run: node scripts/sync_agent_builds.mjs --check + - run: node scripts/build.mjs --check - run: node tests/verify_agent_v2.mjs - run: node tests/verify_agent_v23.mjs - run: node tests/verify_agent_v24.mjs diff --git a/AGENT-V2-SPEC.md b/AGENT-V2-SPEC.md index 64f6a53..65b61be 100644 --- a/AGENT-V2-SPEC.md +++ b/AGENT-V2-SPEC.md @@ -271,8 +271,7 @@ compressed ZIPs. - old lossy transcript/message caps are absent; - a Unicode/binary ZIP fixture round-trips with valid CRCs. -`node scripts/sync_agent_builds.mjs` updates the mobile build from the desktop source before -verification. Provider live calls require user-owned keys and are not part of repository CI. +`node scripts/build.mjs` builds both pages from `src/` before verification. Provider live calls require user-owned keys and are not part of repository CI. ## 12. Official API references used diff --git a/README.md b/README.md index 53c0059..9ab73ee 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,23 @@ skill/ docs/ ├── index.html ................. the landing / launch page ├── kernel.html ................ the notebook -├── kernel-agent.html .......... the agentic notebook (bring your own key) -├── kernel-agent-mobile.html ... the mobile / PWA build of the agent +├── kernel-agent.html .......... the agentic notebook (bring your own key) · built from src/ +├── kernel-agent-mobile.html ... the mobile / PWA build of the agent · built from src/ ├── kernel-agent-sw.js ......... service worker (offline cache for the PWA) └── .nojekyll +src/ .......................... source of the two agent pages (see src/README.md) +├── agent/desktop.html ......... page template for kernel-agent.html +├── agent/mobile.html .......... page template for kernel-agent-mobile.html +├── agent/css/ ................. styles (base, layout, agent, desktop-only, mobile-only) +├── agent/markup/ .............. markup shared by both pages (agent panel, modals) +├── agent/js/notebook/ ......... the notebook: cells, execution, worker, storage, panels +├── agent/js/agent/ ............ the agent: runs, threads, tools, providers, loop, composer +├── agent/js/app/ .............. theme and startup +├── agent/js/mobile/ ........... phone controller and PWA layer +├── agent/python/harness.py .... Python that runs inside Pyodide at boot +└── suite/ ..................... app menu and design tokens shared by the KERNEL pages scripts/ -└── sync_agent_builds.mjs ..... syncs the shared desktop/mobile runtime +└── build.mjs ................. builds docs/kernel-agent*.html from src/ (--check in CI) tests/ ├── verify_agent_v2.mjs ....... provider compatibility and v2 regression checks ├── verify_agent_v23.mjs ...... durability, lineage, safety and handoff checks @@ -123,7 +134,7 @@ Provider model discovery is available from settings. Custom API base URLs are su Release checks: ```bash -node scripts/sync_agent_builds.mjs --check +node scripts/build.mjs --check node tests/verify_agent_v2.mjs node tests/verify_agent_v23.mjs node tests/verify_agent_v24.mjs diff --git a/docs/kernel-agent-mobile.html b/docs/kernel-agent-mobile.html index 5dea2b2..a868f58 100644 --- a/docs/kernel-agent-mobile.html +++ b/docs/kernel-agent-mobile.html @@ -1,4 +1,5 @@ + @@ -466,7 +467,7 @@ /* ── v7 polish · in-app prompt dialog ── */ .prompt-input{width:100%;box-sizing:border-box;font-family:var(--mono);font-size:13px;color:var(--ink);background:var(--surface-2);border:1px solid var(--line-2);border-radius:3px;padding:9px 11px;outline:none;caret-color:var(--accent)} .prompt-input:focus{border-color:color-mix(in srgb,var(--accent) 45%,var(--line-2))} -.prompt-acts{display:flex;justify-content:flex-end;gap:8px;margin-top:14px} +.prompt-acts{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;flex-wrap:wrap} /* ===== KERNEL·A agent panel ===== */ .brand-a{color:var(--accent);font-weight:800;letter-spacing:0} @@ -1213,19 +1214,19 @@

Command mode

info = "" try: if tn == "DataFrame": - info = "%d \u00d7 %d" % (v.shape[0], v.shape[1]) + info = "%d \\u00d7 %d" % (v.shape[0], v.shape[1]) elif tn == "Series": info = "%d values" % len(v) elif tn == "ndarray": sh = getattr(v, "shape", None) - info = " \u00d7 ".join(str(int(n)) for n in sh) if sh else "scalar" + info = " \\u00d7 ".join(str(int(n)) for n in sh) if sh else "scalar" elif tn in ("list", "tuple", "set", "dict", "frozenset"): info = "%d items" % len(v) elif tn in ("str", "bytes"): info = "%d chars" % len(v) else: r = repr(v) - info = r if len(r) <= 60 else (r[:59] + "\u2026") + info = r if len(r) <= 60 else (r[:59] + "\\u2026") except Exception: info = "" b = _var_bytes(v, tn) @@ -1258,7 +1259,7 @@

Command mode

s = str(x) except Exception: s = "?" - return s if len(s) <= 40 else (s[:39] + "\u2026") + return s if len(s) <= 40 else (s[:39] + "\\u2026") def _var_detail(name): v = _KNS.get(name, None) @@ -1326,7 +1327,7 @@

Command mode

else: out["kind"] = "scalar" r = repr(v) - out["repr"] = r if len(r) <= 600 else (r[:600] + "\u2026") + out["repr"] = r if len(r) <= 600 else (r[:600] + "\\u2026") except Exception as e: out["kind"] = "other" out["repr"] = "detail unavailable: %s" % e diff --git a/docs/kernel-agent.html b/docs/kernel-agent.html index adcc084..72de748 100644 --- a/docs/kernel-agent.html +++ b/docs/kernel-agent.html @@ -1,4 +1,5 @@ + @@ -1220,19 +1221,19 @@

Command mode

info = "" try: if tn == "DataFrame": - info = "%d \u00d7 %d" % (v.shape[0], v.shape[1]) + info = "%d \\u00d7 %d" % (v.shape[0], v.shape[1]) elif tn == "Series": info = "%d values" % len(v) elif tn == "ndarray": sh = getattr(v, "shape", None) - info = " \u00d7 ".join(str(int(n)) for n in sh) if sh else "scalar" + info = " \\u00d7 ".join(str(int(n)) for n in sh) if sh else "scalar" elif tn in ("list", "tuple", "set", "dict", "frozenset"): info = "%d items" % len(v) elif tn in ("str", "bytes"): info = "%d chars" % len(v) else: r = repr(v) - info = r if len(r) <= 60 else (r[:59] + "\u2026") + info = r if len(r) <= 60 else (r[:59] + "\\u2026") except Exception: info = "" b = _var_bytes(v, tn) @@ -1265,7 +1266,7 @@

Command mode

s = str(x) except Exception: s = "?" - return s if len(s) <= 40 else (s[:39] + "\u2026") + return s if len(s) <= 40 else (s[:39] + "\\u2026") def _var_detail(name): v = _KNS.get(name, None) @@ -1333,7 +1334,7 @@

Command mode

else: out["kind"] = "scalar" r = repr(v) - out["repr"] = r if len(r) <= 600 else (r[:600] + "\u2026") + out["repr"] = r if len(r) <= 600 else (r[:600] + "\\u2026") except Exception as e: out["kind"] = "other" out["repr"] = "detail unavailable: %s" % e diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..eba4924 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +// Builds the single-file apps in docs/ from the sources in src/. +// +// node scripts/build.mjs write docs/kernel-agent.html and docs/kernel-agent-mobile.html +// node scripts/build.mjs --check exit 1 if either page is out of date with src/ (CI runs this) +// +// Page templates are plain HTML with two directives: +// on a line of its own: replaced by the file (or, with a *, every matching +// file in name order). Paths are relative to src/. Included files may +// include others. +// /*@embed path*/ inside a JavaScript template literal: replaced by the file's text, escaped +// for the literal (this is how src/agent/python/harness.py reaches Python). +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const SRC = path.join(ROOT, "src"); +export const PAGES = [ + ["agent/desktop.html", "docs/kernel-agent.html"], + ["agent/mobile.html", "docs/kernel-agent-mobile.html"], +]; + +const INCLUDE = /^[ \t]*[ \t]*\n/gm; +const EMBED = /\/\*@embed (\S+)\*\//g; + +function read(rel) { + const file = path.join(SRC, rel); + if (!fs.existsSync(file)) throw new Error(`missing source file src/${rel}`); + return fs.readFileSync(file, "utf8"); +} + +function expandGlob(spec) { + if (!spec.includes("*")) return [spec]; + const dir = path.posix.dirname(spec); + const pattern = new RegExp("^" + path.posix.basename(spec).split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"); + const names = fs.readdirSync(path.join(SRC, dir)).filter((name) => pattern.test(name)).sort(); + if (!names.length) throw new Error(`@include ${spec} matched no files`); + return names.map((name) => path.posix.join(dir, name)); +} + +function templateLiteral(text) { + return text.replace(/\n$/, "").replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${"); +} + +function expand(rel, stack = []) { + if (stack.includes(rel)) throw new Error(`include cycle: ${[...stack, rel].join(" -> ")}`); + const text = read(rel); + if (text && !text.endsWith("\n")) throw new Error(`src/${rel} must end with a newline`); + return text + .replace(INCLUDE, (_, spec) => expandGlob(spec).map((file) => expand(file, [...stack, rel])).join("")) + .replace(EMBED, (_, spec) => templateLiteral(read(spec))); +} + +export function build(template) { + return expand(template); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const check = process.argv.includes("--check"); + let stale = 0; + for (const [template, out] of PAGES) { + const html = build(template); + const target = path.join(ROOT, out); + const current = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null; + if (check) { + if (current !== html) { stale += 1; console.error(`${out} is out of date: run node scripts/build.mjs and commit the result`); } + } else if (current !== html) { + fs.writeFileSync(target, html); + console.log(`built ${out} (${Math.round(html.length / 1024)} KB)`); + } else { + console.log(`${out} is up to date`); + } + } + if (stale) process.exit(1); + if (check) console.log("docs/ matches src/"); +} diff --git a/scripts/sync_agent_builds.mjs b/scripts/sync_agent_builds.mjs deleted file mode 100644 index d77d47f..0000000 --- a/scripts/sync_agent_builds.mjs +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env node -import fs from "node:fs"; - -const args = process.argv.slice(2); -const check = args.includes("--check"); -const paths = args.filter((arg) => arg !== "--check"); -const sourcePath = paths[0] || "docs/kernel-agent.html"; -const targetPath = paths[1] || "docs/kernel-agent-mobile.html"; -const source = fs.readFileSync(sourcePath, "utf8"); -const originalTarget = fs.readFileSync(targetPath, "utf8"); -let target = originalTarget; - -function section(text, start, end) { - const a = text.indexOf(start); - const b = text.indexOf(end, a + start.length); - if (a < 0 || b < 0) throw new Error(`Missing section: ${start} … ${end}`); - return text.slice(a, b); -} - -function replaceSection(text, start, end, replacement) { - const a = text.indexOf(start); - const b = text.indexOf(end, a + start.length); - if (a < 0 || b < 0) throw new Error(`Missing target section: ${start} … ${end}`); - return text.slice(0, a) + replacement + text.slice(b); -} - -const v2Style = section(source, ' + + + + + + + + +
+

+ +
+
Booting…
+
+ + + + + + + + + + + + + + +
+
+ +
+ + + +
+
+
+ + +
+
+ + +
+ +
+ 0 cells + + + + Pyodide · loading + ⌘ shortcuts +
+ +
+ + + + + +
+ +
+ +
+ +
+ + + + + + + + + + + + + + diff --git a/src/agent/js/agent/010-overview.js b/src/agent/js/agent/010-overview.js new file mode 100644 index 0000000..8b29f59 --- /dev/null +++ b/src/agent/js/agent/010-overview.js @@ -0,0 +1,52 @@ +/* ════ KERNEL·A v2.4.0 — cache-stable, interruptible, completion-safe provider-neutral agent (spec: AGENT-V24-SPEC.md on the AGENT-V23-SPEC.md foundation) ════ */ +var DOCS="# Authoring great in-browser Python notebooks\n\nThe target is a rich in-browser Python notebook runtime built on Pyodide (CPython on\nWebAssembly): one persistent kernel, Jupyter-style cells, last-expression echo, rich HTML\noutput, inline matplotlib, `%pip install`, and a small set of `display_*` helpers for\ninteractive output. (KERNEL is the reference implementation; everything here applies to any\nruntime that honors the same contract — see `references/runtime.md`.) Your job is to produce\nnotebooks that feel like a thoughtful human analyst wrote them — a narrated argument that\nhappens to be runnable — using exactly the capabilities the runtime has and none it doesn't.\nYou may be **assembling a finished notebook** in one pass, or **driving a live kernel turn by\nturn** with a human watching (see \"Working live, in the loop\"); the craft below applies to\nboth, but the live loop has its own discipline.\n\nA great notebook is not a script with comments. It is a sequence of small, rerunnable steps,\neach introduced by prose that says what is about to happen and why, followed by code,\nfollowed by an output worth looking at, followed by a sentence interpreting it. The reader\nshould be able to skim the markdown alone and understand the whole story.\n\n## The workflow\n\n1. **Clarify the goal and the inputs.** What question does the notebook answer? Is the input\n a dataset the user supplied, existing code to convert, a mounted file (via the **+Data**\n button → read from the working directory), or a synthetic dataset you generate? If you\n were given data, inspect its real columns/shape before writing analysis against guessed\n ones. If you were given code, see \"Working from existing code or data\" below.\n2. **Outline the narrative as section headers** before writing any code. A typical arc:\n framing → setup/imports → load & inspect → clean/feature-build → analysis/model →\n visualize → conclusion. Pick the arc that fits; don't pad.\n3. **Write the cells.** Alternate markdown and code. Keep each code cell to one idea. End\n cells on the value worth showing (see \"Output discipline\").\n4. **Pressure-test the result before you narrate it.** This is the difference between a\n demo and a real analysis. Before writing confident prose about a finding, confirm the\n finding actually exists: that the model converged, the cascade spread, the correlation is\n real, the clusters separate, the fit tracks the data. If the result is degenerate — a\n simulation that fizzles, an R² near zero, one cluster swallowing everything, a flat curve\n — **change the setup until there is real signal**, then narrate the true result. Never\n write a confident story over noise. If a result genuinely is null, say so plainly and\n show why; a clear negative result is honest, a fake positive is not.\n5. **Assemble a valid `.ipynb`** with the bundled script — never hand-write notebook JSON.\n Write a cell spec (a JSON list) and run:\n ```bash\n python scripts/build_notebook.py cells.json output.ipynb \"Notebook Title\"\n ```\n The script emits nbformat 4.5 the runtime imports cleanly. See the script header for the\n spec format.\n6. **Sanity-check the runtime fit.** Re-read every import and chart against\n `references/runtime.md` — no `requests`, no ipywidgets, no LaTeX in markdown, correct\n `%pip` vs. plain import. A notebook that errors on cell 1 is worthless. When you can run\n code, execute the cells in order (with `display_*` stubbed) to catch breakage before\n delivery.\n\n## Working live, in the loop\n\nWhen you are driving a *running* kernel turn by turn rather than assembling a finished file,\nthe job is exploratory analysis with a human watching. The rhythm is a loop, not a one-shot:\n\n1. **Propose one coherent step** — a markdown framing cell and the code cell it sets up, not\n ten cells you haven't seen run. Small steps keep you and the human oriented.\n2. **Run it and read what comes back.** Outputs return *in execution order* as a mix of:\n stream text (stdout/stderr), **figures as PNG images you can actually see**, rich\n tables/HTML rendered to text, and tracebacks. Look before you leap.\n3. **Interpret, then decide the next step from the evidence** — not from what you assumed the\n data would say. Drop a one-sentence takeaway in a markdown cell, then propose the next move.\n4. **Loop with the human.** Surface forks (\"we could model this two ways…\"), pause before\n expensive or destructive steps, and let them redirect. You are a pair-analyst, not an\n autopilot. The pressure-test rule (step 4 of the workflow) still holds: confirm a finding\n is real before you narrate it.\n\n### Reading outputs so you can actually see\n\nYou only \"see\" what a cell emits, so emit deliberately — the output *is* your sensory input:\n\n- **To judge a distribution, shape, fit, or trend, draw it.** A matplotlib / `display_plotly`\n figure comes back to you as an image; that is how you inspect a result visually. If you\n need to see it, plot it — then interpret what the picture shows.\n- **To reason over data, summarize it as text:** `df.head()`, `df.describe()`, `df.dtypes`,\n `value_counts()`, `df.shape`. **Never end a cell on a 10,000-row frame** — the whole thing\n streams back into your context and tells you little; a 10-row head tells you more.\n- **Read tracebacks and repair the cell** before continuing. A run that errors and barrels\n onward is worse than one that stops and fixes itself.\n- **Build on persistent state.** The kernel keeps everything in scope across cells (the\n Variables panel shows what's live) — reuse a frame you built three steps ago instead of\n recomputing it, and inspect an unfamiliar object before you assume its shape.\n\n## Working from existing code or data\n\nOften the input is not a blank prompt but **existing workbench code** or **a dataset** that\nshould become a notebook. Do not dump it into one cell. Convert it into a narrated notebook:\n\n- **Read it for intent first.** Identify the steps the code performs (load, transform,\n model, plot) and the story the data tells. The notebook's structure should follow that\n intent, not the original line order.\n- **Split into rerunnable steps**, one idea per cell, in the section arc above. Hoist\n imports to a single early cell. Pull magic numbers into named constants.\n- **Add the narration the code lacks**: a framing cell, a sentence of setup before each\n step, and a takeaway after each result. This is most of the value you add.\n- **Upgrade the outputs to the runtime.** Replace `print(df)` with a rendered DataFrame;\n turn a static chart into `display_plotly` when interactivity helps; typeset equations with\n `$…$` / `$$…$$` (KaTeX) and make a pipeline or state machine legible with a ```mermaid\n diagram instead of a paragraph describing it.\n- **Preserve behavior.** Don't silently change logic or results while restructuring; if you\n fix a real bug, call it out in prose.\n\n## Structure that reads well\n\n- **Open with a title cell**: an `#` H1, one or two sentences of framing, and a short\n bulleted list of what the reader will see. No code yet.\n- **One imports cell, early.** Group all imports there so first-run package loading happens\n once and later cells are fast. Set display options here too (`pd.set_option`, a seeded\n `np.random.default_rng`, matplotlib style).\n- **Section by section**: each analytical step gets a `##`/`###` header, 1–3 sentences of\n setup, the code, and then — crucially — a sentence of *takeaway* after the output. The\n takeaway is what separates a notebook from a transcript.\n- **Close with a conclusion cell** that states what was found, the caveats, and one or two\n next steps. This is the part readers remember.\n\n### The rhythm, concretely\n\nThree consecutive cells showing the markdown → code → takeaway beat:\n\n> **(markdown)** `## 2. Does engagement predict retention?`
\n> We regress 30-day retention on first-week engagement, controlling for cohort size.\n>\n> **(code)** ```...fit model...; coef_table```  (cell ends on the rich table, not `print`)\n>\n> **(markdown)** Engagement is the dominant signal (β = 0.41, p < 0.001); cohort size\n> barely matters once it's included — so onboarding, not scale, is the lever.\n\nEvery section repeats that beat. No orphan code cells, no walls of prose.\n\n## Output discipline (this is where ordering matters)\n\nThe runtime builds each cell's output as a single ordered stream: prints, figures,\n`display_*` calls, and the cell's final value appear in the exact order they were produced.\nUse that.\n\n- **Let the last expression render.** End a cell on `df.head()`, a `Series`, a metrics\n table, or a fitted-model summary — the runtime shows its rich `_repr_html_` (DataFrames\n become real tables). Do **not** wrap it in `print()`, which collapses it to plain text.\n- **`print()` for narration of values mid-cell** (shapes, counts, chosen parameters); the\n rich result for the headline object at the end.\n- **`display(a, b, c)`** when you need to show several objects in order within one cell.\n- **Figures auto-display.** Just build the figure; it's captured inline. `plt.show()` is\n optional and harmless — figures created before the final value appear before it.\n- A cell that both plots and returns a table renders **plot then table**, matching execution\n order — put the plot code before the final expression if that's the order you want.\n\n## Visualization\n\nDefault to **static matplotlib** for clarity and print-fidelity; reach for **interactive**\nwhen hovering, zooming, or panning genuinely helps the reader explore.\n\n- Every chart: a title, axis labels **with units**, a legend when there is more than one\n series, a sensible `figsize`, and `fig.tight_layout()`. A chart without labels is a\n failure regardless of how pretty it is.\n- One idea per chart. If you're tempted to put five series on one axis, ask whether two\n small-multiples would read better.\n- **Interactive Plotly** is a one-liner: `%pip install plotly` once, then\n `display_plotly(fig, height=520)` — a fully interactive chart (zoom, pan, hover, lasso) in\n a sandboxed frame.\n- **Custom interactive HTML/JS** (d3, a hand-built widget, a Bokeh/Altair export):\n `display_html(html_string, height=520)` drops trusted HTML into a sandboxed iframe.\n- See `references/chartsmanship.md` for concrete recipes, a matplotlib house style, and the\n Plotly / `display_html` patterns.\n\n## Code craftsmanship\n\n- Idiomatic, readable Python: clear names, small functions, vectorized pandas/numpy over\n Python loops. A notebook is read more than it is run.\n- Seed every random process (`np.random.default_rng(SEED)`) so the narrative is reproducible\n across reruns.\n- Comment the *why*, not the *what*. The prose cell already said what; the comment explains a\n non-obvious choice.\n- Prefer composing state across cells (the kernel is persistent) over recomputing — but keep\n each cell independently rerunnable given the cells above it.\n\n## Hard runtime facts (do not violate)\n\nThese come from how the Pyodide runtime actually behaves. Full detail in\n`references/runtime.md`.\n\n- **Bundled scientific stack — just `import`:** numpy, pandas, scipy, scikit-learn,\n matplotlib, sympy, networkx, statsmodels, Pillow, beautifulsoup4, lxml, regex. The runtime\n auto-loads these on first import (the heavy ones — scipy/sklearn/statsmodels — take a\n moment the first time).\n- **Pure-Python extras — `%pip install ` first:** e.g. plotly, altair, humanize.\n micropip pulls them from PyPI.\n- **Will NOT work:** `requests` and raw sockets/threads/`subprocess` (no real OS/network\n layer); ipywidgets and `%matplotlib widget` (no widget comm — use `display_html` /\n `display_plotly` instead); any C-extension package Pyodide hasn't built (torch, tensorflow,\n polars, etc.).\n- **Markdown renders:** headings, **flat** bullet/numbered lists, GFM tables, blockquotes,\n fenced code (Python is syntax-highlighted), inline\n `code`/**bold**/*italic*/~~strike~~/links/images, **LaTeX math via KaTeX** (`$…$` inline,\n `$$…$$` display), and **Mermaid diagrams** inside a ```mermaid fenced block. It does **NOT**\n nest lists. Use `$…$` / `$$…$$` freely for equations, and reach for ```mermaid for\n flowcharts, sequence/state/ER diagrams, and pipeline schematics. (Reserve matplotlib\n mathtext for labels *inside* a chart, where KaTeX can't reach — use `\\frac`, not `\\dfrac`,\n there.)\n- **Data files** mounted via **+Data** land in the working directory; read them with\n `open(\"name\")` / `pd.read_csv(\"name\")`.\n\n## Anti-patterns (rewrite if you catch these)\n\n- One giant cell doing everything → split into rerunnable steps.\n- `print(df)` for a DataFrame → end the cell on the DataFrame so it renders as a table.\n- A chart with no title/labels → add them; state units.\n- Narrating a result you never checked → a fizzled sim or near-zero R² dressed up as a\n finding. Pressure-test first; fix the setup or report the null honestly.\n- `import requests` / network calls that assume a server → generate or mount the data.\n- `$\\sum_i x_i$` rendered as a matplotlib *figure* when the markdown cell would typeset it →\n use `$…$` / `$$…$$` directly; reserve mathtext for labels inside a chart.\n- Walls of markdown with no code, or walls of code with no narration → alternate.\n- Skipping the takeaway sentence after a result → always interpret what was just shown.\n\n## Reference files\n\n- `references/runtime.md` — the full runtime/Pyodide contract: display helpers (exact\n signatures), output ordering, the `%pip` vs import rule, package availability, the markdown\n feature matrix, and gotchas.\n- `references/chartsmanship.md` — a matplotlib house style, small-multiples and twin-axis\n recipes, the `display_plotly` and `display_html` patterns with complete examples, and how\n to choose static vs. interactive.\n- `scripts/build_notebook.py` — assembles a valid nbformat 4.5 `.ipynb` from a JSON cell\n spec. Always build notebooks with this rather than emitting JSON by hand.\n\n## Chart house style (condensed)\n- Every chart: title, axis labels WITH units, legend when >1 series, sensible figsize (~8x4.5), fig.tight_layout().\n- One idea per chart; prefer two small multiples over five series on one axis.\n- Seed RNGs; annotate the takeaway point rather than describing it only in prose.\n- Light grid only when it aids reading; no chartjunk; colorblind-safe default cycle.\n- Interactivity only when hover/zoom genuinely helps: %pip install plotly once, then display_plotly(fig, height=520).\n- Custom HTML/JS output goes through display_html(html, height) — sandboxed iframe.\n- matplotlib mathtext only for math inside a chart; markdown KaTeX ($…$/$$…$$) for narrative equations.\n\n# In-browser Python runtime contract\n\nEverything a notebook author needs to know about how the runtime executes code and renders\noutput. The runtime is CPython on WebAssembly (Pyodide) running entirely in the browser, with\none persistent kernel shared across all cells. (KERNEL is the reference implementation; the\nnames below — `display_html`, `display_plotly`, the ordering rules — are the contract any\ncompatible runtime should honor.)\n\n## Table of contents\n1. Execution model\n2. Output ordering\n3. Display helpers (exact signatures)\n4. matplotlib\n5. Packages: import vs `%pip`\n6. The markdown feature matrix\n7. Data files\n8. Things that don't exist here\n9. Performance notes\n\n## 1. Execution model\n\n- **One persistent namespace.** Variables, imports, and functions defined in one cell are\n available in every later cell, exactly like Jupyter. Build state up incrementally.\n- **Top-level `await` is allowed.** Cells run with `eval_code_async`, so\n `await something()` works at cell top level.\n- **Last-expression echo.** If a cell's final statement is an expression, its value is\n displayed: rich HTML via `_repr_html_` when the object provides it (pandas DataFrame,\n Series, styled objects), otherwise `repr()`. A trailing `;` or ending on an assignment\n suppresses the echo.\n- **`_`** holds the last echoed value, as in a REPL.\n- **Errors** show a trimmed traceback starting at the user's cell frame.\n\n## 2. Output ordering\n\nEach cell's outputs are emitted as a single **ordered** stream in execution order:\nstdout/stderr fragments, figures, `display_*` calls, and finally the cell's return value.\n\nPractical consequences:\n- `print(\"loading...\")` then a plot then a returned table renders as: text, figure, table.\n- Figures created without an explicit `show()` are captured **right before** the cell's\n final value, so they appear above a returned table.\n- `plt.show()` / `fig.show()` are intercepted to flush pending text and capture the figure\n *at that point* in the stream — so multiple show() calls interleaved with prints render\n in the right order. They never emit the Agg \"non-interactive\" warning.\n\n## 3. Display helpers (exact signatures)\n\nThese are injected into every cell's namespace — no import needed.\n\n```python\ndisplay(*objs)\n# Render one or more objects inline, in order. Uses _repr_html_ when available,\n# else repr(). Use when you want to show several things from one cell.\n\ndisplay_html(html: str, height: int = 480)\n# Render a trusted HTML string in a sandboxed