Skip to content

feat(tuval): cron wakes on a timer, starts a job program and reports the run on its tile - #9226

Merged
cansirin merged 7 commits into
mainfrom
can/cron
Sep 15, 2026
Merged

cansirin merged 7 commits into
mainfrom
can/cron

Conversation

@cansirin

@cansirin cansirin commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Tuval had no scheduler. cron is a program on defineProgram that wakes on a timer, starts a job
program it knows only by its ports, and says on its board tile how the run went. It is the first
consumer written on the authoring layer from outside the kernel, and it is the program that
surfaced #8762, #8887, #8898/#8858 and #9220 on the way.

cron({everyMs, prompt, job}) returns the row. job is Program.shape over the AI-agent payloads
out of ai-agent/ports/index.ts — the interface module, the same import the pr-review example
makes — so the config hands it claudeSession({…}) itself and cron imports no session. A tick
spawns the job with on: {result}, sends a well-formed PromptPayload, records the run when the
answer lands and stops the child; a tick while a job runs spawns nothing and the tile says
running since. The stopped cell handles the answer to cron's own stop; its other branch — a
job that died before answering — is written for the day #9227 delivers that event and is
unreachable under today's kernel, stated in the module header and the cell and under Deviations. A restored cron whose
checkpointed child is no longer live records that run as interrupted, so a restart cannot wedge
every later tick. History is bounded at ten. :cron run is a commands cell answering
send("run"), the shape ADR 0372 takes since #9221.

The cron flag is declared on TuvalFeatures/featuresDefault (src/features.ts) the way
prReviewExample is — off there, flipped on by this repo's own layer, so the key reaches the
Features service and the generated browser module and not the row alone. That registration was
missing on the first head and is the second repair in this round; round-2 criterion #8 on #9225.

The first job, in .tuval/tuval.config.ts behind features.cron, is a read-only phoenix brief
through gh: merged PRs, new issues, anything ready-for:human, five lines, every ten minutes.

Gates at this head: pnpm typecheck 0 errors; npx biome check src/cron src/features.ts .tuval src/boot.unit.test.ts src/config.unit.test.ts src/page/dev-server.unit.test.ts clean; pnpm vitest run --project unit 321 passed / 1 failed, the one pre-existing failure named below. Boot with the
flag on: 9 program(s), process cron program=cron … state=running@0, no HandlerFailed.

One earlier run was verified by hand on the first head (20s cadence, ~4 min): .tuval/processes/ cron.json ended with ticks: 11 and seven runs, all ok: true, each a real gh readout off a
live Claude session. That is a manual proof, not an input this gate reads — no checked-in artifact
or test carries it, so read the "one real tick lands ok: true" half of criterion 6 as unverified
by the gate rather than as evidence.

Reviewer's first stop: the result and resume cells in apps/tuval/src/cron/cron.ts.

Fixes #9225

Deviations

Base is main.

🤖 Generated with Claude Code

cansirin and others added 5 commits September 15, 2026 03:09
…the run

The first program written on #8716's authoring layer rather than for it. A dep-keyed
Sub ticks it, a tick spawns the job, the job's spawned answer carries the prompt, and
a routed result lands on the tile as `last run 07:00 · ok`. The history is bounded at
ten runs. `:cron run` is the on-demand path; it answers a spawn rather than an emit
because a command may not reach a process out-port.

The job arrives as an arg typed by its ports alone, so this module names no session
and imports no session package. Unlike the pr-review example the shape is declared
over the real ai-agent payloads — a prompt carrying {text, key, timestamp} and a
result carrying {text, items, ok} — and the tests check those against a live claude
row's own accepts predicates rather than a copy.

