Skip to content

feat: scheduled boards — define & schedule auto-refreshing boards#221

Merged
ivanmkc merged 9 commits into
masterfrom
worktree-scheduled-boards
Jul 4, 2026
Merged

feat: scheduled boards — define & schedule auto-refreshing boards#221
ivanmkc merged 9 commits into
masterfrom
worktree-scheduled-boards

Conversation

@ivanmkc

@ivanmkc ivanmkc commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What

Scheduled boards: a termchart board that an agent re-refreshes on a schedule — a daily 8am "my tasks" board, or a job tracker refreshing every 2 minutes that stops itself when the job ends.

Design: termchart owns two portable pieces — a committed YAML definition (.termchart/boards/<id>.yaml) and a host-agnostic termchart run <id> primitive that hands the board's assembled prompt to an agent (stdin, tokenised spawn, no shell) which gathers data and pushes. Any scheduler calls the same primitive: Claude Code session cron (v1 first-class), GitHub Actions (turnkey scaffolding), or system cron.

Spec: docs/superpowers/specs/2026-06-24-scheduled-boards-design.md

Surface

  • core: validateBoardDef() — id/cron (charset+bounds)/tz/target/lifecycle/agentCommand validation
  • cli: termchart board create|list|show|prompt|scaffold-workflow|delete, termchart run <id> (viewer pre-flight exit 4; lifecycle enforcement: enabled, until, persisted maxRuns counter)
  • workflow scaffolding: version-pinned npx, agent install step, permissions:+GH_TOKEN, timeout, concurrency, and DST-correct scheduling (two seasonal UTC crons + an offset-keyed guard on github.event.schedule — delay-immune, dispatch always runs; day-of-week shifts correctly across UTC midnight incl. Saturday wrap)
  • plugin: /termchart:schedule-board command + scheduled-boards skill
  • versions: CLI 0.5.0→0.7.0 (0.6.0 was already on npm pre-feature), plugin →0.14.0

Verification

  • TDD throughout; suites: core 13, cli 234 (incl. built-bundle regression + a behavioral test that executes the generated guard shell), viewer 492 — all green
  • 4 independent "new user" agent testers dogfooded the feature from docs alone (commit 2 addresses their findings); an independent senior-review pass followed (commit 4 fixes its 2 criticals + hardening: id path-traversal guard, quote-rejecting agentCommand contract, stem/id match, atomic writes)
  • Live-verified end-to-end against the production viewer (run → agent → push → SSE update), then cleaned up

Notes for release

  • npx @ivanmkc/termchart@0.7.0 is pinned in scaffolded workflows — publish the CLI before advertising the GH Actions path (publish is auth-gated).
  • Plugin bump (0.14.0) needs a marketplace update per the usual channel split.

A scheduled board is a normal project/agent board that an agent re-refreshes
on a schedule. termchart owns two portable pieces — a committed YAML definition
(.termchart/boards/<id>.yaml) and a host-agnostic `termchart run <id>` primitive
that hands the board's assembled prompt to an agent which gathers data and pushes.

- core: validateBoardDef() — pure, dependency-free structural validator
- cli: `termchart board create|list|show|prompt|scaffold-workflow|delete`
       `termchart run <id>` — assemble prompt + spawn agentCommand (prompt on
       stdin to avoid E2BIG/shell-quoting), exit-code propagation, disabled skip
- cli: scaffold-workflow generates a GitHub Actions workflow; tz->UTC cron
       conversion for the single-hour case with DST/day-cross warnings
- plugin: /termchart:schedule-board command + scheduled-boards skill
          (session-cron v1, GH Actions turnkey, system cron compatible)

Build: esbuild ESM bundle now injects a createRequire banner so bundled CJS
deps (yaml) resolve node builtins instead of throwing "Dynamic require".

Version bumps: cli 0.5.0->0.6.0, plugin 0.12.0->0.13.0, marketplace ->0.13.0.

Tests: core validator (10), board CRUD, prompt assembly, run primitive,
scaffold-workflow + tz conversion, and a built-bundle regression guard.
…ns, UX

Agent-tester dogfooding (4 fresh "new user" agents) surfaced real gaps; fixes:

Correctness / honesty:
- Lifecycle is now actually enforced by `termchart run`: skip past `until`;
  persistent `lifecycle.runs` counter enforces `maxRuns` (written back to YAML).
  assembleBoardPrompt now always includes a self-unschedule clause for bounded
  OR frequent (sub-daily) boards — the job-tracker case the docs promised.
- `run` pre-flights the viewer env and exits 4 (was: swallowed the failure in
  the agent subprocess and returned 0). Rejects unknown flags; one-line verbosity.
- Version: bump 0.6.0 -> 0.7.0 (0.6.0 was already published to npm without this
  feature); scaffolded workflow pins `@ivanmkc/termchart@0.7.0`.

GitHub Actions turnkey + DST-correct:
- DST handled via two seasonal UTC crons + a local-time guard step (GH cron is
  UTC-only) so the board fires at the intended local time year-round.
- Day-of-week is correctly shifted on UTC midnight crossing (was: emitted a
  knowingly-wrong cron with a warning); restricted day-of-month is warned, not
  mis-shifted.
- Workflow now installs the agent CLI, sets permissions + GH_TOKEN, timeout-
  minutes, and a concurrency guard. Non-claude agentCommand omits the Anthropic
  secret + claude install (gets a TODO note).

