Skip to content

docs(skill): add an app-models reference and fix the skill sync map - #509

Merged
jamesbhobbs merged 2 commits into
worktree-sync-publish-coordinationfrom
docs/skill-app-models
Sep 1, 2026
Merged

jamesbhobbs merged 2 commits into
worktree-sync-publish-coordinationfrom
docs/skill-app-models

Conversation

@jamesbhobbs

@jamesbhobbs jamesbhobbs commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

"Build me a Deepnote app" has five different answers, and the skill said which one to pick nowhere. An agent had to assemble the picture from cli-publish.md, examples/local-runner/cloud-app/README.md, examples/local-runner/run-app/README.md, and the local-runner package docs — and the only place that mentioned app tokens at all was the publish reference, describing the CLI flag rather than the boundary behind it.

skills/deepnote/references/apps.md

Five models, each with what it is made of, where it runs, who hosts it, and what credentials it gets:

  1. Data apps (notebook apps) — notebook blocks presented through Deepnote's app UI. Not deprecated, not a fallback; an agent contributes the notebook, not the app.
  2. Streamlit apps — a .py entrypoint on project hardware. Hosted MCP can activate an existing entrypoint but cannot upload or author the file, so a "build a Streamlit app from scratch in a hosted project" plan does not work.
  3. Published static sites — browser files below _deepnote_static/**, deployed by deepnote publish, which owns that namespace.
  4. Published browser apps with API access — the same, plus a viewer token.
  5. Local Node-backed apps (serveStatic) — a local server runtime, with the complete five-route contract documented.

A decision table opens the file, keyed on the questions that change the plan: notebook blocks vs. custom HTML/JS vs. Python, browser-only vs. local Node server, viewer API access, project hardware, and whether an agent can create the underlying files with its current tools.

The two boundaries worth the file

Publishing ownership. publish owns _deepnote_static/**; the canonical URL comes from the API, never hand-assembled.

The viewer token. A published app never carries the personal token used in local preview. The shell hands it a short-lived, project- and viewer-scoped token over an origin-pinned postMessage handshake, limited to: read the configured notebook's inputs and block metadata without source content, start a detached run, poll that viewer's own run by id, receive sanitized snapshotBlocks. Not notebook enumeration, not run-history enumeration, not arbitrary /v2. The failure mode is silent — the same code works locally with a personal token — which is why examples/local-runner/cloud-app guards on isEmbedded.

AGENTS.md

The sync section said MCP tool changes go in skills/deepnote/references/cli-*.md "(MCP mirrors CLI commands)". That is wrong in both directions: @deepnote/mcp has tools with no CLI equivalent, and the CLI has commands (publish, sync, install-skills) with no MCP tool. It routed local-MCP changes into the CLI references. Replaced with a map from each surface to its owning document, distinguishing three things that get conflated: local @deepnote/mcp, hosted MCP at deepnote.com/mcp, and the codex-plugin consumer of the hosted server.

Scope

Documentation only. cli-sync.md, cli-publish.md, CLI implementation, public docs, and MCP implementation are deliberately untouched — they are moving in #492, #508, and #491.

Relationship to #508

No conflict, and no merge order. The two PRs touch disjoint files (#508 is CLI/cloud source plus cli-publish.md and cli-sync.md; this is AGENTS.md, SKILL.md, and the new reference), and apps.md no longer describes the publish/sync coordination as pending. Note that #508 resolves the overlap by sharing one baseline rather than excluding _deepnote_static from sync --all-files, so this file states only the part an app author needs — publish is the deploying writer for the static root — and defers the mechanism to the CLI references, which #508 owns.

Validation

pnpm test (3086 passed, 1 skipped), pnpm typecheck, pnpm biome:check, pnpm prettier:check, pnpm spell-check all clean. Biome reports 8 noConsole warnings in examples/local-runner/*/serve.mjs, all pre-existing and untouched here.

Packaging verified end to end rather than assumed: pnpm build copies the new reference into packages/cli/dist/skills/deepnote/references/, and deepnote install-skills --agent "Claude Code" in a scratch directory installs apps.md alongside the rest.

No skill validator (quick_validate.py) or skill-creator skill is present in this checkout, so that step could not be run.

🤖 Generated with Claude Code

The skill had no canonical account of Deepnote's app models, so an agent
choosing between them had to infer the differences from `cli-publish.md`,
two example READMEs, and the local-runner package docs — and the one place
that did mention app tokens (`cli-publish.md`) covers only the CLI flag.

