Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
45fc21f
Prepped for OC install
ryaneggz Feb 12, 2026
d285be0
Install scripts
ryaneggz Feb 12, 2026
9656e3d
Update to upfront prompt
ryaneggz Feb 12, 2026
6afb28e
Add unzip
ryaneggz Feb 12, 2026
426e045
Configure git
ryaneggz Feb 12, 2026
7719cb1
prompt user if install openclaw
ryaneggz Feb 12, 2026
133208e
update docs
ryaneggz Feb 12, 2026
48ffeb0
Final Report
ryaneggz Feb 12, 2026
2565e0f
wget install option
ryaneggz Feb 12, 2026
76049bb
Will generate an uninstall script
ryaneggz Feb 12, 2026
f19b34c
Swithc to nvm
ryaneggz Feb 12, 2026
4d11897
Swithc to nodejs
ryaneggz Feb 12, 2026
ee4d9a2
Swithc to nodejs
ryaneggz Feb 12, 2026
acf03b6
update rg package
ryaneggz Feb 13, 2026
9ea390a
Refactor setup.sh to remove user-specific references
ryaneggz Mar 6, 2026
c29ea36
init
ryaneggz Mar 26, 2026
e73e6dc
Replace OpenClaw with Claude Code, rename ubuntu/ to sandbox/
ryaneggz Mar 26, 2026
77b202f
Move Dockerfile to root, sandbox/ contains only copied-in files
ryaneggz Mar 26, 2026
42dd28e
Add run, shell, stop, and clean targets to Makefile
ryaneggz Mar 26, 2026
ffd5da9
Add sandbox user, install user-level tools under sandbox account
ryaneggz Mar 26, 2026
c82a66d
Mount sandbox/ as shared volume at /home/sandbox, add rebuild target
ryaneggz Mar 26, 2026
4fb7c35
Add .bashrc with claude --dangerously-skip-permissions alias
ryaneggz Mar 26, 2026
f41c639
Restructure to install/ and workspace/, system-wide installs, update …
ryaneggz Mar 26, 2026
cde3a4a
Add standalone install instructions with curl/wget from branch
ryaneggz Mar 26, 2026
2739753
Update tag schema to claude-v* format
ryaneggz Mar 26, 2026
0d3c942
Commit plans
ryaneggz Mar 26, 2026
9f6eea3
Add usage examples to README
ryaneggz Mar 26, 2026
462d359
Add multi-agent support, Docker, tmux, named sandboxes, and AgentMail
ryaneggz Mar 27, 2026
d95e3d5
Rebrand tagging and CI to open-harness
ryaneggz Mar 27, 2026
5b19f4d
Fix Docker socket permissions so sandbox user needs no sudo
ryaneggz Mar 27, 2026
8bea5b0
Make Docker opt-in, require NAME, add error handling
ryaneggz Mar 27, 2026
a3969d7
Add agent builder configs, symlink .codex to .claude
ryaneggz Mar 27, 2026
04fd7c8
Add heartbeat, soul, and memory system (OpenClaw-style)
ryaneggz Mar 27, 2026
e013b4e
Add pi example banner extension
ryaneggz Mar 27, 2026
91bd0b2
docs: restyle README with project intentions, benefits, and emoji sec…
ryaneggz Mar 27, 2026
432b6b7
feat: add quickstart — three commands from clone to coding with AI
ryaneggz Mar 27, 2026
d75ccca
chore: add MIT license and LinkedIn launch post
ryaneggz Mar 27, 2026
ac289c5
fix: update license year to 2026
ryaneggz Mar 27, 2026
7a8c8ec
docs: rework LinkedIn post opener
ryaneggz Mar 27, 2026
485e48f
docs: detail unique architecture in LinkedIn post
ryaneggz Mar 27, 2026
94e673f
docs: shift LinkedIn post focus to shared multi-agent workspace
ryaneggz Mar 27, 2026
5a8f151
docs: add X launch post
ryaneggz Mar 27, 2026
5e737a3
docs: trim X post to 268 chars
ryaneggz Mar 27, 2026
5beba0f
chore: update heartbeat interval to 900s for dev testing
ryaneggz Mar 27, 2026
211e388
feat: restructure project layout and add GitHub issue templates
ryaneggz Mar 29, 2026
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
174 changes: 174 additions & 0 deletions .claude/plans/custom-banner-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Custom Banner Extension

## Context

