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
86 changes: 86 additions & 0 deletions .ai/agent-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Working practices for AI agents

Distilled, portable practices for any AI agent (Claude Code or otherwise) working in this
repo. Cross-references [shared-guidelines.md](../shared-guidelines.md) rather than repeating
it — see that file for worktree isolation (§3), no `Co-Authored-By` (§5), and not touching the
live app while a session is mid-run (§1, §2, §6).

## 1. Never simulate behavior that did not happen

A functional framing implying a step occurred requires that step to have actually occurred.
Never describe a test run, a check, or a fix that wasn't executed. If a command fails, report
the failure verbatim — don't paraphrase it into something that reads as success.

## 2. HANDOFF — how agents report results

Any sub-agent or automated task ends its final message with a structured block:

```text
---HANDOFF---
skill: <name | none>
outcome: PASS | FAIL | PARTIAL | REWORK
- What was done/changed (cite files when relevant)
- Key decisions and why
- What remains / blockers / next steps
---
```

- `skill:` and `outcome:` are the first two lines, always present, machine-parseable.
- 3-5 bullets, ≤150 words total. Be specific — no "made progress on X".
- **Out-of-scope discoveries go in the HANDOFF as a note, not as a fix.** Don't scope-creep a
task because you spotted something else wrong nearby.

### Final-message contract

Whoever dispatches an agent only sees that agent's **last message**. Earlier messages
(intermediate findings, a verification run, a report) are not reliably visible to the caller.
Consequences:

- The HANDOFF block must be in the FINAL message, after any verification step — not before it.
- A long-form deliverable (a review, a research writeup) gets **written to a file** under
`.work-files/<topic>/` and the path is cited in the HANDOFF; the HANDOFF itself stays short.
- **If the task ends in a commit + push, the push is the terminal action** — run it, then
confirm with `git log origin/<branch> --oneline -1` that the remote head advanced, and only
then emit the HANDOFF. Never end on a sub-step (e.g. a formatting/lint pass) that leaves the
push undone — that sub-step's own output would become the final message instead.

## 3. Review loop before shipping

A change ships once an implementer ↔ reviewer loop has converged: the reviewer has nothing
left to flag (no unresolved correctness findings), not just "looks fine on a skim". A reviewer
verifies claims with **executed evidence** — actually running the command, the test, the
`git log`/`git show`/`wc -l` check — rather than re-reading the diff and trusting the prose.
Findings get fixed, then re-reviewed, until the loop is clean.

## 4. Shell pitfalls in an agent harness

Commands that read fine to a human can silently misbehave or get blocked in an agent
execution environment. Rules of thumb:

- **No heredocs** (`<< EOF`) — treat multi-line content as data to write to a file, not to
inline into a shell command.
- **No `cd <path> && git ...`** and **no `git -C <path> ...`** for a repo other than the
current one — these patterns are commonly blocked by permission guards against
cross-repo/bare-repo mistakes. If you need another repo's state, `git fetch` into the
current one, or work from a separate worktree/checkout.
- **Avoid shell loops** (`for`/`while`/`until`) in a single command — prefer separate
commands, or `xargs` for a single-command iteration.
- **Use `jq` for JSON parsing**, not inline `python3 -c "import json; ..."` — cleaner and less
fragile to quote.
- **Use `.work-files/`, not `/tmp/`, for scratch files** — it's gitignored, project-scoped, and
visible in the Work Files sidebar tab for debugging.
- **No `sleep N && command` to wait for background work** — if a task runs in the background,
react to its completion signal; don't poll with a fixed sleep.

## 5. Memory / notes hygiene

Any saved note, memory, or prior observation is a **point-in-time snapshot**, not live state.
Before citing something more than about a week old as a current fact — a file's line count, a
commit range, a "this is safe" claim — re-verify it against the actual repo (read the file, run
the check). If it's stale, correct or delete it in the same pass rather than repeating it.

## 6. Scope discipline