`references/apps.md` states the five models, what each is made of, where it
runs, and which credentials it gets: data apps, Streamlit apps, published
static sites, published browser apps with viewer API access, and local
Node-backed `serveStatic` apps. It leads with a decision table keyed on the
questions that actually change the plan, including whether an agent can
create the underlying files at all.

Two boundaries are the load-bearing part. `deepnote publish` owns
`_deepnote_static/**`. And a published app never carries a personal token:
the shell hands it a short-lived, viewer-scoped token limited to reading the
configured notebook's inputs, starting a detached run, polling that run, and
receiving sanitized output blocks — not notebook or run-history enumeration.
That is a quiet failure mode, since the same code works in local preview.

`AGENTS.md` claimed "MCP mirrors CLI commands", which is wrong in both
directions and routed local-MCP changes into the CLI references. It now maps
each surface to its owning document, and distinguishes local `@deepnote/mcp`
from hosted MCP and from the codex-plugin consumer of the hosted server.

SKILL.md gains one pointer; the taxonomy and token contract stay in the
reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.91%. Comparing base (726cbb5) to head (ea8ebb4).
⚠️ Report is 7 commits behind head on worktree-sync-publish-coordination.

Additional details and impacted files
@@                         Coverage Diff                         @@
##           worktree-sync-publish-coordination     #509   +/-   ##
===================================================================
  Coverage                               88.91%   88.91%           
===================================================================
  Files                                     199      199           
  Lines                                   11311    11313    +2     
  Branches                                 3271     3271           
===================================================================
+ Hits                                    10057    10059    +2     
  Misses                                   1252     1252           
  Partials                                    2        2           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ht work

