Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "termchart",
"source": "./plugin",
"description": "A live canvas your AI draws on — instead of walls of text, your agent pushes rich, native visuals to a browser / iPad / second screen in real time: React Flow graphs (architecture, sequence, call-graph, ER/class, state machine, PR-review, journeys, recursion trees), Vega-Lite charts (incl. scientific + Big-O), Mantine dashboards & product comparisons, deep-code walkthroughs, markdown, and split panes, with animated edges and live status overlays (toasts, progress bars, loaders). Deterministic Mermaid → ASCII/Unicode is the terminal fallback. Persona recipes, a showcase gallery, and fullscreen panes.",
"version": "0.12.3",
"version": "0.14.0",
"license": "MIT",
"keywords": [
"mermaid",
Expand Down
71 changes: 71 additions & 0 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: verify
description: How to runtime-verify termchart CLI changes (esp. scheduled boards) — build, drive the real binary against the live viewer, execute generated workflow guards. Use when verifying a termchart diff at its surface instead of re-running CI.
---

# Verifying termchart CLI changes at the surface

## Handle

```bash
cd packages/cli && npm run build # tsc + esbuild → dist/cli.js (single bundle)
node packages/cli/dist/cli.js --version # sanity: version matches src/version.ts
```

Drive `node <repo>/packages/cli/dist/cli.js` as `termchart` — never `import ./src/...`.
The **built bundle** is the surface: esbuild CJS→ESM shims have broken it before while
unit tests stayed green (yaml dep dynamic-require).

## Live viewer

`TERMCHART_VIEWER_URL`/`TERMCHART_VIEWER_TOKEN` live in `~/.profile`. The Bash tool shell
is initialized from the profile, so they are often ALREADY SET — to test the "no viewer"
path use `env -u TERMCHART_VIEWER_URL -u TERMCHART_VIEWER_TOKEN …`, don't assume unset.
Verify pushes landed with `termchart list --project <p>` and round-trip with `pull`.
Always `termchart clear --project <p> --agent <a>` when done; use a unique throwaway scope.

## Scheduled boards flow

Work in `mktemp -d` (definitions are cwd-relative: `.termchart/boards/<id>.yaml`).

```bash
termchart board create --id x --schedule "0 8 * * 1-5" --project <p> --agent <a> \
--prompt "…" [--tz Zone] [--max-runs N] [--agent-command <cmd>]
termchart run x # spawns agentCommand, prompt on stdin
```

- For a deterministic run→push→viewer proof, point `--agent-command` at a stub script that
`cat`s stdin then calls the real binary's `push`. Don't use the real `claude -p` in probes —
it can sit silently until `run`'s timeout (default 600s; override with `--timeout <secs>`,
hung agents are killed → exit 124).
- To exercise DEFAULT agent resolution safely (without a hanging real `claude`), run with a
PATH that has node but not claude: `mkdir nodeonly && ln -s "$(which node)" nodeonly/node;
PATH="$PWD/nodeonly:/usr/bin:/bin" node $BIN run <id>` → expect `cannot launch agent
"claude"` + exit 127. (Plain `PATH=/usr/bin:/bin` loses node itself — fnm installs it
elsewhere.)
- lifecycle checks: `--max-runs 2` → run 3× (third skips, `runs:` persisted in YAML);
`--until <past>` and `--enabled false` → skip lines, exit 0.

## Generated GH workflow

`termchart board scaffold-workflow <id>` → `.github/workflows/termchart-<id>.yml`.
Execute the DST guard locally (don't just read it):

```bash
sed -n '/run: |/,/env:/p' .github/workflows/termchart-<id>.yml \
| sed '1d;$d;s/^ //' | sed 's/^npx .*/echo RAN/' > guard.sh
EVENT_SCHEDULE="<in-season cron>" bash guard.sh # → RAN
EVENT_SCHEDULE="<off-season cron>" bash guard.sh # → skip line
EVENT_SCHEDULE="" bash guard.sh # dispatch → RAN
```

In-season = the cron whose offset matches `TZ=<zone> date +%z` today.

## Gotchas

- System awk is mawk: `{10}` regex intervals silently don't match — use sed.
- Piping the CLI into `head` can produce bogus exit codes (SIGPIPE) — redirect to a file.
- The full vitest suite is load-flaky (viewer-stub `fetch failed`) under a parallel
process; that's contention, not a regression — but /verify shouldn't run tests anyway.
- `board create --force` REPLACES the definition; fields not re-passed (e.g. --tz) are lost —
a stderr warning now lists exactly which were dropped.
240 changes: 240 additions & 0 deletions docs/superpowers/specs/2026-06-24-scheduled-boards-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# Scheduled Boards — Design

**Date:** 2026-06-24
**Status:** Approved (brainstorm) — pending spec review
**Branch:** worktree-scheduled-boards

## Problem

termchart is a push-based system: an *agent* (Claude, `agy`, any CLI caller) gathers
data, decides a layout, and pushes static JSON to a board (a `project/agent` scope on
the viewer). The viewer is a dumb display that live-updates over SSE. Today a board only
changes when a human triggers an agent to push.

We want **boards that refresh themselves on a schedule** — generically enough to cover:

- A **daily morning board** ("today's tasks, calendar, open PRs") that rebuilds at 8am.
- A **fast job-tracker** that refreshes every couple of minutes while a job runs, then
stops when the job finishes.

These differ in cadence and lifetime but share one shape: *on a schedule, an agent
re-runs a saved intent and pushes the result to a fixed board.*

## Decisions (from brainstorming)

| Question | Decision |
|---|---|
| What produces refreshed content? | **An agent re-runs** a saved prompt (not a fixed data pull). Most flexible; covers all cases. |
| What drives the schedule? | **Host-agnostic.** termchart owns a board *definition* + a `run` primitive; any scheduler calls it. |
| First-class host in v1 | **Claude Code session cron** (`CronCreate`), wired by the skill. |
| Where do definitions live? | **Committed repo files** — portable, diffable, a GH Actions runner gets them on checkout. |
| v1 scope | **Core + GitHub Actions scaffolding.** (Viewer badge deferred.) |
| Definition file format | **YAML** (`.termchart/boards/<id>.yaml`). |

### Non-goals

- No cron engine inside the viewer server (it runs on Cloud Run — scales to zero,
ephemeral; a server-side scheduler there is unreliable and contradicts repo-file
definitions).
- No fixed/no-LLM data-pull mode in v1 (the refresh engine is always an agent; a board's
prompt can *tell* the agent to run a command, which covers the deterministic case).
- No viewer "scheduled" badge in v1 (passive label is a clean follow-up).

## Architecture

Three cooperating pieces, each in its existing termchart home:

```
.termchart/boards/<id>.yaml ── board definition (committed, portable)
├── packages/cli ── `termchart board` CRUD + `termchart run <id>` (host-agnostic primitive)
│ + `termchart board scaffold-workflow <id>` (GH Actions YAML)
└── plugin skill ── `/termchart:schedule-board` guides the agent to author a
definition and register it with a scheduler (CronCreate v1)
```

The unifying primitive is the **assembled prompt**: for any board, `termchart board prompt
<id>` prints the exact text handed to an agent. Every host path uses that same text, so an
in-session cron and a headless GH Actions run produce identical behaviour.

### Data flow per refresh

1. A scheduler fires (session cron in-process, or GH Actions / system cron calling
`termchart run <id>`).
2. The board's assembled prompt is handed to an agent (`agentCommand`).
3. The agent gathers data with its own tools and `termchart push`es to the board's
`target` (`project/agent`) on the viewer.
4. The viewer broadcasts the update over SSE; connected browsers refresh instantly —
exactly the existing push path. **No new viewer code is required for v1.**

## Component 1 — Board definition

One YAML file per board at `.termchart/boards/<id>.yaml`:

```yaml
id: daily-standup # also the filename stem; [a-z0-9-]
description: "Morning board: today's tasks, calendar, open PRs"
schedule: "0 8 * * 1-5" # 5-field cron
tz: "America/Los_Angeles" # optional; default = system tz. Used for cron interpretation.
target:
project: me
agent: daily # the board scope the push updates
prompt: |
Gather my open tasks, today's calendar, and assigned PRs.
Build a component board grouped by priority and push it to project=me agent=daily.
agentCommand: "claude -p" # agent-agnostic; prompt delivered on stdin (see below)
host: claude-session # claude-session | github-actions | cron
lifecycle:
enabled: true
until: null # ISO-8601; stop scheduling after this instant
maxRuns: null # stop after N successful runs
```

- `prompt` is the saved intent. It is self-contained: it names the `target` so a headless
agent with no plugin context still knows where to push.
- `target` reuses the existing `project/agent` scope model — **a scheduled board is just a
normal board an agent keeps refreshing.**
- `agentCommand` keeps the system agent-agnostic (Claude, `agy --print`, or a test stub),
honouring the project's cross-tool integration stance.
- `lifecycle` gives the job-tracker case a natural end (`until` / `maxRuns`), and lets a
board be paused (`enabled: false`).

### Validation

A `validateBoardDef()` in `packages/core` (sibling to the existing content validators):
checks `id` shape, required fields, a 5-field cron, valid `tz`, `target.project/agent`
non-empty, `host` enum. Surfaces precise errors used by every CLI subcommand.

## Component 2 — CLI surface (`packages/cli`)

Mirrors the existing `termchart template <cmd>` sub-dispatch pattern.

| Command | Behaviour |
|---|---|
| `termchart board create` | Scaffold a `.termchart/boards/<id>.yaml` from flags (`--id --schedule --project --agent --prompt --host --tz`). No LLM. |
| `termchart board list` | List defined boards: id · schedule · target · host · enabled. `--json`. |
| `termchart board show <id>` | Print the parsed/validated definition + effective defaults. `--json`. (Computed next-run display deferred — needs a cron evaluator; not in v1.) |
| `termchart board delete <id>` | Remove the file. |
| `termchart board prompt <id>` | Print the fully-assembled agent prompt (the canonical text every host hands to the agent). |
| `termchart run <id>` | **Host-agnostic run primitive.** Load + validate def, assemble prompt, execute `agentCommand` with the prompt on **stdin**, return its exit code. The agent does the push. |
| `termchart board scaffold-workflow <id>` | Emit `.github/workflows/termchart-<id>.yml` (`schedule:` cron + `workflow_dispatch`) that runs `termchart run <id>`. |

### Prompt delivery (robustness)

`termchart run` delivers the assembled prompt to `agentCommand` via **stdin**, not as an
argv string. This avoids the `E2BIG` argv-overflow class of failure and all shell-quoting
hazards. `agentCommand` is tokenised on whitespace (not run through a shell; quotes are rejected
at validation) and the prompt is piped in; `claude -p` and `agy --print` both accept a prompt on
stdin. Stdin is the only delivery path — an inline `{prompt}` placeholder was considered and
dropped (YAGNI; it reintroduces the quoting/E2BIG hazards).

### Assembled prompt

`termchart board prompt <id>` composes:

1. The board's `prompt` (the saved intent).
2. An explicit **push instruction**: the exact `target` (`project`/`agent`) and a reminder
to push with the termchart CLI, so a context-free headless agent still succeeds.
3. A **lifecycle instruction** when relevant: e.g. "If the tracked job has finished,
unschedule this board (`CronDelete` in-session, or disable the workflow)." This is how
the job-tracker self-terminates.

## Component 3 — Scheduling / host integration

### v1 first-class — Claude Code session cron

The `/termchart:schedule-board` skill, after authoring the definition, registers a
`CronCreate` whose prompt is the output of `termchart board prompt <id>`. It fires
**in-session** (no subprocess) — ideal for "watch this running job while I work." The skill
records the cron id in the board file (`sessionCronId`, written back) so it can be updated
or `CronDelete`d. Session crons inherit the harness's 7-day auto-expiry. `lifecycle.until`/
`maxRuns` are enforced deterministically by `termchart run` (skip past `until`; a persisted
`lifecycle.runs` counter stops at `maxRuns` — counter-based stops need the YAML to persist, so on
ephemeral GitHub runners use `until`), with the assembled prompt additionally instructing the
agent to self-unschedule when the tracked work completes.

### v1 turnkey — GitHub Actions

`termchart board scaffold-workflow <id>` generates:

```yaml
name: termchart board <id>
on:
schedule:
- cron: "<schedule, converted to UTC>" # comment notes the source tz
workflow_dispatch: {}
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npx -y @ivanmkc/termchart run <id>
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
TERMCHART_VIEWER_URL: ${{ secrets.TERMCHART_VIEWER_URL }}
TERMCHART_VIEWER_TOKEN: ${{ secrets.TERMCHART_VIEWER_TOKEN }}
```

Caveats the scaffolder prints:
- **GH Actions cron is UTC.** The scaffolder converts `schedule` from `tz` to a UTC cron
and annotates it with a comment; it warns that DST shifts the wall-clock time (a fixed
offset is used — exact DST-aware scheduling is out of scope).
- **A cloud runner can't see local files.** Boards whose data is local-only must use
`host: claude-session` or a local `cron` host. The scaffolder refuses (with a clear
message) only if it can detect an obviously local-only intent; otherwise it warns.
- The agent in the runner needs the termchart CLI (via `npx`) and an API key; the assembled
prompt is self-contained re: where to push.

### Compatible — system cron

Documented, not scaffolded in v1 beyond the `run` primitive: a crontab line runs
`termchart run <id>`. Same primitive, same prompt.

## Lifecycle & stop conditions

- `enabled: false` → schedulers skip the board; `run` exits 0 with a notice.
- `until` / `maxRuns` → enforced by the assembled prompt (agent self-unschedules) and
surfaced by `board show` (computed next-run / remaining runs).
- Job-tracker pattern: a short-cadence session-cron board whose prompt ends with "if the
job is done, unschedule" — it cleans itself up.

## Viewer surfacing (deferred)

Out of scope for v1. Follow-up: scheduled pushes set a `source: "schedule:<id>"` marker so
the viewer can badge "auto-updated 08:03". Passive label only — still no server scheduler.

## Testing strategy

Pure, deterministic units (no LLM, no network) cover the core:

- `validateBoardDef()` — happy path + each failure (bad id, bad cron, bad tz, missing
target, bad host enum).
- `board create/list/show/delete` — round-trip a YAML file in a temp dir.
- `board prompt` — golden assembled-prompt output (intent + push instruction + lifecycle).
- `run` with a **stub `agentCommand`** (e.g. a script that records its stdin) — asserts the
prompt is delivered on stdin, exit code propagates, `enabled:false` short-circuits.
- `scaffold-workflow` — golden YAML; tz→UTC conversion; local-data warning.

End-to-end (live verification surface): `termchart run <id>` with a real `agentCommand`
against the live viewer → confirm the target board updates over SSE.

## Release channels

Per termchart's independent channels:

- **CLI** (`@ivanmkc/termchart`) — new `board` + `run` commands, YAML dep, version bump,
publish.
- **Core** (`@ivanmkc/termchart-core`) — `validateBoardDef()`.
- **Plugin** (marketplace) — new `/termchart:schedule-board` skill/command, version bump.
- **Viewer** — unchanged in v1.

## Open implementation notes

- Add a YAML parser to the CLI bundle (`yaml`); esbuild bundles it like existing deps.
- `agentCommand` is tokenised and spawned without a shell (`spawn`, not `exec`) — no shell
injection, prompt on stdin.
- `tz`→UTC cron conversion uses a fixed current-offset translation with an explicit DST
caveat in v1.
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ivanmkc/termchart",
"version": "0.5.0",
"version": "0.7.0",
"description": "Push native diagrams, charts, dashboards & live status to the termchart viewer (browser/iPad) in real time — the CLI for the live canvas your AI draws on; also renders deterministic Mermaid → ASCII/Unicode in the terminal.",
"type": "module",
"bin": {
Expand All @@ -17,12 +17,14 @@
},
"scripts": {
"build:tsc": "tsc -p tsconfig.json",
"build": "npm run build:tsc && esbuild build/cli.js --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.js",
"build": "npm run build:tsc && esbuild build/cli.js --bundle --platform=node --format=esm --target=node18 --outfile=dist/cli.js --banner:js=\"import { createRequire as __tcCreateRequire } from 'module'; const require = __tcCreateRequire(import.meta.url);\"",
"prepublishOnly": "npm run build",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {},
"dependencies": {
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"beautiful-mermaid": "^1.1.3",
Expand Down
Loading
Loading