Repository navigation
Sheets: a spreadsheet where a column runs one of your agents - #19
Merged
Merged
Conversation
…Router
Same recipe the slides kit proved: the pages and components ARE the hosted
product's, and exactly one file differs — lib/sh.js maps the data layer onto
HarnessRouter. A sheet is a session, its content is sheet.json in that
session's workspace, chat is POST /responses. No database, no per-product auth
(the console already authenticated whoever is looking), no billing.
Every route in the data layer was checked against gateway/app.py before it was
written. Five slides-kit bugs were the same bug — a call kept from the hosted
product to an endpoint the OSS gateway does not implement — so delete goes to
/traces/{id}, history to /turns, rename to PATCH /sessions/{id}, and the sheet
is read by path (the live workspace) rather than from the file listing (the
checkpoint, which only exists once a turn ends).
runnableHarnesses() excludes this kit's own Harness at the source of the list.
A sheet whose column ran the sheet's own agent would have that agent editing
sheet.json while the app drives a run over it — recursion with a file-write
race inside it. Not offered rather than validated away.
The harness column itself lands next, against a spec still in review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ
…on every row
A sheet is a session on this kit's Harness and its content is ./sheet.json in that session's
workspace, the same shape the slides kit established. What is new is the agent column: it runs a
DIFFERENT harness from the workspace, once per row, with its input built from the columns to its
left — either interpolated into a prompt as {{Column}} or attached as files an earlier agent
column produced.
Column order IS the execution order. An agent column may only read columns earlier in the array,
enforced at config time (the picker only offers earlier columns), at reorder time (a drag that
would break it is refused with the two column names in the message) and at plan time. That one
rule makes the array a topological order by construction, so there is no sort in this kit and no
cycle branch to carry. The hosted product stored a deps[] array, validated it, rendered a checkbox
list for it — and ignored it at execution; dependencies here are derived from the config that
actually feeds the turn, which is the only thing that can be true.
There is no workflow engine in this deployment and no batch endpoint, so the open tab is the
orchestrator. lib/run.js is pure — plan() plus a Runner over an INJECTED dispatcher — which is why
the rules that matter are tested rather than asserted: a cell waits only for its own row, one
failure skips only that row's downstream cells, concurrency is a real ceiling, and Stop clears
queued cells instead of leaving them queued forever. 43 tests, no network.
Cells dispatch with background:true plus a poll rather than a stream: the session id is on the
immediate reply, and a reopened tab has no stream and needs the polling path anyway — streaming
per cell would be a second mechanism for the same job. Every dispatch carries an idempotency key,
because a retry without one starts a SECOND turn on a session that may already be running one, and
two turns on one session destroy that session's workspace. The response and session ids reach
sheet.json before anything else is dispatched, which is what makes reconcile-on-reopen real: a run
interrupted by closing the tab comes back with each cell's actual outcome instead of a row of
cells stuck at "running".
"completed" is not success. A turn that exits cleanly having produced neither text nor a file did
not fill the cell, and it is recorded as failed with that sentence.
Only the last run is retained, per the ask: starting a run clears the cells it owns. The sessions
are never deleted — the reference is dropped, not the work, and the Run confirm says so.
Also here: skills/sheet-design (the contract, plus validate_sheet.py, the twin of model.js's
validate — the slides kit shipped a skill describing tools that did not exist and every deck
rendered blank until it stated the real schema), templates that ship harness_id:"" because a
template cannot know which agents a workspace has, and .github/workflows/kits.yml, which is the
first thing in this repo that builds a kit app before an image bake does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ
…he UI told
Built the kit end to end on a real instance and every one of these came from watching it, not
from reading it.
1. "completed" is not the answer. A cell recorded "The agent finished without answering." over a
turn that answered perfectly well and wrote a file. Reproduced twice on the box: a background
response reports status "completed" — and the session reports turn_status "done" — about two
seconds BEFORE its output items are written. Neither signal is trustworthy alone and there is
no third to consult; the record IS the answer. A terminal-but-empty response now gets a
bounded grace window and takes whatever turns up, which costs nothing on the normal path.
2. A skipped upstream did not cascade. Only failures did, so a cell whose dependency was skipped
was never dispatched AND never recorded: the run reported six planned, five settled, and the
sixth cell simply blank. Skips cascade now, with their own wording, and any planned cell still
without a state when the walk ends is closed out — planned and settled always agree.
3. The sheet page showed the person a validation error about its own correct output: a skipped
cell legitimately has a run_id and no session_id, and the rule demanded both. Only a cell that
claims to have RUN needs both halves.
4. "Company and Company are empty in this row." — the missing-column list pushed a name once per
{{occurrence}} rather than once per column.
5. Counters were written only when a run ended, so a tab reopened mid-run read a file claiming
zero done over four finished cells.
Also, two places where I had added a second mechanism beside one that already worked, which is
the thing this project keeps paying to delete:
- The copilot already becomes a dismissable drawer below 900px. I added a bottom-sheet scheme on
top of it; mine lost, silently, and the banner actions ended up underneath the panel. Deleted,
and the collapse toggle the existing CSS is written for is back.
- The landing page used class names — .land, .sec, .hero-box — that this stylesheet does not
define, so it rendered with no page gutters and its own second prompt box. It uses .landing,
.section and .promptbox now, like every other page here.
Verified through the UI on the test instance: a template start writes a valid sheet.json and the
agent's own validator passes it; two agent columns configured against two different harnesses;
three rows pasted; Run at 3 → 4 done, 1 skipped, 33s. The decisive one is the second column's
value — "NOTES-FOR-Northwind Analytics" — a string that exists only inside the file the FIRST
column's agent wrote, read by a different agent from the attachment. CSV, XLSX and the run JSON
all produce real bytes; the run JSON carries the session reference of every cell that ran. Asking
the copilot to add a column locks the grid, adds it, and leaves every run result intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ
…ver sees `node --test 'src/lib/*.test.mjs'` passes the pattern to node verbatim, and node only expands it itself from v22 — so it ran locally on 23 and failed on the runner's 20 with "Could not find". The files are named now, which works on every runtime, and the runner is on 22 so local and CI agree. Caught by the kits workflow this PR adds, on its first run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ
meta.title is the truth; the session title is its mirror, and the sheet LIST renders the mirror because it cannot read the file. The mirror was asserted once per distinct title — so the first turn set it correctly and the second turn regenerated it from the message, leaving a sheet called "Competitor scan" listed as "Add a plain text column called Owner at the end." The name was right in the document and wrong in the one place a person looks for it. Re-asserted once per turn now, which is exactly as often as something can undo it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ
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.
A sheet is a session on this kit's Harness; its content is
./sheet.jsonin that session's workspace, the same shape the slides kit established. What is new is the agent column: it runs a different harness from the workspace, once per row, with its input built from the columns to its left — interpolated into a prompt as{{Column}}, or attached as files an earlier agent column produced.The one structural rule
Column order is execution order. An agent column may only read columns earlier in the array, enforced at config time (the picker offers only earlier columns), at reorder time (a drag that would break it is refused, naming both columns) and at plan time. That makes the array a topological order by construction — no sort anywhere in this kit, and no cycle branch to carry.
The hosted product stored a
deps[]array, validated it, rendered a checkbox list for it, and ignored it at execution. Dependencies here are derived from the config that actually feeds the turn.The run
There is no workflow engine and no batch endpoint in this deployment, so the open tab is the orchestrator — and the UI says so before you press Run.
lib/run.jsis pure:plan()plus aRunnerover an injected dispatcher, so the rules that matter are tested rather than asserted. 45 tests, no network.Cells dispatch
background: true+ poll rather than streaming: the session id is on the immediate reply, and a reopened tab has no stream and needs the polling path anyway. Every dispatch carries an idempotency key — without one, a retry starts a second turn on a session that may already be running one, and two turns on one session destroy that session's workspace.Two facts about the API this had to learn the hard way
status: "completed"is not the answer. A background response reportscompleted— and the session reportsturn_status: "done"— about two seconds before its output items are written. Believing either recorded "the agent finished without answering" over turns that answered fine.Only the last run is retained
Starting a run clears the cells it owns. The sessions are never deleted — the reference is dropped, not the work, and the Run confirm says so. Each cell is its own conversation, titled for its column and row, openable in the console.
Verified through the UI on a live instance
Template start → the agent writes a valid
sheet.jsonand its own validator passes it → two agent columns configured against two different harnesses → three rows pasted → Run at 3 → 4 done, 1 skipped, 33s. The decisive check: the second column's value isNOTES-FOR-Northwind Analytics, a string that exists only inside the file the first column's agent wrote — read by a different agent from the attachment. CSV/TSV/XLSX/run-JSON all produce real bytes. Asking the copilot to add a column locks the grid, adds it, and leaves every run result intact. 1440/768/390 all degrade cleanly with no body overflow.Also here
skills/sheet-design/— the contract plusvalidate_sheet.py, the twin ofmodel.js'svalidate(). The slides kit shipped a skill describing tools that did not exist and every deck rendered blank until it stated the real schema.harness_id: ""— a template cannot know which agents a workspace has, and an invented id silently runs the wrong agent..github/workflows/kits.yml— the first thing in this repo that builds a kit app before an image bake does.Requires
reifyui@0.5.1(columnTypes/renderCell/renderColumnConfigand the newreifyui/harnesstransport).🤖 Generated with Claude Code
https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