Fix exactly what was asked. When a task names N specific findings, apply N fixes — no
unrelated cleanup, no rewriting adjacent prose "while you're in there". If you notice something
else that's wrong, say so in the HANDOFF; don't fix it silently.
2 changes: 1 addition & 1 deletion .ai/contexts/session-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ From `derive-project-path.js`: `deriveProjectPath(folderPath)`, `resolveWorktree

## Non-obvious behaviors

- **`resolveWorktreePath` collapses `<repo>/.worktrees/<name>` → `<repo>`** when the parent dir exists. Consequence: many `~/.claude/projects/-home-...workspace-skaleet-ai--worktrees-X` folders derive to the same projectPath. Callers must dedupe (see `get-work-files` IPC for the pattern).
- **`resolveWorktreePath` collapses `<repo>/.worktrees/<name>` → `<repo>`** when the parent dir exists. Consequence: many `~/.claude/projects/-home-...workspace-myproject--worktrees-X` folders derive to the same projectPath. Callers must dedupe (see `get-work-files` IPC for the pattern).
- **Two-table sidebar payload**: projects are aggregated, but each session row has its own `subagentType` field. A `null`/empty `subagentType` means it's a parent session; anything else (e.g. `'general-purpose'`, `'researcher'`) marks a subagent.
- **`fs.watch` debouncing**: the watcher batches per-folder events in a `pendingChanges = Map<folder, Set<filename> | true>` for ~200 ms before flushing to `refreshFolder`. A `true` value means "full walk needed" (rare path).
- **Stats `firstSessionDate`** is computed from `MIN(modified)`, not `MIN(created)`. Old sessions touched by recent reads keep their original `created` but their `modified` reflects the latest indexing — by design (the heatmap measures activity, not creation).
Expand Down
6 changes: 3 additions & 3 deletions .ai/contexts/trigger-watcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

| File | LOC | Role |
|---|---|---|
| `trigger-watcher.js` | ~175 | The entire module: directory setup, `fs.watch` listener, idle-wait logic, PTY write, result file. |
| `trigger-watcher.js` | ~800 | The entire module: directory setup, `fs.watch` listener, idle-wait logic, single + chained trigger processing, submit-with-verify busy-rise/fall polling, input validation, PTY write, result file. |
| `main.js` (wiring) | 15 | `require('./trigger-watcher').start(ctx)` in the `app.whenReady` block, right after `startScheduler`. |

## Public surface
Expand Down Expand Up @@ -43,7 +43,8 @@ Drop a file at `SWITCHBOARD_TRIGGERS_DIR/<uuid>.json` (default `~/.switchboard/t

Fields:
- `sessionId` — must match a key in `activeSessions` (`main.js`)
- `command` — written verbatim as `command + '\r'` to the PTY
- `command` — written to the PTY, then Enter is sent as a SEPARATE write (discrete submit; a `\r` concatenated onto the text can be absorbed by the composer). Mutually exclusive with `chain`.
- `chain` — array of up to 20 `{command, ...}` steps (`MAX_CHAIN_LENGTH`), injected sequentially; each step's submission is verified (busy-rise) with one bare-Enter retry before the next step is sent. Mutually exclusive with `command`; exactly one of the two is required.
- `wait` — `"none"` (default) | `"idle"`. `"idle"` polls `isSessionBusy` every 100 ms until the session goes idle or the timeout fires.
- `timeout_ms` — optional positive integer, ≤ 600 000 ms. Overrides both the env var and the default for this trigger only. On invalid value → `{ok:false, error:"invalid timeout_ms"}`, semaphore released, no PTY write.

Expand Down Expand Up @@ -78,4 +79,3 @@ Trigger file is **deleted** after processing (success or failure).
- If you rename `_cliBusy` on `session` in `main.js`, update `isSessionBusy` in the `start(ctx)` wiring block.
- If you rename `activeSessions` or change the structure (`session.pty` → `session.ptyProcess`), update both `getPtyForSession` and `isSessionBusy` in the wiring block.
- Tests live in `test/trigger-watcher.test.js`. They use `SWITCHBOARD_TRIGGERS_DIR` env override — do not hardcode paths there.
- The convention doc for harness script authors lives at `~/.skaleet-ai/conventions/how-to/switchboard-trigger.md`.
33 changes: 16 additions & 17 deletions .ai/shared-guidelines.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
# Switchboard — Notes for Claude (and other AI agents)

@~/.skaleet-ai/conventions/rules.md
This is JB's fork (`devsuitup/switchboard`, transferred from `JeanBaptisteRenard/switchboard` on 2026-08-11) of `doctly/switchboard`. The fork carries features not (yet) upstream — read this before editing anything.

This is JB's fork (`JeanBaptisteRenard/switchboard`) of `doctly/switchboard`. The fork carries features not (yet) upstream — read this before editing anything.

