# AGENTS.md - packages/canvas-cli

> Local entry for `packages/canvas-cli`.
> Repository harness entry: `../../harness/README.md`.

## Module Positioning

`@pulse-coder/canvas-cli` owns the `pulse-canvas` command-line surface and the
`@pulse-coder/canvas-cli/core` helpers for external agents that need to inspect
or mutate Pulse Canvas workspaces.

Most commands operate directly on the canvas store under
`~/.pulse-coder/canvas/`: workspace manifests, per-workspace `canvas.json`,
edges, nodes, backups, and v2 per-node files. The `agent`, `team`, and `runtime`
command families are different: they require a running `apps/canvas-workspace` instance
and call its loopback runtime-control server using the bearer secret advertised
in `~/.pulse-coder/canvas-runtime/canvas-workspace.json`.

Keep this package a thin bridge over store files and runtime endpoints. The
Electron UI, active PTY lifecycle, storage migration, and runtime server belong
in `apps/canvas-workspace`; runtime-loadable plugin node behavior belongs in
`packages/canvas-nodes`.

## Knowledge Navigation

| Task | Read |
|---|---|
| Repository harness and root validation | `../../harness/README.md`, `../../harness/validate/validation.yaml` |
| Package overview and install flow | `README.md` |
| Current CLI command index | `src/cli.ts` |
| Executable entrypoint | `src/index.ts` |
| Workspace/node/edge/context commands | `src/commands/workspace.ts`, `src/commands/node.ts`, `src/commands/edge.ts`, `src/commands/context.ts` |
| Workspace auto-discovery (which canvas a command targets) | `src/core/workspace-resolution.ts`, `src/commands/options.ts` |
| External-caller surface (status/describe, error contract) | `src/commands/status.ts`, `src/commands/describe.ts`, `src/output.ts` |
| Live runtime commands and capability client | `src/commands/agent.ts`, `src/commands/team.ts`, `src/commands/runtime.ts`, `src/core/runtime-control.ts`, `src/core/runtime-capabilities.ts` |
| v2 recovery command | `src/commands/restore.ts` |
| Consistency check + safe repair (drift/orphans/edges) | `src/commands/doctor.ts`, `src/core/doctor.ts` |
| Layout read/validate/frame-grid | `src/commands/layout.ts`, `src/core/layout.ts` |
| Atomic batch mutation from a plan file | `src/commands/apply.ts`, `src/core/apply.ts` |
| Public core exports | `src/core/index.ts` |
| Store safety and schema compatibility | `src/core/store.ts`, `src/core/storage-v2.ts`, `src/core/types.ts`, `src/core/constants.ts` |
| Store-concurrency incident + lock rationale | `harness/knowledge/storage-concurrency.md` |
| Node and edge behavior | `src/core/nodes.ts`, `src/core/edges.ts` |
| Bundled agent skills | `skills/`, `src/commands/install-skills.ts` |
| Tests | `src/core/__tests__/`, `src/commands/__tests__/` |
| Local validation | `harness/validate/validation.yaml` |

Package-local documentation: `harness/knowledge/storage-concurrency.md` (the
store-concurrency incident behind the locking constraints) and
`harness/validate/validation.yaml`. Beyond those, use the root harness files
above, then the package source/tests.

## Local Constraints

- Treat `~/.pulse-coder/canvas/` and `~/.pulse-coder/canvas-runtime/` as user
  runtime data, never repository source of truth.
- Keep the package test entry on `--no-file-parallelism`: the live-command and
  status suites intentionally exercise the same hard-coded runtime descriptor,
  so running those files concurrently races their backup/write/restore cycle.
- The Electron app ships `dist/index.cjs` as an external-agent executable. Keep
  its runtime dependencies bundled (currently `commander` via tsup
  `noExternal`) so a packaged app can run it with Electron's Node runtime on a
  machine without Node, pnpm, or this monorepo.
- Preserve store safety: workspace/node id validation, manifest locking,
  atomic writes, rolling `.bak` recovery, v2 per-node compatibility, and the
  guard that refuses accidental empty-node overwrites.
- Every canvas write runs inside `withWorkspaceLock` (store.ts): full
  load→mutate→save cycles must hold the per-workspace lock
  (`<storeRoot>/__locks__/<id>.lock`), and v2 saves delete per-node files
  only for ids the mutation explicitly removed — the full-sync sweep
  (`pruneUnknownNodeFiles`) is reserved for restore/repair flows. Both rules
  exist because parallel CLI writers used to drop and even delete each
  other's nodes; regression suite: `src/core/__tests__/storage-race.test.ts`.
  The lock serializes CLI↔CLI only — the app does not take it; app↔CLI
  concurrency still relies on per-node `updatedAt` arbitration.
- `saveCanvas` bumps `canvas.revision` on every write (monotonic CLI write
  counter). `apply --atomic`'s `baseRevision` compares against it: equality
  means "no CLI write intervened", not "no write at all" — the app preserves
  the field on save (spread-through) but does not bump it. Batch mutations
  should prefer one `apply` plan (one lock, one save, all-or-nothing with
  deferred fs effects) over loops of single-node commands.
- Do not make this CLI trigger v2 migrations. `canvas-workspace` owns
  migration; the CLI adapts to the on-disk schema it finds.
- Keep `restore` narrow: it recovers from v1 snapshots and archives live
  `nodes/` data so the app can migrate cleanly later; it is not a general
  migration tool.
- Node types split into `CreatableNodeType` (what `node create` accepts:
  `file`, `terminal`, `frame`, `group`, `agent`, `mindmap`) and the wider
  `KnownNodeType` read set (`text`, `iframe`, `image`, `shape`, `reference`,
  `dynamic-app`, `plugin`). `NodeType = KnownNodeType | (string & {})` so a
  future node type from a newer app still loads and reads as an opaque node
  rather than breaking `loadCanvas`. Terminal/agent nodes created by the CLI
  have no active PTY session.
- The read-only types are surfaced by `readNode`/`context` from persisted
  `data` only — never by fetching the network or reaching into Electron. `node
  read` returns full metadata; `context` excerpts `text` and omits heavy fields
  (iframe `html`/`prompt`, plugin `payload`) to stay prompt-sized. Reading a
  live URL-iframe page body is explicitly out of scope for `node read` — it
  is available only through the runtime-authenticated capability client.
- `reference` and plugin nodes are read-compatible shapes, not CLI creation
  types. Plugin nodes are authored through the Canvas host/plugin tools, not
  this package's generic `node create`.
- `text` nodes are read+write (their markdown lives inline in `data.content`);
  `node write` edits them. Other app-produced types stay read-only.
- Error contract (machine callers): `errorOutput` emits `{ ok, error, code }`
  JSON on stderr under `--format json` (human `Error:` line otherwise); the
  format is set once by the cli.ts `preAction` hook via `setActiveFormat`. Core
  `Result` failures carry an optional `code`; command layer forwards it. Keep
  `code` values stable — external callers branch on them. New error sites should
  pass a `code`.
- `--confine-to-workspace` restricts `file`-node disk paths (from a possibly
  untrusted canvas.json) to the workspace dir: reads fall back to in-memory
  content (`pathConfined: true`), writes fail with `path_confined`. The guard is
  opt-in so existing app-created nodes (paths under `notes/`) are unaffected.
- External-caller surface: `status` (non-fatal store/workspace/runtime probe —
  never exits non-zero for "no workspace") and `describe` (self-describing
  manifest with `describeVersion`) exist so agents can pre-flight and plan.
  `context` output carries `contextVersion`; bump it (and `describeVersion`) on
  any breaking shape change. `node read` takes multiple ids (single → object,
  many → array with per-id error entries); `node search`/`node list --type`/
  `context --types` cut round-trips; `node update` owns layout/title (not data).
  Keep these output shapes and their version fields stable.
- Live `agent` and `team` commands must keep using the runtime file plus bearer
  auth. Do not bypass runtime authentication or reach into Electron memory from
  this package.
- Live `runtime` commands use the same authenticated client. The two external
  `unsafe` exceptions are `runtime eval` (`browser.page.eval`, requiring both
  `agent-runtime-control` and `webview-page-control`, executes in a selected
  guest page) and `runtime host-eval` (`host.renderer.eval`, requiring
  `agent-runtime-control`, executes in the selected workspace's host renderer).
  Neither path exposes Electron main or Node APIs.
- `src/core/runtime-capabilities.ts` is the non-exiting client for experimental
  live-app capabilities. Keep its structured `RuntimeClientResult` contract so
  agent hosts can treat a missing/disabled Canvas runtime as a tool result, not
  a process exit.
- New commands resolve their workspace through `getWorkspaceCommandOptions`
  (`src/commands/options.ts`), not by reading `opts.workspace` directly. The
  fixed discovery order is `--workspace` → `$PULSE_CANVAS_WORKSPACE_ID` →
  `__workspaces__.json.activeId` → hard error; it never guesses (no
  "most recent" / "first in list"). Disk commands require a readable
  `canvas.json`; runtime-mediated (`agent`/`team`/`runtime`) and `restore` pass
  `{ requireReadableCanvas: false }` since the workspace lives in the app or is
  the thing being recovered.
- Changes to command payloads, core exports, node/edge schemas, runtime routes,
  or storage shape are contract changes; use local validation plus the root
  impact overlay when hosts are affected.

## Common Commands

```bash
pnpm --filter @pulse-coder/canvas-cli test
pnpm --filter @pulse-coder/canvas-cli typecheck
pnpm --filter @pulse-coder/canvas-cli build
```

For runtime command smoke checks, first run/build the Electron app so the
runtime file exists; otherwise `pulse-canvas agent ...`,
`pulse-canvas team ...`, and `pulse-canvas runtime ...` are expected to fail
with "No active canvas-workspace runtime found."

## Key Files

- `src/index.ts`: executable entrypoint for `pulse-canvas`.
- `src/cli.ts`: top-level command registration and global options.
- `src/commands/`: workspace, node, edge, context, agent, team, runtime,
  restore, and skill-install commands.
- `src/core/doctor.ts`: consistency analysis + conservative repair (markdown
  wins on drift; orphans adopted, never deleted); CLI face in
  `src/commands/doctor.ts`.
- `src/core/layout.ts`: geometry summary, layout validation (overlaps, frame
  containment, readability, aspect ratio), and frame-grid arrangement;
  containment is geometric (smallest frame holding a node's center). CLI face
  in `src/commands/layout.ts`.
- `src/core/apply.ts`: atomic plan application — validate every op against an
  in-memory copy, defer all fs effects, then one locked save; optimistic
  concurrency via `baseRevision`. CLI face in `src/commands/apply.ts`.
- `src/core/store.ts`: workspace manifests, canvas load/save, locks, backups,
  wipe guard, and node/edge mutation commits.
- `src/core/storage-v2.ts`: compatibility layer for layout-only `canvas.json`
  plus `nodes/<nodeId>.json`.
- `src/core/nodes.ts`: node read/write/create/delete behavior and node
  capability mapping.
- `src/core/edges.ts`: edge create/list/delete behavior.
- `src/core/runtime-control.ts`: runtime discovery and authenticated POST helper.
- `src/core/runtime-capabilities.ts`: authenticated capability discovery/call
  client for external agent hosts.
- `skills/`: bundled Pulse Canvas skills copied by `install-skills`.