Pi's interactive mode displays a **startup header** (banner) showing shortcuts, loaded AGENTS.md files, prompt templates, skills, and extensions. The `ctx.ui.setHeader()` API allows extensions to fully replace this built-in header with a custom component. This plan produces a pi extension that lets the user customize the initial banner via a configuration file and a `/banner` command.

The extension will:
1. Replace the default pi startup header with a user-defined banner
2. Support ASCII art, text, and color theming via a `banner.json` config file
3. Provide a `/banner` command to toggle between custom and built-in headers
4. Provide a `/banner-edit` command to interactively edit the banner text

## API Surface

Key pi APIs used:
- `ctx.ui.setHeader(factory | undefined)` — replace or restore the built-in startup header
- `pi.on("session_start", ...)` — load banner config and apply on startup
- `pi.registerCommand(name, ...)` — register `/banner` and `/banner-edit` commands
- `ctx.ui.editor(title, prefill)` — multi-line editor for banner text editing
- `ctx.ui.notify(msg, level)` — feedback notifications
- `theme.fg(color, text)` / `theme.bold(text)` — themed styling

Reference: `examples/extensions/custom-header.ts` demonstrates `setHeader()` with a mascot graphic.

## Files to Create

### 1. `.pi/extensions/custom-banner.ts` (~120 lines)

The extension module. Exports a default function receiving `ExtensionAPI`.

**On load (`session_start`):**
- Read `banner.json` from `.pi/banner.json` (project) falling back to `~/.pi/agent/banner.json` (global)
- If config exists, parse it and call `ctx.ui.setHeader()` with a factory that renders the configured banner
- If no config exists, create a default `banner.json` with a simple ASCII banner and apply it

**Banner config shape (`BannerConfig`):**

```typescript
interface BannerConfig {
enabled: boolean; // Master toggle
lines: string[]; // Raw text lines (supports \n in each)
color: string; // Theme color key: "accent", "success", "warning", "error", "muted", "dim"
bold: boolean; // Apply bold styling
subtitle?: string; // Optional subtitle line below the banner
subtitleColor?: string; // Theme color for subtitle (default: "muted")
}
```

**`setHeader` factory implementation:**
- Receives `(tui, theme)`, returns `{ render(width), invalidate() }`
- `render(width)`:
- Map each `config.lines` entry through `theme.fg(config.color, ...)` and optionally `theme.bold(...)`
- If `config.subtitle` is set, append a styled subtitle line
- Truncate each line to `width` using `truncateToWidth` from `@mariozechner/pi-tui`
- Return the styled string array
- `invalidate()`: no-op (stateless rendering)

**Commands:**

| Command | Description |
|---------|-------------|
| `/banner` | Toggle between custom banner and built-in header. Updates `enabled` in config and persists. |
| `/banner-edit` | Opens `ctx.ui.editor()` pre-filled with current `lines` joined by `\n`. On submit, updates config, persists, and re-applies header. |

**Helper functions:**
- `loadConfig(cwd: string): BannerConfig | null` — Read and parse banner.json from project then global location
- `saveConfig(cwd: string, config: BannerConfig): void` — Write banner.json back to the location it was loaded from (or project default)
- `applyBanner(ctx, config, pi)` — Call `ctx.ui.setHeader()` with the config, or `ctx.ui.setHeader(undefined)` if `!config.enabled`

### 2. `.pi/banner.json` (new)

Default project-level banner configuration:

```json
{
"enabled": true,
"lines": [
"┌─────────────────────────────────┐",
"│ 🚀 Sandboxes Project 🚀 │",
"└─────────────────────────────────┘"
],
"color": "accent",
"bold": true,
"subtitle": "Ruska AI Development Environment",
"subtitleColor": "muted"
}
```

## Implementation Details

### `custom-banner.ts` Structure