The paragraph described publish/sync coordination as "in flight", which
stops being true the moment #508 merges — and #508 resolves the overlap by
sharing a baseline rather than excluding the namespace from sync, so the
wording would have been wrong in substance too. State the part an app author
needs (publish is the deploying writer for the static root) and defer the
mechanism to the CLI references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamesbhobbs
jamesbhobbs changed the base branch from main to worktree-sync-publish-coordination September 1, 2026 18:00
@jamesbhobbs
jamesbhobbs marked this pull request as ready for review September 1, 2026 18:00
@jamesbhobbs
jamesbhobbs requested a review from a team as a code owner September 1, 2026 18:00
@jamesbhobbs
jamesbhobbs merged commit 89c54e3 into worktree-sync-publish-coordination Sep 1, 2026
18 checks passed
@jamesbhobbs
jamesbhobbs deleted the docs/skill-app-models branch September 1, 2026 18:00
tkislan added a commit that referenced this pull request Sep 4, 2026
…pp-models reference (#508)

* fix(cli): let sync and publish share one baseline for project files

`deepnote publish` deploys into `_deepnote_static/**`, which is a subtree of
the project file store `deepnote sync --all-files` mirrors. Neither command
knew about the other, so they drifted: every publish made the whole static
subtree look changed to sync (re-downloading it on the next run), a stale
local mirror could be pushed back over a live site with no staleness check,
and `publish --prune` left local ghosts that a later edit would resurrect.

Resolved by coordination rather than by dividing the namespace, so both
commands keep working on the same paths:

- Sync gains per-file lost-update protection on push. `uploadProjectFiles`
  never fetched the inventory at all; it now checks every candidate against
  it and routes a file that moved since the manifest baseline through the
  existing `--on-conflict` override-or-skip choice. A pending replacement is
  exempt — that missing cloud copy is sync's own unfinished delete. This also
  fixes the same silent overwrite for ordinary working files edited in the
  Deepnote app.
- Publish updates the sync mirror when the published directory sits inside a
  synced workspace: it writes each file into the project's `.files/` mirror
  and records size, hash, and server `updatedAt`, exactly as a sync download
  would. `--prune` drops pruned paths from both. `--sync-root`/`--no-sync-root`
  control discovery.
- Publish stops before mutating anything if a path it would write has moved on
  in Deepnote since that workspace last synced, since the mirror holds no copy
  of that content; `--force` overrides. Its check is deliberately narrower
  than sync's: publish is a deploy where the local build is authoritative, so
  a path with no baseline is not flagged.
- `PROJECT_STATIC_ROOT` moves to `@deepnote/cloud` so every writer agrees on
  where the boundary is.

The mirror is only updated when the tracked project directory already exists —
creating it would make the next sync read the project as "all notebooks deleted
locally" and push that. Mirror failures are warnings, not errors: the deploy
succeeded, and a stale manifest is safe because the next sync asks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(skill): add an app-models reference and fix the skill sync map (#509)

* docs(skill): add an app-models reference and fix the skill sync map

The skill had no canonical account of Deepnote's app models, so an agent
choosing between them had to infer the differences from `cli-publish.md`,
two example READMEs, and the local-runner package docs — and the one place
that did mention app tokens (`cli-publish.md`) covers only the CLI flag.

`references/apps.md` states the five models, what each is made of, where it
runs, and which credentials it gets: data apps, Streamlit apps, published
static sites, published browser apps with viewer API access, and local
Node-backed `serveStatic` apps. It leads with a decision table keyed on the
questions that actually change the plan, including whether an agent can
create the underlying files at all.

Two boundaries are the load-bearing part. `deepnote publish` owns
`_deepnote_static/**`. And a published app never carries a personal token:
the shell hands it a short-lived, viewer-scoped token limited to reading the
configured notebook's inputs, starting a detached run, polling that run, and
receiving sanitized output blocks — not notebook or run-history enumeration.
That is a quiet failure mode, since the same code works in local preview.

`AGENTS.md` claimed "MCP mirrors CLI commands", which is wrong in both
directions and routed local-MCP changes into the CLI references. It now maps
each surface to its owning document, and distinguishes local `@deepnote/mcp`
from hosted MCP and from the codex-plugin consumer of the hosted server.

SKILL.md gains one pointer; the taxonomy and token contract stay in the
reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(skill): state static-root ownership without dating it on in-flight work

The paragraph described publish/sync coordination as "in flight", which
stops being true the moment #508 merges — and #508 resolves the overlap by
sharing a baseline rather than excluding the namespace from sync, so the
wording would have been wrong in substance too. State the part an app author
needs (publish is the deploying writer for the static root) and defer the
mechanism to the CLI references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(skill): state the publish/sync coordination now that it ships here

`apps.md` deferred the publish/sync overlap to the CLI references because
the mechanism lived in a separate PR. It now lands in this one, so the
reference can say the two things that change how a deploy is scripted:
publish updates a surrounding sync workspace's mirror unless told not to
(`--no-sync-root`, which is what CI wants), and it exits 1 without touching
the project when Deepnote holds changes the workspace has not synced.

Also drops "publish owns the namespace", which read as a split now that the
implementation deliberately shares one baseline instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(cli): manage static-site access

* refactor(cli,cloud): cut publish/sync comments back to the why

The publish/sync coordination landed with long explanatory comments that
repeated the same rationale in the module doc, the function docs, and the
tests. Keep the parts the code cannot state — the file API's missing
conditional write, why creating a project directory would make sync push
"all notebooks deleted", why publish flags fewer paths than sync — and drop
the restatements, the field docs that echo their names, and the pass
labels.

Comments only; no behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWb55u5XKkyCvzvoQEUwKX

* Cleanup code comments

* Remove unnecessary tests

* Use path.join and path.sep

* Revert "Use path.join and path.sep"

This reverts commit 7e0c316.

* docs(skill): state the static site's URL-to-file mapping

Per Tomas's review note on apps.md: root and directory paths serve index.html, every other file serves at its own path, no rewrite from extensionless URLs.

* fix(cli): close pending-upload gaps, fail explicit --sync-root loudly, and trim tokens everywhere

Review-round batch, as announced on the PR:

- publish now clears a path from pendingFileUploads when it writes or prunes it,
  so an interrupted sync replacement that publish already settled can no longer
  brick every later sync (missing local file) or silently redo the replacement.
- a pending path whose cloud copy exists again is a conflict, not a retry: the
  re-created copy came from another writer, so overwriting it needs a choice.
  The kept-files warning now also says plainly that pulling replaces local copies.
- an explicit --sync-root whose tracked project directory is gone is exit 2 like
  the other stated-intent failures, and a manifest that exists but cannot be read
  fails with a pointer at --no-sync-root (exit 2, was 1). Discovery still skips
  silently.
- every command resolves --token / DEEPNOTE_TOKEN through one trimming helper;
  previously five commands did it four different ways and a pasted token with a
  stray space worked in some and failed cryptically in publish.
- cli-publish.md/cli-sync.md state the updatedAt claims' server dependency.

* test(cli): pin the POSIX invariant of the mirror path

Per CodeRabbit's note on 7e0c316 (since reverted in f8b00b3): manifest dirs and mirror paths are POSIX by contract, and a platform separator would weaken the '/'-splitting symbolic-link ancestor checks on Windows. This locks the invariant in.

* fix(cli): keep cloud-deleted baselines, guard plain-synced publishes, share hash/divergence helpers

Round-4 review batch on the sync/publish coordination.

Blocking:
- pull no longer drops the manifest record of a cloud-deleted file that stays
  on disk (no --prune). Dropping it erased the baseline the next push needs to
  see the deletion, so an edited copy silently resurrected a file someone had
  removed (e.g. via publish --prune, whose failure advice sends users here).
  The kept file now keeps its record and the pull warns; a later push surfaces
  it as a conflict. --prune still removes file and record together.
- publish's divergence guard is inert for a workspace with no file baselines
  (only --all-files syncs and earlier publishes record them). It now warns when
  it would overwrite existing remote files unchecked, and the help/README/skill
  docs say the guard covers changes since the last --all-files sync or publish.

Non-blocking:
- resolvePublishMirror wraps every workspace-resolution failure (symlinked or
  unreadable manifest included) in PublishMirrorError with the --no-sync-root
  hint, so they exit 2 as documented instead of a bare exit 1.
- the upload loop re-asserts the buffered-size cap on the bytes it actually
  uploads, closing the plan->prompt->upload TOCTOU gap.
- --dry-run honours an explicit --on-conflict override/skip instead of always
  reporting a skip, matching the notebook-conflict path.
- shared sha256 and baselineDiverged helpers in sync-manifest.ts replace the
  duplicated hash and divergence predicate across publish and sync, which feed
  cross-module comparisons that must not drift; findSyncManifestRoot reuses
  hasSyncManifest; publish imports PROJECT_STATIC_ROOT as STATIC_ROOT directly.
- restore the PROJECT_STATIC_ROOT value assertion in @deepnote/cloud (CLI tests
  mock the constant, so nothing else pins it to the served prefix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): warn per unchecked overwrite in publish and state the usable-baseline contract

Per CodeRabbit on 857a878. The zero-baseline warning only fired when the
workspace had no file baselines at all, so an unrelated baseline silenced it
while a path without one was still overwritten unchecked. Publish now warns per
remote path it will overwrite whose record is absent or lacks the server's
updatedAt, and the help text, README and skill doc state that only such entries
are usable baselines (--all-files syncs always record one, publishes only when
the server echoes it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cli): settle each upload on disk and let a kept re-created conflict resolve by pull

Re-review of the takeover commits found that the re-created-copy conflict
could stick: a run interrupted after its last upload landed but before the
final manifest save left the path pending against the old baseline while the
cloud held our own upload. The next sync raised the conflict; under skip (the
non-TTY default) pending was never cleared, pull cannot run while a path is
pending, so it re-raised forever and only override or a manifest edit got out.

- sync persists the settled baseline right after each successful upload, so an
  interruption later in the run cannot leave that upload looking pending.
- skipping a re-created conflict drops the retry: the path becomes an ordinary
  diverged file, which the next pull brings down and the next push asks about.
- publish's unchecked warning now also covers paths --prune would delete,
  matching the divergence stop, which already included them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(cli): pin that each uploaded baseline is settled on disk before the next pending mark

The per-upload persist had no test owner — deleting it failed nothing. This
captures the manifest writes during a two-file push and asserts the state
where the second file is pending already carries the first file's uploaded
baseline. Fails against the pre-fix code. Also notes in cli-sync.md that
keeping a re-created cloud copy ends the retry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(cli): keep read-only pulls off the stale-path symlink guard and settle dry-run conflicts once

The F6 change moved the symlink-ancestor assert ahead of the prune branch, so a
plain pull errored a whole project when a cloud-deleted file sat under a
symlinked directory it was never going to touch. The guard is back beside the
rm it protects. Dry runs now degrade an 'ask' conflict mode to 'skip' where the
mode is derived, instead of at each prompt site, and the push planner hashes a
file only when its size still matches the baseline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli): drop publish's unchecked-overwrite warning and the duplicated server-echo hedges

The warning listed every remote file without a server-timestamped baseline and
told the user to sync first, but a pull only records the cloud copy as the
baseline, so the next publish passes the stop and overwrites the file anyway.
It protected nothing and printed on every first publish from a workspace. The
usable-baseline contract stays in cli-publish.md; the help text and README no
longer repeat the self-hosted caveat.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(skill): state how the next sync recovers from a failed publish mirror update

A pull re-downloads the published files without a prompt; only a push turns
them into a conflict. The doc claimed the next sync always asks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(cli): address review — real cloud module in test mocks, keep PROJECT_STATIC_ROOT name, align apps.md wording

The publish and static-site-access test mocks now spread the real @deepnote/cloud
module and override only the functions they drive, so the constant pin test in
the cloud package no longer guards anything and is removed. The import alias in
publish.ts goes back to the exported name. apps.md states the publish stop the
same way cli-publish.md does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: tomas <tomas@kislan.sk>
Co-authored-by: Wojciech Apanowicz <wojtek@deepnote.com>
Co-authored-by: Wojtek <voyti@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant