Skip to content

Sheets: a spreadsheet where a column runs one of your agents - #19

Merged
richard-epsilla merged 5 commits into
mainfrom
sheets-kit
Aug 14, 2026
Merged

richard-epsilla merged 5 commits into
mainfrom
sheets-kit

Conversation

@richard-epsilla

Copy link
Copy Markdown
Contributor

A sheet is a session on this kit's Harness; 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 — 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.js is pure: plan() plus a Runner over an injected dispatcher, so the rules that matter are tested rather than asserted. 45 tests, no network.

  • a cell waits only for its own row; rows run in parallel, no column barrier
  • one failure skips only that row's downstream cells
  • concurrency is a real ceiling (default 3, 1/2/3/5/8)
  • Stop clears queued cells instead of leaving them queued forever
  • every planned cell ends with a state — planned and settled always agree

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 reports completed — and the session reports turn_status: "done" — about two seconds before its output items are written. Believing either recorded "the agent finished without answering" over turns that answered fine.
  • A turn that exits cleanly having produced neither text nor a file did not fill the cell. It is recorded as failed, with that sentence.

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.json and 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 is 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/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 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 ship 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 / renderColumnConfig and the new reifyui/harness transport).

🤖 Generated with Claude Code

https://claude.ai/code/session_013DCjqvx3L4VKAPnFaPe7YJ

richard-epsilla and others added 5 commits August 13, 2026 02:42
…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
@richard-epsilla
richard-epsilla merged commit 5b90cd9 into main Aug 14, 2026
3 checks passed
@richard-epsilla
richard-epsilla deleted the sheets-kit branch August 14, 2026 02:48
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