**Caveat on the universal rules import above**: Switchboard is an **Electron desktop app**, not a Skaleet backend service. The following sections from `rules.md` do NOT apply here:
- DDD/CQRS architecture (no Domain/Application/Infrastructure layers — this is a renderer + main-process app)
- `docker compose exec` runtime gating (we run npm / node directly on the host; only deps for the *target* repos are Dockerised)
- `monitor-ci` skill (we use GitHub Actions, not GitLab CI; check status via `gh pr checks`)
- `glab` rules (replaced by `gh` CLI for this fork)
- `/pre-commit` skill (husky pre-commit runs `task check` automatically; the skill is for Skaleet PHP projects)
- Conventional Commits — we use a looser style (`feat(scope): ...`, `fix(scope): ...`, but no strict footer rules)

Everything else (HANDOFF protocol, agent dispatch rules, sub-agent model gate, worktree isolation requirement, no Co-Authored-By, shell-command pitfalls, memory hygiene) **does apply**.
Switchboard is an **Electron desktop app**: renderer + main-process, no Domain/Application/Infrastructure layering. Runtimes (npm, node) run directly on the host — nothing here is Dockerised. CI is GitHub Actions (check status via `gh pr checks`); the `gh` CLI is used for PRs, not `glab`. The husky pre-commit hook runs `task check` automatically. Commit style is a loose `feat(scope): ...` / `fix(scope): ...`, with no strict footer rules.

## Quick orientation

