diff --git a/.gitignore b/.gitignore index 3a78f85..22b4ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ anvilweb .kiro *.elc Assist +anvilsrv diff --git a/IDEAS.md b/IDEAS.md index 45b6618..f48f861 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -1,167 +1,5 @@ # IDEAS.md -## Role-Based Sessions - -Specialized session types with enforced capabilities via context and sandbox configuration. - -### Current Implementation - -Already possible using existing features: -- `context` file defines role behavior -- Sandbox config enforces filesystem boundaries -- Scripts create role-specific sessions (see `scripts/DevReview`, `scripts/Planning`) - -### Example Roles - -**Developer**: -- Context: implements features, stages changes, requests reviews -- Sandbox: read-write in project directory -- Can: modify code, run tests, commit to feature branches -- Cannot: push to main, merge PRs - -**Reviewer**: -- Context: reviews staged changes, provides feedback -- Sandbox: read-only filesystem -- Can: read code, run git diff, analyze changes -- Cannot: modify files, commit, push - -**Planner**: -- Context: analyzes requirements, creates task breakdowns -- Sandbox: read-only filesystem, no git write -- Can: read codebase, write plans to separate files -- Cannot: modify code, commit, push - -**Tester**: -- Context: writes and runs tests -- Sandbox: read-write in test directories only -- Can: create test files, run test commands -- Cannot: modify source code outside test dirs - -### Implementation - -**1. Role Templates** - -Store role definitions in `~/.config/anvillm/roles/`: - -``` -roles/ -├── developer.txt -├── reviewer.txt -├── planner.txt -└── tester.txt -``` - -Scripts load templates: -```bash -cat ~/.config/anvillm/roles/developer.txt | 9p write agent/$SESSION_ID/context -``` - -**2. Role-Specific Sandbox Configs** - -Create per-role sandbox configs in `~/.config/anvillm/sandbox-roles/`: - -```yaml -# sandbox-roles/reviewer.yaml -filesystem: - ro: - - "{CWD}" - - "/usr" - - "/lib" - rw: [] # No write access -network: - enabled: false -``` - -```yaml -# sandbox-roles/developer.yaml -filesystem: - ro: - - "/usr" - - "/lib" - rw: - - "{CWD}" - - "{TMPDIR}" -network: - enabled: true -``` - -Session creation with role: -```bash -echo 'new kiro /project reviewer' | 9p write agent/ctl -# Daemon loads sandbox-roles/reviewer.yaml for this session -``` - -**3. Role Metadata** - -Add `agent/{session}/role` file to 9P filesystem: - -``` -agent//role # "developer" | "reviewer" | "planner" | "tester" -``` - -Benefits: -- UI can filter/group by role -- Scripts can discover sessions by role -- Easier than parsing aliases - -**4. Role Discovery** - -Find sessions by role: -```bash -# List all reviewer sessions -9p read agent/list | while read id alias state pid cwd; do - role=$(9p read agent/$id/role) - [ "$role" = "reviewer" ] && echo "$id $alias" -done -``` - -Or use peer-bot skill (already works): -``` -Find the reviewer agent and send them: "Please review staged changes" -``` - -### Enforcement Layers - -**Soft enforcement** (current): -- Context instructions tell bot what its role is -- Bot follows instructions (usually) - -**Hard enforcement** (with role-specific sandboxes): -- Filesystem boundaries prevent unauthorized writes -- Reviewer literally cannot modify code files -- Planner cannot commit or push - -### Example Workflow - -```bash -# Create developer session with role -DEV_ID=$(echo 'new kiro /project developer' | 9p write agent/ctl && ...) -cat ~/.config/anvillm/roles/developer.txt | 9p write agent/$DEV_ID/context - -# Create reviewer session with role -REV_ID=$(echo 'new kiro /project reviewer' | 9p write agent/ctl && ...) -cat ~/.config/anvillm/roles/reviewer.txt | 9p write agent/$REV_ID/context - -# Developer implements feature -echo "Implement user authentication" | 9p write agent/$DEV_ID/in - -# Developer requests review (via peer-bot skill) -# Reviewer provides feedback -# Loop until LGTM -``` - -### Benefits - -- **Separation of concerns**: Each role has clear responsibilities -- **Safety**: Sandbox prevents accidental damage (reviewer can't modify code) -- **Reusability**: Role templates used across projects -- **Discoverability**: Find sessions by role, not just alias -- **Scalability**: Spawn multiple workers of same role - -### Already Working - -See `scripts/DevReview` and `scripts/Planning` for working examples of role-based multi-agent workflows using context system. - ## Persistent Work Tracking (Beads) Git-backed work units that survive orchestrator/bot crashes and preserve workflow state. diff --git a/anvilsrv b/anvilsrv deleted file mode 100755 index b901e32..0000000 Binary files a/anvilsrv and /dev/null differ diff --git a/cfg/backends/claude.yaml b/cfg/backends/claude.yaml new file mode 100644 index 0000000..ae7cb25 --- /dev/null +++ b/cfg/backends/claude.yaml @@ -0,0 +1,10 @@ +# Claude backend requirements +filesystem: + rw: + - "{HOME}/.claude" + - "{HOME}/.claude.json" + - "{HOME}/.config/anvillm" +network: + unrestricted: true +env: + - ANTHROPIC_API_KEY diff --git a/cfg/backends/kiro-cli.yaml b/cfg/backends/kiro-cli.yaml new file mode 100644 index 0000000..e2e9a3a --- /dev/null +++ b/cfg/backends/kiro-cli.yaml @@ -0,0 +1,10 @@ +# Kiro backend requirements +filesystem: + rw: + - "{HOME}/.kiro" + - "{XDG_DATA_HOME}/kiro-cli" + - "{CWD}/.kiro" +network: + unrestricted: true +env: + - KIRO_API_KEY diff --git a/cfg/global.yaml b/cfg/global.yaml new file mode 100644 index 0000000..3dbe00c --- /dev/null +++ b/cfg/global.yaml @@ -0,0 +1,63 @@ +# AnviLLM Sandbox Configuration +# This file controls landrun sandboxing for all backend sessions +# Documentation: https://github.com/landlock-lsm/landrun +# +# Path templates: +# {CWD} - Session working directory +# {HOME} - User home directory +# {TMPDIR} - Temporary directory (/tmp or $TMPDIR) +# +# WARNING: CLI tools need network access to function! +# - Claude requires connect_tcp for claude.com +# - Kiro requires connect_tcp for kiro.ai +# +# Changes apply to NEW sessions only. Existing sessions are not affected. + +general: + enabled: true + best_effort: true + log_level: error +filesystem: + ro: + - /etc/passwd + - /etc/group + - /etc/localtime + - /etc/nsswitch.conf + - /etc/hosts + - /etc/resolv.conf + - /etc/services + - /etc/ssl/certs + - /proc + - '{XDG_CONFIG_HOME}/anvillm' + - '{HOME}/env' + rox: + - /usr + - /lib + - /lib64 + - /bin + - /sbin + rw: + - '{TMPDIR}' + - '{XDG_CACHE_HOME}' + - /dev/null + - /dev/zero + - /dev/urandom + - /dev/random + rwx: [] +network: + enabled: false + unrestricted: false + bind_tcp: [] + connect_tcp: [] +env: + - HOME + - USER + - PATH + - LANG + - TERM + - NAMESPACE + - DISPLAY + - WAYLAND_DISPLAY +advanced: + ldd: false + add_exec: true diff --git a/cfg/roles/admin.yaml b/cfg/roles/admin.yaml new file mode 100644 index 0000000..0aa759d --- /dev/null +++ b/cfg/roles/admin.yaml @@ -0,0 +1,12 @@ +# Admin role: system administration tasks +filesystem: + ro: + - "{CWD}" + - "/var/log" + rw: + - "/etc" + - "/usr/local/etc" + - "{HOME}" +network: + enabled: true + unrestricted: true diff --git a/cfg/roles/architect.yaml b/cfg/roles/architect.yaml new file mode 100644 index 0000000..9dc6fd9 --- /dev/null +++ b/cfg/roles/architect.yaml @@ -0,0 +1,8 @@ +# Architect role: access to knowledge base and planning documents +filesystem: + ro: + - "{XDG_DOCUMENTS_DIR}/agent-kb" + - "{HOME}/doc/agent-kb" + rw: + - "{XDG_DOCUMENTS_DIR}/plans" + - "{HOME}/doc/plans" diff --git a/cfg/roles/default.yaml b/cfg/roles/default.yaml new file mode 100644 index 0000000..5856b52 --- /dev/null +++ b/cfg/roles/default.yaml @@ -0,0 +1,10 @@ +# Default role: permissive settings for ad-hoc usage +# Equivalent to current global sandbox.yaml behavior +filesystem: + rw: + - "{CWD}" +network: + unrestricted: true +env: + - ANTHROPIC_API_KEY + - KIRO_API_KEY diff --git a/cfg/roles/developer.yaml b/cfg/roles/developer.yaml new file mode 100644 index 0000000..742aa05 --- /dev/null +++ b/cfg/roles/developer.yaml @@ -0,0 +1,8 @@ +# Developer role configuration +filesystem: + rw: + - "{CWD}" + rwx: + - "{HOME}/bin" + - "{HOME}/.local/bin" + - "{HOME}/src" diff --git a/cfg/roles/research.yaml b/cfg/roles/research.yaml new file mode 100644 index 0000000..762320b --- /dev/null +++ b/cfg/roles/research.yaml @@ -0,0 +1,10 @@ +# Research role: access to documentation and knowledge base +filesystem: + ro: + - "{CWD}" + - "{HOME}/src" + - "{XDG_DOCUMENTS_DIR}" + - "{HOME}/doc" + rw: + - "{XDG_DOCUMENTS_DIR}/agent-kb" + - "{HOME}/doc/agent-kb" diff --git a/cfg/roles/reviewer.yaml b/cfg/roles/reviewer.yaml new file mode 100644 index 0000000..b6f26f5 --- /dev/null +++ b/cfg/roles/reviewer.yaml @@ -0,0 +1,8 @@ +# Reviewer role: read-only access to project and dev directories +filesystem: + ro: + - "{CWD}" + - "{HOME}/src" + rox: + - "{HOME}/bin" + - "{HOME}/.local/bin" diff --git a/cfg/roles/techeditor.yaml b/cfg/roles/techeditor.yaml new file mode 100644 index 0000000..f01108e --- /dev/null +++ b/cfg/roles/techeditor.yaml @@ -0,0 +1,8 @@ +# Technical editor role: documentation with code access +filesystem: + ro: + - "{HOME}/src" + rw: + - "{CWD}" + - "{XDG_DOCUMENTS_DIR}" + - "{HOME}/doc" diff --git a/cfg/roles/tester.yaml b/cfg/roles/tester.yaml new file mode 100644 index 0000000..d06e293 --- /dev/null +++ b/cfg/roles/tester.yaml @@ -0,0 +1,9 @@ +# Tester role: execute binaries and read source, no write access +filesystem: + ro: + - "{CWD}" + - "{HOME}/src" + rox: + - "{CWD}" + - "{HOME}/bin" + - "{HOME}/.local/bin" diff --git a/cfg/roles/writer.yaml b/cfg/roles/writer.yaml new file mode 100644 index 0000000..40a7ce8 --- /dev/null +++ b/cfg/roles/writer.yaml @@ -0,0 +1,8 @@ +# Writer role: documentation and content creation +filesystem: + ro: + - "{CWD}" + - "{HOME}/src" + rw: + - "{XDG_DOCUMENTS_DIR}" + - "{HOME}/doc" diff --git a/cfg/tasks/ansible.yaml b/cfg/tasks/ansible.yaml new file mode 100644 index 0000000..f8e1761 --- /dev/null +++ b/cfg/tasks/ansible.yaml @@ -0,0 +1,16 @@ +# Ansible configuration management tool +filesystem: + ro: + - "{HOME}/.ansible" + - "{HOME}/.ansible.cfg" + - "{HOME}/.ssh/config" + - "{HOME}/.ssh/id_*" + - "{HOME}/.ssh/id_*.pub" + rw: + - "{HOME}/.ssh/known_hosts" + - "{HOME}/.ansible/tmp" +network: + enabled: true + connect_tcp: + - "22" # SSH + - "443" # Galaxy, collections diff --git a/cfg/tasks/aws.yaml b/cfg/tasks/aws.yaml new file mode 100644 index 0000000..5ce4cfb --- /dev/null +++ b/cfg/tasks/aws.yaml @@ -0,0 +1,7 @@ +# AWS CLI configuration +filesystem: + ro: + - "{HOME}/.aws" +network: + enabled: true + unrestricted: true diff --git a/cfg/tasks/curl.yaml b/cfg/tasks/curl.yaml new file mode 100644 index 0000000..8a24b86 --- /dev/null +++ b/cfg/tasks/curl.yaml @@ -0,0 +1,4 @@ +# curl/wget network tools +network: + enabled: true + unrestricted: true diff --git a/cfg/tasks/docker.yaml b/cfg/tasks/docker.yaml new file mode 100644 index 0000000..dc22d97 --- /dev/null +++ b/cfg/tasks/docker.yaml @@ -0,0 +1,9 @@ +# Docker container tool configuration +filesystem: + rw: + - "/var/run/docker.sock" + - "{HOME}/.docker" +network: + enabled: true + connect_tcp: + - "443" # Docker Hub, registries diff --git a/cfg/tasks/editor.yaml b/cfg/tasks/editor.yaml new file mode 100644 index 0000000..c67ff94 --- /dev/null +++ b/cfg/tasks/editor.yaml @@ -0,0 +1,11 @@ +# Text editor configuration (vim, emacs, nano, etc.) +filesystem: + rw: + - "{XDG_CONFIG_HOME}/nvim" + - "{XDG_CONFIG_HOME}/vim" + - "{XDG_DATA_HOME}/nvim" + - "{XDG_STATE_HOME}/nvim" + - "{HOME}/.vimrc" + - "{HOME}/.vim" + - "{HOME}/.emacs.d" + - "{HOME}/.config/emacs" diff --git a/cfg/tasks/git.yaml b/cfg/tasks/git.yaml new file mode 100644 index 0000000..ec633ee --- /dev/null +++ b/cfg/tasks/git.yaml @@ -0,0 +1,24 @@ +# Git task: access to git configuration and system resources +filesystem: + ro: + - "{HOME}/.gitconfig" + - "{XDG_CONFIG_HOME}/git/config" + - "{XDG_CONFIG_HOME}/git/ignore" + - "{XDG_CONFIG_HOME}/git/attributes" + - "{HOME}/.ssh" + - "{HOME}/.netrc" + - "/etc/gitconfig" + - "/etc/gitattributes" + - "/etc/ssh/ssh_config" + - "/etc/ssh/ssh_known_hosts" + rw: + - "{HOME}/.ssh/known_hosts" + - "{HOME}/.git-credentials" + - "{XDG_CONFIG_HOME}/git/credentials" + - "{XDG_RUNTIME_DIR}/git/" +network: + enabled: true + connect_tcp: + - "22" # SSH (git@...) + - "443" # HTTPS (https://...) + - "9418" # Git protocol (git://...) diff --git a/cfg/tasks/github-cli.yaml b/cfg/tasks/github-cli.yaml new file mode 100644 index 0000000..df10c6c --- /dev/null +++ b/cfg/tasks/github-cli.yaml @@ -0,0 +1,10 @@ +# GitHub CLI (gh) tool configuration +filesystem: + ro: + - "{XDG_CONFIG_HOME}/gh" + - "{XDG_DATA_HOME}/gh/extensions" +network: + enabled: true + connect_tcp: + - "443" # HTTPS API + - "22" # SSH (optional) diff --git a/cfg/tasks/go.yaml b/cfg/tasks/go.yaml new file mode 100644 index 0000000..a56946c --- /dev/null +++ b/cfg/tasks/go.yaml @@ -0,0 +1,8 @@ +# Go toolchain configuration +filesystem: + rw: + - "{HOME}/go" +network: + enabled: true + connect_tcp: + - "443" # pkg.go.dev, proxy.golang.org diff --git a/cfg/tasks/gpg.yaml b/cfg/tasks/gpg.yaml new file mode 100644 index 0000000..d3591a5 --- /dev/null +++ b/cfg/tasks/gpg.yaml @@ -0,0 +1,8 @@ +# GPG encryption tool +filesystem: + ro: + - "{HOME}/.gnupg" + rw: + - "{HOME}/.gnupg/trustdb.gpg" + - "{HOME}/.gnupg/pubring.kbx" + - "{HOME}/.gnupg/random_seed" diff --git a/cfg/tasks/gradle.yaml b/cfg/tasks/gradle.yaml new file mode 100644 index 0000000..fb1a526 --- /dev/null +++ b/cfg/tasks/gradle.yaml @@ -0,0 +1,8 @@ +# Gradle (Java/Kotlin build tool) configuration +filesystem: + rw: + - "{HOME}/.gradle" +network: + enabled: true + connect_tcp: + - "443" # services.gradle.org, Maven Central, other repos diff --git a/cfg/tasks/jira.yaml b/cfg/tasks/jira.yaml new file mode 100644 index 0000000..3c49726 --- /dev/null +++ b/cfg/tasks/jira.yaml @@ -0,0 +1,9 @@ +# Jira CLI configuration +filesystem: + ro: + - "{HOME}/.jira.d" + - "{XDG_CONFIG_HOME}/jira-cli" +network: + enabled: true + connect_tcp: + - "443" # Jira API diff --git a/cfg/tasks/maven.yaml b/cfg/tasks/maven.yaml new file mode 100644 index 0000000..685937b --- /dev/null +++ b/cfg/tasks/maven.yaml @@ -0,0 +1,8 @@ +# Maven (Java build tool) configuration +filesystem: + rw: + - "{HOME}/.m2" +network: + enabled: true + connect_tcp: + - "443" # repo.maven.apache.org, Maven Central diff --git a/cfg/tasks/mysql.yaml b/cfg/tasks/mysql.yaml new file mode 100644 index 0000000..81a80f9 --- /dev/null +++ b/cfg/tasks/mysql.yaml @@ -0,0 +1,9 @@ +# MySQL client configuration +filesystem: + ro: + - "{HOME}/.my.cnf" + - "{HOME}/.mylogin.cnf" +network: + enabled: true + connect_tcp: + - "3306" # MySQL default port diff --git a/cfg/tasks/nodejs.yaml b/cfg/tasks/nodejs.yaml new file mode 100644 index 0000000..cb6ff6b --- /dev/null +++ b/cfg/tasks/nodejs.yaml @@ -0,0 +1,13 @@ +# Node.js package managers (npm, yarn, pnpm) configuration +filesystem: + rw: + - "{HOME}/.npm" + - "{HOME}/.yarn" + - "{HOME}/.pnpm-store" + - "{XDG_CONFIG_HOME}/yarn" + - "{HOME}/.yarnrc.yml" + - "{HOME}/.npmrc" +network: + enabled: true + connect_tcp: + - "443" # registry.npmjs.org, registry.yarnpkg.com diff --git a/cfg/tasks/plan9.yaml b/cfg/tasks/plan9.yaml new file mode 100644 index 0000000..1ed48bb --- /dev/null +++ b/cfg/tasks/plan9.yaml @@ -0,0 +1,10 @@ +# Plan 9 from User Space toolchain configuration +filesystem: + rwx: + - "{PLAN9}" +network: + enabled: true + connect_tcp: + - "564" # 9P protocol +env: + - PLAN9 diff --git a/cfg/tasks/podman.yaml b/cfg/tasks/podman.yaml new file mode 100644 index 0000000..27074a6 --- /dev/null +++ b/cfg/tasks/podman.yaml @@ -0,0 +1,10 @@ +# Podman container tool configuration +filesystem: + rw: + - "{XDG_RUNTIME_DIR}/podman" + - "{XDG_DATA_HOME}/containers" + - "{XDG_CONFIG_HOME}/containers" +network: + enabled: true + connect_tcp: + - "443" # Container registries diff --git a/cfg/tasks/postgresql.yaml b/cfg/tasks/postgresql.yaml new file mode 100644 index 0000000..c938c6b --- /dev/null +++ b/cfg/tasks/postgresql.yaml @@ -0,0 +1,9 @@ +# PostgreSQL client configuration +filesystem: + ro: + - "{HOME}/.psqlrc" + - "{HOME}/.pgpass" +network: + enabled: true + connect_tcp: + - "5432" # PostgreSQL default port diff --git a/cfg/tasks/python.yaml b/cfg/tasks/python.yaml new file mode 100644 index 0000000..70b488b --- /dev/null +++ b/cfg/tasks/python.yaml @@ -0,0 +1,9 @@ +# Python pip package manager configuration +filesystem: + rw: + - "{XDG_CONFIG_HOME}/pip" + - "{HOME}/.pip" +network: + enabled: true + connect_tcp: + - "443" # pypi.org, files.pythonhosted.org diff --git a/cfg/tasks/rsync.yaml b/cfg/tasks/rsync.yaml new file mode 100644 index 0000000..6d21c15 --- /dev/null +++ b/cfg/tasks/rsync.yaml @@ -0,0 +1,13 @@ +# rsync tool configuration +filesystem: + ro: + - "{HOME}/.ssh/config" + - "{HOME}/.ssh/id_*" + - "{HOME}/.ssh/id_*.pub" + rw: + - "{HOME}/.ssh/known_hosts" +network: + enabled: true + connect_tcp: + - "22" # SSH transport + - "873" # rsync daemon diff --git a/cfg/tasks/rust.yaml b/cfg/tasks/rust.yaml new file mode 100644 index 0000000..5f16d4a --- /dev/null +++ b/cfg/tasks/rust.yaml @@ -0,0 +1,9 @@ +# Rust toolchain (cargo, rustc, rustup) configuration +filesystem: + rw: + - "{HOME}/.cargo" + - "{HOME}/.rustup" +network: + enabled: true + connect_tcp: + - "443" # crates.io, static.rust-lang.org diff --git a/cfg/tasks/ssh.yaml b/cfg/tasks/ssh.yaml new file mode 100644 index 0000000..7a70c7c --- /dev/null +++ b/cfg/tasks/ssh.yaml @@ -0,0 +1,12 @@ +# SSH tool configuration +filesystem: + ro: + - "{HOME}/.ssh/config" + - "{HOME}/.ssh/id_*" + - "{HOME}/.ssh/id_*.pub" + rw: + - "{HOME}/.ssh/known_hosts" +network: + enabled: true + connect_tcp: + - "22" diff --git a/cfg/tasks/systemd.yaml b/cfg/tasks/systemd.yaml new file mode 100644 index 0000000..ebeae4a --- /dev/null +++ b/cfg/tasks/systemd.yaml @@ -0,0 +1,10 @@ +# systemd tools (systemctl, journalctl, etc.) +filesystem: + ro: + - "/run/systemd" + - "/var/log/journal" + - "/etc/systemd" +network: + enabled: true + connect_tcp: + - "9001" # systemd socket activation (if needed) diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 32cb839..882c6e2 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -13,14 +13,20 @@ var ( ErrBackendNotFound = errors.New("backend not found") ) +// SessionOptions contains options for creating a session +type SessionOptions struct { + CWD string + Role string + Tasks []string +} + // Backend represents any chat backend (CLI tool via PTY, or direct API) type Backend interface { // Name returns the backend name Name() string // CreateSession creates a new session instance - // cwd is the working directory (may be ignored by API backends) - CreateSession(ctx context.Context, cwd string) (Session, error) + CreateSession(ctx context.Context, opts SessionOptions) (Session, error) } // Session represents an active conversation session @@ -66,6 +72,12 @@ type Session interface { // SetAlias sets a user-friendly alias for the session SetAlias(alias string) + + // Role returns the session role (empty if not specified) + Role() string + + // Tasks returns the session tasks (empty if not specified) + Tasks() []string } // SessionMetadata provides information about a session @@ -75,6 +87,7 @@ type SessionMetadata struct { Alias string // User-assigned alias Backend string // Backend name (e.g., "kiro-cli", "claude") CreatedAt time.Time // Session creation time + WinID int // Acme window ID (0 if not applicable) Extra map[string]string // Backend-specific metadata } diff --git a/internal/backend/tmux/session.go b/internal/backend/tmux/session.go index 184fb0e..287df36 100644 --- a/internal/backend/tmux/session.go +++ b/internal/backend/tmux/session.go @@ -22,6 +22,9 @@ type Session struct { windowName string // window name within tmux session (same as id) cwd string alias string + role string // session role (e.g., "developer", "reviewer") + tasks []string // session tasks (e.g., ["git", "docker"]) + winID int // Acme window ID (0 if not applicable) pid int state string context string // prepended to every prompt @@ -182,6 +185,7 @@ func (s *Session) Metadata() backend.SessionMetadata { Alias: s.alias, Backend: s.backendName, CreatedAt: s.createdAt, + WinID: s.winID, Extra: map[string]string{ "tmux_session": s.tmuxSession, "tmux_window": s.windowName, @@ -193,6 +197,20 @@ func (s *Session) Commands() backend.CommandHandler { return s.commands } +// Role returns the session role +func (s *Session) Role() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.role +} + +// Tasks returns the session tasks +func (s *Session) Tasks() []string { + s.mu.Lock() + defer s.mu.Unlock() + return s.tasks +} + // GetPid gets the process ID func (s *Session) GetPid() int { s.mu.Lock() @@ -214,6 +232,13 @@ func (s *Session) GetContext() string { return s.context } +// SetWinID sets the Acme window ID +func (s *Session) SetWinID(winID int) { + s.mu.Lock() + defer s.mu.Unlock() + s.winID = winID +} + // reader continuously reads from FIFO and sends to dataCh func (s *Session) reader() { // Capture the reader generation at start to detect if we've been superseded diff --git a/internal/backend/tmux/tmux.go b/internal/backend/tmux/tmux.go index 049705a..620bc2a 100644 --- a/internal/backend/tmux/tmux.go +++ b/internal/backend/tmux/tmux.go @@ -109,19 +109,63 @@ func (b *Backend) Name() string { return b.cfg.Name } -func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Session, error) { +func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions) (backend.Session, error) { id := generateID() windowName := id // Use session ID as window name - debug.Log("[session %s] creating window in tmux session %s", id, b.tmuxSession) + debug.Log("[session %s] creating window in tmux session %s (role=%s, tasks=%v)", id, b.tmuxSession, opts.Role, opts.Tasks) - // Load sandbox configuration fresh for each session - sandboxCfg, err := sandbox.Load() + // Build layered sandbox configuration + // Start with global.yaml as base + baseCfg, err := sandbox.Load() if err != nil { - fmt.Fprintf(os.Stderr, "Error loading sandbox config: %v (using defaults)\n", err) - debug.Log("[session %s] failed to load sandbox config: %v, using defaults", id, err) - sandboxCfg = sandbox.DefaultConfig() + return nil, fmt.Errorf("failed to load global config: %w", err) } + + // Convert base config to layered format + baseLayer := sandbox.LayeredConfig{ + Filesystem: baseCfg.Filesystem, + Network: baseCfg.Network, + Env: baseCfg.Env, + } + layers := []sandbox.LayeredConfig{baseLayer} + + backendLayer, err := sandbox.LoadBackend(b.cfg.Name) + if err != nil { + return nil, fmt.Errorf("failed to load backend config %q: %w", b.cfg.Name, err) + } + layers = append(layers, backendLayer) + + // Add role layer if specified, otherwise use "default" role + role := opts.Role + if role == "" { + role = sandbox.DefaultRole + } + roleLayer, err := sandbox.LoadRole(role) + if err != nil { + return nil, fmt.Errorf("failed to load role %q: %w", role, err) + } + layers = append(layers, roleLayer) + + // Add task layers + for _, taskName := range opts.Tasks { + taskLayer, err := sandbox.LoadTask(taskName) + if err != nil { + return nil, fmt.Errorf("failed to load task %q: %w", taskName, err) + } + layers = append(layers, taskLayer) + } + + // Merge layers into final config + general := sandbox.GeneralConfig{ + BestEffort: false, + LogLevel: "error", + } + advanced := sandbox.AdvancedConfig{ + LDD: false, + AddExec: true, + } + sandboxCfg := sandbox.Merge(general, advanced, layers...) // 1. Ensure persistent tmux session exists if err := b.ensureTmuxSession(); err != nil { @@ -187,11 +231,11 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio killWindow(b.tmuxSession, windowName) fmt.Fprintf(os.Stderr, "ERROR: landrun not available and sandboxing is required\n") fmt.Fprintf(os.Stderr, "Install landrun: go install github.com/landlock-lsm/landrun@latest\n") - fmt.Fprintf(os.Stderr, "Or enable best_effort mode in ~/.config/anvillm/sandbox.yaml (NOT RECOMMENDED)\n") + fmt.Fprintf(os.Stderr, "Or enable best_effort mode in ~/.config/anvillm/global.yaml (NOT RECOMMENDED)\n") return nil, fmt.Errorf("landrun not available") } } - command = sandbox.WrapCommand(sandboxCfg, command, cwd) + command = sandbox.WrapCommand(sandboxCfg, command, opts.CWD) debug.Log("[session %s] sandboxed command: %v", id, command) // 8. Build command string for tmux @@ -209,7 +253,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio } // Change to working directory first - if err := sendKeys(target, fmt.Sprintf("cd \"%s\"", cwd), "C-m"); err != nil { + if err := sendKeys(target, fmt.Sprintf("cd \"%s\"", opts.CWD), "C-m"); err != nil { os.Remove(fifoPath) killWindow(b.tmuxSession, windowName) return nil, fmt.Errorf("failed to change directory: %w", err) @@ -247,7 +291,7 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio // Find the actual backend process (child of bash shell) pid := 0 if panePID != 0 { - time.Sleep(200 * time.Millisecond) // Brief delay for backend to start + time.Sleep(500 * time.Millisecond) // Brief delay for backend to start pid = FindBackendPID(panePID) if pid == 0 { debug.Log("[session %s] warning: backend process not found for pane PID %d", id, panePID) @@ -261,7 +305,9 @@ func (b *Backend) CreateSession(ctx context.Context, cwd string) (backend.Sessio backendName: b.cfg.Name, tmuxSession: b.tmuxSession, windowName: windowName, - cwd: cwd, + cwd: opts.CWD, + role: role, + tasks: opts.Tasks, pid: pid, state: "starting", createdAt: time.Now(), diff --git a/internal/p9/server.go b/internal/p9/server.go index a73a289..b1b7880 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -72,16 +72,19 @@ const ( fileAlias fileBackend fileContext + fileRole + fileTasks fileCount ) -var fileNames = []string{"ctl", "in", "out", "log", "state", "pid", "cwd", "alias", "backend", "context"} +var fileNames = []string{"ctl", "in", "out", "log", "state", "pid", "cwd", "alias", "backend", "context", "role", "tasks"} type Server struct { - mgr *session.Manager - listener net.Listener - socketPath string - mu sync.RWMutex + mgr *session.Manager + listener net.Listener + socketPath string + OnAliasChange func(backend.Session) // Called when session alias changes + mu sync.RWMutex } type connState struct { @@ -322,12 +325,32 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { fmt.Fprintf(os.Stderr, "[DEBUG] ctl write: input=%q args=%v len=%d fc.Count=%d len(fc.Data)=%d\n", input, args, len(args), fc.Count, len(fc.Data)) if len(args) < 2 || args[0] != "new" { - return errFcall(fc, "usage: new ") + return errFcall(fc, "usage: new [role=] [tasks=]") } + backendName := args[1] cwd, _ := os.Getwd() - if len(args) > 2 { - cwd = strings.Trim(args[2], `"`) + var role string + var tasks []string + + // Parse remaining arguments: first non-key=value is cwd, rest are options + cwdSet := false + for i := 2; i < len(args); i++ { + arg := args[i] + if strings.HasPrefix(arg, "role=") { + role = strings.TrimPrefix(arg, "role=") + } else if strings.HasPrefix(arg, "tasks=") { + taskStr := strings.TrimPrefix(arg, "tasks=") + if taskStr != "" { + tasks = strings.Split(taskStr, ",") + } + } else if !cwdSet { + // First positional argument is cwd + cwd = strings.Trim(arg, `"`) + cwdSet = true + } else { + return errFcall(fc, fmt.Sprintf("unexpected argument: %s", arg)) + } } // Validate and clean the path @@ -349,7 +372,12 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return errFcall(fc, fmt.Sprintf("path is not a directory: %s", cleanPath)) } - _, err := s.mgr.New(backendName, cleanPath) + opts := backend.SessionOptions{ + CWD: cleanPath, + Role: role, + Tasks: tasks, + } + _, err := s.mgr.New(opts, backendName) if err != nil { return errFcall(fc, err.Error()) } @@ -425,6 +453,9 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return errFcall(fc, "invalid alias: must match [A-Za-z0-9_-]+") } sess.SetAlias(input) + if s.OnAliasChange != nil { + s.OnAliasChange(sess) + } return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } @@ -651,6 +682,10 @@ func (s *Server) getSessionFile(sess backend.Session, idx int) string { return tmuxSess.GetContext() } return "" + case fileRole: + return sess.Role() + case fileTasks: + return strings.Join(sess.Tasks(), ",") } return "" } diff --git a/internal/sandbox/config.go b/internal/sandbox/config.go index f8b08b5..addbb23 100644 --- a/internal/sandbox/config.go +++ b/internal/sandbox/config.go @@ -1,8 +1,10 @@ package sandbox import ( + "fmt" "os" "path/filepath" + "regexp" "strings" "gopkg.in/yaml.v3" @@ -17,6 +19,13 @@ type Config struct { Advanced AdvancedConfig `yaml:"advanced"` } +// LayeredConfig represents a single layer (system/backend/role/task) +type LayeredConfig struct { + Filesystem FilesystemConfig `yaml:"filesystem"` + Network NetworkConfig `yaml:"network"` + Env []string `yaml:"env"` +} + // GeneralConfig contains general sandbox settings type GeneralConfig struct { BestEffort bool `yaml:"best_effort"` @@ -101,7 +110,7 @@ func DefaultConfig() *Config { // ConfigPath returns the path to the config file func ConfigPath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".config", "anvillm", "sandbox.yaml") + return filepath.Join(home, ".config", "anvillm", "global.yaml") } // Load loads the config from disk, creating defaults if it doesn't exist @@ -167,11 +176,303 @@ func Save(cfg *Config) error { return os.WriteFile(path, data, 0644) } +// DefaultRole is the role used when none is specified +const DefaultRole = "default" + +// validateName checks if a role/task name is safe for use in file paths +func validateName(name string) error { + if name == "" { + return fmt.Errorf("name cannot be empty") + } + if len(name) > 64 { + return fmt.Errorf("name too long (max 64 characters)") + } + // Only allow alphanumeric, hyphen, and underscore + for _, r := range name { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') { + return fmt.Errorf("name contains invalid character: %c (only alphanumeric, hyphen, underscore allowed)", r) + } + } + return nil +} + // expandPath replaces template variables with actual values -func expandPath(pattern, cwd, home, tmpdir string) string { +func expandPath(pattern, cwd string) string { s := pattern + + // CWD is special - passed as parameter, not from env s = strings.ReplaceAll(s, "{CWD}", cwd) - s = strings.ReplaceAll(s, "{HOME}", home) - s = strings.ReplaceAll(s, "{TMPDIR}", tmpdir) + + // Generic environment variable expansion: {VAR_NAME} + re := regexp.MustCompile(`\{([A-Z_][A-Z0-9_]*)\}`) + s = re.ReplaceAllStringFunc(s, func(match string) string { + varName := match[1 : len(match)-1] // Strip { and } + + // Special handling with defaults + home, _ := os.UserHomeDir() + switch varName { + case "HOME": + if home != "" { + return home + } + case "TMPDIR": + if tmpdir := os.Getenv("TMPDIR"); tmpdir != "" { + return tmpdir + } + return "/tmp" + case "XDG_CONFIG_HOME": + if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" { + return xdg + } + return filepath.Join(home, ".config") + case "XDG_DATA_HOME": + if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { + return xdg + } + return filepath.Join(home, ".local/share") + case "XDG_CACHE_HOME": + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return xdg + } + return filepath.Join(home, ".cache") + case "XDG_STATE_HOME": + if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { + return xdg + } + return filepath.Join(home, ".local/state") + case "XDG_RUNTIME_DIR": + if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { + return xdg + } + return fmt.Sprintf("/run/user/%d", os.Getuid()) + case "XDG_DOCUMENTS_DIR": + if xdg := os.Getenv("XDG_DOCUMENTS_DIR"); xdg != "" { + return xdg + } + return filepath.Join(home, "Documents") + case "XDG_DOWNLOAD_DIR": + if xdg := os.Getenv("XDG_DOWNLOAD_DIR"); xdg != "" { + return xdg + } + return filepath.Join(home, "Downloads") + case "XDG_MUSIC_DIR": + if xdg := os.Getenv("XDG_MUSIC_DIR"); xdg != "" { + return xdg + } + return filepath.Join(home, "Music") + case "XDG_PICTURES_DIR": + if xdg := os.Getenv("XDG_PICTURES_DIR"); xdg != "" { + return xdg + } + return filepath.Join(home, "Pictures") + case "XDG_VIDEOS_DIR": + if xdg := os.Getenv("XDG_VIDEOS_DIR"); xdg != "" { + return xdg + } + return filepath.Join(home, "Videos") + } + + // Generic env var lookup + if val := os.Getenv(varName); val != "" { + return val + } + return match // Keep original if env var not set + }) + return s } + +// SystemDefaults returns the system-level defaults (restrictive baseline) +func SystemDefaults() LayeredConfig { + return LayeredConfig{ + Filesystem: FilesystemConfig{ + RO: []string{ + "/etc/passwd", + "/dev/null", + "/proc/meminfo", + "/proc/self/cgroup", + "/proc/self/maps", + "/proc/version", + }, + ROX: []string{ + "/usr", + "/lib", + "/lib64", + "/bin", + "/sbin", + }, + RW: []string{ + "{TMPDIR}", + }, + }, + Network: NetworkConfig{ + Enabled: false, + Unrestricted: false, + }, + Env: []string{ + "HOME", + "USER", + "PATH", + "LANG", + "TERM", + }, + } +} + +// LoadBackend loads backend-specific config +func LoadBackend(name string) (LayeredConfig, error) { + if err := validateName(name); err != nil { + return LayeredConfig{}, fmt.Errorf("invalid backend name: %w", err) + } + + home, _ := os.UserHomeDir() + path := filepath.Join(home, ".config", "anvillm", "backends", name+".yaml") + + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + return LayeredConfig{}, fmt.Errorf("backend %q not found (looked in %s)", name, path) + } + if err != nil { + return LayeredConfig{}, err + } + + var cfg LayeredConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + return LayeredConfig{}, fmt.Errorf("parse %s: %w", path, err) + } + + return cfg, nil +} + +// LoadRole loads role-specific config +func LoadRole(name string) (LayeredConfig, error) { + if err := validateName(name); err != nil { + return LayeredConfig{}, fmt.Errorf("invalid role name: %w", err) + } + + home, _ := os.UserHomeDir() + path := filepath.Join(home, ".config", "anvillm", "roles", name+".yaml") + + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + return LayeredConfig{}, fmt.Errorf("role %q not found (looked in %s)", name, path) + } + if err != nil { + return LayeredConfig{}, err + } + + var cfg LayeredConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + return LayeredConfig{}, fmt.Errorf("parse %s: %w", path, err) + } + + return cfg, nil +} + +// LoadTask loads task-specific config +func LoadTask(name string) (LayeredConfig, error) { + if err := validateName(name); err != nil { + return LayeredConfig{}, fmt.Errorf("invalid task name: %w", err) + } + + home, _ := os.UserHomeDir() + path := filepath.Join(home, ".config", "anvillm", "tasks", name+".yaml") + + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + return LayeredConfig{}, fmt.Errorf("task %q not found (looked in %s)", name, path) + } + if err != nil { + return LayeredConfig{}, err + } + + var cfg LayeredConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + return LayeredConfig{}, fmt.Errorf("parse %s: %w", path, err) + } + + return cfg, nil +} + +// Merge combines multiple layers into a final Config (most permissive wins) +func Merge(general GeneralConfig, advanced AdvancedConfig, layers ...LayeredConfig) *Config { + cfg := &Config{ + General: general, + Advanced: advanced, + Filesystem: FilesystemConfig{ + RO: []string{}, + ROX: []string{}, + RW: []string{}, + RWX: []string{}, + }, + Network: NetworkConfig{ + Enabled: false, + Unrestricted: false, + }, + Env: []string{}, + } + + // Track paths by permission level (higher = more permissive) + pathPerms := make(map[string]int) // 0=none, 1=ro, 2=rox, 3=rw, 4=rwx + envSet := make(map[string]bool) + + for _, layer := range layers { + // Merge filesystem permissions (most permissive wins) + for _, p := range layer.Filesystem.RO { + if pathPerms[p] < 1 { + pathPerms[p] = 1 + } + } + for _, p := range layer.Filesystem.ROX { + if pathPerms[p] < 2 { + pathPerms[p] = 2 + } + } + for _, p := range layer.Filesystem.RW { + if pathPerms[p] < 3 { + pathPerms[p] = 3 + } + } + for _, p := range layer.Filesystem.RWX { + if pathPerms[p] < 4 { + pathPerms[p] = 4 + } + } + + // Merge network (most permissive wins) + if layer.Network.Enabled { + cfg.Network.Enabled = true + } + if layer.Network.Unrestricted { + cfg.Network.Unrestricted = true + } + cfg.Network.BindTCP = append(cfg.Network.BindTCP, layer.Network.BindTCP...) + cfg.Network.ConnectTCP = append(cfg.Network.ConnectTCP, layer.Network.ConnectTCP...) + + // Merge environment (union) + for _, e := range layer.Env { + envSet[e] = true + } + } + + // Convert path permissions back to lists + for path, perm := range pathPerms { + switch perm { + case 1: + cfg.Filesystem.RO = append(cfg.Filesystem.RO, path) + case 2: + cfg.Filesystem.ROX = append(cfg.Filesystem.ROX, path) + case 3: + cfg.Filesystem.RW = append(cfg.Filesystem.RW, path) + case 4: + cfg.Filesystem.RWX = append(cfg.Filesystem.RWX, path) + } + } + + // Convert env set to list + for e := range envSet { + cfg.Env = append(cfg.Env, e) + } + + return cfg +} diff --git a/internal/sandbox/wrapper.go b/internal/sandbox/wrapper.go index 8c0d5c6..ee525c0 100644 --- a/internal/sandbox/wrapper.go +++ b/internal/sandbox/wrapper.go @@ -25,12 +25,6 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string { return originalCmd } - home, _ := os.UserHomeDir() - tmpdir := os.Getenv("TMPDIR") - if tmpdir == "" { - tmpdir = "/tmp" - } - args := []string{"landrun"} // Log level @@ -54,25 +48,25 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string { // Collect all filesystem paths var entries []pathEntry for _, path := range cfg.Filesystem.RO { - expanded := expandPath(path, cwd, home, tmpdir) + expanded := expandPath(path, cwd) if pathExists(expanded) { entries = append(entries, pathEntry{expanded, "--ro"}) } } for _, path := range cfg.Filesystem.ROX { - expanded := expandPath(path, cwd, home, tmpdir) + expanded := expandPath(path, cwd) if pathExists(expanded) { entries = append(entries, pathEntry{expanded, "--rox"}) } } for _, path := range cfg.Filesystem.RW { - expanded := expandPath(path, cwd, home, tmpdir) + expanded := expandPath(path, cwd) if pathExists(expanded) { entries = append(entries, pathEntry{expanded, "--rw"}) } } for _, path := range cfg.Filesystem.RWX { - expanded := expandPath(path, cwd, home, tmpdir) + expanded := expandPath(path, cwd) if pathExists(expanded) { entries = append(entries, pathEntry{expanded, "--rwx"}) } diff --git a/internal/session/manager.go b/internal/session/manager.go index a76e89f..eb14c3e 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -24,7 +24,7 @@ func NewManager(backends map[string]backend.Backend) *Manager { } // New creates a new session in the given working directory using the specified backend -func (m *Manager) New(backendName, cwd string) (backend.Session, error) { +func (m *Manager) New(opts backend.SessionOptions, backendName string) (backend.Session, error) { m.mu.RLock() b, ok := m.backends[backendName] m.mu.RUnlock() @@ -33,7 +33,7 @@ func (m *Manager) New(backendName, cwd string) (backend.Session, error) { return nil, backend.ErrBackendNotFound } - sess, err := b.CreateSession(context.Background(), cwd) + sess, err := b.CreateSession(context.Background(), opts) if err != nil { return nil, err } diff --git a/main.go b/main.go index 4b79a10..ab115a3 100644 --- a/main.go +++ b/main.go @@ -77,7 +77,7 @@ func main() { defer srv.Close() // Rename prompt window when alias changes - srv.OnAliasChange = func(sess Session) { + srv.OnAliasChange = func(sess backend.Session) { meta := sess.Metadata() if meta.WinID > 0 { w, err := acme.Open(meta.WinID, nil) @@ -153,7 +153,8 @@ func main() { fmt.Fprintf(os.Stderr, "Error: Kiro requires a path argument\n") continue } - _, err := mgr.New("kiro-cli", arg) + opts := backend.SessionOptions{CWD: arg} + _, err := mgr.New(opts, "kiro-cli") if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue @@ -165,7 +166,8 @@ func main() { fmt.Fprintf(os.Stderr, "Error: Claude requires a path argument\n") continue } - _, err := mgr.New("claude", arg) + opts := backend.SessionOptions{CWD: arg} + _, err := mgr.New(opts, "claude") if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) continue diff --git a/mkfile b/mkfile index e8404b3..8bca0bc 100644 --- a/mkfile +++ b/mkfile @@ -10,12 +10,14 @@ build:V: cp scripts/Alog $INSTALL_PATH/Alog chmod 0755 $INSTALL_PATH/Alog mkdir -p $INSTALL_PATH/Workflows - cp scripts/DevReview $INSTALL_PATH/Workflows/DevReview + cp workflows/DevReview $INSTALL_PATH/Workflows/DevReview chmod 0755 $INSTALL_PATH/Workflows/DevReview - cp scripts/DevReviewQA $INSTALL_PATH/Workflows/DevReviewQA + cp workflows/DevReviewQA $INSTALL_PATH/Workflows/DevReviewQA chmod 0755 $INSTALL_PATH/Workflows/DevReviewQA - cp scripts/Planning $INSTALL_PATH/Workflows/Planning + cp workflows/Planning $INSTALL_PATH/Workflows/Planning chmod 0755 $INSTALL_PATH/Workflows/Planning + mkdir -p $HOME/.config/anvillm + cp -rf cfg/* $HOME/.config/anvillm/ install:V: build diff --git a/scripts/DevReview b/workflows/DevReview similarity index 86% rename from scripts/DevReview rename to workflows/DevReview index a0a5258..81e0d6f 100755 --- a/scripts/DevReview +++ b/workflows/DevReview @@ -31,22 +31,18 @@ fi AGENT_MOUNT="agent" -# Helper to create session and capture ID -create_session() { - local backend="$1" cwd="$2" - local before after - before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) - echo "new $backend $cwd" | 9p write $AGENT_MOUNT/ctl - after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) - comm -13 <(echo "$before") <(echo "$after") | head -1 -} - echo "Creating developer session..." -DEV_ID=$(create_session "$BACKEND" "$PROJECT_PATH") +before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +echo "new $BACKEND $PROJECT_PATH role=developer tasks=git,editor,go,rust,python,nodejs,docker,podman,maven,gradle,plan9,gpg,github-cli" | 9p write $AGENT_MOUNT/ctl +after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +DEV_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo "Developer session ID: $DEV_ID" echo "Creating reviewer session..." -REVIEWER_ID=$(create_session "$BACKEND" "$PROJECT_PATH") +before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +echo "new $BACKEND $PROJECT_PATH role=reviewer tasks=git,github-cli" | 9p write $AGENT_MOUNT/ctl +after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +REVIEWER_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo "Reviewer session ID: $REVIEWER_ID" # Set aliases diff --git a/scripts/DevReviewQA b/workflows/DevReviewQA similarity index 88% rename from scripts/DevReviewQA rename to workflows/DevReviewQA index bfaf9e6..541fb0a 100755 --- a/scripts/DevReviewQA +++ b/workflows/DevReviewQA @@ -31,26 +31,25 @@ fi AGENT_MOUNT="agent" -# Helper to create session and capture ID -create_session() { - local backend="$1" cwd="$2" - local before after - before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) - echo "new $backend $cwd" | 9p write $AGENT_MOUNT/ctl - after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) - comm -13 <(echo "$before") <(echo "$after") | head -1 -} - echo "Creating developer session..." -DEV_ID=$(create_session "$BACKEND" "$PROJECT_PATH") +before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +echo "new $BACKEND $PROJECT_PATH role=developer tasks=git,editor,go,rust,python,nodejs,docker,podman,maven,gradle,plan9,gpg,github-cli" | 9p write $AGENT_MOUNT/ctl +after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +DEV_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo "Developer session ID: $DEV_ID" echo "Creating reviewer session..." -REVIEWER_ID=$(create_session "$BACKEND" "$PROJECT_PATH") +before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +echo "new $BACKEND $PROJECT_PATH role=reviewer tasks=git,github-cli" | 9p write $AGENT_MOUNT/ctl +after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +REVIEWER_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo "Reviewer session ID: $REVIEWER_ID" echo "Creating QA session..." -QA_ID=$(create_session "$BACKEND" "$PROJECT_PATH") +before=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +echo "new $BACKEND $PROJECT_PATH role=tester tasks=git,github-cli" | 9p write $AGENT_MOUNT/ctl +after=$(9p read $AGENT_MOUNT/list | awk '{print $1}' | sort) +QA_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo "QA session ID: $QA_ID" # Set aliases diff --git a/scripts/Planning b/workflows/Planning similarity index 54% rename from scripts/Planning rename to workflows/Planning index db9d5fe..973bd14 100755 --- a/scripts/Planning +++ b/workflows/Planning @@ -1,5 +1,5 @@ #!/bin/bash -# Planning - Research/Engineering/Tech-Editor workflow +# Planning - Research/Architect/Techeditor workflow # Usage: ./scripts/Planning set -e @@ -26,31 +26,30 @@ fi AGENT="agent" -# Helper to create session and capture ID -create_session() { - local backend="$1" cwd="$2" - local before after - before=$(9p read $AGENT/list | awk '{print $1}' | sort) - echo "new $backend $cwd" | 9p write $AGENT/ctl - after=$(9p read $AGENT/list | awk '{print $1}' | sort) - comm -13 <(echo "$before") <(echo "$after") | head -1 -} - echo "Creating research session..." -RESEARCH_ID=$(create_session "$BACKEND" "$DOC_PATH") +before=$(9p read $AGENT/list | awk '{print $1}' | sort) +echo "new $BACKEND $DOC_PATH role=research" | 9p write $AGENT/ctl +after=$(9p read $AGENT/list | awk '{print $1}' | sort) +RESEARCH_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo " ID: $RESEARCH_ID" -echo "Creating engineering session..." -ENG_ID=$(create_session "$BACKEND" "$DOC_PATH") -echo " ID: $ENG_ID" +echo "Creating architect session..." +before=$(9p read $AGENT/list | awk '{print $1}' | sort) +echo "new $BACKEND $DOC_PATH role=architect tasks=git" | 9p write $AGENT/ctl +after=$(9p read $AGENT/list | awk '{print $1}' | sort) +ARCH_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) +echo " ID: $ARCH_ID" -echo "Creating tech-editor session..." -EDITOR_ID=$(create_session "$BACKEND" "$DOC_PATH") +echo "Creating techeditor session..." +before=$(9p read $AGENT/list | awk '{print $1}' | sort) +echo "new $BACKEND $DOC_PATH role=techeditor tasks=git" | 9p write $AGENT/ctl +after=$(9p read $AGENT/list | awk '{print $1}' | sort) +EDITOR_ID=$(comm -13 <(echo "$before") <(echo "$after") | head -1) echo " ID: $EDITOR_ID" # Set aliases echo "${ALIAS_PREFIX}-research" | 9p write $AGENT/$RESEARCH_ID/alias -echo "${ALIAS_PREFIX}-engineering" | 9p write $AGENT/$ENG_ID/alias +echo "${ALIAS_PREFIX}-architect" | 9p write $AGENT/$ARCH_ID/alias echo "${ALIAS_PREFIX}-techeditor" | 9p write $AGENT/$EDITOR_ID/alias # Research context @@ -62,15 +61,15 @@ Your job is to research system architecture and code for requested information. - Prefer updating existing kb documents over creating new ones - Only create new KB documents if the knowledge discovered is significant and reusable -When you receive a query from engineering, research and respond: -- Use the peer-bot skill to find and communicate with the engineering agent (alias contains "engineering") -- Send your answer back to engineering +When you receive a query from the architect, research and respond: +- Use the peer-bot skill to find and communicate with the architect agent (alias contains "architect") +- Send your answer back to the architect Do not actively await responses, but stop working until you receive an explicit request to conduct research. EOF -# Engineering context -cat <