docs(skill): add an app-models reference and fix the skill sync map - #509
Merged
jamesbhobbs merged 2 commits intoSep 1, 2026
Merged
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
…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
changed the base branch from
main
to
worktree-sync-publish-coordination
September 1, 2026 18:00
jamesbhobbs
marked this pull request as ready for review
September 1, 2026 18:00
jamesbhobbs
merged commit Sep 1, 2026
89c54e3
into
worktree-sync-publish-coordination
18 checks passed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
"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.mdFive models, each with what it is made of, where it runs, who hosts it, and what credentials it gets:
.pyentrypoint 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._deepnote_static/**, deployed bydeepnote publish, which owns that namespace.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.
publishowns_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
postMessagehandshake, 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 sanitizedsnapshotBlocks. 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 whyexamples/local-runner/cloud-appguards onisEmbedded.AGENTS.mdThe sync section said MCP tool changes go in
skills/deepnote/references/cli-*.md"(MCP mirrors CLI commands)". That is wrong in both directions:@deepnote/mcphas 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 atdeepnote.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.mdandcli-sync.md; this isAGENTS.md,SKILL.md, and the new reference), andapps.mdno longer describes the publish/sync coordination as pending. Note that #508 resolves the overlap by sharing one baseline rather than excluding_deepnote_staticfromsync --all-files, so this file states only the part an app author needs —publishis 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-checkall clean. Biome reports 8noConsolewarnings inexamples/local-runner/*/serve.mjs, all pre-existing and untouched here.Packaging verified end to end rather than assumed:
pnpm buildcopies the new reference intopackages/cli/dist/skills/deepnote/references/, anddeepnote install-skills --agent "Claude Code"in a scratch directory installsapps.mdalongside 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