Expand All @@ -26,6 +16,7 @@ Everything else (HANDOFF protocol, agent dispatch rules, sub-agent model gate, w
| Change Plans/Memory/.work-files panels (CodeMirror) | [contexts/viewer-panel.md](contexts/viewer-panel.md) |
| Change the renderer (sidebar, terminal, app.js) | `public/*.js` — entry is `app.js` |
| Write a test | `test/*.test.js` — node:test + jsdom for renderer files |
| Working practices for AI agents (HANDOFF format, shell pitfalls, review loop) | [agent-practices.md](agent-practices.md) |

For a guided tour of the codebase architecture, start at [contexts/README.md](contexts/README.md).

Expand All @@ -44,7 +35,7 @@ task dev # Taskfile already sets SWITCHBOARD_DATA_DIR=~/.switchboard-dev by de

The AppImage uses `~/.switchboard/switchboard.db`. The dev electron uses `~/.switchboard-dev/switchboard.db`. They cannot collide.

### 2. Running `npm run build:linux` CAN kill the running instance — `cp` does not
### 2. Running `npm run build:linux` CAN kill the running instance — and so can the `cp` to ~/Applications

**Corrected 2026-05-31** — the previous version of this section claimed the build was safe. It isn't.

Expand All @@ -67,7 +58,7 @@ After the agent completes, **remove the worktree manually** — `git worktree re

### 4. `.work-files/` is gitignored scratch space

Skaleet workspace convention. Use it for session notes, proposals, plans, scratch JSONLs. It's enumerated by the Work Files sidebar tab — files appear there automatically.
Gitignored scratch space. Use it for session notes, proposals, plans, scratch JSONLs. It's enumerated by the Work Files sidebar tab — files appear there automatically.

### 5. No `Co-Authored-By` trailers in commits

Expand All @@ -77,11 +68,11 @@ Workspace-level rule (`~/workspace/CLAUDE.md`). Applies to commits and MR/PR des

If you're working autonomously (overnight, AFK mode) while the user's AppImage is live with an active session open, treat the app as **read-only from the outside** for the duration: no `npm run build:linux` / `task build` without the `--config.npmRebuild=false` flag (§2), no `cp` to `~/Applications/Switchboard.AppImage` (§2 — `appimagelauncherd` can silently kill the running instance), and no second `npx electron .` (§1 — it just quits and steals focus instead of giving you a usable dev process). None of these produce an obvious error at the time you run them; the damage shows up later as a dead session the user didn't ask to lose. If you need a live process to test against, use `SWITCHBOARD_DATA_DIR` isolation (§1) and only do the disruptive steps (uncontrolled rebuild, `cp` swap) once the user is ready to restart.

> This is a Switchboard-specific writeup of a more general pattern — "don't touch shared mutable state a human is actively using" applies to any AI agent working unattended alongside a live app. Worth considering as a skaleet-ai convention someday; not proposed here.
> This is a Switchboard-specific writeup of a more general pattern — "don't touch shared mutable state a human is actively using" applies to any AI agent working unattended alongside a live app.

## Fork-specific features (not in upstream)

These exist on `JeanBaptisteRenard/switchboard` main but not on `doctly/switchboard` main. If an agent claims a feature is "upstream", verify with `git log upstream/main -- <file>`:
These exist on `devsuitup/switchboard` main but not on `doctly/switchboard` main. If an agent claims a feature is "upstream", verify with `git log upstream/main -- <file>`:

- **Subagent support** — index, search, transcript viewer (PR #47 upstream, merged on fork)
- **Subagent observability** — hierarchy, live transitions, status badges (PR #48 upstream)
Expand All @@ -94,6 +85,14 @@ These exist on `JeanBaptisteRenard/switchboard` main but not on `doctly/switchbo
- **`SWITCHBOARD_DATA_DIR`** env var for DB isolation in dev (fork)
- **Wayland clipboard fix** — main-process IPC + OSC 52 (fork PR #18 = port of upstream PR #55)
- **Missing project remap** — detect + UI + atomic JSONL rewrite (fork PR #20 = port of upstream PR #35, with subagent-aware enum + active-session guard added on top)
- **Trigger watcher** — file-based command injection into open PTYs, single + chained (fork PR #24 and follow-ups); see [contexts/trigger-watcher.md](contexts/trigger-watcher.md)
- **Schedule runner** — in-process cron spawning headless Claude tasks from `schedule-*.md` files; see [contexts/schedule-runner.md](contexts/schedule-runner.md)
- **Session restore** — persist + restore the open working set across restarts (fork PR #80)
- **Perf campaign v0.0.33–41** — 30fps terminal flush cap, WebGL virtualization, LRU xterm cap, targeted refreshes, idle-CPU fixes (fork PRs #55–#70; a second perf wave #73–#76 shipped in v0.0.38)
- **Search off the main thread + bounded FTS query** — worker relay + 48-char cap (fork PR #97, v0.0.44)
- **Resume/fork in real recorded cwd** for worktree sessions (fork PR #96, v0.0.44)

(Not exhaustive — `git log --oneline upstream/main..main` is the ground truth.)

## Patterns to reuse, not reinvent

Expand All @@ -118,7 +117,7 @@ These exist on `JeanBaptisteRenard/switchboard` main but not on `doctly/switchbo

1. `task check` (lint + test). 0 errors. Pre-existing warnings are fine.
2. Squash to clear commits. No `Co-Authored-By`. Imperative subject, brief why-body.
3. `gh pr create` against `JeanBaptisteRenard/switchboard:main` (the fork's main, not upstream's). Title format: `(area): short imperative`.
3. `gh pr create` against `devsuitup/switchboard:main` (the fork's main, not upstream's). Title format: `(area): short imperative`.
4. If the change is a port of an upstream PR, **credit the upstream author** in the body with a link. We want abasiri to see we're not stealing.

## Upstreaming work
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public/codemirror-bundle.js
.cache/
dev-app-update.yml

# Scratch space (Skaleet workspace convention)
# Gitignored scratch space, surfaced in the Work Files sidebar tab
.work-files/


Expand Down
26 changes: 0 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,5 @@
# AGENTS.md

## Universal Rules

Read and follow `~/.skaleet-ai/conventions/rules.md` — it contains mandatory rules for all Skaleet projects (architecture, testing, git, AI agent behavior, shell commands).

## Local Instructions

Read `.ai/shared-guidelines.md` at the start of any work and follow it as the single source of truth for this project.

## Conventions

Shared development conventions are available at `~/.skaleet-ai/conventions/`. Read the relevant guide when working on a specific topic:

- **Architecture**: `conventions/architecture.md` — DDD/CQRS, Clean Architecture, bounded context isolation
- **Code Style**: `conventions/code-style.md` — PSR-12, naming conventions
- **TDD**: `conventions/tdd/_base.md` + language-specific (`php.md`, `typescript.md`)
- **Commit Messages**: `conventions/how-to/commit-messages.md` — Conventional Commits (mandatory)
- **How-To Guides**: `conventions/how-to/` — command-handler, api-endpoint, datagrid, etc.

## CLI Tools

Shared CLI tools are installed in `~/.skaleet-ai/bin/`. Use the full path to invoke them:

- **lsp-query**: Code intelligence via LSP (definition, references, hover, rename, diagnostics). Use when the IDE is not available. Usage: `~/.skaleet-ai/bin/lsp-query <action> <language> <file> [line] [col] [extra]`. Read `~/.skaleet-ai/conventions/how-to/lsp-query.md` for full documentation.

## Project Detection

- `composer.json` with PHP require -> PHP project (read `conventions/tdd/php.md`)
- `tsconfig.json` -> TypeScript project (read `conventions/tdd/typescript.md`)
- Check project's `.ai/shared-guidelines.md` for project-specific rules
Loading
Loading