Two seams are pinned rather than papered over. `shapeOf` reads authoring port decls
and a compiled row publishes predicates, so no live session can ever fit a non-empty
shape; `sessionAsJob` re-declares the two ports the arg asked for and the test fails
the day that changes. And a spawn on a shaped arg still resolves the arg's service
key through the registry (#8762), so every wake logs one UnknownProgram and the
status stays idle, which is the truth.

Registered behind a `cron` flag, default on, and planned as a graph node — boot's box
config case now expects nine programs and four processes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`spawn` on a program-valued arg now resolves the arg's service key back to
the program the config filled it with (#8762), and a row states that fill on
`AuthoredProgram.fill`. `cron` never did, so every tick asked the registry for
`tuval/arg/cron/job` and got `UnknownProgram`. The `job` a config hands `cron`
goes onto the row's `fill`, and a tick spawns the session.

`sessionAsJob` stays. `shapeOf` reads a compiled row now (#8887), but only one
`defineProgram` built — `compilePort` is what publishes a port's schema beside
its predicate, and an AI-agent row's ports are hand-written predicates. Both
headers say so, and the test that pins the blocker says which change retires it.

Verified live: one 20s tick spawned `claude-session` under `cron`, the prompt
went out, and the turn came back `ok`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8b43d754c7aed289da768ff0511ce9fafeb23ebf)
…nnot outlive its turn

An AI-agent session stays up after its turn — it holds a transcript. Cron cleared
`child` only on `stopped`, so that `stopped` never came: every later tick was
dropped, and the orphan was restored on the next boot from the manifest.

`result` now records the run, answers `stop(child)` and clears `child`, so the
next tick spawns. `stopped` keeps the other case — the job dying before it
answers — as a failed run, and ignores the one that answers cron's own `stop`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…estart cannot wedge every tick

A Ctrl-C mid-run left `child` set in cron's checkpoint. On the next boot cron came
back waiting for a `result`/`stopped` that nothing was going to send, so every tick
was dropped for ever and the tile read "running since" against a run that ended with
the process.

`resume` (`authoring/resume.ts`) now sends a `restored` event when the loaded state
holds a child: the half-finished run is written down as failed with "interrupted by
restart", `child`/`startedAt` are cleared, and the next tick spawns.

Cron reconciles the same way whether or not the child came back, and that is the
honest reading rather than a shortcut. `durability/restore.ts` respawns a
checkpointed process through `Processes.spawn` directly — no `on` record, which is
where the spawner's routing lives, and an `unwired` `ProcessPorts` — so a restored
session's `result` fails `PortNotWired` at its own emit and reaches no cron. And cron
may not reap the orphan: `stop`/`send`/`ask` all fail `ProcessNotFound` on a process
that is not live, that failure propagates out of the resume dispatch, and nothing
catches it — so a blind `stop` would fail boot for every child the manifest did not
bring back.

The `any` in the fill test's handler type is narrowed to the two services that spawn
handler actually reads, so `tsc` stops refusing the `runPromise` beneath it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s to its own port

Two things main made possible, landing together because they are one file's worth of
diff and neither reads right without the other.

**The shape wrapper is gone.** `sessionAsJob` existed because an AI-agent row
published hand-written port predicates and nothing else, so `shapeOf` read a live
session as `{in: {}, out: {}}` and no non-empty shape could fit one; a config had to
hand cron the row's id beside a re-declaration of the two ports it was being asked
for. #8887 taught `shapeOf` to read a compiled row and #8959 gave those rows their
payload schemas, so `claudeSession({…})` fits `jobShape` on its own and the config
hands over the row itself.

Restating the payloads was not equivalent, and that is why the wrapper could not
simply be deleted: cron's copy typed `items` as `Schema.Array(Schema.Unknown)` where
the real `TurnResultSchema` carries `Schema.Array(TranscriptItemSchema)`, so the two
do not canonicalise the same and the fit refuses on `result`. `jobShape` is now
declared over `PromptPayloadSchema` and `TurnResultSchema` out of
`ai-agent/ports/index.ts` — the interface module, not any agent's implementation,
the same import `authoring/example/pr-review.ts` makes, and what R15.1 asks for
rather than what it forbids.

**`:cron run` is a send, not a spawn.** The command used to answer a `spawn`, because
when it was written a command could `spawn` and had no way to reach its own program.
#8898 closed both halves: a command may only `send`, and a bare `send("run")`
resolves to the declaring program's own live process. So cron gains a `run` in-port
carrying `RunRequest`, an empty struct that is also the command's own `args` schema —
the command forwards exactly what it was called with and nothing is invented between
the two. Its cell is `tick` minus the counter, and both go through one `startIfIdle`,
so there is one reading of what waking means rather than two that can drift. A wake
landing mid-run records nothing, because `status` already says "running since", which
is the honest answer to "what happened when I asked". `ticks` stays the timer's count.

The spell does not reach the cron the desk boots, and the module header says so
rather than working around it. Resolution is fine — `own-process.ts` reads
`ProcessTable` and finds the planned process — but delivery goes through
`SpawnedProcesses.send`, whose `live` map holds only processes it spawned itself, and
cron is graph-launched. Verified against a running desk over the transport:

    {"ok":false,"error":{"tag":"tuval/commands/UnknownProcess",
     "message":"no live process \"cron\" was spawned through the process spells",
     "path":["cron","run"]}}

That is #8944, not a defect of this program,
and the day it lands the spell starts working with no change to this file.

The timer path was proven on the same boot: eleven ticks at a temporary 20s cadence,
seven finished runs, every one `ok`, each summary a real `gh` readout off a live
Claude session.

`cron.unit.test.ts` used to pin the fit's *failure* and now pins the fit, so the day
a row stops publishing its payload schemas it fails there rather than in a config
that will not boot. `boot.unit.test.ts` pins the new port on the booted row, which is
the end-to-end proof that the command is addressable at all.

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

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview deployed

  • web — Stage pr-9226 torn down.

@cansirin

Copy link
Copy Markdown
Contributor Author

review-code: FAIL @ 0e6b575 content:265d2661134e — repair round

review-code — FAIL

Graded against #9225's seven acceptance criteria, read at head 0e6b5750. Conjunctive: one FAIL row fails the class.

Per-criterion

  • [PASS] 1. cron(options) on defineProgram, job filled through fill, no session package imported — apps/tuval/src/cron/cron.ts L300–314 (defineProgram({...cronProgram(fill), fill: {job: fill.job}, ...})); imports are ../ai-agent/ports/index.ts (the interface module, same import authoring/example/pr-review.ts makes) and ../authoring/* only. cron.unit.test.ts "puts the job on the row's fill" drives the compiled spawn handler and sees claude-session resolved off the fill.
  • [FAIL] 2. A tick spawns, sends a well-formed PromptPayload, records on result and stops the child; a child that ends before answering is recorded as a failed run; a tick mid-run spawns nothing — the first and third clauses hold (startIfIdle, spawned → send({process, port: "prompt"}, {text, key, timestamp}), result → recorded + stop(state.child); tests "sends the configured prompt…", "stops the job it started…", "drops a tick…"). The middle clause does not hold at runtime. The authoring layer produces a stopped event in exactly one place: apps/tuval/src/authoring/define-program.ts L333–336, stopHandler, as the answer to the program's own stop effect. A child that exits on its own publishes {kind: "stopped"} to the ProcessTable stream (apps/tuval/src/process/Processes.ts L289) and its live finalizer in apps/tuval/src/commands/core/process.ts L326–334 deletes the entry; nothing subscribes and dispatches a stopped into the parent's inbox (git grep of stopped( / "stopped" across apps/tuval/src at head finds no other producer). So the stopped cell's "the job died before it answered" branch (cron.ts L217–231) is unreachable under the live kernel: a job that crashes before result leaves child set and every later tick dropped until a restart. cron.unit.test.ts "records a failed run when the job dies before it answers" feeds the event by hand, so it proves the cell, not the behaviour the criterion names. The cell's docblock states the behaviour as delivered ("without it a crashed job would leave … child set forever") — that claim is not grounded in the code read. Repair: either deliver an unsolicited child exit to the spawner (the ProcessTable stopped change for a child spawned with on → the parent's stopped event), with a test that drives it through the compiled handlers rather than testProgram; or, if that is a kernel issue of its own, file it, cite it in the stopped cell and the module header the way process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 is cited, and disclose it under ## Deviations.
  • [PASS] 3. A restored cron whose checkpointed child is not live records the run as interrupted and the next tick spawns — resume (L262) emits restored only when child !== null; the restored cell (L253–261) records INTERRUPTED and clears child; tests "records the half-finished run as failed…", "spawns on the very next tick…", "reconciles once…". Note: the cell records interrupted whether or not the child is live — a superset of the criterion — and L234–252 states why (restore spawns with no on record and unwired ports, verified at apps/tuval/src/durability/restore.ts L26/L55, so a live restored child cannot answer; a blind stop fails ProcessNotFound out of resume). Accepted as documented.
  • [PASS] 4. title/status on the kernel's title@1/status@1 — title/status derived lines (L281–291) compile onto TITLE_PORT/STATUS_PORT (define-program.ts L194–199; self-report.ts L17–18). Tests assert emit(STATUS_PORT, "last run 07:00 · ok") etc.; boot.unit.test.ts pins the booted row's title@1:out(tuval/title/v1),status@1:out(tuval/status/v1) ports.
  • [PASS] 5. :cron run as a commands cell answering send("run"); run cell behaves as a tick; refusal documented as process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 — commands.run (L266–279) returns send("run", request); the run port cell (L184–187) calls the same startIfIdle; the module header and the command docblock name process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 and work nothing around it. resolveOwnProcess (authoring/own-process.ts) and SpawnedProcesses.send's live-only map (commands/core/process.ts L242–250) confirm the header's account of the refusal. Tests under "cron's run command".
  • [FAIL] 6. Config registers cron behind a feature toggle with claudeSession(...); pnpm dev boots with it on; one real tick lands ok: true in .tuval/processes/cron.json — the row is gated on a local features.cron and the job is claudeSession({cwd, scope}) (apps/tuval/.tuval/tuval.config.ts L73–96, L120–123, L129–136), and boot.unit.test.ts boots the box config with cron live. Two misses. (a) cron is not declared on TuvalFeatures / featuresDefault in apps/tuval/src/features.ts. DeclaredFeatures derives its keys from featuresDefault (config.ts L70–76) and its own comment records that an undeclared key "was dropped by the decode" — so the config's cron: true reaches the row (ADR 0375 §1) and nothing else: not the Features service, not the generated browser module. ADR 0375 §3 and features.ts's header ("A flag added here is one edit…") require the flag declared there; prReviewExample, the one precedent, is in both places. Filed as round-2 criterion effect-migration: port resolver-down code to Effect Context.Service #8 on Tuval has no scheduler: a cron program on defineProgram that starts a job program on a timer and reports each run on its tile #9225. (b) The "one real tick lands a run with ok: true" half is evidenced by nothing this gate reads — it lives in PR-body prose, which is not an input; no checked-in artifact or test carries it. Not weighed as a defect, named as unverified.
  • [PASS] 7. cron.unit.test.ts on testProgram; boot.unit.test.ts updated — 25 cases across the tick, result, stopped, restored, run-command and shape seams; boot test moves 8→9 programs, 3→4 processes, +1 spell, and pins the cron process line. The pre-existing assertion changes are the ones the criterion asks for.

Standing checks

  • Test honesty: no pre-existing assertion weakened; the boot-count moves are the criterion's own.
  • Release containment: default-ON, stated with rationale in the config comment. Containment is stated; the toggle itself is unregistered (row 6a).
  • Comment discipline: the cron.ts docblocks are dense but nearly all state kernel constraints the code cannot show (restore wiring, SpawnedProcesses delivery, ADR 0372). One of them (the stopped cell) states a behaviour the kernel does not deliver — row 2.
  • Staleness traps: none. Timer Sub is dep-keyed on everyMs; child is cleared on result, not on the answering stopped.

Fan-out

  • Silent failure: row 2 — an unsolicited child exit is silently absorbed (no event, child stays set).
  • Type design (out of scope, not filed): CronState.child and startedAt are two nullables that must move together; a single running: {child, startedAt} | null would make the half-set state unrepresentable. Nit.
  • Test gap: row 2 — nothing drives a child exit through the compiled handlers.

Deviations

Disclosed Substance Matched against findings
:cron run refuses for a graph-launched cron #8944, documented, not worked around Matches row 5; PASS
attached-desk.unit.test.tsx › closes on Escape red on base pre-existing on bc890986 Not cron's; CI-at-head is the gate's answer
Stopped children accumulate in the manifest #9220 Matches; out of this diff
— undisclosed — stopped cell unreachable for an unsolicited child exit (row 2) Not in the section
— undisclosed — cron flag stated in config but not registered in features.ts (row 6a) Not in the section

deviation-disclosure: FAIL — two findings this gate could see are not in the section.

CI at head

settle settled · ci 0e6b5750 green — 45 check runs, 39 success, 6 skipped; 28 of 40 repo-authored workflows ran at head. Execution evidence is green; the FAIL above is judgement CI cannot make.

Verdict

FAIL on rows 2 and 6. Repair round: deliver (or honestly scope and disclose) the child-exit path, register the cron flag in features.ts, and re-post over the new head.

Verdict-written: 2026-09-15T10:36:18Z

cansirin and others added 2 commits September 15, 2026 03:42
`.tuval/tuval.config.ts` stated `features.cron` and nothing else knew the key:
`DeclaredFeatures` derives its keys from `featuresDefault`, so an undeclared key
is dropped by the decode and reaches the row (ADR 0375 §1) and neither the
`Features` service nor the generated browser module. Declared on
`TuvalFeatures`/`featuresDefault` the way `prReviewExample` is — off there, this
project's layer flips it on — and the config and dev-server records that pin the
flag set move with it.

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

`stopped` is only ever the answer to a program's own `stop` effect
(`stopHandler`); nothing dispatches an unsolicited child exit into a spawner's
inbox, so the cell's "died before answering" branch is unreachable today. That
is #9227, filed against the kernel. The cell and its test stay — the branch is
written for the day #9227 delivers the event — and the module header, the
docblock and the test name now say so, cited the way #8944 is.

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

Copy link
Copy Markdown
Contributor Author

review-code: PASS @ 268e017 content:531c21a57266 — merge-ready

review-code — PASS (repair round 2)

Graded against #9225's eight acceptance criteria (seven original, #8 appended at round 1), read at head 268e017a. Conjunctive.

Per-criterion

  • [PASS] 1. cron(options) on defineProgram, job filled through fill, no session package imported — apps/tuval/src/cron/cron.ts L318–333; imports are ../ai-agent/ports/index.ts (the interface module) and ../authoring/* only. Unchanged from round 1.
  • [PASS] 2. Tick spawns, sends a well-formed PromptPayload, records on result and stops the child; a child that ends before answering is recorded as a failed run; a tick mid-run spawns nothing — first and third clauses as before (startIfIdle, spawned → send({process, port: "prompt"}, {text, key, timestamp}), result → recorded + stop). The middle clause took the repair path the round-1 verdict named: the kernel gap is filed as An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227 (open, describes exactly the single-producer stopHandler at authoring/define-program.ts L331–336 — re-verified at this head, no other stopped producer into a spawner's inbox), cited in the module header and the stopped cell's docblock the way process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 is, the test renamed to say it proves the cell and not live behaviour ("cell, unreachable until An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227…"), and the gap disclosed under ## Deviations. The round-1 misstatement ("without it a crashed job would leave child set forever") is gone; the docblock now states the live consequence honestly. Nothing worked around.
  • [PASS] 3. Restored cron with a checkpointed child records interrupted, next tick spawns — resume L258, restored cell L245–257; tests under "cron, restarted mid-run". Unchanged.
  • [PASS] 4. title/status on title@1/status@1 — L277–287; boot.unit.test.ts pins title@1:out(tuval/title/v1),status@1:out(tuval/status/v1) on the booted row. Unchanged.
  • [PASS] 5. :cron run as a commands cell answering send("run"); run cell is a tick; refusal documented as process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 — commands.run L266–275, run port cell L183–186, header cites process send cannot reach a graph-launched process: only ad-hoc spawns are addressable #8944 (open). Unchanged.
  • [PASS] 6. Config registers cron behind a feature toggle with claudeSession(...); pnpm dev boots with it on; one real tick lands ok: true — .tuval/tuval.config.ts L74–95, L122–123, L129–136; boot.unit.test.ts boots the box config with cron live (9 program(s), 4 process(es), process cron … state=running@0). Round-1 miss (a) is closed by criterion 8 below. The "one real tick lands ok: true" half is still evidenced by nothing this gate reads — PR-body prose says as much itself — so it is named as unverified, not weighed as a defect, the same reading round 1 gave it.
  • [PASS] 7. cron.unit.test.ts on testProgram; boot.unit.test.ts updated — 26 cases; boot test moves 8→9 programs, 3→4 processes, CRON_SPELLS = 1. Unchanged in substance; one test renamed for An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227.
  • [PASS] 8. cron declared on TuvalFeatures and featuresDefault with the ADR 0375 row-flag docblock — apps/tuval/src/features.ts L63–77 and L94: the docblock mirrors prReviewExample's (row stated one layer lower, A Tuval feature flag stated in a config layer never reaches the node side #8595, global layer reaches the Features service and not the row, ADR 0375). DeclaredFeatures derives its fields from featuresDefault (config.ts L76), so cron: true in the config now survives decode. config.unit.test.ts (7 expectation records) and page/dev-server.unit.test.ts (3 generated-module strings) carry cron: false at default.

Standing checks

  • Test honesty: no pre-existing assertion weakened; the boot-count and feature-record moves are criterion 7/8's own.
  • Release containment: default-off in featuresDefault, flipped on by this repo's own layer with the rationale stated in both the config comment and the features.ts docblock. Matches the diff.
  • Comment discipline: dense but load-bearing — restore wiring, SpawnedProcesses delivery, ADR 0372, An authored spawner never hears an unsolicited child exit: stopped is only ever the answer to its own stop effect #9227's unreachable branch are all constraints the code cannot show. No narration of obvious flow.
  • Staleness traps: none. Timer Sub dep-keyed on everyMs; child cleared on result, not on the answering stopped.

Fan-out

Deviations

Disclosed Substance Matched against findings
:cron run refuses for a graph-launched cron #8944, documented, not worked around Row 5; PASS
attached-desk.unit.test.tsx › closes on Escape red on base pre-existing on bc890986 Not cron's; CI-at-head green is the gate's answer
stopped branch unreachable for an unsolicited child exit #9227, cell kept and honestly named Row 2; PASS
Stopped children accumulate in the manifest #9220 Out of this diff

deviation-disclosure: PASS — nothing undisclosed that this gate could see.

CI at head

settle settled · ci 268e017a green — 45 check runs, 39 success, 6 skipped; 28 of 40 repo-authored workflows ran at head.

Verdict

PASS. Both round-1 FAIL rows (2, 6a) are repaired on this head; no new finding routes to an acceptance criterion.

Verdict-written: 2026-09-15T10:55:59Z

@cansirin

Copy link
Copy Markdown
Contributor Author

routed-elsewhere: review-ui @ 268e017 — cron program row + flag default; no rendered surface, component, or style changes

LAW-SOURCE: manifest-prose (ui law exit 13 — untyped registry).

Seven files change, all under apps/tuval; the ui class fired off the apps/tuval/src/ prefix test alone.

  • apps/tuval/src/cron/cron.ts (new): a process program — timer tick, spawn a job program, record the run. Imports are @demlik/tea, effect, authoring/ports/process modules; no JSX, no stylesheet, no page. Its only UI contact is the title/label strings it hands the existing board-tile chrome, which this PR does not modify.
  • apps/tuval/src/features.ts: adds the cron: boolean flag, default false.
  • apps/tuval/.tuval/tuval.config.ts: registers the row and flips the flag on for this repo's own desk.
  • apps/tuval/src/page/dev-server.unit.test.ts: the generated features-module string grows one key.
  • boot.unit.test.ts, config.unit.test.ts, cron.unit.test.ts: tests.

No composition, page, screen, state or style a user sees is added or changed; the existing tile paints one more row of the shape it already paints. Tuval is a local app (no alchemy.run.ts), so there is also no preview deployment to capture. Text judgment belongs to review, which stands PASS at this head.

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.

Tuval has no scheduler: a cron program on defineProgram that starts a job program on a timer and reports each run on its tile

1 participant