CLI/UX:
- Per-subcommand `--help`; `--enabled/--until/--max-runs` flags; create always
  writes a visible lifecycle block. `board list` warns about skipped invalid
  files instead of hiding them. Unknown top-level command says so (instead of
  trying to open it as a file); unknown flag names the flag, not its value;
  missing-required-flag names which; `board delete` shows the expected path;
  YAML parse errors carry the board id.

Docs: skill + command truthed-up (lifecycle enforcement + GH-Actions maxRuns
caveat, turnkey workflow, component payload pointer, run exit-4).

Tests: core 11, cli 228 (incl. built-bundle smoke); re-verified live against the
remote viewer (pre-flight exit 4, maxRuns stop, DST workflow, full push loop).
…ning

Senior-review pass on the branch found 2 critical defects in the GH Actions
scaffolding plus hardening gaps; all fixed with tests.

Critical:
- DST guard was exact-minute wall-clock equality — GitHub's routinely-late cron
  starts (3-20+ min) would skip nearly every run, and workflow_dispatch was
  swallowed too. Now keyed on WHICH cron fired (github.event.schedule via an
  EVENT_SCHEDULE env) vs the zone's CURRENT UTC offset (date +%z): delay-immune,
  fail-open, dispatch always runs. Behavioral test executes the generated shell.
- shiftDow emitted an invalid descending range when a day-of-week shift wrapped
  past Saturday ("4-6"+1 -> "5-0", which GitHub rejects — silent dead schedule).
  Wrapping ranges now split ("5-6,0").

Hardening:
- Board ids are validated (BOARD_ID_RE, exported from core) before any path
  join — no traversal via `board delete ../../x` etc.
- agentCommand containing quotes is rejected at validation (whitespace-split,
  no-shell contract documented in the skill); filename stem must match id
  (loadBoard error + scanBoards issue); saveBoard writes tmp+rename (atomic).
- Cron field values validated in core (charset, per-field bounds, step>0,
  numeric-only) so typos fail at create, not on GitHub.
- scaffold-workflow warns when lifecycle.maxRuns can't persist on an ephemeral
  runner (suggests until) and when the board's host isn't github-actions; the
  bounded-lifecycle prompt clause no longer overclaims enforcement.
- --enabled accepts only true|false; removed unused cronToUtc export.

Also: regenerate package-lock for 0.7.0; spec truth-ups (drop the never-built
{prompt} placeholder, record run-enforced lifecycle, note next-run deferral);
sessionCronId write-back instruction in the schedule-board command.

Tests: core 13, cli 234, viewer 492 — all green; live run->push->viewer loop
re-verified against the remote viewer.
…redicate narrowing

Applied the transferable /dev-coding-standards rules to the branch:

- Single source of truth: "claude -p" default was duplicated in run.ts,
  board.ts (show), and scaffold-workflow.ts — now one DEFAULT_AGENT_COMMAND
  exported from core alongside the BoardDef domain.
- Correct-by-construction: added isBoardDef() type predicate in core; the six
  `as BoardDef` casts in board-store.ts are gone — narrowing is structural.
- Version dual-source (version.ts vs package.json is forced by the standalone
  bundle): added a tripwire test asserting they match, since scaffolded
  workflows pin @ivanmkc/termchart@VERSION.
- Dead code: removed unused BoardDeps.env field and the unconsumed
  BOARDS_SUBDIR export.
- No function-scoped imports: hoisted a require() in scaffold-workflow.test.ts
  to a top-level import.

Behavior unchanged: core 14, cli 235 green (viewer-stub files re-run serially
under load-114 box contention); built binary spot-checked.
…ax-runs parsing

Deep second audit against /dev-coding-standards. One real violation found:
create() smuggled a non-numeric --max-runs value through
`as unknown as number` so the core validator would report it — a deliberate
type-system lie (hidden from grep by its own trailing comment). Now validated
at the flag edge with an error that names --max-runs (test added, RED→GREEN).

Also: JSDoc on BOARD_ID_RE. Everything else audited clean: catch-(e as Error)
is idiomatic TS, cron "*" literals are domain vocabulary, host comparison is
compile-checked via the BoardHost literal type, planSchedule's early returns
are graceful degradation (one strategy), spawn injection is DI not patching.
…pped-field warning

Closes the two findings left open by runtime verification:

- `termchart run` now kills a hung agent: --timeout <secs> (default 600),
  SIGTERM then SIGKILL after 5s, exit 124 (GNU timeout convention). A stuck
  default `claude -p` previously wedged a session-cron/local-cron scheduler
  indefinitely (only GH Actions had a timeout-minutes backstop). Verified
  against the built bundle: `sleep 60` agent killed at 1s → exit 124.

- `board create --force` REPLACES the definition; it now warns on stderr with
  the exact previously-set fields that were dropped because they weren't
  re-passed (description, tz, host, agentCommand, sessionCronId,
  lifecycle.until/maxRuns). This bit live testing when a --force re-create
  silently lost --tz.

Docs updated (skill CLI reference, top-level usage, verify-skill gotchas).
Tests: run 18, board 24, bundle incl. real-process timeout kill — green.
@ivanmkc ivanmkc force-pushed the worktree-scheduled-boards branch from c10b4a7 to 0ae7841 Compare July 4, 2026 02:02
@ivanmkc ivanmkc marked this pull request as ready for review July 4, 2026 02:31
@ivanmkc ivanmkc merged commit 4f9f778 into master Jul 4, 2026
5 checks passed
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.

2 participants