Rename project to Constant and prepare public repo

This commit is contained in:
L'électron rare
2026-04-05 18:09:21 +02:00
parent fc0d3a67a6
commit a7a96ab057
32 changed files with 5561 additions and 225 deletions
+3
View File
@@ -4,6 +4,8 @@
.cache/ .cache/
.tmp/ .tmp/
tmp/ tmp/
__pycache__/
*.pyc
*.log *.log
*.pem *.pem
*.key *.key
@@ -13,6 +15,7 @@ tmp/
*.sqlite *.sqlite
*.sqlite-shm *.sqlite-shm
*.sqlite-wal *.sqlite-wal
output/
codex-profiles/ codex-profiles/
profiles/ profiles/
state/ state/
+66
View File
@@ -0,0 +1,66 @@
# Contributing to Constant
`Constant` is a terminal-first orchestration project.
Contributions should keep that bias visible in the code and in the product shape.
## What We Care About
- local-first workflows before cloud assumptions
- real shells, real sessions, real files
- clean operator ergonomics
- explicit routing and supervision
- durable context instead of prompt spaghetti
- strong terminal aesthetics without gimmick-only UX
## Good Contributions
- make the cockpit more reliable
- improve mission routing or verification
- improve durable memory and repo context
- sharpen the TUI and the `hexapus` buddy rail
- simplify setup without hiding the system
- improve docs with concrete operator value
## Ground Rules
- keep the CLI useful in non-interactive environments
- prefer robust shell and Python over magic abstractions
- preserve local observability
- do not hardcode personal machine names, users, IPs, or paths
- treat clipboard, SSH, and session behavior as product features, not side details
## Style
- favor direct, composable commands
- prefer explicit state over hidden side effects
- keep defaults safe and public-repo friendly
- avoid introducing heavyweight infrastructure unless it clearly pays for itself
- maintain the visual identity: terminal-native, slightly demoscene, still readable
## Before Opening a PR
Run at least the checks relevant to your change.
Examples:
```bash
python3 -m py_compile constant/*.py
bash -n scripts/*.sh
./scripts/Constant doctor
```
If your change affects routing, fleet behavior, or memory shape, include a short explanation of:
- what changed
- how you tested it
- what assumptions still remain
## Design Notes
If you add a new capability, ask:
1. does this help an operator supervise a real machine or fleet?
2. does it make routing or context more explicit?
3. does it still degrade cleanly when the environment is constrained?
If the answer is no, it probably does not belong here yet.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Constant contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+288 -37
View File
@@ -1,67 +1,318 @@
# Zellij AI Triple # Constant
Three-pane Zellij workspace: ```text
██████╗ ██████╗ ███╗ ██╗███████╗████████╗ █████╗ ███╗ ██╗████████╗
██╔════╝██╔═══██╗████╗ ██║██╔════╝╚══██╔══╝██╔══██╗████╗ ██║╚══██╔══╝
██║ ██║ ██║██╔██╗ ██║███████╗ ██║ ███████║██╔██╗ ██║ ██║
██║ ██║ ██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║╚██╗██║ ██║
╚██████╗╚██████╔╝██║ ╚████║███████║ ██║ ██║ ██║██║ ╚████║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝
- left pane: `claude` on the host local-first orchestration for weird builders, fleet nerds, and AI cockpit addicts
- top-right pane: `codex` in Docker with profile 1 ```
- bottom-right pane: `codex` in Docker with profile 2
Each Codex pane gets its own `CODEX_HOME`, so each one can log into a different ChatGPT/OpenAI account. `Constant` is a control room.
## Requirements Not a chatbot skin.
Not a SaaS wrapper.
Not another “agent framework” that only looks good in slides.
It is a pragmatic, local-first orchestration layer that sits on top of:
- a 4-pane Zellij machine cockpit
- a multi-machine fleet view
- host-local CLIs like `claude`, `codex`, `copilot`, and `vibe`
- a mission runner that can route work across machines
- a growing durable memory layer for repo context, decisions, and persona
The vibe is somewhere between:
- demoscene terminal
- distributed workshop
- AI pit crew
- `electron rare` engineering energy
## What Ships Today
Current build status in this repo:
- a 4-pane host-local Zellij session per machine
- a fleet cockpit with one tab per machine
- `Constant`, the orchestration CLI on top of the cockpit
- local mission planning and verification
- host-local execution for `claude`, `codex`, and `vibe`
- `copilot` as a manual lane
- local message bus + cross-machine bridge
- MLX-ready model plumbing for a small local orchestrator stack on macOS
The historical repo name may still say `zellij-ai-triple`.
The product name is now `Constant`.
## Why This Exists
Most AI tooling is designed like a vending machine:
1. send a prompt
2. get an answer
3. pretend orchestration happened
That breaks the moment you want:
- multiple machines
- multiple CLIs
- persistent sessions
- human supervision without tab hell
- routing by task type
- repo memory that survives a single run
`Constant` is for the opposite use case:
- the workstation is real
- the shell is real
- the repos are real
- the agents are messy
- the human is still the one in charge
## Core Shape
### 1. Machine cockpit
Each machine runs the same 4-pane layout:
- left: `claude`
- top-right: `codex`
- middle-right: `copilot`
- bottom-right: `vibe`
Everything runs on the host by default.
No Docker dependency is required for the standard session.
### 2. Fleet cockpit
From your command center machine, you open one Zellij tab per machine.
Each tab attaches to the machines own local session.
That gives you:
- one place to supervise the whole fleet
- local clipboard behavior on the operator machine
- remote sessions that stay remote
- a clean separation between orchestration and execution
### 3. Constant CLI
`Constant` sits above the fleet and handles:
- mission creation
- route selection
- backend selection
- CLI selection
- buddy review
- verification
- delegation
- cockpit handoff
### 4. Durable memory
The memory layer is being built to support:
- workspace-first indexing
- weighted instruction fusion from `.claude`, `.copilot`, `.agent`, `.agents`, `CLAUDE.md`, `AGENTS.md`
- cross-mission summaries
- a durable decision graph
- persistent persona outside mission files
The design goal is simple:
`Constant` should remember how you work, not just what you typed five minutes ago.
## Quick Start
### Requirements
- `zellij` - `zellij`
- `git`
- `node`
- `npm`
- `uv`
- `claude` - `claude`
- `codex` - `codex`
- `docker` - `copilot`
- a local Docker image with `bash` and `git` - `vibe`
Default Codex container image: Recommended CLI install channels:
- `codercom/code-server:latest`
## Usage
```bash ```bash
./scripts/zellij-ai-triple.sh npm install -g @anthropic-ai/claude-code
npm install -g @openai/codex
npm install -g @github/copilot
uv tool install mistral-vibe
``` ```
Useful flags: ### Start the cockpit
```bash ```bash
./scripts/zellij-ai-triple.sh \ ./scripts/constant-machine.sh --workspace "$PWD"
--workspace /path/to/project \
--session ai-triple \
--codex-image codercom/code-server:latest \
--codex1-home "$HOME/.codex-profiles/codex-1" \
--codex2-home "$HOME/.codex-profiles/codex-2" \
--recreate
``` ```
By default, the launcher creates a fresh temporary Zellij config directory for each new session. ### Start the fleet cockpit
This is intentional: it avoids interference from an existing user Zellij config, custom default
layouts, plugins, or resurrected session state. If you really want to use a specific Zellij config
directory, pass:
```bash ```bash
./scripts/zellij-ai-triple.sh --zellij-config-dir /path/to/zellij-config ./scripts/constant-fleet.sh --workspace "$PWD"
``` ```
On first launch of each Codex pane, if `auth.json` is missing in its profile, the pane runs: By default, the machine launcher creates a fresh temporary Zellij config directory for each new session.
That is deliberate: it isolates `Constant` from an existing user Zellij config, custom layouts, plugins,
and stale resurrected state. If you want to pin a specific Zellij config dir, use:
```bash
./scripts/constant-machine.sh --zellij-config-dir /path/to/zellij-config
```
On first launch of the Codex pane, if `auth.json` is missing in its profile, the pane runs:
```bash ```bash
codex login --device-auth codex login --device-auth
``` ```
Use a different account in each pane if you want true account separation. ### Start Constant
## Security ```bash
./scripts/Constant
./scripts/Constant doctor
./scripts/Constant mission create "audit the repo" --workspace "$PWD"
./scripts/Constant mission status
./scripts/Constant cockpit open --workspace "$PWD"
```
This repository is intended to stay secret-free: If `Constant` is on your `PATH`, you can also just run:
- no `auth.json` ```bash
- no `.env` Constant
- no API keys ```
- no profile directories
The provided `.gitignore` excludes common secret-bearing files and local runtime state. ## Fleet Configuration
For a public setup, think in terms of roles, not personal machine names.
Example shape:
```text
command-center=local
builder-a=dev@builder-a
builder-b=dev@builder-b
edge-a=dev@edge-a
lab-a=dev@lab-a
```
The command-center machine is where you run:
- `Constant`
- the fleet cockpit
- local MLX orchestration
- any human-in-the-loop supervision
The workers are where execution happens.
## Messaging
Each machine exposes a local bus:
```text
~/.cache/constant/zellij/<session>/bus
```
Use it from any pane:
```bash
ai-msg.sh send --to codex --message "look at the failing test"
ai-msg.sh send --to vibe --message "explore two alternatives"
ai-msg.sh broadcast --message "new constraints in README"
ai-msg.sh inbox --for claude
```
Bridge between machines from the command center:
```bash
./scripts/ai-bridge.sh send \
--from-machine command-center \
--from claude \
--to-machine lab-a \
--to codex \
--message "take over the deep refactor"
```
## Clipboard
Clipboard behavior is split on purpose:
- local macOS sessions can use `pbcopy`
- remote tabs rely on `OSC52`
- the command center keeps ownership of the actual system clipboard
This matters.
If a remote pane copies text, it should land in the operator machine clipboard, not in some random remote shell session.
## Constant Philosophy
`Constant` is not trying to hide the terminal.
It assumes:
- the shell matters
- sessions matter
- state matters
- local files matter
- naming things matters
- supervision matters
The system should feel like:
- a cockpit
- a score
- a routing table
- a machine that keeps its shape under pressure
Not like a magic trick.
## Public Repo Roadmap
Planned layers for the public `Constant` repo:
- a real demoscene-style TUI
- a central `hexapus` buddy rail
- durable memory with vector + lexical search
- mission summaries and decision graph browsing
- richer repo context and instruction fusion
- cleaner public fleet templates
- screenshots, gifs, and a proper visual identity
## Repo Layout
```text
constant/ Python orchestration core
scripts/Constant canonical CLI entrypoint
scripts/constant-machine.sh canonical single-machine cockpit entrypoint
scripts/constant-fleet.sh canonical fleet cockpit entrypoint
scripts/constant-fleet-install.sh
scripts/ai-msg.sh local agent bus
scripts/ai-bridge.sh inter-machine bridge
```
## Current Caveats
- the shell runtime still keeps some historical compatibility wrappers
- the public-facing TUI is still evolving
- some setup paths and local assumptions are operator-oriented
- `copilot` is manual-only in the current autonomous flow
That is acceptable for now.
The system is being built in the open, from the terminal outward.
## Contributing
If you open this repo and think:
“this is a little unhinged, but technically serious”
then you probably understood the assignment.
+3
View File
@@ -0,0 +1,3 @@
__all__ = ["__version__"]
__version__ = "0.1.0"
+5
View File
@@ -0,0 +1,5 @@
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())
+477
View File
@@ -0,0 +1,477 @@
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
from typing import Any
from . import __version__
from .daemon import daemon_status, request as daemon_request, serve_foreground, start_background, stop_background
from .executors import bridge_sync, execute_step, fleet_check
from .paths import cache_root, repo_root, scripts_dir
from .state import (
append_event,
create_mission,
first_active_step,
fleet_machine,
load_fleet_config,
load_mission,
load_models_config,
list_missions,
mission_events_file,
save_mission,
write_artifact,
)
def _print(data: Any, as_json: bool = False) -> None:
if as_json:
print(json.dumps(data, indent=2, sort_keys=True))
elif isinstance(data, str):
print(data)
else:
print(json.dumps(data, indent=2, sort_keys=True))
def _command_exists(binary: str) -> bool:
for path_dir in os.environ.get("PATH", "").split(":"):
candidate = Path(path_dir) / binary
if candidate.exists() and os.access(candidate, os.X_OK):
return True
return False
def _ensure_planned(mission: dict[str, Any]) -> dict[str, Any]:
if mission["steps"]:
return mission
payload = daemon_request("plan", {"mission": mission})
mission["steps"] = payload["plan"]["steps"]
mission["title"] = payload["plan"]["title"]
mission["status"] = "planned"
mission["planner_summary"] = payload["plan"]["summary"]
mission["buddy_review"] = payload["buddy_review"]
save_mission(mission)
append_event(mission["mission_id"], "mission.planned", payload)
return mission
def _mission_summary(mission: dict[str, Any]) -> dict[str, Any]:
return {
"mission_id": mission["mission_id"],
"title": mission["title"],
"status": mission["status"],
"workspace": mission["workspace"],
"steps": [
{
"step_id": step["step_id"],
"status": step["status"],
"machine": step["machine"],
"backend": step["backend"],
"cli": step["cli"],
"agent": step["agent"],
"attempt": step["attempt"],
}
for step in mission["steps"]
],
}
def cmd_doctor(args: argparse.Namespace) -> int:
status = daemon_status()
health = daemon_request("health", auto_start=False)
fleet = load_fleet_config()
models = load_models_config()
report = {
"version": __version__,
"repo_root": str(repo_root()),
"cache_root": str(cache_root()),
"commands": {
"python3": _command_exists("python3"),
"tmux": _command_exists("tmux"),
"omc": _command_exists("omc"),
"claude": _command_exists("claude"),
"codex": _command_exists("codex"),
"copilot": _command_exists("copilot"),
"vibe": _command_exists("vibe"),
"constant-fleet": (scripts_dir() / "constant-fleet.sh").exists() or (scripts_dir() / "zellij-ai-fleet.sh").exists(),
"ai-bridge": (scripts_dir() / "ai-bridge.sh").exists(),
},
"daemon": status,
"models": models,
"health": health,
"fleet": [{"label": entry["label"], "target": entry["target"]} for entry in fleet["machines"]],
}
_print(report, args.json)
return 0
def cmd_daemon_start(_: argparse.Namespace) -> int:
payload = start_background()
if not payload.get("running"):
payload["mode"] = "inline-fallback"
_print(payload)
return 0
def cmd_daemon_stop(_: argparse.Namespace) -> int:
_print(stop_background())
return 0
def cmd_daemon_status(args: argparse.Namespace) -> int:
payload = {"status": daemon_status(), "health": daemon_request("health", auto_start=False)}
if not payload["status"]["running"]:
payload["mode"] = "inline-fallback"
_print(payload, args.json)
return 0
def cmd_daemon_logs(_: argparse.Namespace) -> int:
from .paths import daemon_log_path
path = daemon_log_path()
if not path.exists():
print("No daemon log yet.")
return 0
print(path.read_text(encoding="utf-8"))
return 0
def cmd_models_status(args: argparse.Namespace) -> int:
payload = {"config": load_models_config(), "daemon": daemon_status(), "health": daemon_request("health", auto_start=False)}
if not payload["daemon"]["running"]:
payload["mode"] = "inline-fallback"
_print(payload, args.json)
return 0
def cmd_fleet_status(args: argparse.Namespace) -> int:
payload = fleet_check()
_print(payload, args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_fleet_sync(args: argparse.Namespace) -> int:
payload = bridge_sync()
_print(payload, args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_cockpit_open(args: argparse.Namespace) -> int:
workspace = args.workspace or os.getcwd()
command = [
str((scripts_dir() / "constant-fleet.sh") if (scripts_dir() / "constant-fleet.sh").exists() else (scripts_dir() / "zellij-ai-fleet.sh")),
"--workspace",
workspace,
"--local-session",
args.local_session or "constant-fleet",
"--session",
args.session or "constant",
]
os.execv(command[0], command)
return 0
def cmd_mission_create(args: argparse.Namespace) -> int:
mission = create_mission(args.prompt, args.workspace)
mission = _ensure_planned(mission)
_print(_mission_summary(mission), args.json)
return 0
def cmd_mission_plan(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id)
payload = daemon_request("plan", {"mission": mission})
mission["steps"] = payload["plan"]["steps"]
mission["title"] = payload["plan"]["title"]
mission["status"] = "planned"
mission["planner_summary"] = payload["plan"]["summary"]
mission["buddy_review"] = payload["buddy_review"]
save_mission(mission)
append_event(mission["mission_id"], "mission.replanned", payload)
_print({"summary": _mission_summary(mission), "buddy_review": payload["buddy_review"]}, args.json)
return 0
def _verify_and_update(mission: dict[str, Any], step: dict[str, Any], execution: dict[str, Any]) -> dict[str, Any]:
verdict = daemon_request("verify", {"mission": mission, "step": step, "execution": execution})
step["verification"] = verdict
step["result_summary"] = verdict.get("summary", "")
return verdict
def cmd_mission_run(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id)
mission = _ensure_planned(mission)
step = first_active_step(mission)
if step is None:
mission["status"] = "done"
save_mission(mission)
_print(_mission_summary(mission), args.json)
return 0
while step is not None:
step["status"] = "running"
step["attempt"] += 1
mission["status"] = "running"
save_mission(mission)
append_event(mission["mission_id"], "step.started", {"step_id": step["step_id"], "machine": step["machine"], "backend": step["backend"], "cli": step["cli"]})
execution = execute_step(step, mission, load_fleet_config())
artifact_path = write_artifact(
mission["mission_id"],
f"{step['step_id']}-attempt-{step['attempt']}.json",
execution,
)
step["artifact_refs"].append(artifact_path)
append_event(mission["mission_id"], "step.executed", {"step_id": step["step_id"], "artifact": artifact_path, "returncode": execution["returncode"]})
verdict = _verify_and_update(mission, step, execution)
decision = verdict.get("decision", "failed")
if decision == "done":
step["status"] = "done"
elif decision == "retry" and step["attempt"] < 2:
step["status"] = "pending"
elif decision == "needs_human":
step["status"] = "needs_human"
mission["status"] = "needs_human"
save_mission(mission)
append_event(mission["mission_id"], "step.needs_human", {"step_id": step["step_id"], "summary": verdict.get("summary", "")})
_print({"summary": _mission_summary(mission), "verdict": verdict}, args.json)
return 1
else:
step["status"] = "failed"
mission["status"] = "failed"
save_mission(mission)
append_event(mission["mission_id"], "step.failed", {"step_id": step["step_id"], "summary": verdict.get("summary", "")})
_print({"summary": _mission_summary(mission), "verdict": verdict}, args.json)
return 1
save_mission(mission)
append_event(mission["mission_id"], "step.verified", {"step_id": step["step_id"], "verdict": verdict})
step = first_active_step(mission)
mission["status"] = "done"
save_mission(mission)
append_event(mission["mission_id"], "mission.done", {"mission_id": mission["mission_id"]})
_print(_mission_summary(mission), args.json)
return 0
def cmd_mission_status(args: argparse.Namespace) -> int:
if args.mission_id:
mission = load_mission(args.mission_id)
_print(_mission_summary(mission) if not args.verbose else mission, args.json)
return 0
missions = [_mission_summary(mission) for mission in list_missions()]
_print({"missions": missions}, args.json)
return 0
def cmd_mission_tail(args: argparse.Namespace) -> int:
path = mission_events_file(args.mission_id)
if not path.exists():
print(f"No events for mission {args.mission_id}")
return 1
if not args.follow:
print(path.read_text(encoding="utf-8"), end="")
return 0
seen_size = 0
while True:
text = path.read_text(encoding="utf-8")
if len(text) > seen_size:
print(text[seen_size:], end="")
seen_size = len(text)
time.sleep(1)
def cmd_mission_verify(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id)
step = next((entry for entry in mission["steps"] if entry["step_id"] == args.step_id), None) if args.step_id else (mission["steps"][-1] if mission["steps"] else None)
if step is None or not step["artifact_refs"]:
print("No step artifact available to verify.")
return 1
artifact = json.loads(Path(step["artifact_refs"][-1]).read_text(encoding="utf-8"))
verdict = _verify_and_update(mission, step, artifact)
save_mission(mission)
append_event(mission["mission_id"], "mission.verify", {"step_id": step["step_id"], "verdict": verdict})
_print({"step_id": step["step_id"], "verdict": verdict}, args.json)
return 0 if verdict.get("decision") == "done" else 1
def cmd_mission_retry(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id)
step = next((entry for entry in mission["steps"] if entry["step_id"] == args.step_id), None) if args.step_id else first_active_step(mission) or (mission["steps"][-1] if mission["steps"] else None)
if step is None:
print("No step to retry.")
return 1
step["status"] = "pending"
mission["status"] = "planned"
save_mission(mission)
append_event(mission["mission_id"], "step.retry_requested", {"step_id": step["step_id"]})
_print(_mission_summary(mission), args.json)
return 0
def cmd_delegate(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id)
step = next((entry for entry in mission["steps"] if entry["step_id"] == args.step_id), None) if args.step_id else first_active_step(mission)
if step is None:
print("No active step to delegate.")
return 1
if args.machine:
fleet_machine(args.machine)
step["machine"] = args.machine
if args.backend:
step["backend"] = args.backend
if args.cli:
step["cli"] = args.cli
if args.agent:
step["agent"] = args.agent
step["status"] = "pending"
mission["status"] = "planned"
save_mission(mission)
append_event(mission["mission_id"], "step.delegated", {"step_id": step["step_id"], "machine": step["machine"], "backend": step["backend"], "cli": step["cli"], "agent": step["agent"]})
_print(_mission_summary(mission), args.json)
return 0
def cmd_buddy_ask(args: argparse.Namespace) -> int:
mission = load_mission(args.mission_id) if args.mission_id else None
answer = daemon_request("buddy", {"mission": mission, "prompt": args.prompt})
if mission:
append_event(mission["mission_id"], "buddy.ask", {"prompt": args.prompt, "answer": answer})
_print(answer, args.json)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="Constant")
parser.add_argument("-V", "--version", action="version", version=__version__)
subparsers = parser.add_subparsers(dest="command", required=True)
doctor = subparsers.add_parser("doctor")
doctor.add_argument("--json", action="store_true")
doctor.set_defaults(func=cmd_doctor)
daemon = subparsers.add_parser("daemon")
daemon_sub = daemon.add_subparsers(dest="daemon_command", required=True)
daemon_start = daemon_sub.add_parser("start")
daemon_start.set_defaults(func=cmd_daemon_start)
daemon_stop = daemon_sub.add_parser("stop")
daemon_stop.set_defaults(func=cmd_daemon_stop)
daemon_status_cmd = daemon_sub.add_parser("status")
daemon_status_cmd.add_argument("--json", action="store_true")
daemon_status_cmd.set_defaults(func=cmd_daemon_status)
daemon_logs = daemon_sub.add_parser("logs")
daemon_logs.set_defaults(func=cmd_daemon_logs)
models = subparsers.add_parser("models")
models_sub = models.add_subparsers(dest="models_command", required=True)
models_status = models_sub.add_parser("status")
models_status.add_argument("--json", action="store_true")
models_status.set_defaults(func=cmd_models_status)
fleet = subparsers.add_parser("fleet")
fleet_sub = fleet.add_subparsers(dest="fleet_command", required=True)
fleet_status_cmd = fleet_sub.add_parser("status")
fleet_status_cmd.add_argument("--json", action="store_true")
fleet_status_cmd.set_defaults(func=cmd_fleet_status)
fleet_sync_cmd = fleet_sub.add_parser("sync")
fleet_sync_cmd.add_argument("--json", action="store_true")
fleet_sync_cmd.set_defaults(func=cmd_fleet_sync)
cockpit = subparsers.add_parser("cockpit")
cockpit_sub = cockpit.add_subparsers(dest="cockpit_command", required=True)
cockpit_open = cockpit_sub.add_parser("open")
cockpit_open.add_argument("--workspace")
cockpit_open.add_argument("--local-session")
cockpit_open.add_argument("--session")
cockpit_open.set_defaults(func=cmd_cockpit_open)
mission = subparsers.add_parser("mission")
mission_sub = mission.add_subparsers(dest="mission_command", required=True)
mission_create = mission_sub.add_parser("create")
mission_create.add_argument("prompt")
mission_create.add_argument("--workspace", default=os.getcwd())
mission_create.add_argument("--json", action="store_true")
mission_create.set_defaults(func=cmd_mission_create)
mission_plan = mission_sub.add_parser("plan")
mission_plan.add_argument("mission_id")
mission_plan.add_argument("--json", action="store_true")
mission_plan.set_defaults(func=cmd_mission_plan)
mission_run = mission_sub.add_parser("run")
mission_run.add_argument("mission_id")
mission_run.add_argument("--json", action="store_true")
mission_run.set_defaults(func=cmd_mission_run)
mission_status = mission_sub.add_parser("status")
mission_status.add_argument("mission_id", nargs="?")
mission_status.add_argument("--verbose", action="store_true")
mission_status.add_argument("--json", action="store_true")
mission_status.set_defaults(func=cmd_mission_status)
mission_tail = mission_sub.add_parser("tail")
mission_tail.add_argument("mission_id")
mission_tail.add_argument("--follow", action="store_true")
mission_tail.set_defaults(func=cmd_mission_tail)
mission_verify = mission_sub.add_parser("verify")
mission_verify.add_argument("mission_id")
mission_verify.add_argument("--step-id")
mission_verify.add_argument("--json", action="store_true")
mission_verify.set_defaults(func=cmd_mission_verify)
mission_retry = mission_sub.add_parser("retry")
mission_retry.add_argument("mission_id")
mission_retry.add_argument("--step-id")
mission_retry.add_argument("--json", action="store_true")
mission_retry.set_defaults(func=cmd_mission_retry)
delegate = subparsers.add_parser("delegate")
delegate.add_argument("mission_id")
delegate.add_argument("--step-id")
delegate.add_argument("--machine")
delegate.add_argument("--backend")
delegate.add_argument("--cli")
delegate.add_argument("--agent")
delegate.add_argument("--json", action="store_true")
delegate.set_defaults(func=cmd_delegate)
buddy = subparsers.add_parser("buddy")
buddy_sub = buddy.add_subparsers(dest="buddy_command", required=True)
buddy_ask = buddy_sub.add_parser("ask")
buddy_ask.add_argument("prompt")
buddy_ask.add_argument("--mission-id")
buddy_ask.add_argument("--json", action="store_true")
buddy_ask.set_defaults(func=cmd_buddy_ask)
hidden = subparsers.add_parser("__serve")
hidden.set_defaults(func=lambda _args: serve_foreground())
return parser
def main(argv: list[str] | None = None) -> int:
if argv is None:
argv = sys.argv[1:]
if not argv:
if sys.stdin.isatty() and sys.stdout.isatty():
argv = ["cockpit", "open", "--workspace", os.getcwd()]
else:
argv = ["doctor"]
parser = build_parser()
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
+222
View File
@@ -0,0 +1,222 @@
from __future__ import annotations
import json
import os
import signal
import socket
import socketserver
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Any
from .paths import daemon_log_path, daemon_pid_path, daemon_port_path
from .planner import PlannerEngine
_INLINE_ENGINE = PlannerEngine()
def _pid_is_alive(pid: int) -> bool:
try:
os.kill(pid, 0)
except OSError:
return False
return True
def daemon_status() -> dict[str, Any]:
pid_path = daemon_pid_path()
port_path = daemon_port_path()
pid = None
running = False
port = None
if pid_path.exists():
try:
pid = int(pid_path.read_text(encoding="utf-8").strip())
running = _pid_is_alive(pid)
except ValueError:
pid = None
if port_path.exists():
try:
port = int(port_path.read_text(encoding="utf-8").strip())
except ValueError:
port = None
endpoint = f"tcp://127.0.0.1:{port}" if port else None
return {
"running": running,
"pid": pid,
"port": port,
"endpoint": endpoint,
"log": str(daemon_log_path()),
}
class _Handler(socketserver.StreamRequestHandler):
engine = _INLINE_ENGINE
def handle(self) -> None:
raw = self.rfile.readline()
if not raw:
return
try:
request = json.loads(raw.decode("utf-8"))
op = request["op"]
if op == "health":
payload = self.engine.health()
elif op == "plan":
payload = self.engine.plan_mission(request["mission"])
elif op == "verify":
payload = self.engine.verify_step(request["mission"], request["step"], request["execution"])
elif op == "buddy":
payload = self.engine.buddy_ask(request.get("mission"), request["prompt"])
else:
raise KeyError(f"Unsupported operation: {op}")
response = {"ok": True, "payload": payload}
except Exception as exc: # noqa: BLE001
response = {"ok": False, "error": str(exc)}
self.wfile.write((json.dumps(response) + "\n").encode("utf-8"))
class _TcpServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
daemon_threads = True
allow_reuse_address = True
def _write_port(port: int) -> None:
daemon_port_path().write_text(str(port), encoding="utf-8")
def _remove_port() -> None:
port_path = daemon_port_path()
if port_path.exists():
port_path.unlink()
def serve_foreground() -> int:
pid_path = daemon_pid_path()
log_path = daemon_log_path()
port_path = daemon_port_path()
port_path.parent.mkdir(parents=True, exist_ok=True)
log_path.parent.mkdir(parents=True, exist_ok=True)
_remove_port()
server = _TcpServer(("127.0.0.1", 0), _Handler)
_write_port(int(server.server_address[1]))
pid_path.write_text(str(os.getpid()), encoding="utf-8")
def _shutdown(*_: object) -> None:
thread = threading.Thread(target=server.shutdown, daemon=True)
thread.start()
signal.signal(signal.SIGTERM, _shutdown)
signal.signal(signal.SIGINT, _shutdown)
try:
server.serve_forever()
finally:
server.server_close()
_remove_port()
if pid_path.exists():
pid_path.unlink()
return 0
def start_background() -> dict[str, Any]:
status = daemon_status()
if status["running"]:
return status
log_path = Path(status["log"])
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as handle:
process = subprocess.Popen(
[sys.executable, "-m", "constant.cli", "__serve"],
stdout=handle,
stderr=handle,
start_new_session=True,
)
for _ in range(50):
status = daemon_status()
if status["running"] and status["port"]:
return status
time.sleep(0.1)
status = daemon_status()
status["mode"] = "inline-fallback"
return status
def stop_background() -> dict[str, Any]:
status = daemon_status()
if not status["running"] or not status["pid"]:
return status
os.kill(status["pid"], signal.SIGTERM)
for _ in range(50):
next_status = daemon_status()
if not next_status["running"]:
return next_status
time.sleep(0.1)
return daemon_status()
def _direct_request(op: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
request_payload = payload or {}
if op == "health":
return _INLINE_ENGINE.health()
if op == "plan":
return _INLINE_ENGINE.plan_mission(request_payload["mission"])
if op == "verify":
return _INLINE_ENGINE.verify_step(
request_payload["mission"],
request_payload["step"],
request_payload["execution"],
)
if op == "buddy":
return _INLINE_ENGINE.buddy_ask(
request_payload.get("mission"),
request_payload["prompt"],
)
raise RuntimeError(f"Unsupported operation: {op}")
def request(op: str, payload: dict[str, Any] | None = None, auto_start: bool = True) -> dict[str, Any]:
if auto_start and not daemon_status()["running"]:
start_background()
status = daemon_status()
if not status["running"] or not status["port"]:
return _direct_request(op, payload)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", int(status["port"])))
with sock:
message = {"op": op}
if payload:
message.update(payload)
sock.sendall((json.dumps(message) + "\n").encode("utf-8"))
raw = b""
while not raw.endswith(b"\n"):
chunk = sock.recv(65536)
if not chunk:
break
raw += chunk
except OSError:
return _direct_request(op, payload)
response = json.loads(raw.decode("utf-8"))
if not response.get("ok"):
raise RuntimeError(response.get("error", "daemon request failed"))
return response["payload"]
+140
View File
@@ -0,0 +1,140 @@
from __future__ import annotations
import shlex
import subprocess
import time
from pathlib import Path
from typing import Any
from .paths import repo_root, scripts_dir
from .state import fleet_machine
def _run_command(args: list[str], cwd: str | None = None) -> dict[str, Any]:
started = time.time()
process = subprocess.run(args, capture_output=True, text=True, cwd=cwd)
return {
"argv": args,
"returncode": process.returncode,
"stdout": process.stdout,
"stderr": process.stderr,
"duration_s": round(time.time() - started, 3),
}
def _local_agent_path_export() -> str:
return 'export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH";'
def _claude_command(prompt: str) -> list[str]:
return ["claude", "-p", "--output-format", "json", "--permission-mode", "acceptEdits", prompt]
def _codex_command(prompt: str, workspace: str) -> list[str]:
return ["codex", "exec", "--json", "--full-auto", "--skip-git-repo-check", "-C", workspace, prompt]
def _vibe_command(prompt: str, workspace: str) -> list[str]:
return ["vibe", "-p", prompt, "--output", "json", "--workdir", workspace]
def _omc_command(cli: str, prompt: str) -> list[str]:
if cli not in {"claude", "codex"}:
raise RuntimeError(f"OMC backend only supports claude/codex in v1, not {cli}")
return ["omc", "ask", cli, "--print", prompt]
def build_local_command(step: dict[str, Any], workspace: str) -> list[str]:
cli = step["cli"]
prompt = step["prompt"]
backend = step["backend"]
if backend == "omc":
return _omc_command(cli, prompt)
if cli == "claude":
return _claude_command(prompt)
if cli == "codex":
return _codex_command(prompt, workspace)
if cli == "vibe":
return _vibe_command(prompt, workspace)
raise RuntimeError(f"Unsupported auto CLI: {cli}")
def execute_step(step: dict[str, Any], mission: dict[str, Any], fleet: dict[str, Any]) -> dict[str, Any]:
workspace = mission["workspace"]
machine = fleet_machine(step["machine"])
backend = step["backend"]
if step["cli"] == "copilot":
return {
"argv": [],
"returncode": 1,
"stdout": "",
"stderr": "copilot is manual-only in Constant v1",
"duration_s": 0.0,
}
if backend == "zellij":
cockpit = str((scripts_dir() / "constant-fleet.sh") if (scripts_dir() / "constant-fleet.sh").exists() else (scripts_dir() / "zellij-ai-fleet.sh"))
return {
"argv": [cockpit, "--workspace", workspace],
"returncode": 0,
"stdout": f"Open cockpit manually with: {cockpit} --workspace {workspace}",
"stderr": "",
"duration_s": 0.0,
}
command = build_local_command(step, workspace)
if backend == "cli-local" or backend == "omc":
return _run_command(command, cwd=workspace)
if backend == "cli-ssh":
quoted = shlex.join(command)
remote_shell = f'{_local_agent_path_export()} cd {shlex.quote(workspace)} && {quoted}'
return _run_command(["ssh", machine["target"], "bash", "-lc", remote_shell])
raise RuntimeError(f"Unsupported backend: {backend}")
def fleet_check() -> dict[str, Any]:
check_script = repo_root() / "scripts" / "constant-fleet-install.sh"
if not check_script.exists():
check_script = repo_root() / "scripts" / "zellij-ai-fleet-install.sh"
process = subprocess.run([str(check_script), "check"], capture_output=True, text=True)
machines: list[dict[str, Any]] = []
current: dict[str, Any] | None = None
for raw_line in process.stdout.splitlines():
line = raw_line.strip()
if not line:
continue
if line.startswith("===") and "(" in line:
if current:
machines.append(current)
label = line.split()[1]
current = {"label": label}
continue
if "=" in line and current is not None:
key, value = line.split("=", 1)
current[key] = value
if current:
machines.append(current)
return {
"returncode": process.returncode,
"machines": machines,
"stderr": process.stderr,
}
def bridge_sync() -> dict[str, Any]:
bridge_script = repo_root() / "scripts" / "ai-bridge.sh"
process = subprocess.run([str(bridge_script), "sync"], capture_output=True, text=True)
return {
"returncode": process.returncode,
"stdout": process.stdout,
"stderr": process.stderr,
}
+1097
View File
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parents[1]
def scripts_dir() -> Path:
return repo_root() / "scripts"
def cache_root() -> Path:
return Path.home() / ".cache" / "constant"
def config_root() -> Path:
return Path.home() / ".config" / "constant"
def data_root() -> Path:
return Path.home() / ".local" / "share" / "constant"
def planner_dir() -> Path:
return cache_root() / "planner"
def missions_dir() -> Path:
return cache_root() / "missions"
def fleet_config_path() -> Path:
return config_root() / "fleet.yaml"
def models_config_path() -> Path:
return config_root() / "models.yaml"
def memory_config_path() -> Path:
return config_root() / "memory.yaml"
def daemon_pid_path() -> Path:
return planner_dir() / "daemon.pid"
def daemon_log_path() -> Path:
return planner_dir() / "daemon.log"
def daemon_port_path() -> Path:
return planner_dir() / "daemon.port"
def memory_store_path() -> Path:
return data_root() / "memory.sqlite"
def persona_path() -> Path:
return data_root() / "persona.md"
def indexes_dir() -> Path:
return data_root() / "indexes"
def memory_sources_dir() -> Path:
return data_root() / "sources"
+407
View File
@@ -0,0 +1,407 @@
from __future__ import annotations
import importlib.util
import json
import re
import threading
from dataclasses import dataclass
from typing import Any
from .state import load_fleet_config, load_models_config
def _fleet_labels() -> dict[str, str]:
fleet = load_fleet_config()
labels = [machine["label"] for machine in fleet["machines"]]
local_machine = fleet.get("local_machine", labels[0] if labels else "command-center")
return {
"local": local_machine,
"builder_a": labels[1] if len(labels) > 1 else "builder-a",
"builder_b": labels[2] if len(labels) > 2 else "builder-b",
"edge_a": labels[3] if len(labels) > 3 else "edge-a",
"lab_a": labels[4] if len(labels) > 4 else "lab-a",
}
def _strip_code_fences(text: str) -> str:
stripped = text.strip()
if stripped.startswith("```"):
stripped = re.sub(r"^```[a-zA-Z0-9_-]*\n", "", stripped)
stripped = re.sub(r"\n```$", "", stripped)
return stripped.strip()
def _extract_json(text: str) -> dict[str, Any]:
stripped = _strip_code_fences(text)
try:
return json.loads(stripped)
except json.JSONDecodeError:
start = stripped.find("{")
end = stripped.rfind("}")
if start >= 0 and end > start:
return json.loads(stripped[start : end + 1])
raise
def _lower(value: str) -> str:
return value.lower()
def _route_machine(goal: str) -> str:
labels = _fleet_labels()
goal_l = _lower(goal)
if any(token in goal_l for token in ("ssh", "shell", "fleet", "ops", "network", "infra")):
return labels["edge_a"]
if any(token in goal_l for token in ("refactor", "performance", "deep", "cuda", "compiler", "benchmark")):
return labels["builder_b"]
if any(token in goal_l for token in ("review", "audit", "test", "qa", "docs")):
return labels["builder_a"]
if any(token in goal_l for token in ("experiment", "prototype", "sandbox", "branch")):
return labels["lab_a"]
return labels["local"]
def _route_cli(goal: str) -> str:
goal_l = _lower(goal)
if any(token in goal_l for token in ("brainstorm", "idea", "alternative", "explore", "compare")):
return "vibe"
if any(token in goal_l for token in ("spec", "summary", "summarize", "review", "docs")):
return "claude"
return "codex"
def _route_backend(machine: str, cli: str, goal: str) -> str:
local_machine = _fleet_labels()["local"]
goal_l = _lower(goal)
if machine == local_machine and cli in {"claude", "codex"} and any(token in goal_l for token in ("parallel", "team", "multi-agent", "compare")):
return "omc"
if machine == local_machine:
return "cli-local"
return "cli-ssh"
def _route_agent(cli: str) -> str:
return {
"claude": "planner",
"codex": "executor",
"vibe": "analyst",
"copilot": "assistant",
}.get(cli, "executor")
def _heuristic_plan(goal: str, workspace: str, mission_id: str) -> dict[str, Any]:
machine = _route_machine(goal)
cli = _route_cli(goal)
backend = _route_backend(machine, cli, goal)
return {
"title": goal.strip().splitlines()[0][:80] or mission_id,
"summary": f"Route the mission to {machine} using {cli} via {backend}.",
"steps": [
{
"step_id": "step-1",
"kind": "task",
"title": f"Execute mission on {machine}",
"prompt": goal,
"machine": machine,
"backend": backend,
"cli": cli,
"agent": _route_agent(cli),
"status": "pending",
"attempt": 0,
"depends_on": [],
"llama_plan": "heuristic planner fallback",
"qwen_review": "",
"result_summary": "",
"artifact_refs": [],
}
],
}
def _heuristic_buddy_review(goal: str, plan: dict[str, Any]) -> dict[str, Any]:
step = plan["steps"][0]
cli = step["cli"]
machine = step["machine"]
local_machine = _fleet_labels()["local"]
suggestions: list[str] = []
agrees = True
if cli == "copilot":
agrees = False
suggestions.append("copilot is manual-only in v1; use codex or claude instead")
if any(token in _lower(goal) for token in ("fix", "implement", "refactor", "bug")) and cli != "codex":
agrees = False
suggestions.append("technical execution is better routed to codex")
if machine == local_machine and step["backend"] == "cli-ssh":
agrees = False
suggestions.append("local machine should use cli-local or omc, not cli-ssh")
summary = "Buddy review agrees with the plan." if agrees else "; ".join(suggestions)
return {
"agrees": agrees,
"summary": summary,
"suggested_cli": "codex" if any("codex" in entry for entry in suggestions) else None,
"suggested_backend": "cli-local" if machine == local_machine and step["backend"] == "cli-ssh" else None,
}
def _heuristic_verify(step: dict[str, Any], execution: dict[str, Any]) -> dict[str, Any]:
stdout = execution.get("stdout", "")
stderr = execution.get("stderr", "")
combined = f"{stdout}\n{stderr}".lower()
return_code = execution.get("returncode", 1)
if any(token in combined for token in ("not logged in", "/login", "device-auth", "device auth", "authentication required", "please login", "please log in")):
return {
"decision": "needs_human",
"summary": "The CLI needs an interactive login or credentials refresh.",
"confidence": "high",
}
if return_code != 0:
if step.get("attempt", 0) <= 1:
return {
"decision": "retry",
"summary": "Execution failed once; retry is reasonable.",
"confidence": "medium",
}
return {
"decision": "failed",
"summary": "Execution failed after retry budget was exhausted.",
"confidence": "high",
}
if "error" in combined and "success" not in combined:
return {
"decision": "needs_human",
"summary": "Output contains an error marker despite zero exit status.",
"confidence": "medium",
}
return {
"decision": "done",
"summary": "Execution completed successfully.",
"confidence": "medium",
}
def _heuristic_buddy_answer(prompt: str, mission: dict[str, Any] | None) -> dict[str, Any]:
title = mission["title"] if mission else "mission"
return {
"answer": f"Qwen buddy heuristic view for {title}: focus on route correctness, CLI fit, and whether codex should own the technical step. Prompt: {prompt}",
"mode": "heuristic",
}
@dataclass
class ModelHealth:
role: str
model_id: str
available: bool
loaded: bool
backend: str
class PlannerEngine:
def __init__(self) -> None:
self._models = load_models_config()
self._loaded: dict[str, tuple[Any, Any]] = {}
self._lock = threading.Lock()
self._mlx_python = importlib.util.find_spec("mlx_lm") is not None
def health(self) -> dict[str, Any]:
health = {}
for role in ("planner", "buddy", "verify"):
spec = self._models[role]
health[role] = ModelHealth(
role=role,
model_id=spec["model_id"],
available=self._mlx_python,
loaded=role in self._loaded,
backend="mlx-python" if self._mlx_python else "heuristic",
).__dict__
return {
"mlx_python": self._mlx_python,
"models": health,
"fallback_mode": self._models.get("fallback_mode", "heuristic"),
}
def _load_model(self, role: str) -> tuple[Any, Any]:
if role in self._loaded:
return self._loaded[role]
if not self._mlx_python:
raise RuntimeError("mlx_lm is not available")
with self._lock:
if role in self._loaded:
return self._loaded[role]
from mlx_lm import load # type: ignore
model_id = self._models[role]["model_id"]
model, tokenizer = load(model_id)
self._loaded[role] = (model, tokenizer)
return model, tokenizer
def _call_model_json(self, role: str, system_prompt: str, user_prompt: str) -> dict[str, Any]:
if not self._mlx_python:
raise RuntimeError("mlx_lm is not available")
model, tokenizer = self._load_model(role)
from mlx_lm import generate # type: ignore
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
text = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=self._models[role]["max_tokens"],
verbose=False,
)
return _extract_json(text)
def _call_model_text(self, role: str, system_prompt: str, user_prompt: str) -> str:
if not self._mlx_python:
raise RuntimeError("mlx_lm is not available")
model, tokenizer = self._load_model(role)
from mlx_lm import generate # type: ignore
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
return generate(
model,
tokenizer,
prompt=prompt,
max_tokens=self._models[role]["max_tokens"],
verbose=False,
).strip()
def plan_mission(self, mission: dict[str, Any]) -> dict[str, Any]:
plan = _heuristic_plan(mission["goal"], mission["workspace"], mission["mission_id"])
review = _heuristic_buddy_review(mission["goal"], plan)
fleet = load_fleet_config()
machine_labels = [machine["label"] for machine in fleet["machines"]]
local_machine = fleet.get("local_machine", machine_labels[0] if machine_labels else "command-center")
if self._mlx_python:
system = (
"You are Llama, the main orchestrator for a 5-machine coding fleet. "
"Return strict JSON with keys: title, summary, steps. "
"Each step must include: step_id, kind, title, prompt, machine, backend, cli, agent, depends_on."
)
user = json.dumps(
{
"mission_id": mission["mission_id"],
"goal": mission["goal"],
"workspace": mission["workspace"],
"machines": machine_labels,
"allowed_backends": ["omc", "cli-local", "cli-ssh", "zellij"],
"allowed_clis": ["claude", "codex", "vibe"],
},
indent=2,
)
try:
llama_plan = self._call_model_json("planner", system, user)
if "steps" in llama_plan and llama_plan["steps"]:
plan = {
"title": llama_plan.get("title", plan["title"]),
"summary": llama_plan.get("summary", plan["summary"]),
"steps": [],
}
for index, step in enumerate(llama_plan["steps"], start=1):
plan["steps"].append(
{
"step_id": step.get("step_id", f"step-{index}"),
"kind": step.get("kind", "task"),
"title": step.get("title", f"Step {index}"),
"prompt": step.get("prompt", mission["goal"]),
"machine": step.get("machine", _route_machine(mission["goal"])),
"backend": step.get("backend", _route_backend(step.get("machine", local_machine), step.get("cli", "codex"), mission["goal"])),
"cli": step.get("cli", _route_cli(mission["goal"])),
"agent": step.get("agent", _route_agent(step.get("cli", "codex"))),
"status": "pending",
"attempt": 0,
"depends_on": step.get("depends_on", []),
"llama_plan": llama_plan.get("summary", ""),
"qwen_review": "",
"result_summary": "",
"artifact_refs": [],
}
)
except Exception:
pass
buddy_system = (
"You are Qwen, the local technical buddy. Return strict JSON with keys: "
"agrees, summary, suggested_cli, suggested_backend."
)
buddy_user = json.dumps({"goal": mission["goal"], "plan": plan}, indent=2)
try:
review = self._call_model_json("buddy", buddy_system, buddy_user)
except Exception:
pass
for step in plan["steps"]:
step["qwen_review"] = review.get("summary", "")
if not review.get("agrees", True):
if review.get("suggested_cli") and step["cli"] == "claude":
step["cli"] = review["suggested_cli"]
step["agent"] = _route_agent(step["cli"])
if review.get("suggested_backend"):
step["backend"] = review["suggested_backend"]
return {
"plan": plan,
"buddy_review": review,
}
def verify_step(self, mission: dict[str, Any], step: dict[str, Any], execution: dict[str, Any]) -> dict[str, Any]:
result = _heuristic_verify(step, execution)
if self._mlx_python:
system = (
"You are the orchestrator verifier. Return strict JSON with keys: "
"decision, summary, confidence. Decisions: done, retry, failed, needs_human."
)
user = json.dumps(
{
"mission_title": mission["title"],
"step": step,
"execution": {
"returncode": execution.get("returncode"),
"stdout_tail": execution.get("stdout", "")[-4000:],
"stderr_tail": execution.get("stderr", "")[-4000:],
},
},
indent=2,
)
try:
result = self._call_model_json("verify", system, user)
except Exception:
pass
return result
def buddy_ask(self, mission: dict[str, Any] | None, prompt: str) -> dict[str, Any]:
result = _heuristic_buddy_answer(prompt, mission)
if self._mlx_python:
system = "You are Qwen, the local buddy. Give a concise technical answer."
user = json.dumps({"mission": mission, "prompt": prompt}, indent=2)
try:
result = {
"answer": self._call_model_text("buddy", system, user),
"mode": "mlx",
}
except Exception:
pass
return result
+244
View File
@@ -0,0 +1,244 @@
from __future__ import annotations
import json
import uuid
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from . import __version__
from .paths import (
cache_root,
config_root,
data_root,
fleet_config_path,
indexes_dir,
memory_config_path,
memory_sources_dir,
missions_dir,
models_config_path,
)
DEFAULT_FLEET: dict[str, Any] = {
"version": 1,
"local_machine": "command-center",
"machines": [
{
"label": "command-center",
"target": "local",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["omc", "cli-local", "zellij"],
},
{
"label": "builder-a",
"target": "dev@builder-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "zellij"],
},
{
"label": "builder-b",
"target": "dev@builder-b",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "zellij"],
},
{
"label": "edge-a",
"target": "dev@edge-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "zellij"],
},
{
"label": "lab-a",
"target": "dev@lab-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "zellij"],
},
],
}
DEFAULT_MODELS: dict[str, Any] = {
"version": 1,
"planner": {
"role": "planner",
"model_id": "mlx-community-staging/Llama-3.2-3B-Instruct-mlx-4Bit",
"max_tokens": 900,
},
"buddy": {
"role": "buddy",
"model_id": "mlx-community/Qwen2.5-Coder-3B-Instruct-4bit",
"max_tokens": 900,
},
"verify": {
"role": "verify",
"model_id": "mlx-community-staging/Llama-3.2-3B-Instruct-mlx-4Bit",
"max_tokens": 700,
},
"fallback_mode": "heuristic",
}
DEFAULT_MEMORY: dict[str, Any] = {
"version": 1,
"local_store_path": str(data_root() / "memory.sqlite"),
"qdrant_url": "",
"qdrant_collection": "constant_memory",
"workspace_enrollments": [],
"instruction_weights": {
"workspace": 1.0,
"repo": 0.85,
"ancestor": 0.65,
"user": 0.45,
"default": 0.2,
},
"max_chunks_per_query": 8,
"vector_dimensions": 96,
}
def now_utc() -> str:
return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def ensure_runtime_dirs() -> None:
for path in (cache_root(), config_root(), data_root(), missions_dir(), indexes_dir(), memory_sources_dir()):
path.mkdir(parents=True, exist_ok=True)
def _read_json_yaml(path: Path, default: dict[str, Any]) -> dict[str, Any]:
ensure_runtime_dirs()
if not path.exists():
_write_json_yaml(path, default)
return deepcopy(default)
return json.loads(path.read_text(encoding="utf-8"))
def _write_json_yaml(path: Path, payload: dict[str, Any]) -> None:
ensure_runtime_dirs()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def load_fleet_config() -> dict[str, Any]:
return _read_json_yaml(fleet_config_path(), DEFAULT_FLEET)
def load_models_config() -> dict[str, Any]:
return _read_json_yaml(models_config_path(), DEFAULT_MODELS)
def load_memory_config() -> dict[str, Any]:
return _read_json_yaml(memory_config_path(), DEFAULT_MEMORY)
def save_memory_config(payload: dict[str, Any]) -> None:
_write_json_yaml(memory_config_path(), payload)
def fleet_machine(label: str) -> dict[str, Any]:
fleet = load_fleet_config()
for machine in fleet["machines"]:
if machine["label"] == label or machine["target"] == label:
return machine
raise KeyError(f"Unknown machine: {label}")
def mission_dir(mission_id: str) -> Path:
return missions_dir() / mission_id
def mission_file(mission_id: str) -> Path:
return mission_dir(mission_id) / "mission.json"
def mission_events_file(mission_id: str) -> Path:
return mission_dir(mission_id) / "events.ndjson"
def mission_artifacts_dir(mission_id: str) -> Path:
return mission_dir(mission_id) / "artifacts"
def create_mission(goal: str, workspace: str, routing_overrides: dict[str, Any] | None = None) -> dict[str, Any]:
mission_id = uuid.uuid4().hex[:12]
mission = {
"mission_id": mission_id,
"title": goal.strip().splitlines()[0][:80] or f"mission-{mission_id}",
"goal": goal,
"workspace": workspace,
"status": "draft",
"priority": "normal",
"created_at": now_utc(),
"updated_at": now_utc(),
"planner_model": load_models_config()["planner"]["model_id"],
"buddy_model": load_models_config()["buddy"]["model_id"],
"verify_model": load_models_config()["verify"]["model_id"],
"owner": "Constant",
"routing_overrides": routing_overrides or {},
"steps": [],
"artifacts": [],
"meta": {
"schema_version": 1,
"tool_version": __version__,
},
}
save_mission(mission)
append_event(mission_id, "mission.created", {"goal": goal, "workspace": workspace})
return mission
def save_mission(mission: dict[str, Any]) -> None:
path = mission_file(mission["mission_id"])
path.parent.mkdir(parents=True, exist_ok=True)
mission["updated_at"] = now_utc()
path.write_text(json.dumps(mission, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def load_mission(mission_id: str) -> dict[str, Any]:
return json.loads(mission_file(mission_id).read_text(encoding="utf-8"))
def list_missions() -> list[dict[str, Any]]:
ensure_runtime_dirs()
missions: list[dict[str, Any]] = []
for path in sorted(missions_dir().glob("*/mission.json")):
missions.append(json.loads(path.read_text(encoding="utf-8")))
return missions
def append_event(mission_id: str, event_type: str, payload: dict[str, Any]) -> None:
path = mission_events_file(mission_id)
path.parent.mkdir(parents=True, exist_ok=True)
event = {
"timestamp": now_utc(),
"type": event_type,
"payload": payload,
}
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
def write_artifact(mission_id: str, name: str, payload: dict[str, Any]) -> str:
artifact_dir = mission_artifacts_dir(mission_id)
artifact_dir.mkdir(parents=True, exist_ok=True)
path = artifact_dir / name
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
mission = load_mission(mission_id)
mission["artifacts"].append(str(path))
save_mission(mission)
return str(path)
def first_active_step(mission: dict[str, Any]) -> dict[str, Any] | None:
for step in mission["steps"]:
if step["status"] not in {"done", "failed", "needs_human"}:
return step
return None
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
default_python="$HOME/.local/share/constant/venv/bin/python3"
python_bin="${CONSTANT_PYTHON:-}"
if [[ -z "$python_bin" ]]; then
if [[ -x "$default_python" ]]; then
python_bin="$default_python"
else
python_bin="python3"
fi
fi
export PYTHONPATH="$repo_dir${PYTHONPATH+:$PYTHONPATH}"
exec "$python_bin" -m constant "$@"
+373
View File
@@ -0,0 +1,373 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [options]
Commands:
send Relay one message to one machine
broadcast Relay one message to all machines
sync Refresh a local cache of remote inbox summaries
tail Poll all machines and print new summaries
Defaults:
repo-dir: $(zellij_ai_default_repo_dir)
session: $(zellij_ai_default_session)
EOF
}
repo_dir="$(zellij_ai_default_repo_dir)"
session="$(zellij_ai_default_session)"
cache_dir="$(zellij_ai_bridge_cache_dir)"
machines=()
default_machine_specs_array() {
local line
while IFS= read -r line; do
[[ -z "$line" ]] && continue
printf '%s\n' "$line"
done < <(zellij_ai_default_machine_specs)
}
load_machines() {
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(default_machine_specs_array)
fi
}
resolve_machine() {
local needle="$1"
local resolved
if ! resolved="$(zellij_ai_resolve_machine_spec "$needle")"; then
echo "Unknown machine: $needle" >&2
exit 2
fi
printf '%s\n' "$resolved"
}
run_ai_msg() {
local label="$1"
local target="$2"
shift 2
if zellij_ai_is_local_target "$target"; then
ZELLIJ_AI_MACHINE_NAME="$label" \
ZELLIJ_AI_SESSION="$session" \
"$(zellij_ai_expand_home_path "$repo_dir")/scripts/ai-msg.sh" "$@"
return 0
fi
ssh "$target" bash -s -- "$label" "$session" "$repo_dir" "$@" <<'EOF'
set -euo pipefail
label="$1"
shift
session="$1"
shift
repo_dir_input="$1"
shift
expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
repo_dir="$(expand_home_path "$repo_dir_input")"
export ZELLIJ_AI_MACHINE_NAME="$label"
export ZELLIJ_AI_SESSION="$session"
exec "$repo_dir/scripts/ai-msg.sh" "$@"
EOF
}
sync_once() {
local machine_spec resolved label target cache_file
mkdir -p "$cache_dir"
load_machines
for machine_spec in "${machines[@]}"; do
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
cache_file="$cache_dir/${label}.tsv"
run_ai_msg "$label" "$target" list --for all >"$cache_file"
done
}
command_send() {
local from_machine="$(zellij_ai_local_machine_label)"
local from_role="bridge"
local to_machine=""
local to_role=""
local message_text=""
local message_file=""
local resolved label target
while [[ $# -gt 0 ]]; do
case "$1" in
--from-machine)
from_machine="$2"
shift 2
;;
--from)
from_role="$2"
shift 2
;;
--to-machine)
to_machine="$2"
shift 2
;;
--to)
to_role="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for send: $1" >&2
exit 2
;;
esac
done
if [[ -z "$to_machine" || -z "$to_role" ]]; then
echo "--to-machine and --to are required." >&2
exit 2
fi
IFS=$'\t' read -r label target <<<"$(resolve_machine "$to_machine")"
if [[ -n "$message_text" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
elif [[ -n "$message_file" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
elif [[ ! -t 0 ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to "$to_role" --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
else
echo "Provide --message, --file, or stdin." >&2
exit 2
fi
}
command_broadcast() {
local from_machine="$(zellij_ai_local_machine_label)"
local from_role="bridge"
local message_text=""
local message_file=""
local machine_spec resolved label target
while [[ $# -gt 0 ]]; do
case "$1" in
--from-machine)
from_machine="$2"
shift 2
;;
--from)
from_role="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for broadcast: $1" >&2
exit 2
;;
esac
done
load_machines
for machine_spec in "${machines[@]}"; do
IFS=$'\t' read -r label target <<<"$(resolve_machine "$machine_spec")"
if [[ -n "$message_text" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --message "$message_text"
elif [[ -n "$message_file" ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)" --file "$message_file"
elif [[ ! -t 0 ]]; then
run_ai_msg "$label" "$target" send --from "$from_role" --to all --source-machine "$from_machine" --source-role "$from_role" --relay-via "$(zellij_ai_local_machine_label)"
else
echo "Provide --message, --file, or stdin." >&2
exit 2
fi
done
}
command_sync() {
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for sync: $1" >&2
exit 2
;;
esac
done
sync_once
find "$cache_dir" -type f -name '*.tsv' | sort | while IFS= read -r file; do
cat "$file"
done
}
command_tail() {
local interval=3
declare -A seen=()
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
--interval)
interval="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for tail: $1" >&2
exit 2
;;
esac
done
while true; do
sync_once
while IFS= read -r file; do
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ -n "${seen[$line]:-}" ]]; then
continue
fi
seen["$line"]=1
printf '%s\n' "$line"
done <"$file"
done < <(find "$cache_dir" -type f -name '*.tsv' | sort)
sleep "$interval"
done
}
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
command="$1"
shift
case "$command" in
send)
command_send "$@"
;;
broadcast)
command_broadcast "$@"
;;
sync)
command_sync "$@"
;;
tail)
command_tail "$@"
;;
-h|--help|help)
usage
;;
*)
echo "Unknown command: $command" >&2
usage >&2
exit 2
;;
esac
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: ai-copy.sh [options]
Copy text into the macOS clipboard on the current machine.
Options:
--text TEXT Copy this text directly
--file FILE Copy the contents of FILE
-h, --help Show this help
If neither --text nor --file is provided, stdin is copied.
EOF
}
text=""
file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--text)
text="$2"
shift 2
;;
--file)
file="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -n "$text" && -n "$file" ]]; then
echo "Use either --text or --file, not both." >&2
exit 2
fi
if ! command -v pbcopy >/dev/null 2>&1; then
echo "pbcopy is not available on this machine." >&2
exit 1
fi
if [[ -n "$text" ]]; then
printf '%s' "$text" | pbcopy
elif [[ -n "$file" ]]; then
cat "$file" | pbcopy
elif [[ ! -t 0 ]]; then
cat | pbcopy
else
echo "Nothing to copy. Provide --text, --file, or pipe stdin." >&2
exit 2
fi
printf 'Copied to the macOS clipboard.\n'
+376
View File
@@ -0,0 +1,376 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
cat <<'EOF'
Usage: ai-msg.sh <command> [options]
Commands:
send Send a message to one role
broadcast Send a message to all roles on the current machine
inbox Show messages addressed to one role
tail Poll and print new messages for one role
list List matching messages in TSV form
Environment defaults:
ZELLIJ_AI_BUS_DIR
ZELLIJ_AI_MACHINE_NAME
ZELLIJ_AI_ROLE
ZELLIJ_AI_SESSION
EOF
}
machine_name="$(zellij_ai_current_machine_name)"
session="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
role_default="${ZELLIJ_AI_ROLE:-all}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session")/bus}"
messages_dir="${bus_dir}/messages"
ensure_messages_dir() {
mkdir -p "$messages_dir"
}
random_suffix() {
od -An -N4 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || printf '%s' "$$"
}
message_id() {
printf '%s\n' "${machine_name}-${session}-$(date -u +%Y%m%dT%H%M%SZ)-$$-$(random_suffix)"
}
timestamp_utc() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
read_body_input() {
local message_text="$1"
local message_file="$2"
if [[ -n "$message_text" ]]; then
printf '%s' "$message_text"
return 0
fi
if [[ -n "$message_file" ]]; then
cat "$message_file"
return 0
fi
if [[ ! -t 0 ]]; then
cat
return 0
fi
echo "No message body provided. Use --message, --file, or stdin." >&2
return 1
}
read_header() {
local key="$1"
local file="$2"
sed -n "s/^${key}: //p" "$file" | head -n 1
}
read_body() {
awk 'seen_blank { print } /^$/ { seen_blank = 1 }' "$1"
}
message_matches_role() {
local file="$1"
local role="$2"
local target
target="$(read_header target "$file")"
if [[ "$role" == "all" ]]; then
return 0
fi
[[ "$target" == "$role" || "$target" == "all" ]]
}
list_message_files() {
if [[ ! -d "$messages_dir" ]]; then
return 0
fi
find "$messages_dir" -type f -name '*.msg' | sort
}
print_message_tsv() {
local file="$1"
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(read_header id "$file")" \
"$(read_header timestamp "$file")" \
"$(read_header machine "$file")" \
"$(read_header session "$file")" \
"$(read_header sender "$file")" \
"$(read_header target "$file")" \
"$(read_header source_machine "$file")" \
"$(read_header source_role "$file")" \
"$file"
}
print_message_pretty() {
local file="$1"
local id timestamp sender target stored_machine source_machine source_role relay_via
id="$(read_header id "$file")"
timestamp="$(read_header timestamp "$file")"
sender="$(read_header sender "$file")"
target="$(read_header target "$file")"
stored_machine="$(read_header machine "$file")"
source_machine="$(read_header source_machine "$file")"
source_role="$(read_header source_role "$file")"
relay_via="$(read_header relay_via "$file")"
printf '[%s] %s -> %s on %s (%s)\n' "$timestamp" "$sender" "$target" "$stored_machine" "$id"
if [[ -n "$source_machine" || -n "$source_role" ]]; then
printf 'source: %s/%s\n' "${source_machine:-unknown}" "${source_role:-unknown}"
fi
if [[ -n "$relay_via" ]]; then
printf 'relay: %s\n' "$relay_via"
fi
printf '%s\n' '---'
read_body "$file"
printf '\n'
}
write_message_file() {
local sender="$1"
local target="$2"
local source_machine="$3"
local source_role="$4"
local relay_via="$5"
local body="$6"
local id timestamp file tmp_file
ensure_messages_dir
id="$(message_id)"
timestamp="$(timestamp_utc)"
file="${messages_dir}/${id}.msg"
tmp_file="$(mktemp "${messages_dir}/.tmp.XXXXXX")"
{
printf 'id: %s\n' "$id"
printf 'timestamp: %s\n' "$timestamp"
printf 'machine: %s\n' "$machine_name"
printf 'session: %s\n' "$session"
printf 'sender: %s\n' "$sender"
printf 'target: %s\n' "$target"
printf 'source_machine: %s\n' "$source_machine"
printf 'source_role: %s\n' "$source_role"
printf 'relay_via: %s\n' "$relay_via"
printf '\n'
printf '%s\n' "$body"
} >"$tmp_file"
mv "$tmp_file" "$file"
printf '%s\n' "$file"
}
command_send() {
local sender="$role_default"
local target=""
local message_text=""
local message_file=""
local source_machine=""
local source_role=""
local relay_via=""
local body file
while [[ $# -gt 0 ]]; do
case "$1" in
--from)
sender="$2"
shift 2
;;
--to)
target="$2"
shift 2
;;
--message)
message_text="$2"
shift 2
;;
--file)
message_file="$2"
shift 2
;;
--source-machine)
source_machine="$2"
shift 2
;;
--source-role)
source_role="$2"
shift 2
;;
--relay-via)
relay_via="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for send: $1" >&2
exit 2
;;
esac
done
if [[ -z "$target" ]]; then
echo "--to is required for send." >&2
exit 2
fi
body="$(read_body_input "$message_text" "$message_file")"
file="$(write_message_file "$sender" "$target" "$source_machine" "$source_role" "$relay_via" "$body")"
printf 'stored: %s\n' "$file"
}
command_broadcast() {
command_send --to all "$@"
}
command_list() {
local role="all"
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for list: $1" >&2
exit 2
;;
esac
done
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
print_message_tsv "$file"
done < <(list_message_files)
}
command_inbox() {
local role="$role_default"
local found=false
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for inbox: $1" >&2
exit 2
;;
esac
done
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
print_message_pretty "$file"
found=true
done < <(list_message_files)
if ! $found; then
printf 'No messages for %s in %s\n' "$role" "$messages_dir"
fi
}
command_tail() {
local role="$role_default"
local interval=2
declare -A seen=()
while [[ $# -gt 0 ]]; do
case "$1" in
--for)
role="$2"
shift 2
;;
--interval)
interval="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option for tail: $1" >&2
exit 2
;;
esac
done
while true; do
while IFS= read -r file; do
message_matches_role "$file" "$role" || continue
if [[ -n "${seen[$file]:-}" ]]; then
continue
fi
seen["$file"]=1
print_message_pretty "$file"
done < <(list_message_files)
sleep "$interval"
done
}
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
command="$1"
shift
case "$command" in
send)
command_send "$@"
;;
broadcast)
command_broadcast "$@"
;;
list)
command_list "$@"
;;
inbox)
command_inbox "$@"
;;
tail)
command_tail "$@"
;;
-h|--help|help)
usage
;;
*)
echo "Unknown command: $command" >&2
usage >&2
exit 2
;;
esac
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-fleet-install.sh" "$@"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-fleet.sh" "$@"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
export CONSTANT_SCRIPT_NAME="$(basename "$0")"
exec "$script_dir/zellij-ai-triple.sh" "$@"
+19 -7
View File
@@ -1,7 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
bootstrap_file="${ZELLIJ_AI_BOOTSTRAP_FILE:-}" bootstrap_file="${ZELLIJ_AI_BOOTSTRAP_FILE:-}"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}" workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
@@ -12,17 +18,23 @@ fi
if [[ -n "$bootstrap_file" && ! -f "$bootstrap_file" ]]; then if [[ -n "$bootstrap_file" && ! -f "$bootstrap_file" ]]; then
: >"$bootstrap_file" : >"$bootstrap_file"
printf -v codex1_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "1" printf -v codex_cmd '%q' "$script_dir/zellij-ai-codex-pane.sh"
printf -v codex2_cmd '%q %q' "$script_dir/zellij-ai-codex-pane.sh" "2" printf -v copilot_cmd '%q' "$script_dir/zellij-ai-copilot-pane.sh"
printf -v vibe_cmd '%q' "$script_dir/zellij-ai-vibe-pane.sh"
zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX1_LABEL:-codex-1}" --cwd "$workspace" zellij action new-pane -d right -n "${ZELLIJ_AI_CODEX_LABEL:-codex}" --cwd "$workspace"
zellij action move-focus right zellij action move-focus right
zellij action write-chars "$codex1_cmd" zellij action write-chars "$codex_cmd"
zellij action write 10 zellij action write 10
zellij action new-pane -d down -n "${ZELLIJ_AI_CODEX2_LABEL:-codex-2}" --cwd "$workspace" zellij action new-pane -d down -n "${ZELLIJ_AI_COPILOT_LABEL:-copilot}" --cwd "$workspace"
zellij action move-focus down zellij action move-focus down
zellij action write-chars "$codex2_cmd" zellij action write-chars "$copilot_cmd"
zellij action write 10
zellij action new-pane -d down -n "${ZELLIJ_AI_VIBE_LABEL:-vibe}" --cwd "$workspace"
zellij action move-focus down
zellij action write-chars "$vibe_cmd"
zellij action write 10 zellij action write 10
zellij action move-focus left zellij action move-focus left
+29
View File
@@ -1,7 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}" workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
if [[ ! -d "$workspace" ]]; then if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2 echo "Workspace not found: $workspace" >&2
@@ -12,10 +25,26 @@ if [[ -n "${ZELLIJ_AI_CLAUDE_CONFIG_DIR:-}" ]]; then
export CLAUDE_CONFIG_DIR="$ZELLIJ_AI_CLAUDE_CONFIG_DIR" export CLAUDE_CONFIG_DIR="$ZELLIJ_AI_CLAUDE_CONFIG_DIR"
fi fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
export ZELLIJ_AI_ROLE="claude"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace" cd "$workspace"
echo "Claude pane" echo "Claude pane"
echo "machine: $machine_name"
echo "workspace: $workspace" echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
echo "config: $CLAUDE_CONFIG_DIR" echo "config: $CLAUDE_CONFIG_DIR"
fi fi
+72 -121
View File
@@ -1,160 +1,111 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: zellij-ai-codex-pane.sh <slot> Usage: zellij-ai-codex-pane.sh [slot]
slot: slot:
1 Launch the first Codex container/profile 1 Deprecated alias for the single host-local Codex pane
2 Launch the second Codex container/profile 2 Deprecated alias for the single host-local Codex pane
EOF EOF
} }
find_codex_binary() { warn_deprecated() {
if [[ -n "${ZELLIJ_AI_CODEX_BINARY:-}" && -x "${ZELLIJ_AI_CODEX_BINARY}" ]]; then printf 'Warning: %s\n' "$*" >&2
printf '%s\n' "${ZELLIJ_AI_CODEX_BINARY}"
return 0
fi
if [[ -x "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex" ]]; then
printf '%s\n' "$HOME/.npm-global/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex"
return 0
fi
if [[ -n "$(command -v codex || true)" ]]; then
local wrapper_dir package_dir candidate
wrapper_dir="$(cd "$(dirname "$(command -v codex)")" && pwd -P)"
package_dir="$(cd "$wrapper_dir/../lib/node_modules/@openai/codex/node_modules/@openai" 2>/dev/null && pwd -P || true)"
if [[ -n "$package_dir" ]]; then
candidate="$(find "$package_dir" -path '*/vendor/*/codex/codex' -type f -print -quit 2>/dev/null || true)"
if [[ -n "$candidate" && -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
fi
fi
return 1
} }
if [[ $# -ne 1 ]]; then if [[ $# -gt 1 ]]; then
usage >&2 usage >&2
exit 2 exit 2
fi fi
slot="$1" if [[ $# -eq 1 ]]; then
case "$1" in
case "$slot" in 1)
1) warn_deprecated "slot 1 is deprecated; Codex now runs as a single host-local pane."
label="${ZELLIJ_AI_CODEX1_LABEL:-codex-1}" ;;
profile_dir="${ZELLIJ_AI_CODEX1_HOME:-$HOME/.codex-profiles/codex-1}" 2)
;; warn_deprecated "slot 2 is deprecated; launching the single host-local Codex pane."
2) ;;
label="${ZELLIJ_AI_CODEX2_LABEL:-codex-2}" -h|--help|help)
profile_dir="${ZELLIJ_AI_CODEX2_HOME:-$HOME/.codex-profiles/codex-2}" usage
;; exit 0
*) ;;
usage >&2 *)
exit 2 usage >&2
;; exit 2
esac ;;
esac
fi
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}" workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
image="${ZELLIJ_AI_CODEX_IMAGE:-codercom/code-server:latest}" profile_dir="${ZELLIJ_AI_CODEX_HOME:-$(zellij_ai_default_codex_home)}"
label="${ZELLIJ_AI_CODEX_LABEL:-codex}"
main_codex_config="${ZELLIJ_AI_MAIN_CODEX_CONFIG:-$HOME/.codex/config.toml}" main_codex_config="${ZELLIJ_AI_MAIN_CODEX_CONFIG:-$HOME/.codex/config.toml}"
codex_binary="$(find_codex_binary || true)" repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
term_value="${ZELLIJ_AI_CODEX_TERM:-${TERM:-xterm-256color}}" session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
color_term_value="${COLORTERM:-truecolor}" bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
machine_name="$(zellij_ai_current_machine_name)"
if [[ "$term_value" == "dumb" ]]; then agent_path="$(zellij_ai_agent_path)"
term_value="xterm-256color"
fi
if [[ ! -d "$workspace" ]]; then if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2 echo "Workspace not found: $workspace" >&2
exit 1 exit 1
fi fi
if [[ -z "$codex_binary" || ! -x "$codex_binary" ]]; then export PATH="$agent_path"
echo "Unable to locate the native Codex binary on the host." >&2
echo "Set ZELLIJ_AI_CODEX_BINARY if Codex is installed elsewhere." >&2 if ! command -v codex >/dev/null 2>&1; then
echo "Codex CLI not found on the host." >&2
echo "Install it with: npm install -g @openai/codex" >&2
exit 1 exit 1
fi fi
platform_dir="$(dirname "$(dirname "$codex_binary")")" profile_dir="$(mkdir -p "$(zellij_ai_expand_home_path "$profile_dir")" && cd "$(zellij_ai_expand_home_path "$profile_dir")" && pwd -P)"
rg_binary="$platform_dir/path/rg" mkdir -p "$bus_dir/messages"
if [[ ! -x "$rg_binary" ]]; then
echo "Bundled rg binary not found next to Codex: $rg_binary" >&2
exit 1
fi
if ! docker image inspect "$image" >/dev/null 2>&1; then
echo "Docker image not found locally: $image" >&2
echo "Pull or build the image first, or relaunch with --codex-image." >&2
exit 1
fi
mkdir -p "$profile_dir"
if [[ ! -f "$profile_dir/config.toml" && -f "$main_codex_config" ]]; then if [[ ! -f "$profile_dir/config.toml" && -f "$main_codex_config" ]]; then
cp "$main_codex_config" "$profile_dir/config.toml" cp "$main_codex_config" "$profile_dir/config.toml"
fi fi
container_name="zellij-$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]._-' '-')" if [[ -d "$repo_dir/scripts" ]]; then
uid="$(id -u)" export PATH="$repo_dir/scripts:$PATH"
gid="$(id -g)" fi
read -r -d '' inner_script <<EOF || true export CODEX_HOME="$profile_dir"
set -euo pipefail export ZELLIJ_AI_ROLE="$label"
cd /workspace export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export HOME=/codex-home export ZELLIJ_AI_SESSION="$session_name"
export CODEX_HOME=/codex-home export ZELLIJ_AI_BUS_DIR="$bus_dir"
echo "Codex pane: $label"
echo "workspace: /workspace" cd "$workspace"
echo "profile: /codex-home"
echo "image: $image" echo "Codex pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "profile: $profile_dir"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo echo
if [ ! -f /codex-home/auth.json ]; then
echo "No auth.json found for $label."
echo "Starting: codex login --device-auth"
echo "Use a different ChatGPT/OpenAI account here than in the other pane."
echo
codex login --device-auth
echo
fi
exec codex --no-alt-screen -C /workspace
EOF
docker_args=( if [[ ! -f "$CODEX_HOME/auth.json" ]]; then
run echo "No auth.json found for $label."
--rm echo "Starting: codex login --device-auth"
--init echo
-it codex login --device-auth
--name "$container_name" echo
--hostname "$container_name"
--user "${uid}:${gid}"
--workdir /workspace
--entrypoint bash
-e HOME=/codex-home
-e CODEX_HOME=/codex-home
-e USER="${USER:-$(id -un)}"
-e TERM="$term_value"
-e COLORTERM="$color_term_value"
-e LANG="${LANG:-C.UTF-8}"
-v "${workspace}:/workspace"
-v "${profile_dir}:/codex-home"
-v "${codex_binary}:/usr/local/bin/codex:ro"
-v "${rg_binary}:/usr/local/bin/rg:ro"
)
if [[ -f "$HOME/.gitconfig" ]]; then
docker_args+=(-v "$HOME/.gitconfig:/codex-home/.gitconfig:ro")
fi fi
docker_args+=("$image" -lc "$inner_script") exec codex --no-alt-screen -C "$workspace"
exec docker "${docker_args[@]}"
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env bash
zellij_ai_default_session() {
printf '%s\n' "constant"
}
zellij_ai_default_local_session() {
printf '%s\n' "constant-fleet"
}
zellij_ai_default_repo_dir() {
printf '%s\n' '$HOME/constant'
}
zellij_ai_default_codex_home() {
printf '%s\n' '$HOME/.codex-profiles/codex'
}
zellij_ai_default_codex_image() {
printf '%s\n' "codercom/code-server:latest"
}
zellij_ai_runtime_root() {
printf '%s\n' "${ZELLIJ_AI_RUNTIME_ROOT:-$HOME/.cache/constant/zellij}"
}
zellij_ai_session_state_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/$1"
}
zellij_ai_fleet_state_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/$1-fleet"
}
zellij_ai_bridge_cache_dir() {
printf '%s\n' "$(zellij_ai_runtime_root)/bridge-cache"
}
zellij_ai_default_machine_specs() {
cat <<'EOF'
command-center=local
builder-a=dev@builder-a
builder-b=dev@builder-b
edge-a=dev@edge-a
lab-a=dev@lab-a
EOF
}
zellij_ai_strip_ansi() {
sed -E 's/\x1b\[[0-9;]*m//g'
}
zellij_ai_session_exists() {
zellij list-sessions 2>/dev/null | zellij_ai_strip_ansi | awk '{print $1}' | grep -Fxq "$1"
}
zellij_ai_kdl_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
zellij_ai_expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
zellij_ai_expand_home_path_for_home() {
local value="$1"
local target_home="$2"
case "$value" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${target_home}${value#\$HOME}"
;;
"~")
printf '%s\n' "$target_home"
;;
"~/"*)
printf '%s\n' "$target_home/${value#~/}"
;;
*)
printf '%s\n' "$value"
;;
esac
}
zellij_ai_is_local_target() {
local value="$1"
local current_full current_short
current_full="$(hostname || true)"
current_short="$(hostname -s 2>/dev/null || printf '%s' "$current_full")"
case "$value" in
local|localhost|127.0.0.1|::1|"${current_full}"|"${current_short}")
return 0
;;
*)
return 1
;;
esac
}
zellij_ai_parse_machine_spec() {
local spec="$1"
local label target
if [[ "$spec" == *"="* ]]; then
label="${spec%%=*}"
target="${spec#*=}"
else
label="$spec"
target="$spec"
fi
if [[ -z "$label" || -z "$target" ]]; then
return 1
fi
printf '%s\t%s\n' "$label" "$target"
}
zellij_ai_resolve_machine_spec() {
local needle="$1"
local line label target
while IFS= read -r line; do
[[ -z "$line" ]] && continue
IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$line")" || continue
if [[ "$needle" == "$label" || "$needle" == "$target" || "$needle" == "$line" ]]; then
printf '%s\t%s\n' "$label" "$target"
return 0
fi
done < <(zellij_ai_default_machine_specs)
if IFS=$'\t' read -r label target <<<"$(zellij_ai_parse_machine_spec "$needle" 2>/dev/null)"; then
printf '%s\t%s\n' "$label" "$target"
return 0
fi
return 1
}
zellij_ai_require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Required command not found: $1" >&2
return 1
fi
}
zellij_ai_current_machine_name() {
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
else
hostname -s 2>/dev/null || hostname || printf 'local'
fi
}
zellij_ai_local_machine_label() {
printf '%s\n' "command-center"
}
zellij_ai_agent_path() {
local path_value="$PATH"
if [[ -d "$HOME/.local/bin" ]]; then
path_value="$HOME/.local/bin:$path_value"
fi
if [[ -d "$HOME/.npm-global/bin" ]]; then
path_value="$HOME/.npm-global/bin:$path_value"
fi
printf '%s\n' "$path_value"
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
label="${ZELLIJ_AI_COPILOT_LABEL:-copilot}"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
exit 1
fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
if ! command -v copilot >/dev/null 2>&1; then
echo "GitHub Copilot CLI not found on the host." >&2
echo "Install it with: npm install -g @github/copilot" >&2
exit 1
fi
mkdir -p "$bus_dir/messages"
export ZELLIJ_AI_ROLE="$label"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Copilot pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo "login: start copilot and use /login if needed"
echo
copilot
status=$?
if [[ $status -ne 0 ]]; then
echo
echo "Copilot exited with status $status."
echo "If macOS reports 'SecItemCopyMatching failed -50', open copilot manually once and re-run /login."
fi
exit "$status"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
tabs_file="${ZELLIJ_AI_FLEET_TABS_FILE:-}"
tabs_created_file="${ZELLIJ_AI_FLEET_CREATED_FILE:-}"
current_label="${ZELLIJ_AI_FLEET_CURRENT_LABEL:-}"
current_wrapper="${ZELLIJ_AI_FLEET_CURRENT_WRAPPER:-}"
if [[ -z "$current_wrapper" || ! -x "$current_wrapper" ]]; then
echo "Fleet bootstrap wrapper not found: $current_wrapper" >&2
exit 1
fi
if [[ -n "$current_label" ]]; then
zellij action rename-tab "$current_label"
fi
if [[ -n "$tabs_created_file" ]]; then
mkdir -p "$(dirname "$tabs_created_file")"
fi
if [[ -n "$tabs_file" && -f "$tabs_file" && -n "$tabs_created_file" && ! -f "$tabs_created_file" ]]; then
: >"$tabs_created_file"
while IFS=$'\t' read -r tab_label layout_file tab_cwd; do
if [[ -z "$tab_label" || -z "$layout_file" ]]; then
continue
fi
if [[ -n "$tab_cwd" ]]; then
zellij action new-tab -n "$tab_label" -l "$layout_file" --cwd "$tab_cwd"
else
zellij action new-tab -n "$tab_label" -l "$layout_file"
fi
done <"$tabs_file"
if [[ -n "$current_label" ]]; then
zellij action go-to-tab-name "$current_label"
fi
fi
exec "$current_wrapper"
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_source="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF
Usage: ${script_name} <check|install> [options]
Check or install the Constant fleet runtime on one or more machines.
Options:
--machine SPEC Machine definition or label to target
--repo-dir DIR Repository path on each machine
default: $(zellij_ai_default_repo_dir)
--codex-image IMAGE Deprecated, ignored
--yes Required for non-interactive install mode
-h, --help Show this help
EOF
}
warn_deprecated() {
printf '[fleet-install] %s\n' "$*" >&2
}
require_local_tools() {
local tool
for tool in bash ssh tar rsync; do
zellij_ai_require_command "$tool"
done
}
remote_home() {
ssh "$1" 'printf "%s\n" "$HOME"'
}
remote_repo_dir() {
local target="$1"
local repo_dir_input="$2"
local target_home
if zellij_ai_is_local_target "$target"; then
zellij_ai_expand_home_path "$repo_dir_input"
return 0
fi
target_home="$(remote_home "$target")"
zellij_ai_expand_home_path_for_home "$repo_dir_input" "$target_home"
}
sync_repo_to_remote() {
local target="$1"
local repo_dir_input="$2"
local target_repo_dir local_real target_real
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir_input")"
if zellij_ai_is_local_target "$target"; then
mkdir -p "$target_repo_dir"
local_real="$repo_source"
target_real="$(cd "$target_repo_dir" && pwd -P)"
if [[ "$local_real" == "$target_real" ]]; then
return 0
fi
rsync -a --delete --exclude '.git/' "$repo_source/" "$target_repo_dir/"
return 0
fi
ssh "$target" "mkdir -p $(printf '%q' "$target_repo_dir")"
if ssh "$target" 'command -v rsync >/dev/null 2>&1'; then
rsync -a --delete --exclude '.git/' "$repo_source/" "${target}:${target_repo_dir}/"
else
(
cd "$repo_source"
tar --exclude '.git' -cf - .
) | ssh "$target" "tar -xf - -C $(printf '%q' "$target_repo_dir")"
fi
}
mode="${1:-}"
if [[ -z "$mode" ]]; then
usage >&2
exit 2
fi
case "$mode" in
-h|--help|help)
usage
exit 0
;;
esac
shift || true
machines=()
repo_dir="$(zellij_ai_default_repo_dir)"
yes=false
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored."
shift 2
;;
--yes)
yes=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
case "$mode" in
check|install)
;;
*)
echo "Unknown mode: $mode" >&2
usage >&2
exit 2
;;
esac
require_local_tools
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(zellij_ai_default_machine_specs)
fi
overall_status=0
for machine_spec in "${machines[@]}"; do
if ! resolved="$(zellij_ai_resolve_machine_spec "$machine_spec")"; then
echo "Unknown machine: $machine_spec" >&2
overall_status=1
continue
fi
IFS=$'\t' read -r label target <<<"$resolved"
printf '=== %s (%s) ===\n' "$label" "$target"
target_repo_dir="$(remote_repo_dir "$target" "$repo_dir")"
if [[ "$mode" == "install" ]]; then
sync_repo_to_remote "$target" "$repo_dir" || overall_status=1
fi
install_args=("$mode" --repo-dir "$repo_dir")
if $yes; then
install_args+=(--yes)
fi
if zellij_ai_is_local_target "$target"; then
ZELLIJ_AI_MACHINE_NAME="$label" "$script_dir/zellij-ai-machine-install.sh" "${install_args[@]}" || overall_status=1
continue
fi
if [[ "$mode" == "install" ]]; then
remote_script="${target_repo_dir}/scripts/zellij-ai-machine-install.sh"
ssh -tt "$target" env ZELLIJ_AI_MACHINE_NAME="$label" "$remote_script" "${install_args[@]}" || overall_status=1
continue
fi
if $yes; then
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
else
ssh "$target" env ZELLIJ_AI_MACHINE_NAME="$label" bash -s -- "${install_args[@]}" <"$script_dir/zellij-ai-machine-install.sh" || overall_status=1
fi
done
exit "$overall_status"
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF
Usage: ${script_name} [options]
Open a local Zellij cockpit with one tab per machine.
Each tab attaches to the machine's own local Constant machine session.
Options:
--machine SPEC Machine definition. Repeat once per machine.
format: [TAB_NAME=]TARGET
default fleet: command-center, builder-a, builder-b, edge-a, lab-a
--session NAME Remote/local machine session name
default: $(zellij_ai_default_session)
--local-session NAME Cockpit session name on this machine
default: $(zellij_ai_default_local_session)
--repo-dir DIR Repository path on each machine
default: $(zellij_ai_default_repo_dir)
--workspace DIR Workspace path passed to each machine session
required for the fixed 5-machine fleet
--codex-image IMAGE Deprecated, ignored
--claude-config DIR Optional Claude config override passed to each machine
--remote-recreate Recreate each machine session before attaching
--recreate Recreate the local cockpit session before launching
-h, --help Show this help
EOF
}
machines=()
remote_session="$(zellij_ai_default_session)"
local_session="$(zellij_ai_default_local_session)"
remote_repo_dir="$(zellij_ai_default_repo_dir)"
workspace=""
codex_image=""
claude_config_dir=""
recreate=false
remote_recreate=false
while [[ $# -gt 0 ]]; do
case "$1" in
--machine)
machines+=("$2")
shift 2
;;
--session)
remote_session="$2"
shift 2
;;
--local-session)
local_session="$2"
shift 2
;;
--repo-dir)
remote_repo_dir="$2"
shift 2
;;
--workspace)
workspace="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
codex_image="$2"
shift 2
;;
--claude-config)
claude_config_dir="$2"
shift 2
;;
--remote-recreate)
remote_recreate=true
shift
;;
--recreate)
recreate=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ ${#machines[@]} -eq 0 ]]; then
mapfile -t machines < <(zellij_ai_default_machine_specs)
fi
if [[ -z "$workspace" ]]; then
echo "--workspace is required for fleet mode." >&2
exit 2
fi
workspace="$(cd "$workspace" && pwd -P)"
for required in \
zellij \
ssh \
"$script_dir/zellij-ai-common.sh" \
"$script_dir/zellij-ai-fleet-bootstrap.sh" \
"$script_dir/zellij-ai-remote-tab.sh"
do
if [[ "$required" == */* ]]; then
if [[ ! -x "$required" ]]; then
echo "Helper script not executable: $required" >&2
exit 1
fi
else
zellij_ai_require_command "$required"
fi
done
state_dir="$(zellij_ai_fleet_state_dir "$local_session")"
tabs_file="$state_dir/tabs.tsv"
tabs_created_file="$state_dir/tabs-created"
first_layout_file="$state_dir/first-tab.kdl"
mkdir -p "$state_dir"
: >"$tabs_file"
rm -f "$tabs_created_file"
bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-fleet-bootstrap.sh")"
first_label=""
first_wrapper=""
seen_labels='|'
machine_index=0
for machine_spec in "${machines[@]}"; do
if ! parsed_spec="$(zellij_ai_parse_machine_spec "$machine_spec")"; then
echo "Invalid machine spec: $machine_spec" >&2
exit 2
fi
IFS=$'\t' read -r machine_label machine_target <<<"$parsed_spec"
if [[ "$seen_labels" == *"|${machine_label}|"* ]]; then
echo "Duplicate machine label: $machine_label" >&2
exit 2
fi
seen_labels+="${machine_label}|"
wrapper_path="$state_dir/machine-${machine_index}.sh"
layout_path="$state_dir/machine-${machine_index}.kdl"
wrapper_cmd=(
"$script_dir/zellij-ai-remote-tab.sh"
--label "$machine_label"
--target "$machine_target"
--session "$remote_session"
--repo-dir "$remote_repo_dir"
)
if [[ -n "$workspace" ]]; then
wrapper_cmd+=(--workspace "$workspace")
fi
if [[ -n "$codex_image" ]]; then
wrapper_cmd+=(--codex-image "$codex_image")
fi
if [[ -n "$claude_config_dir" ]]; then
wrapper_cmd+=(--claude-config "$claude_config_dir")
fi
if $remote_recreate; then
wrapper_cmd+=(--remote-recreate)
fi
printf -v wrapper_exec '%q ' "${wrapper_cmd[@]}"
wrapper_exec="${wrapper_exec% }"
cat >"$wrapper_path" <<EOF
#!/usr/bin/env bash
set -euo pipefail
exec $wrapper_exec
EOF
chmod +x "$wrapper_path"
wrapper_path_escaped="$(zellij_ai_kdl_escape "$wrapper_path")"
cat >"$layout_path" <<EOF
layout {
pane size=1 borderless=true {
plugin location="tab-bar"
}
pane command="$wrapper_path_escaped"
pane size=2 borderless=true {
plugin location="status-bar"
}
}
EOF
if [[ $machine_index -eq 0 ]]; then
first_label="$machine_label"
first_wrapper="$wrapper_path"
else
printf '%s\t%s\t%s\n' "$machine_label" "$layout_path" "$repo_dir" >>"$tabs_file"
fi
machine_index=$((machine_index + 1))
done
if [[ -z "$first_label" || -z "$first_wrapper" ]]; then
echo "No machine tabs were generated" >&2
exit 1
fi
cat >"$first_layout_file" <<EOF
layout {
pane size=1 borderless=true {
plugin location="tab-bar"
}
pane command="$bootstrap_path"
pane size=2 borderless=true {
plugin location="status-bar"
}
}
EOF
export ZELLIJ_AI_FLEET_TABS_FILE="$tabs_file"
export ZELLIJ_AI_FLEET_CREATED_FILE="$tabs_created_file"
export ZELLIJ_AI_FLEET_CURRENT_LABEL="$first_label"
export ZELLIJ_AI_FLEET_CURRENT_WRAPPER="$first_wrapper"
if $recreate && zellij_ai_session_exists "$local_session"; then
zellij kill-session "$local_session" >/dev/null 2>&1 || true
fi
if zellij_ai_session_exists "$local_session"; then
exec zellij attach "$local_session"
fi
exec zellij --new-session-with-layout "$first_layout_file" --session "$local_session"
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
if [[ -f "$script_dir/zellij-ai-common.sh" ]]; then
source "$script_dir/zellij-ai-common.sh"
else
zellij_ai_default_repo_dir() {
printf '%s\n' '$HOME/constant'
}
zellij_ai_default_codex_home() {
printf '%s\n' '$HOME/.codex-profiles/codex'
}
zellij_ai_expand_home_path() {
case "$1" in
'$HOME'|'$HOME/'*)
printf '%s\n' "${HOME}${1#\$HOME}"
;;
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#~/}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
zellij_ai_current_machine_name() {
if [[ -n "${ZELLIJ_AI_MACHINE_NAME:-}" ]]; then
printf '%s\n' "${ZELLIJ_AI_MACHINE_NAME}"
else
hostname -s 2>/dev/null || hostname || printf 'local'
fi
}
zellij_ai_agent_path() {
local path_value="$PATH"
if [[ -d "$HOME/.local/bin" ]]; then
path_value="$HOME/.local/bin:$path_value"
fi
if [[ -d "$HOME/.npm-global/bin" ]]; then
path_value="$HOME/.npm-global/bin:$path_value"
fi
printf '%s\n' "$path_value"
}
fi
usage() {
cat <<EOF
Usage: $(basename "$0") <check|install> [options]
Options:
--repo-dir DIR Repository path on this machine
default: $(zellij_ai_default_repo_dir)
--codex-image IMAGE Deprecated, ignored
--yes Required for non-interactive install mode
-h, --help Show this help
EOF
}
log() {
printf '[install] %s\n' "$*"
}
fail() {
printf '[install] %s\n' "$*" >&2
exit 1
}
confirm_install() {
if $yes; then
return 0
fi
if [[ ! -t 0 ]]; then
fail "Install mode requires --yes when stdin is not interactive."
fi
printf 'Proceed with installation changes on this machine? [y/N] '
read -r answer
[[ "$answer" == "y" || "$answer" == "Y" ]]
}
need_sudo() {
[[ "${EUID:-$(id -u)}" -ne 0 ]]
}
run_root() {
if ! need_sudo; then
"$@"
return 0
fi
if command -v sudo >/dev/null 2>&1; then
sudo "$@"
return 0
fi
fail "sudo is required to run: $*"
}
ensure_command() {
command -v "$1" >/dev/null 2>&1
}
warn_deprecated() {
printf '[install] %s\n' "$*" >&2
}
refresh_path() {
export PATH="$(zellij_ai_agent_path)"
if [[ -x /opt/homebrew/bin/brew ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -x /usr/local/bin/brew ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
}
ensure_homebrew() {
if command -v brew >/dev/null 2>&1; then
return 0
fi
ensure_command curl || fail "curl is required to install Homebrew."
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
refresh_path
ensure_command brew || fail "Homebrew installation did not make brew available."
}
ensure_uv() {
if command -v uv >/dev/null 2>&1; then
return 0
fi
case "$os_name" in
Darwin)
ensure_homebrew
brew install uv
;;
Linux)
ensure_command curl || fail "curl is required to install uv."
curl -LsSf https://astral.sh/uv/install.sh | sh
;;
*)
fail "Unsupported OS for uv installation: $os_name"
;;
esac
refresh_path
ensure_command uv || fail "uv installation did not make uv available."
}
ensure_npm_global() {
local package_name="$1"
local binary_name="$2"
if command -v "$binary_name" >/dev/null 2>&1; then
return 0
fi
mkdir -p "$HOME/.npm-global"
export PATH="$(zellij_ai_agent_path)"
if npm install -g --prefix "$HOME/.npm-global" "$package_name"; then
return 0
fi
fail "Failed to install ${package_name} with npm."
}
ensure_uv_tool() {
local package_name="$1"
local binary_name="$2"
if command -v "$binary_name" >/dev/null 2>&1; then
return 0
fi
ensure_uv
uv tool install "$package_name"
refresh_path
ensure_command "$binary_name" || fail "Failed to install ${package_name} with uv."
}
os_name="$(uname -s)"
linux_id=""
repo_dir="$(zellij_ai_expand_home_path "$(zellij_ai_default_repo_dir)")"
codex_home="$(zellij_ai_expand_home_path "$(zellij_ai_default_codex_home)")"
yes=false
refresh_path
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
source /etc/os-release
linux_id="${ID:-}"
fi
if [[ $# -lt 1 ]]; then
usage >&2
exit 2
fi
mode="$1"
case "$mode" in
-h|--help|help)
usage
exit 0
;;
esac
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-dir)
repo_dir="$(zellij_ai_expand_home_path "$2")"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored."
shift 2
;;
--yes)
yes=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
print_status() {
local key="$1"
local value="$2"
printf '%s=%s\n' "$key" "$value"
}
run_check() {
local ok=true
local repo_state="missing"
local codex_home_state="missing"
local triple_help_state="missing"
local binary
refresh_path
print_status machine "$(zellij_ai_current_machine_name)"
print_status os "$os_name"
[[ -n "$linux_id" ]] && print_status linux_id "$linux_id"
print_status repo_dir "$repo_dir"
print_status codex_home "$codex_home"
for binary in bash git zellij node npm uv claude codex copilot vibe; do
if command -v "$binary" >/dev/null 2>&1; then
print_status "$binary" "$(command -v "$binary")"
else
print_status "$binary" "missing"
ok=false
fi
done
if [[ -x "$repo_dir/scripts/constant-machine.sh" || -x "$repo_dir/scripts/zellij-ai-triple.sh" ]]; then
repo_state="present"
else
ok=false
fi
print_status repo "$repo_state"
if [[ -d "$codex_home" ]]; then
codex_home_state="present"
else
ok=false
fi
print_status codex_profile "$codex_home_state"
if [[ -x "$repo_dir/scripts/constant-machine.sh" ]] && "$repo_dir/scripts/constant-machine.sh" --help >/dev/null 2>&1; then
triple_help_state="ok"
elif [[ -x "$repo_dir/scripts/zellij-ai-triple.sh" ]] && "$repo_dir/scripts/zellij-ai-triple.sh" --help >/dev/null 2>&1; then
triple_help_state="ok"
else
ok=false
fi
print_status triple_help "$triple_help_state"
$ok
}
install_darwin() {
ensure_homebrew
brew install git zellij node uv
}
install_linux_ubuntu() {
run_root apt-get update
run_root apt-get install -y git curl
if ! command -v node >/dev/null 2>&1; then
run_root apt-get install -y nodejs
fi
if ! command -v npm >/dev/null 2>&1; then
run_root apt-get install -y npm
fi
if ! command -v zellij >/dev/null 2>&1; then
if command -v snap >/dev/null 2>&1; then
run_root snap install zellij --classic
else
run_root apt-get install -y zellij
fi
fi
}
install_linux_photon() {
local packages=()
if ! command -v git >/dev/null 2>&1; then
packages+=(git)
fi
if ! command -v curl >/dev/null 2>&1; then
packages+=(curl)
fi
if ! command -v node >/dev/null 2>&1; then
packages+=(nodejs)
fi
if ! command -v npm >/dev/null 2>&1; then
packages+=(npm)
fi
if [[ ${#packages[@]} -gt 0 ]]; then
run_root tdnf install -y "${packages[@]}"
fi
if ! command -v zellij >/dev/null 2>&1; then
fail "zellij is missing on Photon OS and no native install path is scripted here."
fi
}
run_install() {
confirm_install || fail "Installation cancelled."
mkdir -p "$repo_dir"
case "$os_name" in
Darwin)
install_darwin
;;
Linux)
case "$linux_id" in
ubuntu|debian)
install_linux_ubuntu
;;
photon)
install_linux_photon
;;
*)
fail "Unsupported Linux distribution: ${linux_id:-unknown}"
;;
esac
;;
*)
fail "Unsupported OS: $os_name"
;;
esac
refresh_path
mkdir -p "$codex_home"
ensure_npm_global "@anthropic-ai/claude-code" claude
ensure_npm_global "@openai/codex" codex
ensure_npm_global "@github/copilot" copilot
ensure_uv_tool "mistral-vibe" vibe
run_check
}
case "$mode" in
check)
run_check
;;
install)
run_install
;;
*)
echo "Unknown mode: $mode" >&2
usage >&2
exit 2
;;
esac
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
warn_deprecated() {
printf 'Warning: %s\n' "$*" >&2
}
usage() {
cat <<'EOF'
Usage: zellij-ai-remote-tab.sh [options]
Open or attach the local AI Zellij session for one machine.
Options:
--label NAME Display label for logs and tab naming
--target TARGET SSH target, or "local" for the current machine
--session NAME Remote/local Zellij session name
default: constant
--repo-dir DIR Repository path on the target machine
default: $HOME/constant
--workspace DIR Workspace path passed to constant-machine.sh
--codex-image IMAGE Deprecated, ignored
--claude-config DIR Optional Claude config override
--remote-recreate Recreate the target machine session before attaching
-h, --help Show this help
EOF
}
label=""
target=""
session="$(zellij_ai_default_session)"
repo_dir="$(zellij_ai_default_repo_dir)"
workspace=""
codex_image=""
claude_config_dir=""
remote_recreate=false
while [[ $# -gt 0 ]]; do
case "$1" in
--label)
label="$2"
shift 2
;;
--target)
target="$2"
shift 2
;;
--session)
session="$2"
shift 2
;;
--repo-dir)
repo_dir="$2"
shift 2
;;
--workspace)
workspace="$2"
shift 2
;;
--codex-image)
warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
codex_image="$2"
shift 2
;;
--claude-config)
claude_config_dir="$2"
shift 2
;;
--remote-recreate)
remote_recreate=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$target" ]]; then
echo "--target is required" >&2
usage >&2
exit 2
fi
if [[ -z "$label" ]]; then
label="$target"
fi
if zellij_ai_is_local_target "$target"; then
local_repo_dir="$(zellij_ai_expand_home_path "$repo_dir")"
local_workspace="$(zellij_ai_expand_home_path "$workspace")"
local_claude_config_dir="$(zellij_ai_expand_home_path "$claude_config_dir")"
local_launcher="$local_repo_dir/scripts/constant-machine.sh"
if [[ ! -x "$local_launcher" ]]; then
local_launcher="$local_repo_dir/scripts/zellij-ai-triple.sh"
fi
if [[ ! -x "$local_launcher" ]]; then
echo "Local launcher not found for $label: $local_launcher" >&2
exit 1
fi
cmd=("$local_launcher" --session "$session")
if [[ -n "$workspace" ]]; then
cmd+=(--workspace "$local_workspace")
fi
if [[ -n "$codex_image" ]]; then
cmd+=(--codex-image "$codex_image")
fi
if [[ -n "$claude_config_dir" ]]; then
cmd+=(--claude-config "$local_claude_config_dir")
fi
if $remote_recreate; then
cmd+=(--recreate)
fi
exec env ZELLIJ_AI_MACHINE_NAME="$label" "${cmd[@]}"
fi
printf -v session_q '%q' "$session"
printf -v repo_dir_q '%q' "$repo_dir"
printf -v workspace_q '%q' "$workspace"
printf -v codex_image_q '%q' "$codex_image"
printf -v claude_config_dir_q '%q' "$claude_config_dir"
printf -v label_q '%q' "$label"
remote_recreate_literal=false
if $remote_recreate; then
remote_recreate_literal=true
fi
read -r -d '' remote_shell <<EOF || true
set -euo pipefail
expand_home_path() {
case "\$1" in
'\$HOME'|'\$HOME/'*)
printf '%s\n' "\${HOME}\${1#\\\$HOME}"
;;
"~")
printf '%s\n' "\$HOME"
;;
"~/"*)
printf '%s\n' "\$HOME/\${1#~/}"
;;
*)
printf '%s\n' "\$1"
;;
esac
}
session=$session_q
repo_dir_input=$repo_dir_q
workspace_input=$workspace_q
codex_image_input=$codex_image_q
claude_config_dir_input=$claude_config_dir_q
remote_recreate=$remote_recreate_literal
repo_dir="\$(expand_home_path "\$repo_dir_input")"
workspace="\$(expand_home_path "\$workspace_input")"
claude_config_dir="\$(expand_home_path "\$claude_config_dir_input")"
launcher="\$repo_dir/scripts/constant-machine.sh"
if [[ ! -x "\$launcher" ]]; then
launcher="\$repo_dir/scripts/zellij-ai-triple.sh"
fi
if [[ ! -x "\$launcher" ]]; then
echo "Remote launcher not found: \$launcher" >&2
exit 1
fi
cmd=("\$launcher" --session "\$session")
if [[ -n "\$workspace_input" ]]; then
cmd+=(--workspace "\$workspace")
fi
if [[ -n "\$codex_image_input" ]]; then
cmd+=(--codex-image "\$codex_image_input")
fi
if [[ -n "\$claude_config_dir_input" ]]; then
cmd+=(--claude-config "\$claude_config_dir")
fi
if \$remote_recreate; then
cmd+=(--recreate)
fi
export ZELLIJ_AI_MACHINE_NAME=$label_q
export ZELLIJ_AI_FORCE_OSC52=true
exec "\${cmd[@]}"
EOF
exec ssh -tt "$target" "bash -lc $(printf '%q' "$remote_shell")"
+110 -60
View File
@@ -1,61 +1,64 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
repo_dir="$(cd "$script_dir/.." && pwd -P)" repo_dir="$(cd "$script_dir/.." && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
usage() { usage() {
local script_name="${CONSTANT_SCRIPT_NAME:-$(basename "$0")}"
cat <<EOF cat <<EOF
Usage: $(basename "$0") [options] Usage: ${script_name} [options]
Start a 3-pane Zellij session: Start a 4-pane Constant machine session:
- left pane: Claude on the host - left pane: Claude on the host
- top-right pane: Codex in Docker with profile 1 - top-right pane: Codex on the host
- bottom-right pane: Codex in Docker with profile 2 - middle-right pane: Copilot CLI on the host
- bottom-right pane: Mistral Vibe on the host
Options: Options:
--workspace DIR Workspace mounted in both Codex containers and used by Claude --workspace DIR Workspace used by all panes
default: current directory default: current directory
--session NAME Zellij session name --session NAME Session name
default: ai-triple default: $(zellij_ai_default_session)
--codex-image IMAGE Docker image used for Codex panes --codex-home DIR Persistent CODEX_HOME for the Codex pane
default: codercom/code-server:latest default: $(zellij_ai_default_codex_home)
--codex1-home DIR Persistent CODEX_HOME for the first Codex pane --codex-label LABEL Display label for the Codex pane
default: \$HOME/.codex-profiles/codex-1 default: codex
--codex2-home DIR Persistent CODEX_HOME for the second Codex pane --copilot-label LABEL Display label for the Copilot pane
default: \$HOME/.codex-profiles/codex-2 default: copilot
--codex1-label LABEL Display label for the first Codex pane --vibe-label LABEL Display label for the Vibe pane
default: codex-1 default: vibe
--codex2-label LABEL Display label for the second Codex pane
default: codex-2
--claude-config DIR Optional CLAUDE_CONFIG_DIR override for the Claude pane --claude-config DIR Optional CLAUDE_CONFIG_DIR override for the Claude pane
--zellij-config-dir DIR --zellij-config-dir DIR
Optional isolated Zellij config dir to use for session creation Optional isolated Zellij config dir for session creation
default: a fresh temporary config dir per new session default: a fresh temporary config dir per new session
--recreate Kill the existing session before recreating it --codex-image IMAGE Deprecated, ignored
--codex1-home DIR Deprecated alias for --codex-home
--codex2-home DIR Deprecated, ignored
--codex1-label LABEL Deprecated alias for --codex-label
--codex2-label LABEL Deprecated, ignored
--recreate Recreate the session and purge stale Zellij state first
-h, --help Show this help -h, --help Show this help
EOF EOF
} }
strip_ansi() { warn_deprecated() {
sed -E 's/\x1b\[[0-9;]*m//g' printf 'Warning: %s\n' "$*" >&2
}
session_exists() {
zellij list-sessions 2>/dev/null | strip_ansi | awk '{print $1}' | grep -Fxq "$1"
}
kdl_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
} }
workspace="$PWD" workspace="$PWD"
session="ai-triple" session="$(zellij_ai_default_session)"
codex_image="codercom/code-server:latest" codex_home="$(zellij_ai_default_codex_home)"
codex1_home="$HOME/.codex-profiles/codex-1" codex_label="codex"
codex2_home="$HOME/.codex-profiles/codex-2" copilot_label="copilot"
codex1_label="codex-1" vibe_label="vibe"
codex2_label="codex-2"
claude_config_dir="" claude_config_dir=""
zellij_config_dir="" zellij_config_dir=""
recreate=false recreate=false
@@ -70,24 +73,42 @@ while [[ $# -gt 0 ]]; do
session="$2" session="$2"
shift 2 shift 2
;; ;;
--codex-home)
codex_home="$2"
shift 2
;;
--codex-label)
codex_label="$2"
shift 2
;;
--copilot-label)
copilot_label="$2"
shift 2
;;
--vibe-label)
vibe_label="$2"
shift 2
;;
--codex-image) --codex-image)
codex_image="$2" warn_deprecated "--codex-image is deprecated and ignored; Codex now runs on the host."
shift 2 shift 2
;; ;;
--codex1-home) --codex1-home)
codex1_home="$2" warn_deprecated "--codex1-home is deprecated; use --codex-home."
codex_home="$2"
shift 2 shift 2
;; ;;
--codex2-home) --codex2-home)
codex2_home="$2" warn_deprecated "--codex2-home is deprecated and ignored."
shift 2 shift 2
;; ;;
--codex1-label) --codex1-label)
codex1_label="$2" warn_deprecated "--codex1-label is deprecated; use --codex-label."
codex_label="$2"
shift 2 shift 2
;; ;;
--codex2-label) --codex2-label)
codex2_label="$2" warn_deprecated "--codex2-label is deprecated and ignored."
shift 2 shift 2
;; ;;
--claude-config) --claude-config)
@@ -115,11 +136,14 @@ while [[ $# -gt 0 ]]; do
done done
workspace="$(cd "$workspace" && pwd -P)" workspace="$(cd "$workspace" && pwd -P)"
codex1_home="$(mkdir -p "$codex1_home" && cd "$codex1_home" && pwd -P)" codex_home="$(mkdir -p "$(zellij_ai_expand_home_path "$codex_home")" && cd "$(zellij_ai_expand_home_path "$codex_home")" && pwd -P)"
codex2_home="$(mkdir -p "$codex2_home" && cd "$codex2_home" && pwd -P)"
bootstrap_file="$HOME/.cache/zellij-ai/${session}.bootstrapped" machine_name="$(zellij_ai_current_machine_name)"
layout_file="/tmp/zellij-ai-layout-${session}.kdl" state_dir="$(zellij_ai_session_state_dir "$session")"
bus_dir="$state_dir/bus"
bootstrap_file="$state_dir/bootstrapped"
layout_file="$state_dir/layout.kdl"
clipboard_config_file="$state_dir/zellij.config.kdl"
if [[ ! -d "$workspace" ]]; then if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2 echo "Workspace not found: $workspace" >&2
@@ -129,7 +153,10 @@ fi
for required in \ for required in \
"$script_dir/zellij-ai-claude-pane.sh" \ "$script_dir/zellij-ai-claude-pane.sh" \
"$script_dir/zellij-ai-codex-pane.sh" \ "$script_dir/zellij-ai-codex-pane.sh" \
"$script_dir/zellij-ai-bootstrap.sh" "$script_dir/zellij-ai-copilot-pane.sh" \
"$script_dir/zellij-ai-vibe-pane.sh" \
"$script_dir/zellij-ai-bootstrap.sh" \
"$script_dir/zellij-ai-common.sh"
do do
if [[ ! -x "$required" ]]; then if [[ ! -x "$required" ]]; then
echo "Helper script not executable: $required" >&2 echo "Helper script not executable: $required" >&2
@@ -137,36 +164,58 @@ do
fi fi
done done
mkdir -p "$bus_dir/messages"
if [[ "${ZELLIJ_AI_FORCE_OSC52:-false}" == "true" || "${ZELLIJ_AI_FORCE_OSC52:-0}" == "1" ]]; then
cat >"$clipboard_config_file" <<'EOF'
copy_clipboard "system"
EOF
elif [[ "$(uname -s)" == "Darwin" && -x /usr/bin/pbcopy ]]; then
cat >"$clipboard_config_file" <<'EOF'
copy_command "pbcopy"
copy_clipboard "system"
EOF
else
cat >"$clipboard_config_file" <<'EOF'
copy_clipboard "system"
EOF
fi
export ZELLIJ_AI_WORKSPACE="$workspace" export ZELLIJ_AI_WORKSPACE="$workspace"
export ZELLIJ_AI_CODEX_IMAGE="$codex_image" export ZELLIJ_AI_CODEX_HOME="$codex_home"
export ZELLIJ_AI_CODEX1_HOME="$codex1_home" export ZELLIJ_AI_CODEX_LABEL="$codex_label"
export ZELLIJ_AI_CODEX2_HOME="$codex2_home" export ZELLIJ_AI_COPILOT_LABEL="$copilot_label"
export ZELLIJ_AI_CODEX1_LABEL="$codex1_label" export ZELLIJ_AI_VIBE_LABEL="$vibe_label"
export ZELLIJ_AI_CODEX2_LABEL="$codex2_label"
export ZELLIJ_AI_MAIN_CODEX_CONFIG="$HOME/.codex/config.toml" export ZELLIJ_AI_MAIN_CODEX_CONFIG="$HOME/.codex/config.toml"
export ZELLIJ_AI_BOOTSTRAP_FILE="$bootstrap_file" export ZELLIJ_AI_BOOTSTRAP_FILE="$bootstrap_file"
export ZELLIJ_AI_REPO_DIR="$repo_dir"
export ZELLIJ_AI_SESSION="$session"
export ZELLIJ_AI_STATE_DIR="$state_dir"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
if [[ -n "$claude_config_dir" ]]; then if [[ -n "$claude_config_dir" ]]; then
export ZELLIJ_AI_CLAUDE_CONFIG_DIR="$claude_config_dir" export ZELLIJ_AI_CLAUDE_CONFIG_DIR="$claude_config_dir"
fi fi
if $recreate && session_exists "$session"; then zellij_attach_cmd=(zellij --config "$clipboard_config_file")
if $recreate && zellij_ai_session_exists "$session"; then
zellij delete-session -f "$session" >/dev/null 2>&1 || true zellij delete-session -f "$session" >/dev/null 2>&1 || true
fi fi
if session_exists "$session"; then if zellij_ai_session_exists "$session"; then
exec zellij attach "$session" exec "${zellij_attach_cmd[@]}" attach "$session"
fi fi
zellij delete-session "$session" >/dev/null 2>&1 || true zellij delete-session "$session" >/dev/null 2>&1 || true
mkdir -p "$state_dir"
mkdir -p "$(dirname "$bootstrap_file")"
rm -f "$bootstrap_file" rm -f "$bootstrap_file"
bootstrap_path="$(kdl_escape "$script_dir/zellij-ai-bootstrap.sh")" bootstrap_path="$(zellij_ai_kdl_escape "$script_dir/zellij-ai-bootstrap.sh")"
cat >"$layout_file" <<EOF cat >"$layout_file" <<EOF
layout { layout {
tab name="AI Triple" { tab name="Constant" {
pane size=1 borderless=true { pane size=1 borderless=true {
plugin location="tab-bar" plugin location="tab-bar"
} }
@@ -179,9 +228,10 @@ layout {
EOF EOF
if [[ -z "$zellij_config_dir" ]]; then if [[ -z "$zellij_config_dir" ]]; then
zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/zellij-ai-triple-${session}.XXXXXX")" zellij_config_dir="$(mktemp -d "${TMPDIR:-/tmp}/constant-zellij-${session}.XXXXXX")"
else else
zellij_config_dir="$(zellij_ai_expand_home_path "$zellij_config_dir")"
mkdir -p "$zellij_config_dir" mkdir -p "$zellij_config_dir"
fi fi
exec zellij --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session" exec zellij --config "$clipboard_config_file" --config-dir "$zellij_config_dir" --new-session-with-layout "$layout_file" --session "$session"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
script_source="${BASH_SOURCE[0]:-$0}"
while [[ -L "$script_source" ]]; do
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
script_source="$(readlink "$script_source")"
[[ "$script_source" != /* ]] && script_source="$script_dir/$script_source"
done
script_dir="$(cd "$(dirname "$script_source")" && pwd -P)"
source "$script_dir/zellij-ai-common.sh"
workspace="${ZELLIJ_AI_WORKSPACE:-$PWD}"
machine_name="${ZELLIJ_AI_MACHINE_NAME:-unknown}"
session_name="${ZELLIJ_AI_SESSION:-$(zellij_ai_default_session)}"
repo_dir="${ZELLIJ_AI_REPO_DIR:-$(cd "$script_dir/.." && pwd -P)}"
bus_dir="${ZELLIJ_AI_BUS_DIR:-$(zellij_ai_session_state_dir "$session_name")/bus}"
label="${ZELLIJ_AI_VIBE_LABEL:-vibe}"
if [[ ! -d "$workspace" ]]; then
echo "Workspace not found: $workspace" >&2
exit 1
fi
export PATH="$(zellij_ai_agent_path)"
if [[ -n "$repo_dir" && -d "$repo_dir/scripts" ]]; then
export PATH="$repo_dir/scripts:$PATH"
fi
if ! command -v vibe >/dev/null 2>&1; then
echo "Mistral Vibe CLI not found on the host." >&2
echo "Install it with: uv tool install mistral-vibe" >&2
exit 1
fi
mkdir -p "$bus_dir/messages"
export ZELLIJ_AI_ROLE="$label"
export ZELLIJ_AI_MACHINE_NAME="$machine_name"
export ZELLIJ_AI_SESSION="$session_name"
export ZELLIJ_AI_BUS_DIR="$bus_dir"
cd "$workspace"
echo "Vibe pane"
echo "machine: $machine_name"
echo "workspace: $workspace"
echo "bus: $bus_dir"
if [[ -n "$repo_dir" ]]; then
echo "helper: $repo_dir/scripts/ai-msg.sh"
fi
echo
exec vibe