```
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
import { truncateToWidth } from "@mariozechner/pi-tui";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";

interface BannerConfig { ... }

const PROJECT_CONFIG = ".pi/banner.json";
const GLOBAL_CONFIG_DIR = process.env.PI_CODING_AGENT_DIR || join(process.env.HOME!, ".pi", "agent");
const GLOBAL_CONFIG = join(GLOBAL_CONFIG_DIR, "banner.json");

function loadConfig(cwd: string): { config: BannerConfig; path: string } | null { ... }
function saveConfig(filePath: string, config: BannerConfig): void { ... }
function applyBanner(ctx: ExtensionContext, config: BannerConfig): void { ... }

export default function (pi: ExtensionAPI) {
let currentConfig: BannerConfig | null = null;
let configPath: string | null = null;

pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
const result = loadConfig(ctx.cwd);
if (result) {
currentConfig = result.config;
configPath = result.path;
} else {
// Create default config in project
currentConfig = { ...DEFAULT_CONFIG };
configPath = join(ctx.cwd, PROJECT_CONFIG);
saveConfig(configPath, currentConfig);
}
if (currentConfig.enabled) {
applyBanner(ctx, currentConfig);
}
});

pi.registerCommand("banner", { ... }); // Toggle enabled
pi.registerCommand("banner-edit", { ... }); // Edit lines via ctx.ui.editor
}
```

### `applyBanner` implementation

```typescript
function applyBanner(ctx: ExtensionContext, config: BannerConfig): void {
ctx.ui.setHeader((_tui, theme) => ({
render(width: number): string[] {
const result: string[] = [""];
for (const line of config.lines) {
let styled = theme.fg(config.color as any, line);
if (config.bold) styled = theme.bold(styled);
result.push(truncateToWidth(styled, width));
}
if (config.subtitle) {
const subColor = (config.subtitleColor || "muted") as any;
result.push(theme.fg(subColor, config.subtitle));
}
result.push("");
return result;
},
invalidate() {},
}));
}
```

## Implementation Order

1. Create `.pi/banner.json` with default project banner config
2. Create `.pi/extensions/custom-banner.ts` with full extension logic
3. Test with `pi -e .pi/extensions/custom-banner.ts` to verify header replacement
4. Test `/banner` toggle and `/banner-edit` editing

## Verification

1. Start pi in the project — custom banner replaces the default startup header
2. `/banner` — toggles back to built-in header, notification confirms
3. `/banner` again — restores custom banner
4. `/banner-edit` — opens editor with current lines, edit and submit → banner updates immediately
5. Restart pi — banner persists from `.pi/banner.json`
6. Delete `.pi/banner.json`, restart — extension creates default config automatically
7. `pi -p "hello"` (print mode) — extension skips UI (`ctx.hasUI` check), no errors
85 changes: 85 additions & 0 deletions .claude/plans/goofy-fluttering-quilt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Crontab-Driven Multi-Heartbeat System

## Context

The current heartbeat system uses a bash daemon with a `while true; sleep $INTERVAL` loop — a single interval, single file. The user wants crontab-driven heartbeats so multiple heartbeat files can run at different intervals (e.g., memory distill every 4h, deployment checks every 15m, daily summary at 8pm).

## Approach

Replace the sleep-loop daemon with crontab entries inside the container. A `heartbeats.conf` config file maps heartbeat `.md` files to cron schedules. On `sync`, the script parses the config, generates an `env.sh` (so cron has API keys + PATH), and installs crontab entries. On container boot, entrypoint auto-syncs.

## Files to Modify

### 1. `docker/Dockerfile`
- Add `cron` to apt-get install line

### 2. `install/entrypoint.sh`
- Start cron daemon (`service cron start`)
- Auto-sync heartbeat schedules if `heartbeats.conf` or legacy `HEARTBEAT.md` exists

### 3. `install/heartbeat.sh` (full rewrite)
Replace daemon loop with cron-based subcommands:

- **`sync` (default/start)** — Parse `heartbeats.conf`, generate `~/.heartbeat/env.sh` (captures ANTHROPIC_API_KEY, PATH, etc.), install crontab entries. If no `heartbeats.conf` exists, fall back to legacy `HEARTBEAT.md` + `HEARTBEAT_INTERVAL` env var.
- **`run <file> [agent] [active_range]`** — Single heartbeat execution (called by cron). Uses `flock` to prevent overlapping runs of the same file. Preserves all existing gates: `is_active_hours()`, `is_heartbeat_empty()`, SOUL.md injection, `is_heartbeat_ok()`.
- **`stop`** — Remove all heartbeat crontab entries (filter out lines matching `heartbeat.sh run`)
- **`status`** — Show installed crontab entries, cron daemon status, recent log lines
- **`migrate`** — Convert `HEARTBEAT_INTERVAL` seconds to cron expression, generate `heartbeats.conf`

Preserve all existing helpers: `log()`, `rotate_log()`, `is_heartbeat_empty()`, `is_active_hours()`, `is_heartbeat_ok()`, agent dispatch (`claude`, `codex`, generic).

Key detail — `env.sh` generation during sync:
```bash
# Captures current env so cron jobs have API keys, PATH, etc.
env | grep -E '^(ANTHROPIC_|OPENAI_|HEARTBEAT_|GH_|GITHUB_|PATH=|HOME=|USER=)' \
| sed "s/^/export /" > ~/.heartbeat/env.sh
```

Each crontab entry:
```
*/15 * * * * . ~/.heartbeat/env.sh && /home/sandbox/install/heartbeat.sh run "file" "agent" "active_range" >> ~/.heartbeat/heartbeat.log 2>&1
```

### 4. `workspace/heartbeats.conf` (new)
Default config template:
```
# Format: <cron> | <file> | [agent] | [active_start-active_end]
*/30 * * * * | HEARTBEAT.md
```

### 5. `workspace/heartbeats/` (new directory)
- `.gitkeep`
- `example.md` — sample heartbeat file (not in conf by default)

### 6. `Makefile`
- Remove `HEARTBEAT_INTERVAL` variable (keep others as global defaults)
- `heartbeat` target: `docker exec --user sandbox $(NAME) heartbeat.sh sync` (no longer `-d` detached)
- Add `heartbeat-migrate` target
- Update `.PHONY`

### 7. `docker/docker-compose.yml`
- Remove `HEARTBEAT_INTERVAL` from environment (schedule now in `heartbeats.conf`)
- Keep `HEARTBEAT_ACTIVE_START`, `HEARTBEAT_ACTIVE_END`, `HEARTBEAT_AGENT`

### 8. `workspace/AGENTS.md`
- Update Heartbeat section to document multi-file cron system

### 9. `README.md`
- Update heartbeat documentation, config examples, make targets

## Backward Compatibility

- If no `heartbeats.conf` exists, `sync` auto-generates a crontab entry from `HEARTBEAT_INTERVAL` env var + `HEARTBEAT.md` (zero-config migration)
- `make heartbeat-migrate` explicitly creates `heartbeats.conf` from current settings
- Legacy `HEARTBEAT.md` can be referenced from `heartbeats.conf` like any other file

## Verification

1. `make NAME=test quickstart` — container starts, cron daemon running, heartbeats auto-synced
2. Inside container: `crontab -l` shows expected entries
3. `cat ~/.heartbeat/env.sh` has API keys and PATH
4. `make NAME=test heartbeat-status` — shows schedules and log
5. Edit `heartbeats.conf` → `make NAME=test heartbeat` → `crontab -l` updated
6. `make NAME=test heartbeat-stop` → `crontab -l` has no heartbeat entries
7. Empty heartbeat file → log shows "skipped"
8. Container restart → schedules re-installed automatically
38 changes: 38 additions & 0 deletions .claude/plans/memoized-cuddling-moore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Plan: Update tag schema to `claude-v*`

## Context

The CI workflow currently triggers on `sandbox-*` tags and parses `sandbox-<type>-<version>`. Since this branch is specifically for the Claude Code sandbox, the tag schema should be simplified to `claude-v*` (e.g. `claude-v1.0.0`). This produces images tagged as `ghcr.io/ruska-ai/sandbox:claude-v1.0.0` and `ghcr.io/ruska-ai/sandbox:claude-latest`.

## Changes

### 1. `.github/workflows/build.yml`

- Tag filter: `"sandbox-*"` → `"claude-v*"`
- Simplify parse step: extract version directly from `claude-v<version>` (no more sandbox/type split)
- Image tags: `ghcr.io/ruska-ai/sandbox:claude-v1.0.0` + `ghcr.io/ruska-ai/sandbox:claude-latest`

### 2. `README.md`

- Add a "Releases" or "Tagging" section documenting the tag schema
- Example: `git tag claude-v1.0.0 && git push origin claude-v1.0.0`

### 3. `Makefile`

- Update IMAGE to align: `ghcr.io/ruska-ai/sandbox:claude-$(TAG)`
- `TAG ?= latest` remains default for local builds

---

## Files to modify

- `.github/workflows/build.yml`
- `README.md`
- `Makefile`

## Verification

1. Workflow parses `claude-v1.0.0` tag correctly
2. Image names: `ghcr.io/ruska-ai/sandbox:claude-v1.0.0` and `ghcr.io/ruska-ai/sandbox:claude-latest`
3. `make build` still works locally with default tag
4. README documents the tag format
Loading