Migrate Constant to a Rust-only runtime

This commit is contained in:
L'électron rare
2026-04-06 09:57:34 +02:00
parent 31e1204e8e
commit f5e0850252
31 changed files with 3127 additions and 6904 deletions
+11 -4
View File
@@ -17,14 +17,14 @@ Contributions should keep that bias visible in the code and in the product shape
- make the cockpit more reliable
- improve mission routing or verification
- improve durable memory and repo context
- sharpen the TUI and the `hexapus` buddy rail
- sharpen the native Rust TUI and operator ergonomics
- 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
- prefer robust shell and Rust 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
@@ -44,9 +44,16 @@ Run at least the checks relevant to your change.
Examples:
```bash
python3 -m py_compile constant/*.py
cargo test --no-run
bash -n scripts/*.sh
./scripts/Constant --help
```
For doc-only changes, also sanity-check that the public surface still matches the code:
```bash
rg -n "fleet discover|fleet configure|fleet deploy|CONSTANT_USE_PYTHON|python3" README.md CONTRIBUTING.md scripts src tests
bash -n scripts/*.sh
./scripts/Constant doctor
```
If your change affects routing, fleet behavior, or memory shape, include a short explanation of:
+1
View File
@@ -15,3 +15,4 @@ ratatui = "0.29"
[[bin]]
name = "constant"
path = "src/main.rs"
test = false
+26 -31
View File
@@ -40,35 +40,33 @@ Current build status in this repo:
- a 4-pane host-local `tmux` session per machine
- a fleet cockpit with one window per machine plus a central `Constant` window
- `Constant`, the orchestration CLI on top of the cockpit
- a terminal TUI with a central `hexapus` buddy rail
- a native Rust chat-first TUI with composer, threads, and cockpit panes
- live cockpit controls for focus, capture, send, and pane restart
- 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
- heuristic local routing and buddy/planner responses without a Python runtime
- workspace-first durable memory with lexical + local vector search
The canonical script surface is `constant-*`.
Legacy `zellij-ai-*` aliases remain only as compatibility shims.
Migration note:
Runtime notes:
- the entrypoint is now moving to Rust
- `doctor`, `agents`, `skills`, `tui`, `cockpit`, `mission create`, `mission plan`, `mission run`, `mission status`, `mission tail`, `mission verify`, `mission retry`, `mission summarize`, and `delegate` now run from the Rust binary
- Rust now prefers `fleet.toml`, `models.toml`, and `memory.toml`, while keeping JSON mirrors for compatibility during migration
- `memory`, `buddy`, and `fleet` still hand off to the existing Python core during migration
- the public `./scripts/Constant` wrapper now tries the Rust binary first, but runs a fast startup probe before handing control over
- if macOS stalls in code-signing / policy evaluation for freshly built binaries, the wrapper falls back automatically to the stable Python path
- when `constant` is launched from inside the Codex runner itself, the wrapper skips the Rust path entirely and goes straight to Python to avoid the known macOS startup stall in that environment
- use `CONSTANT_USE_RUST=1 ./scripts/Constant ...` to force the raw Rust path, or `CONSTANT_USE_PYTHON=1 ./scripts/Constant ...` to force Python
- after changing macOS security settings, use `CONSTANT_RUST_RECHECK=1 ./scripts/Constant ...` to force a fresh Rust startup probe instead of waiting for the fallback cache TTL
- the public runtime is now Rust-only
- `doctor`, `agents`, `skills`, `models`, `memory`, `buddy`, `fleet`, `tui`, `cockpit`, and mission commands all execute from the Rust binary
- the public `./scripts/Constant` wrapper rebuilds the Rust binary when sources change, then launches it directly
- the wrapper ad-hoc signs the rebuilt binary on macOS when `codesign` is available
- chat thread indexes now live in persisted sidecars under `~/.local/share/constant/indexes/chat`
- unread state persists separately per workspace/user view
- `constant mission delete <mission_id>` is now enforced in Rust and refuses `draft`, `planned`, or `running` missions
- `constant cockpit status-line` now powers the tmux chat dock and surfaces disabled autorestart warnings
macOS note:
- if Rust keeps falling back from a normal terminal app, check `System Settings -> Privacy & Security -> Developer Tools`
- allow your terminal app there so it can run locally built binaries without extra security-policy stalls
- then rerun with `CONSTANT_RUST_RECHECK=1 ./scripts/Constant --help`
- if Gatekeeper blocks a freshly built binary from your terminal, allow that terminal under `System Settings -> Privacy & Security -> Developer Tools`
- then rerun `./scripts/Constant --help`
## Why This Exists
@@ -177,7 +175,7 @@ Create and route missions through these skills today with the Rust CLI:
./scripts/Constant delegate <mission_id> --skill architecture-brainstorm --json
```
The richer conversation-first chat surface is still part of the Python-side migration path.
The conversation-first chat surface is now native to the Rust TUI.
### 4. Durable memory
@@ -248,16 +246,18 @@ codex login --device-auth
./scripts/Constant doctor
./scripts/Constant cockpit doctor --json
./scripts/Constant cockpit status --json
./scripts/Constant cockpit status-line --workspace "$PWD"
./scripts/Constant cockpit focus --machine command-center --pane codex
./scripts/Constant cockpit capture --machine command-center --pane claude
./scripts/Constant mission create "audit the repo" --workspace "$PWD"
./scripts/Constant mission delete <mission_id> --json
./scripts/Constant mission status
./scripts/Constant memory rebuild --workspace "$PWD"
./scripts/Constant memory search "buddy rail" --workspace "$PWD"
./scripts/Constant memory sync-qdrant --workspace "$PWD" --json
./scripts/Constant cockpit open --workspace "$PWD"
./scripts/Constant fleet discover --json
./scripts/Constant fleet configure
./scripts/Constant fleet deploy
./scripts/Constant fleet status --json
./scripts/Constant fleet sync --json
```
If `Constant` is on your `PATH`, you can also just run:
@@ -275,7 +275,7 @@ That means:
- the `Constant` TUI inside the central `Constant` window
- one machine window per host in the fleet
For now, the central `Constant` window is intentionally forced onto the richer Python chat-first TUI so the UX stays closer to a Claude Code-style conversation surface while the Rust TUI is still catching up.
The central `Constant` window now runs the native Rust chat-first TUI directly.
The current interaction model is:
@@ -310,10 +310,6 @@ Examples:
./scripts/constant-deploy.sh scan --json
./scripts/constant-deploy.sh configure
./scripts/constant-deploy.sh deploy
Constant fleet discover --json
Constant fleet configure --host dev@builder-a --host dev@edge-a
Constant fleet deploy --repo-dir '$HOME/constant'
```
You can pass raw SSH seeds such as:
@@ -327,7 +323,7 @@ If `fleet.toml` or `fleet.json` contains `repo_dir`, the shell launchers and fle
For a fully non-interactive run, pass explicit hosts and `--yes`:
```bash
Constant fleet configure \
./scripts/constant-deploy.sh configure \
--host builder-a \
--host builder-b \
--user dev \
@@ -345,7 +341,7 @@ Constant fleet configure \
- a chat-focus mode that behaves closer to Claude Code
- a detailed cockpit view you can toggle back in when needed
- a capture popup for pane output
- a `hexapus` buddy rail
- persistent unread tracking across sessions
- a bottom status / key strip
Useful keys:
@@ -378,7 +374,7 @@ The command-center machine is where you run:
- `Constant`
- the fleet cockpit
- local MLX orchestration
- local heuristic orchestration
- any human-in-the-loop supervision
The workers are where execution happens.
@@ -391,7 +387,7 @@ cp examples/fleet.example.toml ~/.config/constant/fleet.toml
```
Legacy `~/.config/constant/fleet.json` and `fleet.yaml` are still read for compatibility, but the native format is now TOML.
The shell launchers read both `fleet.toml` and `fleet.json`, so fleet tabs, install/check, and bridge helpers stay aligned during the migration.
The shell launchers and Rust CLI read both `fleet.toml` and `fleet.json`, so fleet tabs, install/check, and bridge helpers stay aligned.
## Messaging
@@ -458,7 +454,6 @@ Not like a magic trick.
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
@@ -468,8 +463,8 @@ Planned layers for the public `Constant` repo:
## Repo Layout
```text
constant/ Python orchestration core
src/ Rust CLI, missions, cockpit, and native TUI
src/ Rust CLI, missions, cockpit, native TUI, memory, and chat store
tests/ Rust integration tests
examples/fleet.example.json public fleet template
examples/fleet.example.toml public fleet template in the native config format
scripts/Constant canonical CLI entrypoint
-3
View File
@@ -1,3 +0,0 @@
__all__ = ["__version__"]
__version__ = "0.1.0"
-5
View File
@@ -1,5 +0,0 @@
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())
-482
View File
@@ -1,482 +0,0 @@
from __future__ import annotations
from typing import Any
DEFAULT_AGENTS: list[dict[str, Any]] = [
{
"id": "planner",
"label": "Planner",
"role": "planner",
"primary_cli": "claude",
"capabilities": ["spec", "planning", "review", "summarization", "delegation"],
"preferred_layers": ["reflection", "execution-prep"],
"manual_only": False,
},
{
"id": "executor",
"label": "Executor",
"role": "executor",
"primary_cli": "codex",
"capabilities": ["implementation", "debugging", "refactor", "tooling", "ops"],
"preferred_layers": ["execution"],
"manual_only": False,
},
{
"id": "analyst",
"label": "Analyst",
"role": "analyst",
"primary_cli": "vibe",
"capabilities": ["brainstorm", "alternatives", "research", "comparison"],
"preferred_layers": ["reflection"],
"manual_only": False,
},
{
"id": "assistant",
"label": "Assistant",
"role": "assistant",
"primary_cli": "copilot",
"capabilities": ["interactive-help", "inline-suggestions"],
"preferred_layers": ["manual"],
"manual_only": True,
},
]
DEFAULT_SKILLS: list[dict[str, Any]] = [
{
"id": "spec-planner",
"label": "Spec Planner",
"layer": "reflection",
"visibility": "public",
"summary": "Transformer une demande floue en spec exploitable avant de coder.",
"when_to_use": [
"quand on part d'une idee brute",
"quand il faut cadrer avant implementation",
"quand on veut eviter de coder trop tot",
],
"input_examples": [
"Je veux ajouter un systeme de notifications",
"On doit refondre l'auth OAuth",
],
"output_expected": [
"objectif",
"perimetre",
"hors_perimetre",
"contraintes",
"hypotheses",
"criteres_acceptation",
"risques",
"questions_ouvertes",
"plan_minimal",
],
"usage_prompt": (
"Fais une spec executable a partir de cette demande. "
"Identifie les zones floues, propose des hypotheses raisonnables, "
"liste les criteres d'acceptation et termine par un plan d'implementation minimal."
),
"keywords": [
"spec planner",
"spec-planner",
"spec",
"cadrer",
"clarifier",
"clarify",
"requirements",
"scope",
"perimetre",
"oauth",
"notifications",
"idea brute",
"je veux",
"i want",
"on doit",
"we need",
"refondre",
"before coding",
],
"aliases": ["summarize", "summary", "brief"],
"preferred_cli": "claude",
"preferred_agent": "planner",
},
{
"id": "architecture-brainstorm",
"label": "Architecture Brainstorm",
"layer": "reflection",
"visibility": "public",
"summary": "Comparer plusieurs options d'architecture avant de choisir.",
"when_to_use": [
"quand plusieurs approches sont possibles",
"quand on veut comparer les trade-offs",
"quand il faut une vraie discussion d'architecture",
],
"input_examples": [
"Doit-on faire un backend event-driven ou un monolithe modulaire ?",
"Quel runtime pour le cockpit interactif ?",
],
"output_expected": [
"options",
"avantages",
"inconvenients",
"complexite",
"impacts_perf",
"impacts_securite",
"impacts_dx",
"recommendation",
"raisons_du_choix",
],
"usage_prompt": (
"Analyse ce probleme comme un review partner senior. "
"Propose plusieurs architectures possibles, compare les compromis, "
"signale les pieges, puis recommande une option avec justification."
),
"keywords": [
"architecture",
"brainstorm",
"architecture-brainstorm",
"trade-off",
"tradeoff",
"option",
"compare",
"alternatives",
"design choice",
"choix technique",
],
"aliases": ["brainstorm"],
"preferred_cli": "vibe",
"preferred_agent": "analyst",
},
{
"id": "repo-onboarding",
"label": "Repo Onboarding",
"layer": "execution-prep",
"visibility": "public",
"summary": "Comprendre rapidement un repo avant la premiere intervention.",
"when_to_use": [
"quand on arrive sur un nouveau projet",
"quand il faut cartographier une codebase",
"quand on veut savoir ou modifier sans casser le reste",
],
"input_examples": [
"Explore ce repo comme si tu preparais une premiere intervention",
"Dis-moi ou intervenir pour cette tache",
],
"output_expected": [
"structure_repo",
"points_entree",
"modules_cles",
"conventions",
"commandes_utiles",
"zones_a_risque",
"strategie_modification",
],
"usage_prompt": (
"Explore ce repo comme si tu preparais une premiere intervention. "
"Resume sa structure, les modules critiques, les conventions implicites, "
"les commandes de dev/test, puis dis ou intervenir pour cette tache."
),
"keywords": [
"repo onboarding",
"repo-onboarding",
"onboarding",
"explore repo",
"new repo",
"codebase",
"structure du repo",
"points d'entree",
"where to change",
"cartographier",
],
"aliases": [],
"preferred_cli": "claude",
"preferred_agent": "planner",
},
{
"id": "task-decomposer",
"label": "Task Decomposer",
"layer": "execution-prep",
"visibility": "public",
"summary": "Transformer une spec ou direction choisie en plan d'execution operationnel.",
"when_to_use": [
"quand la direction est choisie",
"quand on veut passer du brainstorming a l'action",
"quand il faut un ordre d'execution clair",
],
"input_examples": [
"Decoupe cette refonte en sous-taches concretement executables",
"Fais-moi un plan d'implementation directement actionnable",
],
"output_expected": [
"etapes_ordonnees",
"dependances",
"quick_wins",
"parallelisation",
"tests",
"definition_of_done",
],
"usage_prompt": (
"Decoupe cette tache en sous-taches concretes, ordonnees, avec dependances, "
"risques, validations et tests associes. "
"Le plan doit etre directement executable par un agent de code."
),
"keywords": [
"task decomposer",
"task-decomposer",
"decompose",
"plan d'execution",
"execution plan",
"break down",
"subtasks",
"steps",
"roadmap",
"implementation plan",
],
"aliases": [],
"preferred_cli": "claude",
"preferred_agent": "planner",
},
{
"id": "pr-review-prep",
"label": "PR Review Prep",
"layer": "execution-prep",
"visibility": "public",
"summary": "Preparer une PR review-ready et anticiper les objections du reviewer.",
"when_to_use": [
"quand l'implementation est terminee",
"quand il faut une bonne PR",
"quand on veut preparer la review",
],
"input_examples": [
"Prepare une PR clean a partir de ces changements",
"Resume le pourquoi, le quoi et les risques pour le reviewer",
],
"output_expected": [
"resume_du_changement",
"impact_fonctionnel",
"impact_technique",
"migrations",
"tests",
"points_attention_reviewer",
"message_de_pr",
],
"usage_prompt": (
"Prepare une PR review-ready a partir de ces changements. "
"Resume le pourquoi, le quoi, les impacts, les risques, "
"les tests effectues et les points a surveiller pour le reviewer."
),
"keywords": [
"pr review prep",
"pr-review-prep",
"pr",
"pull request",
"review prep",
"release notes",
"review-ready",
"reviewer",
],
"aliases": ["review"],
"preferred_cli": "claude",
"preferred_agent": "planner",
},
{
"id": "implementation",
"label": "Implementation",
"layer": "execution",
"visibility": "internal",
"summary": "Ship code changes and concrete fixes.",
"when_to_use": [
"quand il faut modifier du code maintenant",
"quand la spec est deja suffisamment claire",
],
"input_examples": ["Fix the flaky test", "Implement the API endpoint"],
"output_expected": ["code_changes", "validation", "tests"],
"usage_prompt": "Implement the requested change directly and validate it.",
"keywords": ["fix", "implement", "build", "write", "patch", "feature", "code"],
"aliases": [],
"preferred_cli": "codex",
"preferred_agent": "executor",
},
{
"id": "debug-restoration",
"label": "Debug Restoration",
"layer": "execution",
"visibility": "internal",
"summary": "Investigate failures and restore a working state.",
"when_to_use": [
"quand quelque chose casse",
"quand il faut comprendre puis restaurer un etat sain",
],
"input_examples": ["The pane dies instantly", "This build started failing today"],
"output_expected": ["root_cause", "fix", "validation"],
"usage_prompt": "Investigate the failure, isolate the cause, and restore a working state.",
"keywords": ["bug", "debug", "failure", "broken", "error", "crash", "repair"],
"aliases": ["debug"],
"preferred_cli": "codex",
"preferred_agent": "executor",
},
{
"id": "ops-deployment",
"label": "Ops Deployment",
"layer": "execution",
"visibility": "internal",
"summary": "Handle shell, fleet, infra, network, and remote machine work.",
"when_to_use": [
"quand il faut deployer, scanner, installer, diagnostiquer des machines",
"quand la tache est principalement shell/infra",
],
"input_examples": ["Deploy the runtime on all machines", "Scan SSH hosts and configure the fleet"],
"output_expected": ["ops_actions", "machine_state", "validation"],
"usage_prompt": "Handle the shell, infra, fleet, and remote machine operations safely.",
"keywords": ["ssh", "shell", "fleet", "ops", "network", "infra", "deploy"],
"aliases": ["ops"],
"preferred_cli": "codex",
"preferred_agent": "executor",
},
]
def _normalize_skill_id(value: str) -> str:
return value.strip().lower()
def _skill_entries(include_internal: bool = True) -> list[dict[str, Any]]:
if include_internal:
return DEFAULT_SKILLS
return [skill for skill in DEFAULT_SKILLS if skill.get("visibility") != "internal"]
def list_agents() -> list[dict[str, Any]]:
return [dict(item) for item in DEFAULT_AGENTS]
def list_skills(include_internal: bool = True) -> list[dict[str, Any]]:
return [dict(item) for item in _skill_entries(include_internal=include_internal)]
def skill_catalog(include_internal: bool = False) -> dict[str, list[dict[str, Any]]]:
payload = {"reflection": [], "execution-prep": [], "execution": [], "manual": []}
for skill in _skill_entries(include_internal=include_internal):
payload.setdefault(str(skill["layer"]), []).append(dict(skill))
return payload
def skill_catalog_brief(include_internal: bool = False) -> list[dict[str, Any]]:
return [
{
"id": skill["id"],
"layer": skill["layer"],
"summary": skill["summary"],
"preferred_cli": skill["preferred_cli"],
"preferred_agent": skill["preferred_agent"],
}
for skill in _skill_entries(include_internal=include_internal)
]
def recommended_skill_stack() -> dict[str, Any]:
return {
"minimal": ["spec-planner", "architecture-brainstorm", "task-decomposer"],
"workflow": [
"spec-planner",
"architecture-brainstorm",
"repo-onboarding",
"task-decomposer",
"pr-review-prep",
],
"layers": {
"reflection": ["spec-planner", "architecture-brainstorm"],
"execution": ["repo-onboarding", "task-decomposer", "pr-review-prep"],
},
}
def agent_by_id(agent_id: str) -> dict[str, Any]:
for agent in DEFAULT_AGENTS:
if agent["id"] == agent_id:
return dict(agent)
raise KeyError(f"Unknown agent: {agent_id}")
def agent_for_cli(cli: str) -> dict[str, Any]:
for agent in DEFAULT_AGENTS:
if agent["primary_cli"] == cli:
return dict(agent)
return agent_by_id("executor")
def agent_hint(agent_id: str) -> dict[str, Any]:
return agent_by_id(agent_id)
def skill_by_id(skill_id: str, include_internal: bool = True) -> dict[str, Any]:
normalized = _normalize_skill_id(skill_id)
for skill in _skill_entries(include_internal=include_internal):
aliases = [_normalize_skill_id(alias) for alias in skill.get("aliases", [])]
if normalized == skill["id"] or normalized in aliases:
return dict(skill)
raise KeyError(f"Unknown skill: {skill_id}")
def _score_keywords(goal_l: str, skill: dict[str, Any]) -> int:
score = 0
for keyword in skill.get("keywords", []):
if keyword in goal_l:
score += 3 if " " in keyword or "-" in keyword else 1
if skill["id"] in goal_l:
score += 5
for alias in skill.get("aliases", []):
if alias in goal_l:
score += 3
return score
def match_skill(goal: str, include_internal: bool = True) -> dict[str, Any]:
goal_l = goal.lower()
best = skill_by_id("implementation", include_internal=include_internal) if include_internal else skill_by_id("spec-planner", include_internal=False)
best_score = -1
for skill in _skill_entries(include_internal=include_internal):
score = _score_keywords(goal_l, skill)
if score > best_score:
best_score = score
best = skill
if best_score <= 0:
if any(token in goal_l for token in ("repo", "codebase", "onboarding", "where to change", "first intervention")):
return skill_by_id("repo-onboarding", include_internal=include_internal)
if any(token in goal_l for token in ("je veux", "i want", "on doit", "we need", "idea brute", "high level idea")):
return skill_by_id("spec-planner", include_internal=include_internal)
if any(token in goal_l for token in ("architecture", "trade-off", "tradeoff", "option", "compare")):
return skill_by_id("architecture-brainstorm", include_internal=include_internal)
if any(token in goal_l for token in ("plan", "sub-task", "subtask", "decompose", "step-by-step", "roadmap")):
return skill_by_id("task-decomposer", include_internal=include_internal)
if any(token in goal_l for token in ("pr", "pull request", "reviewer", "release notes")):
return skill_by_id("pr-review-prep", include_internal=include_internal)
if include_internal and any(token in goal_l for token in ("bug", "crash", "failure", "broken", "error")):
return skill_by_id("debug-restoration", include_internal=True)
if include_internal and any(token in goal_l for token in ("ssh", "fleet", "deploy", "network", "machine", "infra")):
return skill_by_id("ops-deployment", include_internal=True)
return best
return dict(best)
def resolve_skill_and_agent(
*,
goal: str | None = None,
skill_id: str | None = None,
agent_id: str | None = None,
cli: str | None = None,
) -> dict[str, Any]:
skill = skill_by_id(skill_id) if skill_id else match_skill(goal or "")
if cli:
agent = agent_for_cli(cli)
elif agent_id:
agent = agent_by_id(agent_id)
cli = str(agent["primary_cli"])
else:
agent = agent_by_id(str(skill["preferred_agent"]))
cli = str(skill["preferred_cli"])
return {
"skill": skill,
"agent": agent,
"cli": cli,
}
-943
View File
@@ -1,943 +0,0 @@
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
from . import __version__
from .capabilities import (
agent_for_cli,
list_agents,
list_skills,
recommended_skill_stack,
resolve_skill_and_agent,
skill_catalog,
skill_by_id,
)
from .cockpit import capture_pane, cockpit_doctor, focus_machine, restart_pane, runtime_status, send_to_pane
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 .memory import (
enroll_workspace,
instruction_skill_sources,
list_decisions,
memory_status,
persona_markdown,
rebuild_workspace_memory,
search_memory,
summarize_mission,
sync_qdrant,
)
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,
)
from .tui import run_tui
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 _read_wrapper_stamp(path: Path) -> dict[str, Any] | None:
try:
raw = path.read_text(encoding="utf-8").strip()
except OSError:
return None
if not raw:
return None
parts = raw.split()
if len(parts) < 2:
return {"raw": raw}
signature, epoch_text = parts[0], parts[1]
payload: dict[str, Any] = {"signature": signature, "epoch": epoch_text}
try:
payload["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(epoch_text)))
except ValueError:
pass
return payload
def _read_wrapper_mode(path: Path) -> dict[str, Any] | None:
try:
raw = path.read_text(encoding="utf-8").strip()
except OSError:
return None
if not raw:
return None
parts = raw.split()
if len(parts) < 3:
return {"raw": raw}
mode, reason, epoch_text = parts[0], parts[1], parts[2]
payload: dict[str, Any] = {"mode": mode, "reason": reason, "epoch": epoch_text}
try:
payload["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(epoch_text)))
except ValueError:
pass
return payload
def _run_quick_command(command: list[str]) -> dict[str, Any]:
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=3,
check=False,
)
except Exception as exc: # noqa: BLE001
return {"ok": False, "error": str(exc), "command": command}
return {
"ok": completed.returncode == 0,
"returncode": completed.returncode,
"stdout": completed.stdout.strip(),
"stderr": completed.stderr.strip(),
"command": command,
}
def _wrapper_status() -> dict[str, Any]:
wrapper_dir = cache_root() / "wrapper"
rust_bin = repo_root() / "target" / "debug" / "constant"
payload: dict[str, Any] = {
"wrapper_dir": str(wrapper_dir),
"rust_bin": str(rust_bin),
"rust_bin_exists": rust_bin.exists(),
"forced_python": os.environ.get("CONSTANT_USE_PYTHON") == "1",
"forced_rust": os.environ.get("CONSTANT_USE_RUST") == "1",
"force_recheck": os.environ.get("CONSTANT_RUST_RECHECK") == "1",
"last_mode": _read_wrapper_mode(wrapper_dir / "last-mode"),
"probe_timeout_sec": os.environ.get("CONSTANT_RUST_PROBE_TIMEOUT_SEC", "2"),
"probe_fail_ttl_sec": os.environ.get("CONSTANT_RUST_PROBE_FAIL_TTL_SEC", "300"),
"cache": {
"rust_ok": _read_wrapper_stamp(wrapper_dir / "rust-ok"),
"rust_fail": _read_wrapper_stamp(wrapper_dir / "rust-fail"),
},
}
if rust_bin.exists():
payload["codesign_verify"] = _run_quick_command(
["codesign", "--verify", "--verbose=2", str(rust_bin)]
)
payload["spctl_assess"] = _run_quick_command(["spctl", "--assess", "-vv", str(rust_bin)])
payload["xattr_provenance"] = _run_quick_command(["xattr", "-p", "com.apple.provenance", str(rust_bin)])
payload["hint"] = (
"If spctl reports a Code Signing subsystem error from a normal terminal, allow that terminal in "
"System Settings -> Privacy & Security -> Developer Tools. Then rerun with CONSTANT_RUST_RECHECK=1 "
"to force a fresh startup probe."
)
return payload
def _warm_memory(workspace: str) -> dict[str, Any] | None:
try:
return rebuild_workspace_memory(workspace, enroll=True)
except Exception as exc: # noqa: BLE001
return {"error": str(exc), "workspace": workspace}
def _finalize_mission_memory(mission_id: str) -> dict[str, Any] | None:
try:
return summarize_mission(mission_id)
except Exception: # noqa: BLE001
return None
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"],
"agent_role": step.get("agent_role"),
"skill": step.get("skill"),
"skill_summary": step.get("skill_summary"),
"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()
cockpit = cockpit_doctor()
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(),
"ai-bridge": (scripts_dir() / "ai-bridge.sh").exists(),
},
"daemon": status,
"models": models,
"health": health,
"memory": memory_status(),
"fleet": [{"label": entry["label"], "target": entry["target"]} for entry in fleet["machines"]],
"cockpit": cockpit,
"wrapper": _wrapper_status(),
}
_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_agents(args: argparse.Namespace) -> int:
payload = {"agents": list_agents(), "recommended_skill_stack": recommended_skill_stack()}
_print(payload, args.json)
return 0
def cmd_skills(args: argparse.Namespace) -> int:
payload = {
"skills": list_skills(include_internal=not getattr(args, "public_only", False)),
"catalog": skill_catalog(include_internal=not getattr(args, "public_only", False)),
"recommended_skill_stack": recommended_skill_stack(),
}
if getattr(args, "workspace", None):
payload["instruction_skill_sources"] = instruction_skill_sources(args.workspace)
_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 _exec_fleet_deploy_script(mode: str, args: argparse.Namespace) -> int:
script = scripts_dir() / "constant-deploy.sh"
command = [str(script), mode]
for host in args.host or []:
command.extend(["--host", host])
if args.user:
command.extend(["--user", args.user])
if args.repo_dir:
command.extend(["--repo-dir", args.repo_dir])
if args.local_label:
command.extend(["--local-label", args.local_label])
if args.output:
command.extend(["--output", args.output])
if getattr(args, "json", False):
command.append("--json")
if getattr(args, "yes", False):
command.append("--yes")
if getattr(args, "all_reachable", False):
command.append("--all-reachable")
if getattr(args, "install", False):
command.append("--install")
if getattr(args, "no_ssh_config", False):
command.append("--no-ssh-config")
if getattr(args, "no_known_hosts", False):
command.append("--no-known-hosts")
if getattr(args, "no_arp", False):
command.append("--no-arp")
os.execv(command[0], command)
return 0
def cmd_fleet_discover(args: argparse.Namespace) -> int:
return _exec_fleet_deploy_script("scan", args)
def cmd_fleet_configure(args: argparse.Namespace) -> int:
return _exec_fleet_deploy_script("configure", args)
def cmd_fleet_deploy(args: argparse.Namespace) -> int:
return _exec_fleet_deploy_script("deploy", args)
def cmd_cockpit_open(args: argparse.Namespace) -> int:
workspace = args.workspace or os.getcwd()
command = [
str(scripts_dir() / "constant-fleet.sh"),
"--workspace",
workspace,
"--local-session",
args.local_session or "constant-fleet",
"--session",
args.session or "constant",
]
if getattr(args, "recreate", False):
command.append("--recreate")
if getattr(args, "remote_recreate", False):
command.append("--remote-recreate")
os.execv(command[0], command)
return 0
def cmd_cockpit_attach(args: argparse.Namespace) -> int:
command = [
str(scripts_dir() / "constant-fleet.sh"),
"--attach-only",
"--local-session",
args.local_session or "constant-fleet",
]
os.execv(command[0], command)
return 0
def cmd_cockpit_doctor(args: argparse.Namespace) -> int:
payload = cockpit_doctor(local_session=args.local_session or "constant-fleet", machine_session=args.session or "constant")
_print(payload, args.json)
return 0 if payload["tmux"]["available"] else 1
def cmd_cockpit_focus(args: argparse.Namespace) -> int:
payload = focus_machine(
args.machine,
args.pane,
local_session=args.local_session or "constant-fleet",
machine_session=args.session or "constant",
)
_print(payload, args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_cockpit_send(args: argparse.Namespace) -> int:
payload = send_to_pane(args.machine, args.pane, args.command, machine_session=args.session or "constant")
_print(payload, args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_cockpit_capture(args: argparse.Namespace) -> int:
payload = capture_pane(args.machine, args.pane, lines=args.lines, machine_session=args.session or "constant")
_print(payload if args.json else payload.get("stdout", ""), args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_cockpit_restart(args: argparse.Namespace) -> int:
payload = restart_pane(args.machine, args.pane, machine_session=args.session or "constant")
_print(payload, args.json)
return 0 if payload["returncode"] == 0 else 1
def cmd_cockpit_status(args: argparse.Namespace) -> int:
payload = runtime_status(local_session=args.local_session or "constant-fleet", machine_session=args.session or "constant")
_print(payload, args.json)
return 0
def cmd_tui(args: argparse.Namespace) -> int:
workspace = str(Path(args.workspace or os.getcwd()).expanduser().resolve())
action = run_tui(
workspace,
local_session=args.local_session or "constant-fleet",
machine_session=args.session or "constant",
)
if action and action.get("action") == "cockpit":
return cmd_cockpit_open(
argparse.Namespace(
workspace=action.get("workspace", workspace),
local_session=args.local_session,
session=args.session,
recreate=False,
remote_recreate=False,
)
)
return 0
def cmd_mission_create(args: argparse.Namespace) -> int:
_warm_memory(args.workspace)
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)
_warm_memory(mission["workspace"])
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", "")})
_finalize_mission_memory(mission["mission_id"])
_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", "")})
_finalize_mission_memory(mission["mission_id"])
_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"]})
_finalize_mission_memory(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.skill or args.agent or args.cli:
resolved_cli = args.cli
resolved_agent = args.agent
if not args.skill and not resolved_cli:
resolved_cli = step.get("cli")
if not args.skill and not resolved_agent:
resolved_agent = step.get("agent")
resolved = resolve_skill_and_agent(
goal=step.get("prompt") or mission.get("goal", ""),
skill_id=args.skill or step.get("skill"),
agent_id=resolved_agent,
cli=resolved_cli,
)
step["skill"] = resolved["skill"]["id"]
step["skill_summary"] = resolved["skill"]["summary"]
step["agent"] = resolved["agent"]["id"]
step["agent_role"] = resolved["agent"]["role"]
step["cli"] = resolved["cli"]
elif step.get("cli") and not step.get("agent"):
agent = agent_for_cli(step["cli"])
step["agent"] = agent["id"]
step["agent_role"] = agent["role"]
if step.get("skill"):
try:
step["skill_summary"] = skill_by_id(step["skill"])["summary"]
except KeyError:
pass
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"], "skill": step.get("skill")})
_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
if mission:
_warm_memory(mission["workspace"])
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 cmd_memory_status(args: argparse.Namespace) -> int:
_print(memory_status(args.workspace), args.json)
return 0
def cmd_memory_rebuild(args: argparse.Namespace) -> int:
_print(rebuild_workspace_memory(args.workspace, enroll=not args.no_enroll), args.json)
return 0
def cmd_memory_enroll(args: argparse.Namespace) -> int:
_print(enroll_workspace(args.path), args.json)
return 0
def cmd_memory_search(args: argparse.Namespace) -> int:
_print(search_memory(args.query, args.workspace, args.limit), args.json)
return 0
def cmd_memory_persona_show(args: argparse.Namespace) -> int:
payload = {"persona": persona_markdown()} if args.json else persona_markdown()
_print(payload, args.json)
return 0
def cmd_memory_decisions(args: argparse.Namespace) -> int:
_print(list_decisions(args.workspace, args.mission_id), args.json)
return 0
def cmd_memory_sync_qdrant(args: argparse.Namespace) -> int:
payload = sync_qdrant(args.workspace)
_print(payload, args.json)
return 0 if payload.get("ok") or payload.get("skipped") else 1
def cmd_mission_summarize(args: argparse.Namespace) -> int:
_print(summarize_mission(args.mission_id), args.json)
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog=os.environ.get("CONSTANT_PROG_NAME", "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)
tui = subparsers.add_parser("tui")
tui.add_argument("--workspace")
tui.add_argument("--local-session")
tui.add_argument("--session")
tui.set_defaults(func=cmd_tui)
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)
agents = subparsers.add_parser("agents")
agents.add_argument("--json", action="store_true")
agents.set_defaults(func=cmd_agents)
skills = subparsers.add_parser("skills")
skills.add_argument("--workspace")
skills.add_argument("--public-only", action="store_true")
skills.add_argument("--json", action="store_true")
skills.set_defaults(func=cmd_skills)
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)
for fleet_cmd_name, fleet_cmd_func, allow_json in (
("discover", cmd_fleet_discover, True),
("configure", cmd_fleet_configure, False),
("deploy", cmd_fleet_deploy, False),
):
fleet_cmd = fleet_sub.add_parser(fleet_cmd_name)
fleet_cmd.add_argument("--host", action="append")
fleet_cmd.add_argument("--user")
fleet_cmd.add_argument("--repo-dir")
fleet_cmd.add_argument("--local-label")
fleet_cmd.add_argument("--output")
fleet_cmd.add_argument("--yes", action="store_true")
fleet_cmd.add_argument("--all-reachable", action="store_true")
fleet_cmd.add_argument("--no-ssh-config", action="store_true")
fleet_cmd.add_argument("--no-known-hosts", action="store_true")
fleet_cmd.add_argument("--no-arp", action="store_true")
if fleet_cmd_name == "configure":
fleet_cmd.add_argument("--install", action="store_true")
if allow_json:
fleet_cmd.add_argument("--json", action="store_true")
fleet_cmd.set_defaults(func=fleet_cmd_func)
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.add_argument("--recreate", action="store_true")
cockpit_open.add_argument("--remote-recreate", action="store_true")
cockpit_open.set_defaults(func=cmd_cockpit_open)
cockpit_attach = cockpit_sub.add_parser("attach")
cockpit_attach.add_argument("--local-session")
cockpit_attach.set_defaults(func=cmd_cockpit_attach)
cockpit_doctor_cmd = cockpit_sub.add_parser("doctor")
cockpit_doctor_cmd.add_argument("--local-session")
cockpit_doctor_cmd.add_argument("--session")
cockpit_doctor_cmd.add_argument("--json", action="store_true")
cockpit_doctor_cmd.set_defaults(func=cmd_cockpit_doctor)
cockpit_status_cmd = cockpit_sub.add_parser("status")
cockpit_status_cmd.add_argument("--local-session")
cockpit_status_cmd.add_argument("--session")
cockpit_status_cmd.add_argument("--json", action="store_true")
cockpit_status_cmd.set_defaults(func=cmd_cockpit_status)
cockpit_focus_cmd = cockpit_sub.add_parser("focus")
cockpit_focus_cmd.add_argument("--machine", required=True)
cockpit_focus_cmd.add_argument("--pane")
cockpit_focus_cmd.add_argument("--local-session")
cockpit_focus_cmd.add_argument("--session")
cockpit_focus_cmd.add_argument("--json", action="store_true")
cockpit_focus_cmd.set_defaults(func=cmd_cockpit_focus)
cockpit_send_cmd = cockpit_sub.add_parser("send")
cockpit_send_cmd.add_argument("--machine", required=True)
cockpit_send_cmd.add_argument("--pane", required=True)
cockpit_send_cmd.add_argument("--command", required=True)
cockpit_send_cmd.add_argument("--session")
cockpit_send_cmd.add_argument("--json", action="store_true")
cockpit_send_cmd.set_defaults(func=cmd_cockpit_send)
cockpit_capture_cmd = cockpit_sub.add_parser("capture")
cockpit_capture_cmd.add_argument("--machine", required=True)
cockpit_capture_cmd.add_argument("--pane", required=True)
cockpit_capture_cmd.add_argument("--lines", type=int, default=120)
cockpit_capture_cmd.add_argument("--session")
cockpit_capture_cmd.add_argument("--json", action="store_true")
cockpit_capture_cmd.set_defaults(func=cmd_cockpit_capture)
cockpit_restart_cmd = cockpit_sub.add_parser("restart")
cockpit_restart_cmd.add_argument("--machine", required=True)
cockpit_restart_cmd.add_argument("--pane", required=True)
cockpit_restart_cmd.add_argument("--session")
cockpit_restart_cmd.add_argument("--json", action="store_true")
cockpit_restart_cmd.set_defaults(func=cmd_cockpit_restart)
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)
mission_summarize = mission_sub.add_parser("summarize")
mission_summarize.add_argument("mission_id")
mission_summarize.add_argument("--json", action="store_true")
mission_summarize.set_defaults(func=cmd_mission_summarize)
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("--skill")
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)
memory = subparsers.add_parser("memory")
memory_sub = memory.add_subparsers(dest="memory_command", required=True)
memory_status_cmd = memory_sub.add_parser("status")
memory_status_cmd.add_argument("--workspace")
memory_status_cmd.add_argument("--json", action="store_true")
memory_status_cmd.set_defaults(func=cmd_memory_status)
memory_rebuild_cmd = memory_sub.add_parser("rebuild")
memory_rebuild_cmd.add_argument("--workspace", default=os.getcwd())
memory_rebuild_cmd.add_argument("--no-enroll", action="store_true")
memory_rebuild_cmd.add_argument("--json", action="store_true")
memory_rebuild_cmd.set_defaults(func=cmd_memory_rebuild)
memory_enroll_cmd = memory_sub.add_parser("enroll")
memory_enroll_cmd.add_argument("path")
memory_enroll_cmd.add_argument("--json", action="store_true")
memory_enroll_cmd.set_defaults(func=cmd_memory_enroll)
memory_search_cmd = memory_sub.add_parser("search")
memory_search_cmd.add_argument("query")
memory_search_cmd.add_argument("--workspace")
memory_search_cmd.add_argument("--limit", type=int)
memory_search_cmd.add_argument("--json", action="store_true")
memory_search_cmd.set_defaults(func=cmd_memory_search)
memory_persona_cmd = memory_sub.add_parser("persona")
memory_persona_sub = memory_persona_cmd.add_subparsers(dest="memory_persona_command", required=True)
memory_persona_show_cmd = memory_persona_sub.add_parser("show")
memory_persona_show_cmd.add_argument("--json", action="store_true")
memory_persona_show_cmd.set_defaults(func=cmd_memory_persona_show)
memory_decisions_cmd = memory_sub.add_parser("decisions")
memory_decisions_cmd.add_argument("--workspace")
memory_decisions_cmd.add_argument("--mission-id")
memory_decisions_cmd.add_argument("--json", action="store_true")
memory_decisions_cmd.set_defaults(func=cmd_memory_decisions)
memory_sync_cmd = memory_sub.add_parser("sync-qdrant")
memory_sync_cmd.add_argument("--workspace")
memory_sync_cmd.add_argument("--json", action="store_true")
memory_sync_cmd.set_defaults(func=cmd_memory_sync_qdrant)
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())
-218
View File
@@ -1,218 +0,0 @@
from __future__ import annotations
import shlex
import subprocess
import socket
from pathlib import Path
from typing import Any
from .paths import scripts_dir
from .state import fleet_machine, load_fleet_config
ROLES = ("claude", "codex", "copilot", "vibe")
def _run(args: list[str]) -> dict[str, Any]:
process = subprocess.run(args, capture_output=True, text=True)
return {
"argv": args,
"returncode": process.returncode,
"stdout": process.stdout,
"stderr": process.stderr,
}
def _tmux_list_command(session_name: str) -> list[str]:
return [
"tmux",
"list-panes",
"-t",
session_name,
"-F",
"#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_index}\t#{@constant_role}\t#{pane_title}\t#{pane_current_command}\t#{pane_active}\t#{pane_dead}\t#{pane_dead_status}",
]
def _ssh_command(target: str, inner: str) -> list[str]:
remote_shell = (
'PATH="$HOME/.local/bin:$HOME/.npm-global/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"; '
"export PATH; "
f"{inner}"
)
return ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=1", target, remote_shell]
def _machine_session_name(session_name: str, machine_label: str) -> str:
return f"{session_name}:{machine_label}"
def _parse_panes(stdout: str) -> list[dict[str, Any]]:
panes: list[dict[str, Any]] = []
for raw in stdout.splitlines():
parts = raw.split("\t")
if len(parts) != 10:
continue
session_name, window_name, pane_id, pane_index, pane_role, pane_title, pane_command, pane_active, pane_dead, pane_dead_status = parts
role = pane_role or (pane_title if pane_title in ROLES else pane_command)
panes.append(
{
"session_name": session_name,
"window_name": window_name,
"pane_id": pane_id,
"pane_index": int(pane_index),
"role": role,
"pane_role": pane_role,
"pane_title": pane_title,
"pane_command": pane_command,
"active": pane_active == "1",
"dead": pane_dead == "1",
"dead_status": int(pane_dead_status or "0"),
}
)
return sorted(panes, key=lambda item: item["pane_index"])
def _machine_tmux_status(machine: dict[str, Any], session: str) -> dict[str, Any]:
label = machine["label"]
target = machine["target"]
local_names = {socket.gethostname(), socket.getfqdn(), socket.gethostname().split(".")[0], "local", "localhost", "127.0.0.1", "::1"}
is_local = target in local_names
session_target = _machine_session_name(session, label)
if is_local:
result = _run(_tmux_list_command(session_target))
if result["returncode"] != 0:
for fallback_label in (socket.gethostname().split(".")[0], socket.getfqdn(), socket.gethostname()):
fallback_target = _machine_session_name(session, fallback_label)
result = _run(_tmux_list_command(fallback_target))
if result["returncode"] == 0:
session_target = fallback_target
break
else:
inner = shlex.join(_tmux_list_command(session_target))
result = _run(_ssh_command(target, inner))
panes = _parse_panes(result["stdout"]) if result["returncode"] == 0 else []
roles = {
role: next((pane for pane in panes if pane["role"] == role), None)
for role in ROLES
}
return {
"label": label,
"target": target,
"session": session_target,
"reachable": result["returncode"] == 0 or is_local,
"attached_window": label,
"session_exists": result["returncode"] == 0,
"panes": panes,
"roles": roles,
"stderr": result["stderr"].strip(),
}
def runtime_status(local_session: str = "constant-fleet", machine_session: str = "constant") -> dict[str, Any]:
fleet = load_fleet_config()
local_tmux = _run(["tmux", "list-windows", "-t", local_session, "-F", "#{window_name}\t#{window_active}"])
fleet_windows: list[str] = []
focused_machine = None
if local_tmux["returncode"] == 0:
for raw in local_tmux["stdout"].splitlines():
window_name, _, *rest = raw.split("\t") + [""]
active = raw.split("\t")[1] if "\t" in raw else "0"
fleet_windows.append(window_name)
if active == "1":
focused_machine = window_name
machines = [_machine_tmux_status(machine, machine_session) for machine in fleet["machines"]]
focused_role = None
for machine in machines:
if machine["label"] != focused_machine:
continue
for role in ROLES:
pane = machine["roles"].get(role)
if pane and pane.get("active"):
focused_role = role
break
break
return {
"local_session": local_session,
"machine_session": machine_session,
"fleet_session_exists": local_tmux["returncode"] == 0,
"fleet_windows": fleet_windows,
"focused_machine": focused_machine,
"focused_role": focused_role,
"machines": machines,
"fleet_stderr": local_tmux["stderr"].strip(),
}
def cockpit_doctor(local_session: str = "constant-fleet", machine_session: str = "constant") -> dict[str, Any]:
tmux_check = _run(["tmux", "-V"])
payload = runtime_status(local_session=local_session, machine_session=machine_session)
payload["tmux"] = {
"available": tmux_check["returncode"] == 0,
"stdout": tmux_check["stdout"].strip(),
"stderr": tmux_check["stderr"].strip(),
}
return payload
def _machine_control_script(machine_label: str, machine_session: str) -> tuple[dict[str, Any], Path]:
machine = fleet_machine(machine_label)
script = scripts_dir() / "constant-machine.sh"
return machine, script
def _machine_command_args(machine: dict[str, Any], args: list[str]) -> list[str]:
return ["env", f"ZELLIJ_AI_MACHINE_NAME={machine['label']}", *args]
def _run_machine_command(machine: dict[str, Any], args: list[str]) -> dict[str, Any]:
command_args = _machine_command_args(machine, args)
target = machine["target"]
local_names = {socket.gethostname(), socket.getfqdn(), socket.gethostname().split(".")[0], "local", "localhost", "127.0.0.1", "::1"}
if target in local_names:
return _run(command_args)
quoted = shlex.join(command_args)
return _run(_ssh_command(target, quoted))
def focus_machine(machine_label: str, pane_role: str | None, local_session: str = "constant-fleet", machine_session: str = "constant") -> dict[str, Any]:
machine, script = _machine_control_script(machine_label, machine_session)
if pane_role:
payload = _run_machine_command(machine, [str(script), "--session", machine_session, "--focus-pane", pane_role])
if payload["returncode"] != 0:
return payload
select_window = _run(["tmux", "select-window", "-t", f"{local_session}:{machine_label}"])
return {
"returncode": select_window["returncode"],
"stdout": select_window["stdout"],
"stderr": select_window["stderr"],
"machine": machine_label,
"pane": pane_role,
}
def send_to_pane(machine_label: str, pane_role: str, command: str, machine_session: str = "constant") -> dict[str, Any]:
machine, script = _machine_control_script(machine_label, machine_session)
return _run_machine_command(
machine,
[str(script), "--session", machine_session, "--send-pane", pane_role, "--command", command],
)
def capture_pane(machine_label: str, pane_role: str, lines: int = 120, machine_session: str = "constant") -> dict[str, Any]:
machine, script = _machine_control_script(machine_label, machine_session)
return _run_machine_command(
machine,
[str(script), "--session", machine_session, "--capture-pane", pane_role, "--lines", str(lines)],
)
def restart_pane(machine_label: str, pane_role: str, machine_session: str = "constant") -> dict[str, Any]:
machine, script = _machine_control_script(machine_label, machine_session)
return _run_machine_command(
machine,
[str(script), "--session", machine_session, "--restart-pane", pane_role],
)
-240
View File
@@ -1,240 +0,0 @@
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"])
elif op == "chat":
payload = self.engine.chat(
request["message"],
request.get("mission"),
request["workspace"],
request.get("selected_machine"),
request.get("selected_role"),
request.get("chat_history"),
)
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"],
)
if op == "chat":
return _INLINE_ENGINE.chat(
request_payload["message"],
request_payload.get("mission"),
request_payload["workspace"],
request_payload.get("selected_machine"),
request_payload.get("selected_role"),
request_payload.get("chat_history"),
)
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"]
-138
View File
@@ -1,138 +0,0 @@
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 in {"zellij", "cockpit"}:
cockpit = str(scripts_dir() / "constant-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"
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,
}
-1199
View File
File diff suppressed because it is too large Load Diff
-75
View File
@@ -1,75 +0,0 @@
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 chat_root() -> Path:
return cache_root() / "chat"
def fleet_config_path() -> Path:
return config_root() / "fleet.json"
def models_config_path() -> Path:
return config_root() / "models.json"
def memory_config_path() -> Path:
return config_root() / "memory.json"
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"
-739
View File
@@ -1,739 +0,0 @@
from __future__ import annotations
import importlib.util
import json
import re
import subprocess
import sys
import threading
from dataclasses import dataclass
from typing import Any
from .capabilities import (
agent_for_cli,
list_agents,
list_skills,
match_skill,
recommended_skill_stack,
resolve_skill_and_agent,
skill_by_id,
skill_catalog_brief,
)
from .memory import instruction_skill_sources, search_memory
from .state import load_fleet_config, load_models_config
CHAT_ROLES = ("claude", "codex", "copilot", "vibe")
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 _public_skill_ids() -> list[str]:
return [item["id"] for item in list_skills(include_internal=False)]
def _extract_explicit_skill(message: str) -> tuple[str, dict[str, Any] | None]:
raw = message.strip()
lowered = raw.lower()
if lowered.startswith("skill:"):
payload = raw[len("skill:") :].strip()
if payload:
parts = payload.split(maxsplit=1)
try:
skill = skill_by_id(parts[0], include_internal=False)
except KeyError:
skill = None
if skill:
remainder = parts[1].strip() if len(parts) > 1 else ""
return remainder or raw, skill
if lowered.startswith("/skill "):
payload = raw[len("/skill ") :].strip()
if payload:
parts = payload.split(maxsplit=1)
try:
skill = skill_by_id(parts[0], include_internal=False)
except KeyError:
skill = None
if skill:
remainder = parts[1].strip() if len(parts) > 1 else ""
return remainder or raw, skill
if raw.startswith("/"):
parts = raw[1:].split(maxsplit=1)
if parts:
try:
skill = skill_by_id(parts[0], include_internal=False)
except KeyError:
skill = None
if skill:
remainder = parts[1].strip() if len(parts) > 1 else ""
return remainder or raw, skill
for skill_id in _public_skill_ids():
token = skill_id.lower()
if token in lowered:
try:
return raw, skill_by_id(skill_id, include_internal=False)
except KeyError:
continue
return raw, None
def _route_machine(goal: str, skill_id: str | None = None) -> str:
labels = _fleet_labels()
goal_l = _lower(goal)
if skill_id in {"spec-planner", "repo-onboarding", "task-decomposer"}:
return labels["local"]
if skill_id == "architecture-brainstorm":
return labels["lab_a"]
if skill_id == "pr-review-prep":
return labels["builder_a"]
if skill_id == "ops-deployment":
return labels["edge_a"]
if skill_id == "debug-restoration" and any(token in goal_l for token in ("performance", "deep", "benchmark", "compiler", "cuda")):
return labels["builder_b"]
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:
skill = match_skill(goal)
if skill.get("preferred_cli"):
return str(skill["preferred_cli"])
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 agent_for_cli(cli)["id"]
def _heuristic_plan(goal: str, workspace: str, mission_id: str, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
overrides = overrides or {}
resolved = resolve_skill_and_agent(
goal=goal,
skill_id=overrides.get("skill"),
agent_id=overrides.get("agent"),
cli=overrides.get("cli"),
)
skill = resolved["skill"]
cli = str(resolved["cli"])
agent = resolved["agent"]
machine = str(overrides.get("machine") or _route_machine(goal, str(skill["id"])))
backend = str(overrides.get("backend") or _route_backend(machine, cli, goal))
skill_sources = instruction_skill_sources(workspace, query=goal, limit=4) if workspace else []
return {
"title": goal.strip().splitlines()[0][:80] or mission_id,
"summary": f"Route the mission to {machine} using {cli} via {backend} for skill {skill['id']}.",
"steps": [
{
"step_id": "step-1",
"kind": "task",
"title": f"Execute mission on {machine}",
"prompt": goal,
"machine": machine,
"backend": backend,
"cli": cli,
"agent": agent["id"],
"agent_role": agent["role"],
"skill": skill["id"],
"skill_summary": skill["summary"],
"skill_sources": [item["path"] for item in skill_sources],
"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"
skill = match_skill(prompt)
return {
"answer": (
f"Qwen buddy heuristic view for {title}: "
f"skill={skill['id']}, focus on route correctness, CLI fit, and whether the selected agent matches the workflow stage. "
f"Prompt: {prompt}"
),
"mode": "heuristic",
}
def _match_machine_label(message: str) -> str | None:
message_l = _lower(message)
for machine in load_fleet_config()["machines"]:
label = machine["label"]
if label.lower() in message_l:
return label
return None
def _match_role(message: str) -> str | None:
message_l = _lower(message)
for role in CHAT_ROLES:
if role in message_l:
return role
return None
def _heuristic_chat(
message: str,
mission: dict[str, Any] | None,
workspace: str,
selected_machine: str | None,
selected_role: str | None,
) -> dict[str, Any]:
prompt, explicit_skill = _extract_explicit_skill(message)
prompt = prompt.strip()
prompt_l = _lower(prompt)
memory_hits = search_memory(prompt, workspace=workspace, limit=4).get("hits", []) if workspace else []
skill_sources = instruction_skill_sources(workspace, query=prompt, limit=4) if workspace else []
memory_lines = [f"{hit['kind']} {hit['path']} :: {hit['snippet']}" for hit in memory_hits[:3]]
buddy_note = None
cockpit_action: dict[str, Any] | None = None
intent = "plain_chat"
target_machine = _match_machine_label(prompt) or selected_machine or _fleet_labels()["local"]
target_role = _match_role(prompt) or selected_role or "codex"
matched_skill = explicit_skill or (match_skill(prompt) if prompt else None)
if any(token in prompt_l for token in ("open cockpit", "attach cockpit", "show cockpit")):
intent = "cockpit_open"
cockpit_action = {"type": "open"}
reply = "I can hand off to the full cockpit now."
elif any(token in prompt_l for token in ("restart", "relance", "respawn")):
intent = "cockpit_restart"
cockpit_action = {"type": "restart", "machine": target_machine, "pane": target_role}
reply = f"I'll restart {target_machine}:{target_role}."
elif any(token in prompt_l for token in ("capture", "log", "logs", "show pane", "see pane")):
intent = "cockpit_capture"
cockpit_action = {"type": "capture", "machine": target_machine, "pane": target_role}
reply = f"I'll capture {target_machine}:{target_role}."
elif any(token in prompt_l for token in ("focus", "jump", "go to", "ouvre", "open machine")):
intent = "cockpit_focus"
cockpit_action = {"type": "focus", "machine": target_machine, "pane": target_role}
reply = f"I'll focus {target_machine}:{target_role}."
elif any(token in prompt_l for token in ("memory", "remember", "decision", "persona", "what do we know", "qu'est-ce qu", "souviens")):
intent = "memory_lookup"
reply = "Memory lookup ready."
elif explicit_skill is None and prompt.endswith("?") and not any(
token in prompt_l for token in ("fix", "build", "implement", "write", "create", "deploy", "restart", "capture", "focus")
):
intent = "plain_chat"
reply = "Here's the operator view."
else:
intent = "mission_create"
routing_overrides = {
"skill": matched_skill["id"] if matched_skill else None,
"agent": matched_skill["preferred_agent"] if matched_skill else None,
"cli": matched_skill["preferred_cli"] if matched_skill else None,
}
preview = _heuristic_plan(prompt, workspace, "chat-preview", routing_overrides)
review = _heuristic_buddy_review(prompt, preview)
step = preview["steps"][0]
buddy_note = {
"answer": review["summary"],
"mode": "heuristic",
}
matched_skill_id = preview["steps"][0]["skill"]
reply = (
f"I turned that into a mission. Route preview: "
f"{step['machine']}/{step['cli']}/{step['backend']} "
f"skill={matched_skill_id} agent={step['agent']}."
)
if intent != "mission_create" and any(
token in prompt_l for token in ("route", "reroute", "which machine", "which cli", "codex", "claude", "vibe", "copilot")
):
buddy_note = _heuristic_buddy_answer(prompt, mission)
if intent == "memory_lookup":
if memory_lines:
reply = "Memory echoes:\n- " + "\n- ".join(memory_lines[:3])
else:
reply = "No strong memory hits for that query yet."
elif intent == "plain_chat":
title = mission["title"] if mission else "global cockpit"
route_hint = f" selected={selected_machine or '-'}:{selected_role or '-'}"
reply = f"Constant view for {title}.{route_hint}"
if memory_lines:
reply += "\nMemory echoes:\n- " + "\n- ".join(memory_lines[:2])
return {
"intent": intent,
"reply": reply,
"message": prompt,
"mode": "heuristic",
"cockpit_action": cockpit_action,
"buddy_note": buddy_note,
"memory_hits": memory_hits,
"skill_sources": skill_sources,
"workspace": workspace,
"mission_goal": prompt if intent == "mission_create" else None,
"skill": matched_skill,
"routing_overrides": {
"skill": matched_skill["id"] if matched_skill else None,
"agent": matched_skill["preferred_agent"] if matched_skill else None,
"cli": matched_skill["preferred_cli"] if matched_skill else None,
}
if intent == "mission_create" and matched_skill
else {},
}
def _budget_chat_history(chat_history: list[dict[str, Any]] | None, limit: int = 8) -> list[dict[str, Any]]:
if not chat_history:
return []
important = [
entry for entry in chat_history
if entry.get("intent") in {"mission_create", "cockpit_error", "buddy_answer", "memory_lookup"}
]
recent = list(chat_history[-limit:])
merged: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for entry in [*important[-3:], *recent]:
key = (str(entry.get("timestamp", "")), str(entry.get("content", "")))
if key in seen:
continue
seen.add(key)
merged.append(entry)
return merged[-limit:]
@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_probe = self._probe_mlx()
self._mlx_python = self._mlx_probe["available"]
def _probe_mlx(self) -> dict[str, Any]:
enable_setting = self._models.get("enable_mlx", "auto")
requested = str(enable_setting).lower() not in {"0", "false", "off", "no"}
package_present = importlib.util.find_spec("mlx_lm") is not None
if not requested:
return {"requested": False, "package_present": package_present, "available": False, "reason": "disabled"}
if not package_present:
return {"requested": True, "package_present": False, "available": False, "reason": "mlx_lm not installed"}
probe = subprocess.run(
[sys.executable, "-c", "import mlx.core as mx; print(mx.default_device())"],
capture_output=True,
text=True,
)
return {
"requested": True,
"package_present": True,
"available": probe.returncode == 0,
"reason": probe.stderr.strip() or ("ok" if probe.returncode == 0 else "mlx probe failed"),
"stdout": probe.stdout.strip(),
"returncode": probe.returncode,
}
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,
"mlx_probe": self._mlx_probe,
"models": health,
"fallback_mode": self._models.get("fallback_mode", "heuristic"),
"agents": list_agents(),
"skills": list_skills(),
"recommended_skill_stack": recommended_skill_stack(),
}
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]:
overrides = mission.get("routing_overrides") or {}
plan = _heuristic_plan(mission["goal"], mission["workspace"], mission["mission_id"], overrides)
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"],
"routing_overrides": overrides,
"machines": machine_labels,
"allowed_backends": ["omc", "cli-local", "cli-ssh", "cockpit"],
"allowed_clis": ["claude", "codex", "vibe"],
"skill_catalog": skill_catalog_brief(include_internal=True),
},
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):
resolved = resolve_skill_and_agent(
goal=step.get("prompt", mission["goal"]),
skill_id=step.get("skill") or overrides.get("skill"),
agent_id=step.get("agent") or overrides.get("agent"),
cli=step.get("cli") or overrides.get("cli"),
)
resolved_skill = resolved["skill"]
resolved_agent = resolved["agent"]
resolved_cli = str(resolved["cli"])
resolved_machine = step.get("machine") or overrides.get("machine") or _route_machine(step.get("prompt", mission["goal"]), resolved_skill["id"])
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": resolved_machine,
"backend": step.get("backend", _route_backend(resolved_machine, resolved_cli, mission["goal"])),
"cli": resolved_cli,
"agent": resolved_agent["id"],
"agent_role": resolved_agent.get("role"),
"skill": resolved_skill["id"],
"skill_summary": step.get("skill_summary", resolved_skill["summary"]),
"skill_sources": step.get("skill_sources", [item["path"] for item in instruction_skill_sources(mission["workspace"], query=step.get("prompt", mission["goal"]), limit=4)]),
"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"]
agent = agent_for_cli(step["cli"])
step["agent"] = agent["id"]
step["agent_role"] = agent["role"]
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
def chat(
self,
message: str,
mission: dict[str, Any] | None,
workspace: str,
selected_machine: str | None,
selected_role: str | None,
chat_history: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
result = _heuristic_chat(message, mission, workspace, selected_machine, selected_role)
if self._mlx_python:
system = (
"You are Constant, the main cockpit operator for a multi-machine coding fleet. "
"Return strict JSON with keys: intent, reply. "
"Valid intents: mission_create, cockpit_focus, cockpit_capture, cockpit_restart, cockpit_open, "
"buddy_answer, memory_lookup, plain_chat."
)
user = json.dumps(
{
"message": message,
"workspace": workspace,
"mission": mission,
"selected_machine": selected_machine,
"selected_role": selected_role,
"chat_history": _budget_chat_history(chat_history),
"memory_hits": result.get("memory_hits", [])[:4],
"buddy_note": result.get("buddy_note"),
"skills": list_skills(),
"skill_catalog": skill_catalog_brief(include_internal=True),
"recommended_skill_stack": recommended_skill_stack(),
"agents": list_agents(),
},
indent=2,
)
try:
model_result = self._call_model_json("planner", system, user)
result["intent"] = str(model_result.get("intent", result["intent"]))
result["reply"] = str(model_result.get("reply", result["reply"]))
result["mode"] = "mlx"
except Exception:
pass
if result["intent"] in {"plain_chat", "memory_lookup"} and result.get("buddy_note") is None:
prompt_l = _lower(message)
if any(token in prompt_l for token in ("route", "reroute", "machine", "cli", "pane", "codex", "claude", "vibe", "copilot")):
result["buddy_note"] = self.buddy_ask(mission, message)
return result
-329
View File
@@ -1,329 +0,0 @@
from __future__ import annotations
import json
import hashlib
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,
chat_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",
"repo_dir": "$HOME/constant",
"machines": [
{
"label": "command-center",
"target": "local",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["omc", "cli-local", "cockpit"],
},
{
"label": "builder-a",
"target": "dev@builder-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "cockpit"],
},
{
"label": "builder-b",
"target": "dev@builder-b",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "cockpit"],
},
{
"label": "edge-a",
"target": "dev@edge-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "cockpit"],
},
{
"label": "lab-a",
"target": "dev@lab-a",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "cockpit"],
},
],
}
DEFAULT_MODELS: dict[str, Any] = {
"version": 1,
"enable_mlx": "auto",
"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(), chat_root(), config_root(), data_root(), missions_dir(), indexes_dir(), memory_sources_dir()):
path.mkdir(parents=True, exist_ok=True)
def _legacy_config_path(path: Path) -> Path:
if path.suffix == ".json":
return path.with_suffix(".yaml")
return path
def _read_json_yaml(path: Path, default: dict[str, Any]) -> dict[str, Any]:
ensure_runtime_dirs()
legacy_path = _legacy_config_path(path)
if not path.exists() and legacy_path != path and legacy_path.exists():
return json.loads(legacy_path.read_text(encoding="utf-8"))
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]:
payload = _read_json_yaml(fleet_config_path(), DEFAULT_FLEET)
for machine in payload.get("machines", []):
backends = []
for backend in machine.get("backends", []):
backends.append("cockpit" if backend == "zellij" else backend)
machine["backends"] = backends
return payload
def load_models_config() -> dict[str, Any]:
payload = _read_json_yaml(models_config_path(), DEFAULT_MODELS)
merged = deepcopy(DEFAULT_MODELS)
for key, value in payload.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key].update(value)
else:
merged[key] = value
return merged
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 _workspace_chat_slug(workspace: str) -> str:
normalized = str(Path(workspace).expanduser().resolve())
name = Path(normalized).name or "workspace"
digest = hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:10]
return f"{name}-{digest}"
def chat_file(workspace: str, mission_id: str | None = None) -> Path:
ensure_runtime_dirs()
if mission_id:
return chat_root() / "missions" / f"{mission_id}.ndjson"
return chat_root() / "workspaces" / f"{_workspace_chat_slug(workspace)}.ndjson"
def read_chat_history(workspace: str, mission_id: str | None = None, limit: int = 80) -> list[dict[str, Any]]:
path = chat_file(workspace, mission_id=mission_id)
if not path.exists():
return []
entries: list[dict[str, Any]] = []
for raw in path.read_text(encoding="utf-8").splitlines()[-limit:]:
try:
entries.append(json.loads(raw))
except json.JSONDecodeError:
continue
return entries
def append_chat_message(
role: str,
content: str,
*,
workspace: str,
mission_id: str | None = None,
intent: str | None = None,
machine: str | None = None,
pane: str | None = None,
meta: dict[str, Any] | None = None,
) -> dict[str, Any]:
path = chat_file(workspace, mission_id=mission_id)
path.parent.mkdir(parents=True, exist_ok=True)
entry = {
"timestamp": now_utc(),
"role": role,
"content": content,
"intent": intent or "plain_chat",
"workspace": str(Path(workspace).expanduser().resolve()),
"mission_id": mission_id,
"machine": machine,
"pane": pane,
"meta": meta or {},
}
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(entry, sort_keys=True) + "\n")
return entry
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
-1725
View File
File diff suppressed because it is too large Load Diff
+17 -127
View File
@@ -9,149 +9,39 @@ while [[ -L "$script_source" ]]; do
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"
rust_bin="$repo_dir/target/debug/constant"
cache_root="${XDG_CACHE_HOME:-$HOME/.cache}/constant/wrapper"
probe_timeout_sec="${CONSTANT_RUST_PROBE_TIMEOUT_SEC:-2}"
probe_fail_ttl_sec="${CONSTANT_RUST_PROBE_FAIL_TTL_SEC:-300}"
force_rust_recheck="${CONSTANT_RUST_RECHECK:-0}"
ancestor_comm_contains() {
local needle="$1"
local pid="${PPID:-}"
local depth=0
while [[ -n "$pid" && "$pid" =~ ^[0-9]+$ && "$pid" -gt 1 && $depth -lt 12 ]]; do
local comm=""
comm="$(ps -o comm= -p "$pid" 2>/dev/null | tr -d '\n')" || break
[[ -z "$comm" ]] && break
if [[ "$comm" == *"$needle"* ]]; then
return 0
fi
pid="$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d '[:space:]')" || break
(( depth += 1 ))
done
return 1
}
stat_signature() {
local target="$1"
/usr/bin/stat -f '%m:%z' "$target" 2>/dev/null || printf 'missing\n'
}
record_wrapper_mode() {
local mode="$1"
local reason="$2"
local reason="$1"
mkdir -p "$cache_root"
printf '%s %s %s\n' "$mode" "$reason" "$(date +%s)" > "$cache_root/last-mode"
printf '%s %s %s\n' "rust" "$reason" "$(date +%s)" > "$cache_root/last-mode"
export CONSTANT_WRAPPER_LAST_REASON="$reason"
}
cache_matches() {
local cache_file="$1"
local expected_signature="$2"
[[ -f "$cache_file" ]] || return 1
local cached_signature=""
local cached_epoch=""
IFS=' ' read -r cached_signature cached_epoch < "$cache_file" || return 1
[[ "$cached_signature" == "$expected_signature" ]] || return 1
printf '%s\n' "${cached_epoch:-0}"
sign_binary_if_possible() {
command -v codesign >/dev/null 2>&1 || return 0
codesign --force -s - "$rust_bin" >/dev/null 2>&1 || true
}
run_rust_probe() {
local probe_target="$1"
local timeout_sec="$2"
local probe_log="$cache_root/rust-probe.log"
mkdir -p "$cache_root"
("$probe_target" --version >"$probe_log" 2>&1) &
local probe_pid=$!
local waited=0
local deadline=$(( timeout_sec * 10 ))
while (( waited < deadline )); do
if ! kill -0 "$probe_pid" 2>/dev/null; then
local status=0
wait "$probe_pid" || status=$?
[[ $status -eq 0 ]]
return
needs_build() {
[[ -x "$rust_bin" ]] || return 0
[[ "$repo_dir/Cargo.toml" -nt "$rust_bin" ]] && return 0
[[ -f "$repo_dir/Cargo.lock" && "$repo_dir/Cargo.lock" -nt "$rust_bin" ]] && return 0
if find "$repo_dir/src" "$repo_dir/tests" -type f -newer "$rust_bin" -print -quit 2>/dev/null | grep -q .; then
return 0
fi
sleep 0.1
(( waited += 1 ))
done
kill "$probe_pid" 2>/dev/null || true
wait "$probe_pid" 2>/dev/null || true
return 1
}
warn_rust_fallback() {
[[ -t 2 ]] || return 0
printf '%s\n' "constant: Rust startup probe failed in this macOS session, falling back to Python." >&2
}
warn_codex_rust_bypass() {
[[ -t 2 ]] || return 0
printf '%s\n' "constant: running under Codex runner, skipping Rust startup path and using Python fallback." >&2
}
if [[ "${CONSTANT_USE_RUST:-0}" == "1" && "${CONSTANT_USE_PYTHON:-0}" != "1" ]]; then
if needs_build; then
cargo build --manifest-path "$repo_dir/Cargo.toml" --quiet
record_wrapper_mode "rust" "forced-rust"
export CONSTANT_REPO_DIR="$repo_dir"
export CONSTANT_PROG_NAME="${CONSTANT_PROG_NAME:-$(basename "$0")}"
exec "$rust_bin" "$@"
fi
if [[ "${CONSTANT_USE_PYTHON:-0}" != "1" ]]; then
if ancestor_comm_contains "codex-aarch64-apple-darwin"; then
record_wrapper_mode "python" "codex-runner"
warn_codex_rust_bypass
sign_binary_if_possible
record_wrapper_mode "rebuilt"
else
cargo build --manifest-path "$repo_dir/Cargo.toml" --quiet
rust_signature="$(stat_signature "$rust_bin")"
mkdir -p "$cache_root"
record_wrapper_mode "reused-binary"
fi
ok_epoch="$(cache_matches "$cache_root/rust-ok" "$rust_signature" || true)"
if [[ "$force_rust_recheck" != "1" && -n "$ok_epoch" ]]; then
record_wrapper_mode "rust" "cached-rust-ok"
export CONSTANT_PROG_NAME="${CONSTANT_PROG_NAME:-$(basename "$0")}"
export CONSTANT_REPO_DIR="$repo_dir"
export CONSTANT_PROG_NAME="${CONSTANT_PROG_NAME:-$(basename "$0")}"
exec "$rust_bin" "$@"
fi
fail_epoch="$(cache_matches "$cache_root/rust-fail" "$rust_signature" || true)"
now_epoch="$(date +%s)"
if [[ "$force_rust_recheck" != "1" && -n "$fail_epoch" ]] && (( now_epoch - fail_epoch < probe_fail_ttl_sec )); then
record_wrapper_mode "python" "cached-rust-fail"
:
elif run_rust_probe "$rust_bin" "$probe_timeout_sec"; then
printf '%s %s\n' "$rust_signature" "$now_epoch" > "$cache_root/rust-ok"
rm -f "$cache_root/rust-fail"
record_wrapper_mode "rust" "probe-ok"
export CONSTANT_REPO_DIR="$repo_dir"
export CONSTANT_PROG_NAME="${CONSTANT_PROG_NAME:-$(basename "$0")}"
exec "$rust_bin" "$@"
else
printf '%s %s\n' "$rust_signature" "$now_epoch" > "$cache_root/rust-fail"
rm -f "$cache_root/rust-ok"
record_wrapper_mode "python" "probe-failed"
warn_rust_fallback
fi
fi
fi
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}"
export CONSTANT_PROG_NAME="${CONSTANT_PROG_NAME:-$(basename "$0")}"
if [[ "${CONSTANT_USE_PYTHON:-0}" == "1" ]]; then
record_wrapper_mode "python" "forced-python"
fi
exec "$python_bin" -m constant "$@"
+21
View File
@@ -0,0 +1,21 @@
#!/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)"
rust_bin="$repo_dir/target/debug/constant"
if [[ ! -x "$rust_bin" ]]; then
cargo build --manifest-path "$repo_dir/Cargo.toml" --quiet
if command -v codesign >/dev/null 2>&1; then
codesign --force -s - "$rust_bin" >/dev/null 2>&1 || true
fi
fi
exec "$rust_bin" cockpit status-line "$@"
+27 -18
View File
@@ -44,11 +44,30 @@ require_arg() {
fi
}
window_pane_id() {
local session_name="$1"
local window_name="$2"
tmux list-panes -t "${session_name}:${window_name}" -F '#{pane_id}' 2>/dev/null | head -n 1
}
mark_window_managed() {
local session_name="$1"
local window_name="$2"
local role_label="$3"
local command_string="$4"
local pane_id
pane_id="$(window_pane_id "$session_name" "$window_name")"
if [[ -z "$pane_id" ]]; then
return 1
fi
constant_tmux_set_managed_pane "$pane_id" "$role_label" "$command_string"
}
build_constant_window_command() {
local cmd=(
env
"PATH=$(zellij_ai_agent_path)"
"CONSTANT_USE_PYTHON=1"
"$repo_dir/scripts/Constant"
tui
--workspace "$workspace"
@@ -111,23 +130,9 @@ link_local_window() {
configure_fleet_session() {
local session_name="$1"
tmux rename-window -t "${session_name}:0" Constant >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g set-clipboard on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g remain-on-exit on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g mouse on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-position top >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-justify left >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-left-length 24 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-right-length 48 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-left '#[fg=black,bg=green,bold] Constant #[default] ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-right '#[fg=colour250]#S #[fg=colour244]| #[fg=colour252]%H:%M #[fg=colour244]%d-%b' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g status-style 'bg=colour234,fg=colour252' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g window-status-separator ' ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g window-status-format '#[fg=colour245,bg=colour236] #I:#W ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g window-status-current-format '#[fg=colour16,bg=colour46,bold] #I:#W ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g window-status-current-style 'fg=colour16,bg=colour46,bold' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g allow-rename off >/dev/null 2>&1 || true
tmux set-option -t "$session_name" -g automatic-rename off >/dev/null 2>&1 || true
constant_tmux_configure_autorestart_hook "$session_name"
constant_tmux_configure_status_chrome "$session_name" "Constant" "green"
constant_tmux_configure_chat_dock "$session_name" "$repo_dir" "$workspace" "fleet" ""
}
attach_fleet_session() {
@@ -154,8 +159,10 @@ ensure_constant_window() {
if window_pane_dead "$local_session" "Constant"; then
tmux respawn-pane -k -t "${local_session}:Constant" "$command_string"
fi
mark_window_managed "$local_session" "Constant" "constant" "$command_string" >/dev/null 2>&1 || true
else
tmux new-window -d -t "${local_session}:" -n Constant "$command_string"
mark_window_managed "$local_session" "Constant" "constant" "$command_string" >/dev/null 2>&1 || true
fi
}
@@ -168,8 +175,10 @@ ensure_remote_window() {
if window_pane_dead "$local_session" "$label"; then
tmux respawn-pane -k -t "${local_session}:${label}" "$command_string"
fi
mark_window_managed "$local_session" "$label" "$label" "$command_string" >/dev/null 2>&1 || true
else
tmux new-window -d -t "${local_session}:" -n "$label" "$command_string"
mark_window_managed "$local_session" "$label" "$label" "$command_string" >/dev/null 2>&1 || true
fi
}
+30 -10
View File
@@ -72,6 +72,12 @@ pane_command() {
set_pane_meta() {
local pane_id="$1"
local role="$2"
local command_string="${3:-}"
local reset_state="${4:-0}"
if [[ -n "$command_string" ]]; then
constant_tmux_set_managed_pane "$pane_id" "$role" "$command_string" "$reset_state"
return
fi
tmux select-pane -t "$pane_id" -T "$role" >/dev/null 2>&1 || true
tmux set-option -p -t "$pane_id" @constant_role "$role" >/dev/null 2>&1 || true
}
@@ -87,10 +93,9 @@ create_session() {
vibe_cmd="$(pane_command vibe)"
tmux new-session -d -s "$session" -n "$machine_name" "$claude_cmd"
tmux set-option -t "$session" -g set-clipboard on >/dev/null 2>&1 || true
tmux set-option -t "$session" -g remain-on-exit on >/dev/null 2>&1 || true
tmux set-option -t "$session" -g mouse on >/dev/null 2>&1 || true
tmux set-option -t "$session" -g status-position top >/dev/null 2>&1 || true
constant_tmux_configure_autorestart_hook "$session"
constant_tmux_configure_status_chrome "$session" "$machine_name" "colour81"
constant_tmux_configure_chat_dock "$session" "$repo_dir" "$workspace" "$machine_name" "$machine_name"
tmux set-window-option -t "$target_window" -g pane-base-index 0 >/dev/null 2>&1 || true
claude_pane="$(tmux list-panes -t "$target_window" -F '#{pane_id}' | head -n 1)"
@@ -100,14 +105,15 @@ create_session() {
tmux select-layout -t "$target_window" main-vertical >/dev/null
tmux resize-pane -t "$claude_pane" -x 120 >/dev/null 2>&1 || true
set_pane_meta "$claude_pane" claude
set_pane_meta "$codex_pane" codex
set_pane_meta "$copilot_pane" copilot
set_pane_meta "$vibe_pane" vibe
set_pane_meta "$claude_pane" claude "$claude_cmd" 1
set_pane_meta "$codex_pane" codex "$codex_cmd" 1
set_pane_meta "$copilot_pane" copilot "$copilot_cmd" 1
set_pane_meta "$vibe_pane" vibe "$vibe_cmd" 1
tmux select-pane -t "$claude_pane" >/dev/null
}
ensure_session() {
local role pane_id command_string
mkdir -p "$state_dir" "$bus_dir/messages"
if $recreate && constant_tmux_session_exists "$session"; then
@@ -119,11 +125,25 @@ ensure_session() {
return 0
fi
constant_tmux_configure_autorestart_hook "$session"
constant_tmux_configure_status_chrome "$session" "$machine_name" "colour81"
constant_tmux_configure_chat_dock "$session" "$repo_dir" "$workspace" "$machine_name" "$machine_name"
if ! constant_tmux_window_exists "$session" "$machine_name"; then
recreate=true
tmux kill-session -t "$session" >/dev/null 2>&1 || true
create_session
return 0
fi
for role in claude codex copilot vibe; do
pane_id="$(role_pane_id "$role")"
if [[ -z "$pane_id" ]]; then
continue
fi
command_string="$(pane_command "$role")"
set_pane_meta "$pane_id" "$role" "$command_string"
done
}
focus_role() {
@@ -154,7 +174,7 @@ restart_role() {
command_string="$(pane_command "$role")"
tmux respawn-pane -k -t "$pane_id" "$command_string"
set_pane_meta "$pane_id" "$role"
set_pane_meta "$pane_id" "$role" "$command_string" 1
tmux select-layout -t "$(tmux_target_window)" main-vertical >/dev/null
}
@@ -190,7 +210,7 @@ send_role() {
session="$(zellij_ai_default_session)"
workspace="$PWD"
repo_dir="$(zellij_ai_expand_home_path "$(zellij_ai_default_repo_dir)")"
repo_dir="$(cd "$script_dir/.." && pwd -P)"
codex_home="$(zellij_ai_expand_home_path "$(zellij_ai_default_codex_home)")"
claude_config_dir=""
machine_name="${ZELLIJ_AI_MACHINE_NAME:-$(zellij_ai_local_machine_label)}"
+100 -46
View File
@@ -28,56 +28,16 @@ zellij_ai_fleet_config_path() {
zellij_ai_fleet_config_query() {
local expr="$1"
local config_path
local common_source common_dir wrapper
config_path="$(zellij_ai_fleet_config_path 2>/dev/null || true)"
[[ -n "$config_path" ]] || return 1
command -v python3 >/dev/null 2>&1 || return 1
common_source="${BASH_SOURCE[0]:-$0}"
common_dir="$(cd "$(dirname "$common_source")" && pwd -P)"
wrapper="$common_dir/Constant"
[[ -x "$wrapper" ]] || return 1
python3 - "$config_path" "$expr" <<'PY'
import json
import sys
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
tomllib = None
path = Path(sys.argv[1])
expr = sys.argv[2]
try:
if path.suffix == ".toml":
if tomllib is None:
raise SystemExit(1)
payload = tomllib.loads(path.read_text(encoding="utf-8"))
else:
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
if expr == "repo_dir":
value = payload.get("repo_dir")
if isinstance(value, str) and value:
print(value)
raise SystemExit(0)
if expr == "local_machine":
value = payload.get("local_machine")
if isinstance(value, str) and value:
print(value)
raise SystemExit(0)
if expr == "machine_specs":
for machine in payload.get("machines", []):
label = machine.get("label")
target = machine.get("target")
if isinstance(label, str) and label and isinstance(target, str) and target:
print(f"{label}={target}")
raise SystemExit(0)
raise SystemExit(1)
PY
"$wrapper" fleet config-get "$expr"
}
zellij_ai_default_session() {
@@ -297,6 +257,100 @@ constant_tmux_window_exists() {
tmux list-windows -t "$session_name" -F '#{window_name}' 2>/dev/null | grep -Fxq "$window_name"
}
constant_tmux_set_managed_pane() {
local pane_id="$1"
local role="$2"
local command_string="$3"
local reset_state="${4:-0}"
tmux select-pane -t "$pane_id" -T "$role" >/dev/null 2>&1 || true
tmux set-option -p -t "$pane_id" @constant_role "$role" >/dev/null 2>&1 || true
tmux set-option -p -t "$pane_id" @constant_command "$command_string" >/dev/null 2>&1 || true
tmux set-option -p -t "$pane_id" @constant_autorestart 1 >/dev/null 2>&1 || true
if [[ "$reset_state" == "1" ]]; then
tmux set-option -p -t "$pane_id" @constant_restart_failures 0 >/dev/null 2>&1 || true
tmux set-option -p -t "$pane_id" @constant_autorestart_disabled 0 >/dev/null 2>&1 || true
else
if [[ -z "$(tmux show-options -p -t "$pane_id" -v @constant_restart_failures 2>/dev/null || true)" ]]; then
tmux set-option -p -t "$pane_id" @constant_restart_failures 0 >/dev/null 2>&1 || true
fi
if [[ -z "$(tmux show-options -p -t "$pane_id" -v @constant_autorestart_disabled 2>/dev/null || true)" ]]; then
tmux set-option -p -t "$pane_id" @constant_autorestart_disabled 0 >/dev/null 2>&1 || true
fi
fi
}
constant_tmux_autorestart_hook_command() {
cat <<'EOF'
run-shell -b 'sleep 0.8; pane="#{hook_pane}"; dead=$(tmux display-message -p -t "$pane" "#{pane_dead}" 2>/dev/null || true); managed=$(tmux show-options -p -t "$pane" -v @constant_autorestart 2>/dev/null || true); disabled=$(tmux show-options -p -t "$pane" -v @constant_autorestart_disabled 2>/dev/null || true); cmd=$(tmux show-options -p -t "$pane" -v @constant_command 2>/dev/null || true); failures=$(tmux show-options -p -t "$pane" -v @constant_restart_failures 2>/dev/null || printf 0); if [ "$dead" = "1" ] && [ "$managed" = "1" ] && [ "$disabled" != "1" ] && [ -n "$cmd" ]; then failures=$((failures + 1)); tmux set-option -p -t "$pane" @constant_restart_failures "$failures" >/dev/null 2>&1 || true; if [ "$failures" -ge 3 ]; then tmux set-option -p -t "$pane" @constant_autorestart_disabled 1 >/dev/null 2>&1 || true; else tmux respawn-pane -k -t "$pane" "$cmd"; fi; fi'
EOF
}
constant_tmux_configure_autorestart_hook() {
local session_name="$1"
tmux set-hook -t "$session_name" pane-died "$(constant_tmux_autorestart_hook_command)" >/dev/null 2>&1 || true
}
constant_tmux_configure_status_chrome() {
local session_name="$1"
local left_label="$2"
local left_bg="${3:-green}"
tmux set-option -t "$session_name" set-clipboard on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" remain-on-exit on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" mouse on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status on >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-position top >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-justify left >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-left-length 32 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-right-length 64 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-left "#[fg=colour16,bg=${left_bg},bold] ${left_label} #[default] " >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-right '#[fg=colour250]#S #[fg=colour244]| #[fg=colour252]%H:%M #[fg=colour244]%d-%b' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-style 'bg=colour234,fg=colour252' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" window-status-separator ' ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" window-status-format '#[fg=colour245,bg=colour236] #I:#W ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" window-status-current-format '#[fg=colour16,bg=colour46,bold] #I:#W ' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" window-status-current-style 'fg=colour16,bg=colour46,bold' >/dev/null 2>&1 || true
tmux set-option -t "$session_name" allow-rename off >/dev/null 2>&1 || true
tmux set-option -t "$session_name" automatic-rename off >/dev/null 2>&1 || true
}
constant_tmux_chat_dock_command() {
local repo_dir="$1"
local workspace="$2"
local scope_label="${3:-}"
local machine_label="${4:-}"
local cmd=(
"$repo_dir/scripts/constant-chat-dock.sh"
--workspace "$workspace"
)
if [[ -n "$scope_label" ]]; then
cmd+=(--scope-label "$scope_label")
fi
if [[ -n "$machine_label" ]]; then
cmd+=(--machine-label "$machine_label")
fi
printf '%q ' "${cmd[@]}"
}
constant_tmux_configure_chat_dock() {
local session_name="$1"
local repo_dir="$2"
local workspace="$3"
local scope_label="${4:-}"
local machine_label="${5:-}"
local dock_cmd dock_line
dock_cmd="$(constant_tmux_chat_dock_command "$repo_dir" "$workspace" "$scope_label" "$machine_label")"
dock_line="#[fg=colour16,bg=colour153,bold] Chat #[fg=colour231,bg=colour238] #($dock_cmd)"
tmux set-option -t "$session_name" status 2 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-interval 4 >/dev/null 2>&1 || true
tmux set-option -t "$session_name" status-format[1] "$dock_line" >/dev/null 2>&1 || true
}
constant_tmux_base_command() {
local role="$1"
local launcher="$2"
+2 -76
View File
@@ -196,32 +196,7 @@ probe_seed() {
render_scan_json() {
local candidates_file="$1"
python3 - "$candidates_file" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
candidates = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
seed, user, host, port, reachable, remote_name, remote_os, remote_home, error = raw.split("\t", 8)
candidates.append(
{
"seed": seed,
"user": user,
"host": host,
"port": int(port),
"reachable": reachable == "yes",
"remote_name": remote_name,
"remote_os": remote_os,
"remote_home": remote_home,
"error": error,
}
)
print(json.dumps({"candidates": candidates}, indent=2))
PY
"$script_dir/Constant" fleet render-scan-json "$candidates_file"
}
print_scan_table() {
@@ -426,55 +401,7 @@ write_fleet_config() {
local finalized_file="$1"
local output_path="$2"
local repo_dir="$3"
python3 - "$finalized_file" "$output_path" "$repo_dir" <<'PY'
import json
import sys
from pathlib import Path
finalized = Path(sys.argv[1])
output = Path(sys.argv[2]).expanduser()
repo_dir = sys.argv[3]
machines = []
local_machine = None
for raw in finalized.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
label, role, user, seed = raw.split("\t", 3)
if role == "local":
local_machine = label
machines.append(
{
"label": label,
"target": "local",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["omc", "cli-local", "cockpit"],
}
)
else:
machines.append(
{
"label": label,
"target": f"{user}@{seed}",
"auto_clis": ["codex", "vibe", "claude"],
"manual_clis": ["copilot"],
"backends": ["cli-ssh", "cockpit"],
}
)
payload = {
"version": 1,
"local_machine": local_machine or "command-center",
"repo_dir": repo_dir,
"machines": machines,
}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(str(output))
PY
"$script_dir/Constant" fleet write-config "$finalized_file" --output "$output_path" --repo-dir "$repo_dir"
}
run_install() {
@@ -583,7 +510,6 @@ if [[ -z "$default_user" ]]; then
fi
zellij_ai_require_command ssh
zellij_ai_require_command python3
candidates_file="$(discover_candidates "$explicit_hosts_file" "$default_user" "$use_ssh_config" "$use_known_hosts" "$use_arp")"
rm -f "$explicit_hosts_file"
+558
View File
@@ -0,0 +1,558 @@
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use serde::{Deserialize, Serialize};
use crate::paths;
use crate::state::{list_missions, load_mission, now_utc};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChatEntry {
pub timestamp: String,
pub role: String,
pub content: String,
pub intent: String,
pub workspace: String,
pub mission_id: Option<String>,
pub machine: Option<String>,
pub pane: Option<String>,
#[serde(default)]
pub meta: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ThreadIndex {
pub thread_key: String,
pub workspace: String,
pub mission_id: Option<String>,
pub kind: String,
pub title: String,
pub mission_status: Option<String>,
pub message_count: u64,
pub last_role: String,
pub last_preview: String,
pub last_timestamp: String,
pub chat_mtime: u64,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ThreadViewState {
#[serde(default)]
pub seen_counts: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ThreadSummary {
pub thread_key: String,
pub workspace: String,
pub mission_id: Option<String>,
pub kind: String,
pub title: String,
pub mission_status: Option<String>,
pub message_count: u64,
pub unread_count: u64,
pub last_role: String,
pub last_preview: String,
pub last_timestamp: String,
}
pub fn resolve_workspace(workspace: &str) -> Result<String, String> {
let expanded = paths::expand_home_string(workspace);
match expanded.canonicalize() {
Ok(path) => Ok(path.display().to_string()),
Err(_) => Ok(expanded.display().to_string()),
}
}
pub fn workspace_chat_slug(workspace: &str) -> Result<String, String> {
let resolved = resolve_workspace(workspace)?;
let name = Path::new(&resolved)
.file_name()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty())
.unwrap_or("workspace");
Ok(format!("{name}-{}", stable_hash_hex(&resolved)[..10].to_string()))
}
pub fn thread_key(workspace: &str, mission_id: Option<&str>) -> Result<String, String> {
if let Some(mission_id) = mission_id {
return Ok(format!("mission:{mission_id}"));
}
Ok(format!("workspace:{}", resolve_workspace(workspace)?))
}
pub fn chat_file(workspace: &str, mission_id: Option<&str>) -> Result<PathBuf, String> {
paths::ensure_runtime_dirs()?;
if let Some(mission_id) = mission_id {
return Ok(paths::chat_root().join("missions").join(format!("{mission_id}.ndjson")));
}
Ok(paths::chat_root().join("workspaces").join(format!(
"{}.ndjson",
workspace_chat_slug(workspace)?
)))
}
pub fn append_chat_message(
role: &str,
content: &str,
workspace: &str,
mission_id: Option<&str>,
intent: Option<&str>,
machine: Option<&str>,
pane: Option<&str>,
meta: Option<serde_json::Map<String, serde_json::Value>>,
) -> Result<ChatEntry, String> {
let resolved_workspace = resolve_workspace(workspace)?;
let path = chat_file(&resolved_workspace, mission_id)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("cannot create {}: {err}", parent.display()))?;
}
let entry = ChatEntry {
timestamp: now_utc(),
role: role.to_string(),
content: content.to_string(),
intent: intent.unwrap_or("plain_chat").to_string(),
workspace: resolved_workspace.clone(),
mission_id: mission_id.map(ToString::to_string),
machine: machine.map(ToString::to_string),
pane: pane.map(ToString::to_string),
meta: meta.unwrap_or_default(),
};
let line = serde_json::to_string(&entry).map_err(|err| format!("cannot encode chat entry: {err}"))?;
let mut handle = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| format!("cannot open {}: {err}", path.display()))?;
handle
.write_all(format!("{line}\n").as_bytes())
.map_err(|err| format!("cannot append {}: {err}", path.display()))?;
let mission = mission_id.map(load_mission).transpose()?;
let thread_index = refresh_thread_index(
&resolved_workspace,
mission_id,
mission
.as_ref()
.map(|value| value.title.as_str())
.unwrap_or("Workspace chat"),
mission.as_ref().map(|value| value.status.as_str()),
)?;
if thread_index.is_none() {
return Err(format!("failed to update thread index for {}", path.display()));
}
Ok(entry)
}
pub fn read_chat_history(
workspace: &str,
mission_id: Option<&str>,
limit: usize,
) -> Result<Vec<ChatEntry>, String> {
let path = chat_file(workspace, mission_id)?;
if !path.exists() {
return Ok(Vec::new());
}
let raw_lines = tail_lines(&path, limit, 256 * 1024)?;
let mut entries = Vec::new();
for raw in raw_lines {
if raw.trim().is_empty() {
continue;
}
if let Ok(entry) = serde_json::from_str::<ChatEntry>(&raw) {
entries.push(entry);
}
}
Ok(entries)
}
pub fn refresh_thread_index(
workspace: &str,
mission_id: Option<&str>,
title: &str,
mission_status: Option<&str>,
) -> Result<Option<ThreadIndex>, String> {
let resolved_workspace = resolve_workspace(workspace)?;
let path = chat_file(&resolved_workspace, mission_id)?;
if !path.exists() {
remove_thread_index(&resolved_workspace, mission_id)?;
return Ok(None);
}
let mtime = file_mtime_secs(&path)?;
if let Some(index) = load_thread_index(&resolved_workspace, mission_id)? {
if index.chat_mtime >= mtime {
let mut refreshed = index;
refreshed.title = title.to_string();
refreshed.mission_status = mission_status.map(ToString::to_string);
save_thread_index(&refreshed)?;
return Ok(Some(refreshed));
}
}
let mut line_count = 0_u64;
let mut last_entry: Option<ChatEntry> = None;
let file = File::open(&path).map_err(|err| format!("cannot open {}: {err}", path.display()))?;
for raw in BufReader::new(file).lines() {
let raw = raw.map_err(|err| format!("cannot read {}: {err}", path.display()))?;
if raw.trim().is_empty() {
continue;
}
line_count += 1;
if let Ok(entry) = serde_json::from_str::<ChatEntry>(&raw) {
last_entry = Some(entry);
}
}
let Some(last_entry) = last_entry else {
return Ok(None);
};
let index = ThreadIndex {
thread_key: thread_key(&resolved_workspace, mission_id)?,
workspace: resolved_workspace.clone(),
mission_id: mission_id.map(ToString::to_string),
kind: if mission_id.is_some() {
"mission".to_string()
} else {
"workspace".to_string()
},
title: title.to_string(),
mission_status: mission_status.map(ToString::to_string),
message_count: line_count,
last_role: last_entry.role.clone(),
last_preview: content_preview(&last_entry.content, 96),
last_timestamp: last_entry.timestamp.clone(),
chat_mtime: mtime,
updated_at: now_utc(),
};
save_thread_index(&index)?;
Ok(Some(index))
}
pub fn list_thread_summaries(workspace: &str) -> Result<Vec<ThreadSummary>, String> {
let resolved_workspace = resolve_workspace(workspace)?;
let view_state = load_thread_view_state(&resolved_workspace)?;
let workspace_index = refresh_thread_index(&resolved_workspace, None, "Workspace chat", None)?;
let workspace_summary = workspace_index.map(|index| to_summary(index, &view_state));
let mut missions = list_missions()?
.into_iter()
.filter(|mission| mission.workspace == resolved_workspace)
.collect::<Vec<_>>();
missions.sort_by(|left, right| right.updated_at.cmp(&left.updated_at));
let mut summaries = Vec::new();
if let Some(summary) = workspace_summary {
summaries.push(summary);
} else {
summaries.push(ThreadSummary {
thread_key: thread_key(&resolved_workspace, None)?,
workspace: resolved_workspace.clone(),
mission_id: None,
kind: "workspace".to_string(),
title: "Workspace chat".to_string(),
mission_status: None,
message_count: 0,
unread_count: 0,
last_role: String::new(),
last_preview: "Start a conversation".to_string(),
last_timestamp: String::new(),
});
}
for mission in missions {
if let Some(index) = refresh_thread_index(
&resolved_workspace,
Some(&mission.mission_id),
&mission.title,
Some(&mission.status),
)? {
summaries.push(to_summary(index, &view_state));
}
}
Ok(summaries)
}
pub fn latest_chat_summary(workspace: &str) -> Result<Option<ThreadSummary>, String> {
let mut summaries = list_thread_summaries(workspace)?;
summaries.retain(|summary| summary.message_count > 0);
summaries.sort_by(|left, right| {
right
.last_timestamp
.cmp(&left.last_timestamp)
.then_with(|| right.message_count.cmp(&left.message_count))
});
Ok(summaries.into_iter().next())
}
pub fn render_chat_dock_line(
workspace: &str,
scope_label: Option<&str>,
machine_label: Option<&str>,
max_length: usize,
) -> Result<String, String> {
let workspace_name = Path::new(&resolve_workspace(workspace)?)
.file_name()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty())
.unwrap_or("workspace")
.to_string();
let mut context = vec!["workspace".to_string(), workspace_name];
if let Some(scope_label) = scope_label.filter(|value| !value.is_empty()) {
context.insert(0, scope_label.to_string());
} else if let Some(machine_label) = machine_label.filter(|value| !value.is_empty()) {
context.insert(0, machine_label.to_string());
}
let line = if let Some(summary) = latest_chat_summary(workspace)? {
let thread_prefix = if summary.kind == "mission" {
"latest mission"
} else {
"latest chat"
};
format!(
"{} | {}: {} | {}: {}",
context.join(" | "),
thread_prefix,
summary.title,
role_label(&summary.last_role),
summary.last_preview
)
} else {
format!(
"{} | No chats yet. Open Constant to start.",
context.join(" | ")
)
};
Ok(clip_line(&line, max_length))
}
pub fn load_thread_view_state(workspace: &str) -> Result<ThreadViewState, String> {
let path = thread_view_state_path(workspace)?;
if !path.exists() {
return Ok(ThreadViewState::default());
}
let text = fs::read_to_string(&path).map_err(|err| format!("cannot read {}: {err}", path.display()))?;
serde_json::from_str(&text).map_err(|err| format!("cannot parse {}: {err}", path.display()))
}
pub fn mark_thread_seen(workspace: &str, thread_key: &str, seen_count: u64) -> Result<(), String> {
let path = thread_view_state_path(workspace)?;
let mut view_state = load_thread_view_state(workspace)?;
view_state
.seen_counts
.insert(thread_key.to_string(), seen_count);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("cannot create {}: {err}", parent.display()))?;
}
let payload = serde_json::to_string_pretty(&view_state)
.map_err(|err| format!("cannot encode thread view state: {err}"))?;
fs::write(&path, format!("{payload}\n"))
.map_err(|err| format!("cannot write {}: {err}", path.display()))
}
pub fn delete_mission_thread(mission_id: &str) -> Result<(), String> {
paths::ensure_runtime_dirs()?;
let mission = load_mission(mission_id)?;
if matches!(mission.status.as_str(), "draft" | "planned" | "running") {
return Err(format!(
"Mission {mission_id} is {} and cannot be deleted yet.",
mission.status
));
}
let workspace = resolve_workspace(&mission.workspace)?;
let mission_chat = chat_file(&workspace, Some(mission_id))?;
if mission_chat.exists() {
fs::remove_file(&mission_chat)
.map_err(|err| format!("cannot remove {}: {err}", mission_chat.display()))?;
}
let mission_dir = paths::mission_dir(mission_id);
if mission_dir.exists() {
fs::remove_dir_all(&mission_dir)
.map_err(|err| format!("cannot remove {}: {err}", mission_dir.display()))?;
}
remove_thread_index(&workspace, Some(mission_id))?;
let thread_key = thread_key(&workspace, Some(mission_id))?;
let mut view_state = load_thread_view_state(&workspace)?;
if view_state.seen_counts.remove(&thread_key).is_some() {
let path = thread_view_state_path(&workspace)?;
let payload = serde_json::to_string_pretty(&view_state)
.map_err(|err| format!("cannot encode thread view state: {err}"))?;
fs::write(&path, format!("{payload}\n"))
.map_err(|err| format!("cannot write {}: {err}", path.display()))?;
}
Ok(())
}
fn to_summary(index: ThreadIndex, view_state: &ThreadViewState) -> ThreadSummary {
let seen_count = view_state
.seen_counts
.get(&index.thread_key)
.copied()
.unwrap_or(0);
ThreadSummary {
unread_count: index.message_count.saturating_sub(seen_count),
thread_key: index.thread_key,
workspace: index.workspace,
mission_id: index.mission_id,
kind: index.kind,
title: index.title,
mission_status: index.mission_status,
message_count: index.message_count,
last_role: index.last_role,
last_preview: if index.last_preview.is_empty() {
"Start a conversation".to_string()
} else {
index.last_preview
},
last_timestamp: index.last_timestamp,
}
}
fn thread_index_path(workspace: &str, mission_id: Option<&str>) -> Result<PathBuf, String> {
if let Some(mission_id) = mission_id {
return Ok(paths::chat_threads_index_dir().join(format!("mission-{mission_id}.json")));
}
Ok(paths::chat_threads_index_dir().join(format!(
"workspace-{}.json",
workspace_chat_slug(workspace)?
)))
}
fn remove_thread_index(workspace: &str, mission_id: Option<&str>) -> Result<(), String> {
let path = thread_index_path(workspace, mission_id)?;
if path.exists() {
fs::remove_file(&path).map_err(|err| format!("cannot remove {}: {err}", path.display()))?;
}
Ok(())
}
fn load_thread_index(workspace: &str, mission_id: Option<&str>) -> Result<Option<ThreadIndex>, String> {
let path = thread_index_path(workspace, mission_id)?;
if !path.exists() {
return Ok(None);
}
let text = fs::read_to_string(&path).map_err(|err| format!("cannot read {}: {err}", path.display()))?;
let payload = serde_json::from_str(&text).map_err(|err| format!("cannot parse {}: {err}", path.display()))?;
Ok(Some(payload))
}
fn save_thread_index(index: &ThreadIndex) -> Result<(), String> {
let path = thread_index_path(&index.workspace, index.mission_id.as_deref())?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("cannot create {}: {err}", parent.display()))?;
}
let payload = serde_json::to_string_pretty(index)
.map_err(|err| format!("cannot encode thread index: {err}"))?;
fs::write(&path, format!("{payload}\n"))
.map_err(|err| format!("cannot write {}: {err}", path.display()))
}
fn thread_view_state_path(workspace: &str) -> Result<PathBuf, String> {
Ok(paths::chat_views_dir().join(format!(
"workspace-{}.json",
workspace_chat_slug(workspace)?
)))
}
fn role_label(role: &str) -> &'static str {
match role {
"user" => "YOU",
"constant" => "CONSTANT",
"buddy" => "BUDDY",
"system" => "SYSTEM",
_ => "CHAT",
}
}
fn content_preview(text: &str, limit: usize) -> String {
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
if normalized.len() <= limit {
return normalized;
}
format!("{}...", normalized.chars().take(limit.saturating_sub(3)).collect::<String>().trim_end())
}
fn clip_line(line: &str, max_length: usize) -> String {
if line.len() <= max_length {
return line.to_string();
}
format!(
"{}...",
line.chars()
.take(max_length.saturating_sub(3))
.collect::<String>()
.trim_end()
)
}
fn tail_lines(path: &Path, limit: usize, max_bytes: usize) -> Result<Vec<String>, String> {
let mut file = File::open(path).map_err(|err| format!("cannot open {}: {err}", path.display()))?;
let file_len = file
.metadata()
.map_err(|err| format!("cannot stat {}: {err}", path.display()))?
.len() as usize;
let mut chunk = 8192_usize;
let mut consumed = 0_usize;
let mut buffer = Vec::new();
while consumed < file_len && consumed < max_bytes {
let next = chunk.min(file_len - consumed).min(max_bytes - consumed);
consumed += next;
let offset = (file_len - consumed) as u64;
file.seek(SeekFrom::Start(offset))
.map_err(|err| format!("cannot seek {}: {err}", path.display()))?;
let mut bytes = vec![0_u8; next];
file.read_exact(&mut bytes)
.map_err(|err| format!("cannot read {}: {err}", path.display()))?;
bytes.extend(buffer);
buffer = bytes;
let newline_count = buffer.iter().filter(|byte| **byte == b'\n').count();
if newline_count > limit {
break;
}
chunk = (chunk * 2).min(max_bytes.max(1));
}
let text = String::from_utf8_lossy(&buffer);
let mut lines = text.lines().map(ToString::to_string).collect::<Vec<_>>();
if lines.len() > limit {
lines = lines.split_off(lines.len() - limit);
}
Ok(lines)
}
fn file_mtime_secs(path: &Path) -> Result<u64, String> {
let modified = path
.metadata()
.and_then(|meta| meta.modified())
.map_err(|err| format!("cannot stat {}: {err}", path.display()))?;
Ok(modified
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0))
}
fn stable_hash_hex(value: &str) -> String {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in value.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
+51 -2
View File
@@ -21,6 +21,8 @@ pub struct PaneStatus {
pub active: bool,
pub dead: bool,
pub dead_status: i32,
pub autorestart_failures: u32,
pub autorestart_disabled: bool,
}
#[derive(Debug, Clone, Serialize)]
@@ -141,6 +143,23 @@ pub fn cockpit_doctor(local_session: &str, machine_session: &str) -> Result<Valu
}))
}
pub fn cockpit_status_line(
workspace: &str,
scope_label: Option<&str>,
machine_label: Option<&str>,
max_length: usize,
local_session: &str,
machine_session: &str,
) -> Result<String, String> {
let chat_line =
crate::chat::render_chat_dock_line(workspace, scope_label, machine_label, max_length)?;
let runtime = runtime_status(local_session, machine_session)?;
if let Some(warning) = autorestart_warning(&runtime) {
return Ok(clip_line(&format!("{chat_line} | {warning}"), max_length));
}
Ok(chat_line)
}
pub fn cockpit_open(
workspace: &str,
local_session: &str,
@@ -325,7 +344,7 @@ fn parse_panes(stdout: &str) -> Vec<PaneStatus> {
.lines()
.filter_map(|raw| {
let parts = raw.split('\t').collect::<Vec<_>>();
if parts.len() != 10 {
if parts.len() != 12 {
return None;
}
let role = if !parts[4].is_empty() {
@@ -347,6 +366,8 @@ fn parse_panes(stdout: &str) -> Vec<PaneStatus> {
active: parts[7] == "1",
dead: parts[8] == "1",
dead_status: parts[9].parse().unwrap_or(0),
autorestart_failures: parts[10].parse().unwrap_or(0),
autorestart_disabled: parts[11] == "1",
})
})
.collect::<Vec<_>>();
@@ -361,10 +382,38 @@ fn tmux_list_command(session_name: &str) -> Vec<String> {
"-t".to_string(),
session_name.to_string(),
"-F".to_string(),
"#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_index}\t#{@constant_role}\t#{pane_title}\t#{pane_current_command}\t#{pane_active}\t#{pane_dead}\t#{pane_dead_status}".to_string(),
"#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_index}\t#{@constant_role}\t#{pane_title}\t#{pane_current_command}\t#{pane_active}\t#{pane_dead}\t#{pane_dead_status}\t#{@constant_restart_failures}\t#{@constant_autorestart_disabled}".to_string(),
]
}
fn autorestart_warning(runtime: &FleetRuntimeStatus) -> Option<String> {
for machine in &runtime.machines {
for pane in &machine.panes {
if pane.autorestart_disabled {
return Some(format!(
"respawn disabled after {} failures: {}",
pane.autorestart_failures.max(1),
pane.role
));
}
}
}
None
}
fn clip_line(line: &str, max_length: usize) -> String {
if line.len() <= max_length {
return line.to_string();
}
format!(
"{}...",
line.chars()
.take(max_length.saturating_sub(3))
.collect::<String>()
.trim_end()
)
}
fn ssh_command(target: &str, inner: &str) -> Vec<String> {
vec![
"ssh".to_string(),
+35
View File
@@ -173,6 +173,23 @@ pub fn load_fleet_config() -> Result<FleetConfig, String> {
Ok(config)
}
pub fn read_fleet_config_if_present() -> Result<Option<FleetConfig>, String> {
paths::ensure_runtime_dirs()?;
let mut config = if paths::fleet_toml_path().exists() {
parse_toml_file(&paths::fleet_toml_path())?
} else if paths::fleet_json_path().exists() {
parse_json_file(&paths::fleet_json_path())?
} else if paths::fleet_yaml_path().exists() {
parse_json_file(&paths::fleet_yaml_path())?
} else {
return Ok(None);
};
normalize_fleet(&mut config);
Ok(Some(config))
}
pub fn load_models_config() -> Result<ModelsConfig, String> {
let config = load_with_legacy(
&paths::models_toml_path(),
@@ -212,6 +229,24 @@ pub fn save_memory_config(config: &MemoryConfig) -> Result<(), String> {
)
}
pub fn write_fleet_config_file(path: &Path, config: &FleetConfig) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("cannot create {}: {err}", parent.display()))?;
}
let ext = path.extension().and_then(|value| value.to_str()).unwrap_or("");
let text = if ext.eq_ignore_ascii_case("toml") {
toml::to_string_pretty(config).map_err(|err| format!("cannot encode TOML: {err}"))?
} else {
serde_json::to_string_pretty(config)
.map_err(|err| format!("cannot encode JSON: {err}"))?
};
fs::write(path, format!("{text}\n"))
.map_err(|err| format!("cannot write {}: {err}", path.display()))
}
pub fn fleet_machine<'a>(
fleet: &'a FleetConfig,
needle: &str,
+123
View File
@@ -1,5 +1,9 @@
use std::fs;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use crate::config::{FleetConfig, MachineConfig, read_fleet_config_if_present, write_fleet_config_file};
use crate::paths;
pub fn fleet_check() -> Result<Value, String> {
@@ -54,3 +58,122 @@ pub fn bridge_sync() -> Result<Value, String> {
"stderr": String::from_utf8_lossy(&output.stderr),
}))
}
pub fn fleet_config_query(expr: &str) -> Result<Option<Vec<String>>, String> {
let Some(config) = read_fleet_config_if_present()? else {
return Ok(None);
};
let values = match expr {
"repo_dir" => vec![config.repo_dir],
"local_machine" => vec![config.local_machine],
"machine_specs" => config
.machines
.into_iter()
.map(|machine| format!("{}={}", machine.label, machine.target))
.collect(),
other => return Err(format!("unknown fleet config query: {other}")),
};
Ok(Some(values))
}
pub fn render_scan_json(path: &Path) -> Result<Value, String> {
let text =
fs::read_to_string(path).map_err(|err| format!("cannot read {}: {err}", path.display()))?;
let mut candidates = Vec::new();
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() != 9 {
return Err(format!(
"cannot parse {}: expected 9 tab-separated fields, got {}",
path.display(),
fields.len()
));
}
candidates.push(json!({
"seed": fields[0],
"user": fields[1],
"host": fields[2],
"port": fields[3].parse::<u16>().map_err(|_| {
format!("cannot parse port '{}' in {}", fields[3], path.display())
})?,
"reachable": fields[4] == "yes",
"remote_name": fields[5],
"remote_os": fields[6],
"remote_home": fields[7],
"error": fields[8],
}));
}
Ok(json!({ "candidates": candidates }))
}
pub fn write_fleet_config(
finalized_file: &Path,
output_path: &Path,
repo_dir: &str,
) -> Result<PathBuf, String> {
let text = fs::read_to_string(finalized_file)
.map_err(|err| format!("cannot read {}: {err}", finalized_file.display()))?;
let mut machines = Vec::new();
let mut local_machine: Option<String> = None;
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() != 4 {
return Err(format!(
"cannot parse {}: expected 4 tab-separated fields, got {}",
finalized_file.display(),
fields.len()
));
}
let label = fields[0].to_string();
let role = fields[1];
let user = fields[2];
let seed = fields[3];
if role == "local" {
local_machine = Some(label.clone());
machines.push(MachineConfig {
label,
target: "local".to_string(),
auto_clis: vec!["codex".into(), "vibe".into(), "claude".into()],
manual_clis: vec!["copilot".into()],
backends: vec!["omc".into(), "cli-local".into(), "cockpit".into()],
});
} else {
machines.push(MachineConfig {
label,
target: format!("{user}@{seed}"),
auto_clis: vec!["codex".into(), "vibe".into(), "claude".into()],
manual_clis: vec!["copilot".into()],
backends: vec!["cli-ssh".into(), "cockpit".into()],
});
}
}
let config = FleetConfig {
version: 1,
local_machine: local_machine.unwrap_or_else(|| "command-center".to_string()),
repo_dir: repo_dir.to_string(),
machines,
};
let resolved_output = if output_path.is_absolute() {
output_path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|err| format!("cannot resolve current directory: {err}"))?
.join(output_path)
};
write_fleet_config_file(&resolved_output, &config)?;
Ok(resolved_output)
}
+443 -151
View File
@@ -1,17 +1,18 @@
mod buddy;
mod capabilities;
mod chat;
mod cockpit;
mod config;
mod executor;
mod fleet;
mod memory;
mod mission;
mod operator;
mod paths;
mod state;
mod tui;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
@@ -23,15 +24,22 @@ use serde_json::{Value, json};
use buddy::buddy_ask;
use capabilities::{AGENTS, MINIMAL_STACK, SKILLS, VERSION, WORKFLOW_STACK};
use chat::delete_mission_thread;
use cockpit::{
capture_pane, cockpit_attach, cockpit_doctor, cockpit_open, focus_machine, restart_pane,
runtime_status, send_to_pane,
capture_pane, cockpit_attach, cockpit_doctor, cockpit_open, cockpit_status_line,
focus_machine, restart_pane, runtime_status, send_to_pane,
};
use config::{fleet_machine, load_fleet_config, load_memory_config, load_models_config};
use executor::{run_mission, summarize_mission_command, verify_mission};
use fleet::{bridge_sync, fleet_check};
use memory::summarize_mission_to_memory;
use fleet::{
bridge_sync, fleet_check, fleet_config_query, render_scan_json, write_fleet_config,
};
use memory::{
enroll_workspace, list_decisions, memory_status, persona_markdown, rebuild_workspace_memory,
search_memory, summarize_mission_to_memory, sync_qdrant,
};
use mission::{delegate_step, plan_mission, retry_mission};
use operator::{health_value, models_status_value};
use state::{
append_event, list_missions, load_mission, mission_events_text, mission_summary_value,
new_mission, save_mission,
@@ -65,15 +73,16 @@ fn run() -> Result<ExitCode, String> {
}
"skills" => handle_skills(&args[1..]),
"agents" => handle_agents(&args[1..]),
"models" => handle_models(&args[1..]),
"doctor" => handle_doctor(&args[1..]),
"tui" => handle_tui(&args[1..]),
"cockpit" => handle_cockpit(&args[1..]),
"mission" => handle_mission(&args[1..]),
"delegate" => handle_delegate(&args[1..]),
"buddy" => handle_buddy(&args[1..]),
"memory" => handoff_to_python(&args),
"memory" => handle_memory(&args[1..]),
"fleet" => handle_fleet(&args[1..]),
_ => handoff_to_python(&args),
other => Err(format!("unknown command: {other}")),
}
}
@@ -104,21 +113,24 @@ Rust front-controller for Constant.
Commands handled in Rust:
doctor [--json]
models status [--json]
agents [--json]
skills [--json] [--public-only]
tui [--workspace DIR] [--local-session NAME] [--session NAME]
cockpit open|attach|status|doctor|focus|send|capture|restart
cockpit open|attach|status|status-line|doctor|focus|send|capture|restart
mission create <prompt> [--workspace DIR] [--json]
mission plan <mission_id> [--json]
mission run <mission_id> [--json]
mission delete <mission_id> [--json]
mission status [mission_id] [--verbose] [--json]
mission tail <mission_id> [--follow]
mission verify <mission_id> [--step-id ID] [--json]
mission retry <mission_id> [--step-id ID] [--json]
mission summarize <mission_id> [--json]
delegate <mission_id> [--step-id ID] [--machine LABEL] [--backend NAME] [--cli NAME] [--agent ID] [--skill ID] [--json]
buddy ask <prompt> [--mission-id ID] [--json]
memory status|rebuild|enroll|search|persona show|decisions|sync-qdrant
Other commands still hand off to the existing Python runtime during migration.
Running `{prog}` with no arguments opens or attaches the full fleet cockpit.
Use `{prog} tui --workspace DIR` for the standalone TUI."
);
@@ -135,19 +147,29 @@ fn handle_skills(args: &[String]) -> Result<ExitCode, String> {
match arg.as_str() {
"--json" => as_json = true,
"--public-only" => public_only = true,
_ => return handoff_to_python_with_prefix("skills", args),
other => return Err(format!("unknown skills option: {other}")),
}
}
print_skills(as_json, public_only)?;
Ok(ExitCode::SUCCESS)
}
fn handle_models(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() {
return Err("models requires a subcommand".to_string());
}
match args[0].as_str() {
"status" => models_status_cmd(&args[1..]),
other => Err(format!("unknown models subcommand: {other}")),
}
}
fn handle_agents(args: &[String]) -> Result<ExitCode, String> {
let mut as_json = false;
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
_ => return handoff_to_python_with_prefix("agents", args),
other => return Err(format!("unknown agents option: {other}")),
}
}
print_agents(as_json)?;
@@ -159,13 +181,25 @@ fn handle_doctor(args: &[String]) -> Result<ExitCode, String> {
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
_ => return handoff_to_python_with_prefix("doctor", args),
other => return Err(format!("unknown doctor option: {other}")),
}
}
print_doctor(as_json)?;
Ok(ExitCode::SUCCESS)
}
fn models_status_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut as_json = false;
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
other => return Err(format!("unknown models status option: {other}")),
}
}
print_value(&models_status_value()?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn handle_tui(args: &[String]) -> Result<ExitCode, String> {
let mut workspace = env::current_dir()
.map_err(|err| format!("cannot resolve current directory: {err}"))?
@@ -198,17 +232,13 @@ fn handle_tui(args: &[String]) -> Result<ExitCode, String> {
.cloned()
.ok_or_else(|| "--session requires a value".to_string())?;
}
_ => return handoff_to_python_with_prefix("tui", args),
other => return Err(format!("unknown tui option: {other}")),
}
index += 1;
}
let workspace = canonical_workspace(&workspace)?;
match run_tui(
workspace.clone(),
local_session.clone(),
machine_session.clone(),
)? {
match run_tui(workspace.clone(), local_session.clone(), machine_session.clone())? {
TuiAction::Exit => Ok(ExitCode::SUCCESS),
TuiAction::OpenCockpit => Ok(exit_code_from_status(Some(cockpit_open(
&workspace,
@@ -228,16 +258,13 @@ fn handle_cockpit(args: &[String]) -> Result<ExitCode, String> {
"open" => cockpit_open_cmd(&args[1..]),
"attach" => cockpit_attach_cmd(&args[1..]),
"status" => cockpit_status_cmd(&args[1..]),
"status-line" => cockpit_status_line_cmd(&args[1..]),
"doctor" => cockpit_doctor_cmd(&args[1..]),
"focus" => cockpit_focus_cmd(&args[1..]),
"send" => cockpit_send_cmd(&args[1..]),
"capture" => cockpit_capture_cmd(&args[1..]),
"restart" => cockpit_restart_cmd(&args[1..]),
_ => {
let mut forwarded = vec!["cockpit".to_string()];
forwarded.extend(args.iter().cloned());
handoff_to_python(&forwarded)
}
other => Err(format!("unknown cockpit subcommand: {other}")),
}
}
@@ -250,16 +277,13 @@ fn handle_mission(args: &[String]) -> Result<ExitCode, String> {
"create" => mission_create(&args[1..]),
"plan" => mission_plan(&args[1..]),
"run" => mission_run_cmd(&args[1..]),
"delete" => mission_delete_cmd(&args[1..]),
"status" => mission_status(&args[1..]),
"tail" => mission_tail(&args[1..]),
"verify" => mission_verify_cmd(&args[1..]),
"retry" => mission_retry_cmd(&args[1..]),
"summarize" => mission_summarize_cmd(&args[1..]),
_ => {
let mut forwarded = vec!["mission".to_string()];
forwarded.extend(args.iter().cloned());
handoff_to_python(&forwarded)
}
other => Err(format!("unknown mission subcommand: {other}")),
}
}
@@ -269,12 +293,24 @@ fn handle_buddy(args: &[String]) -> Result<ExitCode, String> {
}
match args[0].as_str() {
"ask" => buddy_ask_cmd(&args[1..]),
_ => {
let mut forwarded = vec!["buddy".to_string()];
forwarded.extend(args.iter().cloned());
handoff_to_python(&forwarded)
other => Err(format!("unknown buddy subcommand: {other}")),
}
}
fn handle_memory(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() {
return Err("memory requires a subcommand".to_string());
}
match args[0].as_str() {
"status" => memory_status_cmd(&args[1..]),
"rebuild" => memory_rebuild_cmd(&args[1..]),
"enroll" => memory_enroll_cmd(&args[1..]),
"search" => memory_search_cmd(&args[1..]),
"persona" => memory_persona_cmd(&args[1..]),
"decisions" => memory_decisions_cmd(&args[1..]),
"sync-qdrant" => memory_sync_qdrant_cmd(&args[1..]),
other => Err(format!("unknown memory subcommand: {other}")),
}
}
fn handle_fleet(args: &[String]) -> Result<ExitCode, String> {
@@ -284,11 +320,10 @@ fn handle_fleet(args: &[String]) -> Result<ExitCode, String> {
match args[0].as_str() {
"status" => fleet_status_cmd(&args[1..]),
"sync" => fleet_sync_cmd(&args[1..]),
_ => {
let mut forwarded = vec!["fleet".to_string()];
forwarded.extend(args.iter().cloned());
handoff_to_python(&forwarded)
}
"config-get" => fleet_config_get_cmd(&args[1..]),
"render-scan-json" => fleet_render_scan_json_cmd(&args[1..]),
"write-config" => fleet_write_config_cmd(&args[1..]),
other => Err(format!("unknown fleet subcommand: {other}")),
}
}
@@ -311,12 +346,10 @@ fn mission_create(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--workspace requires a value".to_string())?;
}
"--json" => as_json = true,
value if value.starts_with("--") => {
return handoff_mission_subcommand("create", args);
}
value if value.starts_with("--") => return Err(format!("unknown mission create option: {value}")),
value => {
if prompt.is_some() {
return handoff_mission_subcommand("create", args);
return Err("mission create accepts a single prompt".to_string());
}
prompt = Some(value.to_string());
}
@@ -360,10 +393,10 @@ fn mission_plan(args: &[String]) -> Result<ExitCode, String> {
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
value if value.starts_with("--") => return handoff_mission_subcommand("plan", args),
value if value.starts_with("--") => return Err(format!("unknown mission plan option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("plan", args);
return Err("mission plan accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -402,10 +435,10 @@ fn mission_run_cmd(args: &[String]) -> Result<ExitCode, String> {
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
value if value.starts_with("--") => return handoff_mission_subcommand("run", args),
value if value.starts_with("--") => return Err(format!("unknown mission run option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("run", args);
return Err("mission run accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -427,10 +460,10 @@ fn mission_status(args: &[String]) -> Result<ExitCode, String> {
match arg.as_str() {
"--verbose" => verbose = true,
"--json" => as_json = true,
value if value.starts_with("--") => return handoff_mission_subcommand("status", args),
value if value.starts_with("--") => return Err(format!("unknown mission status option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("status", args);
return Err("mission status accepts at most one mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -464,10 +497,10 @@ fn mission_tail(args: &[String]) -> Result<ExitCode, String> {
for arg in args {
match arg.as_str() {
"--follow" => follow = true,
value if value.starts_with("--") => return handoff_mission_subcommand("tail", args),
value if value.starts_with("--") => return Err(format!("unknown mission tail option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("tail", args);
return Err("mission tail accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -513,10 +546,10 @@ fn mission_verify_cmd(args: &[String]) -> Result<ExitCode, String> {
);
}
"--json" => as_json = true,
value if value.starts_with("--") => return handoff_mission_subcommand("verify", args),
value if value.starts_with("--") => return Err(format!("unknown mission verify option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("verify", args);
return Err("mission verify accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -549,10 +582,10 @@ fn mission_retry_cmd(args: &[String]) -> Result<ExitCode, String> {
);
}
"--json" => as_json = true,
value if value.starts_with("--") => return handoff_mission_subcommand("retry", args),
value if value.starts_with("--") => return Err(format!("unknown mission retry option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("retry", args);
return Err("mission retry accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -575,12 +608,10 @@ fn mission_summarize_cmd(args: &[String]) -> Result<ExitCode, String> {
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
value if value.starts_with("--") => {
return handoff_mission_subcommand("summarize", args);
}
value if value.starts_with("--") => return Err(format!("unknown mission summarize option: {value}")),
value => {
if mission_id.is_some() {
return handoff_mission_subcommand("summarize", args);
return Err("mission summarize accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
@@ -598,6 +629,27 @@ fn mission_summarize_cmd(args: &[String]) -> Result<ExitCode, String> {
Ok(ExitCode::SUCCESS)
}
fn mission_delete_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut mission_id: Option<String> = None;
let mut as_json = false;
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
value if value.starts_with("--") => return Err(format!("unknown mission delete option: {value}")),
value => {
if mission_id.is_some() {
return Err("mission delete accepts a single mission_id".to_string());
}
mission_id = Some(value.to_string());
}
}
}
let mission_id = mission_id.ok_or_else(|| "mission delete requires a mission_id".to_string())?;
delete_mission_thread(&mission_id)?;
print_value(&json!({ "deleted": true, "mission_id": mission_id }), as_json)?;
Ok(ExitCode::SUCCESS)
}
fn handle_delegate(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() {
return Err("delegate requires a mission_id".to_string());
@@ -663,10 +715,8 @@ fn handle_delegate(args: &[String]) -> Result<ExitCode, String> {
);
}
"--json" => as_json = true,
value if value.starts_with("--") => {
return handoff_to_python_with_prefix("delegate", args);
}
_ => return handoff_to_python_with_prefix("delegate", args),
value if value.starts_with("--") => return Err(format!("unknown delegate option: {value}")),
value => return Err(format!("unexpected delegate argument: {value}")),
}
index += 1;
}
@@ -707,16 +757,10 @@ fn buddy_ask_cmd(args: &[String]) -> Result<ExitCode, String> {
);
}
"--json" => as_json = true,
value if value.starts_with("--") => {
let mut forwarded = vec!["buddy".to_string(), "ask".to_string()];
forwarded.extend(args.iter().cloned());
return handoff_to_python(&forwarded);
}
value if value.starts_with("--") => return Err(format!("unknown buddy ask option: {value}")),
value => {
if prompt.is_some() {
let mut forwarded = vec!["buddy".to_string(), "ask".to_string()];
forwarded.extend(args.iter().cloned());
return handoff_to_python(&forwarded);
return Err("buddy ask accepts a single prompt".to_string());
}
prompt = Some(value.to_string());
}
@@ -731,6 +775,197 @@ fn buddy_ask_cmd(args: &[String]) -> Result<ExitCode, String> {
Ok(ExitCode::SUCCESS)
}
fn memory_status_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut workspace: Option<String> = None;
let mut as_json = false;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?,
);
}
"--json" => as_json = true,
other => return Err(format!("unknown memory status option: {other}")),
}
index += 1;
}
print_value(&memory_status(workspace.as_deref())?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn memory_rebuild_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut workspace = env::current_dir()
.map_err(|err| format!("cannot resolve current directory: {err}"))?
.to_string_lossy()
.into_owned();
let mut enroll = true;
let mut as_json = false;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = args
.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?;
}
"--no-enroll" => enroll = false,
"--json" => as_json = true,
other => return Err(format!("unknown memory rebuild option: {other}")),
}
index += 1;
}
print_value(&rebuild_workspace_memory(&workspace, enroll)?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn memory_enroll_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut path: Option<String> = None;
let mut as_json = false;
for arg in args {
match arg.as_str() {
"--json" => as_json = true,
value if value.starts_with("--") => return Err(format!("unknown memory enroll option: {value}")),
value => {
if path.is_some() {
return Err("memory enroll accepts a single path".to_string());
}
path = Some(value.to_string());
}
}
}
let path = path.ok_or_else(|| "memory enroll requires a path".to_string())?;
print_value(&enroll_workspace(&path)?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn memory_search_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut query: Option<String> = None;
let mut workspace: Option<String> = None;
let mut limit: Option<usize> = None;
let mut as_json = false;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?,
);
}
"--limit" => {
index += 1;
limit = Some(
args.get(index)
.ok_or_else(|| "--limit requires a value".to_string())?
.parse::<usize>()
.map_err(|_| "--limit must be an integer".to_string())?,
);
}
"--json" => as_json = true,
value if value.starts_with("--") => return Err(format!("unknown memory search option: {value}")),
value => {
if query.is_some() {
return Err("memory search accepts a single query".to_string());
}
query = Some(value.to_string());
}
}
index += 1;
}
let query = query.ok_or_else(|| "memory search requires a query".to_string())?;
print_value(&search_memory(&query, workspace.as_deref(), limit)?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn memory_persona_cmd(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() {
return Err("memory persona requires a subcommand".to_string());
}
match args[0].as_str() {
"show" => {
let as_json = args.iter().skip(1).any(|arg| arg == "--json");
if args.iter().skip(1).any(|arg| arg != "--json") {
return Err("unknown memory persona show option".to_string());
}
let persona = persona_markdown()?;
if as_json {
print_value(&json!({ "persona": persona }), true)?;
} else {
println!("{persona}");
}
Ok(ExitCode::SUCCESS)
}
other => Err(format!("unknown memory persona subcommand: {other}")),
}
}
fn memory_decisions_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut workspace: Option<String> = None;
let mut mission_id: Option<String> = None;
let mut as_json = false;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?,
);
}
"--mission-id" => {
index += 1;
mission_id = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--mission-id requires a value".to_string())?,
);
}
"--json" => as_json = true,
other => return Err(format!("unknown memory decisions option: {other}")),
}
index += 1;
}
print_value(&list_decisions(workspace.as_deref(), mission_id.as_deref())?, as_json)?;
Ok(ExitCode::SUCCESS)
}
fn memory_sync_qdrant_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut workspace: Option<String> = None;
let mut as_json = false;
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?,
);
}
"--json" => as_json = true,
other => return Err(format!("unknown memory sync-qdrant option: {other}")),
}
index += 1;
}
let payload = sync_qdrant(workspace.as_deref())?;
let ok = payload.get("ok").and_then(Value::as_bool).unwrap_or(false)
|| payload.get("skipped").and_then(Value::as_bool).unwrap_or(false);
print_value(&payload, as_json)?;
Ok(exit_code_from_status(Some(if ok { 0 } else { 1 })))
}
fn fleet_status_cmd(args: &[String]) -> Result<ExitCode, String> {
let as_json = args.iter().any(|arg| arg == "--json");
let payload = fleet_check()?;
@@ -755,16 +990,63 @@ fn fleet_sync_cmd(args: &[String]) -> Result<ExitCode, String> {
Ok(exit_code_from_status(Some(if ok { 0 } else { 1 })))
}
fn handoff_mission_subcommand(prefix: &str, tail: &[String]) -> Result<ExitCode, String> {
let mut args = vec!["mission".to_string(), prefix.to_string()];
args.extend(tail.iter().cloned());
handoff_to_python(&args)
fn fleet_config_get_cmd(args: &[String]) -> Result<ExitCode, String> {
if args.len() != 1 {
return Err("fleet config-get requires exactly one query name".to_string());
}
if let Some(lines) = fleet_config_query(&args[0])? {
for line in lines {
println!("{line}");
}
}
Ok(ExitCode::SUCCESS)
}
fn handoff_cockpit_subcommand(prefix: &str, tail: &[String]) -> Result<ExitCode, String> {
let mut args = vec!["cockpit".to_string(), prefix.to_string()];
args.extend(tail.iter().cloned());
handoff_to_python(&args)
fn fleet_render_scan_json_cmd(args: &[String]) -> Result<ExitCode, String> {
if args.len() != 1 {
return Err("fleet render-scan-json requires a candidates file".to_string());
}
let path = Path::new(&args[0]);
print_value(&render_scan_json(path)?, true)?;
Ok(ExitCode::SUCCESS)
}
fn fleet_write_config_cmd(args: &[String]) -> Result<ExitCode, String> {
if args.is_empty() {
return Err("fleet write-config requires a finalized selection file".to_string());
}
let finalized_file = Path::new(&args[0]);
let mut output_path: Option<PathBuf> = None;
let mut repo_dir: Option<String> = None;
let mut index = 1;
while index < args.len() {
match args[index].as_str() {
"--output" => {
index += 1;
output_path = Some(PathBuf::from(
args.get(index)
.ok_or_else(|| "--output requires a value".to_string())?,
));
}
"--repo-dir" => {
index += 1;
repo_dir = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--repo-dir requires a value".to_string())?,
);
}
other => return Err(format!("unknown fleet write-config option: {other}")),
}
index += 1;
}
let output_path = output_path.ok_or_else(|| "--output is required".to_string())?;
let repo_dir = repo_dir.ok_or_else(|| "--repo-dir is required".to_string())?;
let written = write_fleet_config(finalized_file, &output_path, &repo_dir)?;
println!("{}", written.display());
Ok(ExitCode::SUCCESS)
}
fn cockpit_open_cmd(args: &[String]) -> Result<ExitCode, String> {
@@ -802,7 +1084,7 @@ fn cockpit_open_cmd(args: &[String]) -> Result<ExitCode, String> {
}
"--recreate" => recreate = true,
"--remote-recreate" => remote_recreate = true,
_ => return handoff_cockpit_subcommand("open", args),
other => return Err(format!("unknown cockpit open option: {other}")),
}
index += 1;
}
@@ -828,7 +1110,7 @@ fn cockpit_attach_cmd(args: &[String]) -> Result<ExitCode, String> {
.cloned()
.ok_or_else(|| "--local-session requires a value".to_string())?;
}
_ => return handoff_cockpit_subcommand("attach", args),
other => return Err(format!("unknown cockpit attach option: {other}")),
}
index += 1;
}
@@ -858,7 +1140,7 @@ fn cockpit_status_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("status", args),
other => return Err(format!("unknown cockpit status option: {other}")),
}
index += 1;
}
@@ -889,7 +1171,7 @@ fn cockpit_doctor_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("doctor", args),
other => return Err(format!("unknown cockpit doctor option: {other}")),
}
index += 1;
}
@@ -938,7 +1220,7 @@ fn cockpit_focus_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("focus", args),
other => return Err(format!("unknown cockpit focus option: {other}")),
}
index += 1;
}
@@ -989,7 +1271,7 @@ fn cockpit_send_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("send", args),
other => return Err(format!("unknown cockpit send option: {other}")),
}
index += 1;
}
@@ -1044,7 +1326,7 @@ fn cockpit_capture_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("capture", args),
other => return Err(format!("unknown cockpit capture option: {other}")),
}
index += 1;
}
@@ -1094,7 +1376,7 @@ fn cockpit_restart_cmd(args: &[String]) -> Result<ExitCode, String> {
.ok_or_else(|| "--session requires a value".to_string())?;
}
"--json" => as_json = true,
_ => return handoff_cockpit_subcommand("restart", args),
other => return Err(format!("unknown cockpit restart option: {other}")),
}
index += 1;
}
@@ -1107,41 +1389,80 @@ fn cockpit_restart_cmd(args: &[String]) -> Result<ExitCode, String> {
Ok(exit_code_from_status(Some(payload.returncode)))
}
fn handoff_to_python_with_prefix(prefix: &str, tail: &[String]) -> Result<ExitCode, String> {
let mut args = vec![prefix.to_string()];
args.extend(tail.iter().cloned());
handoff_to_python(&args)
fn cockpit_status_line_cmd(args: &[String]) -> Result<ExitCode, String> {
let mut workspace = env::current_dir()
.map_err(|err| format!("cannot resolve current directory: {err}"))?
.to_string_lossy()
.into_owned();
let mut scope_label: Option<String> = None;
let mut machine_label: Option<String> = None;
let mut max_length = 180_usize;
let mut local_session = "constant-fleet".to_string();
let mut machine_session = "constant".to_string();
let mut index = 0;
while index < args.len() {
match args[index].as_str() {
"--workspace" => {
index += 1;
workspace = args
.get(index)
.cloned()
.ok_or_else(|| "--workspace requires a value".to_string())?;
}
fn handoff_to_python(args: &[String]) -> Result<ExitCode, String> {
let repo_root = paths::repo_root();
let default_python = paths::home_dir()
.map(|home| home.join(".local/share/constant/venv/bin/python3"))
.filter(|path| path.exists());
let python_bin = env::var_os("CONSTANT_PYTHON")
.or_else(|| default_python.map(Into::into))
.unwrap_or_else(|| OsString::from("python3"));
let mut command = Command::new(python_bin);
command.arg("-m").arg("constant");
command.args(args);
command.env("CONSTANT_PROG_NAME", program_name());
command.env("CONSTANT_RUST_HANDOFF", "1");
command.env("PYTHONPATH", python_path_with_repo(&repo_root));
let status = command
.status()
.map_err(|err| format!("python handoff failed: {err}"))?;
Ok(exit_code_from_status(status.code()))
"--scope-label" => {
index += 1;
scope_label = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--scope-label requires a value".to_string())?,
);
}
fn python_path_with_repo(repo_root: &Path) -> OsString {
let mut value = OsString::from(repo_root.as_os_str());
if let Some(existing) = env::var_os("PYTHONPATH") {
value.push(":");
value.push(existing);
"--machine-label" => {
index += 1;
machine_label = Some(
args.get(index)
.cloned()
.ok_or_else(|| "--machine-label requires a value".to_string())?,
);
}
value
"--max-length" => {
index += 1;
max_length = args
.get(index)
.ok_or_else(|| "--max-length requires a value".to_string())?
.parse::<usize>()
.map_err(|_| "--max-length must be an integer".to_string())?;
}
"--local-session" => {
index += 1;
local_session = args
.get(index)
.cloned()
.ok_or_else(|| "--local-session requires a value".to_string())?;
}
"--session" => {
index += 1;
machine_session = args
.get(index)
.cloned()
.ok_or_else(|| "--session requires a value".to_string())?;
}
other => return Err(format!("unknown cockpit status-line option: {other}")),
}
index += 1;
}
println!(
"{}",
cockpit_status_line(
&canonical_workspace(&workspace)?,
scope_label.as_deref(),
machine_label.as_deref(),
max_length,
&local_session,
&machine_session,
)?
);
Ok(ExitCode::SUCCESS)
}
fn canonical_workspace(input: &str) -> Result<String, String> {
@@ -1216,6 +1537,7 @@ fn doctor_value() -> Result<Value, String> {
let fleet = load_fleet_config()?;
let models = load_models_config()?;
let memory = load_memory_config()?;
let health = health_value()?;
Ok(json!({
"version": VERSION,
@@ -1225,10 +1547,9 @@ fn doctor_value() -> Result<Value, String> {
"data_root": paths::data_root().display().to_string(),
"rust": {
"front_controller": true,
"python_handoff": command_exists("python3"),
"python_handoff": false,
},
"commands": {
"python3": command_exists("python3"),
"tmux": command_exists("tmux"),
"cargo": command_exists("cargo"),
"claude": command_exists("claude"),
@@ -1247,6 +1568,7 @@ fn doctor_value() -> Result<Value, String> {
"machines": fleet.machines,
},
"models": models,
"health": health,
"memory": memory,
"wrapper": wrapper_status_value(),
}))
@@ -1259,45 +1581,15 @@ fn wrapper_status_value() -> Value {
"wrapper_dir": wrapper_dir.display().to_string(),
"rust_bin": rust_bin.display().to_string(),
"rust_bin_exists": rust_bin.exists(),
"forced_python": env::var("CONSTANT_USE_PYTHON").ok().as_deref() == Some("1"),
"forced_rust": env::var("CONSTANT_USE_RUST").ok().as_deref() == Some("1"),
"force_recheck": env::var("CONSTANT_RUST_RECHECK").ok().as_deref() == Some("1"),
"mode": "rust-only",
"last_mode": read_wrapper_mode(&wrapper_dir.join("last-mode")),
"cache": {
"rust_ok": read_wrapper_stamp(&wrapper_dir.join("rust-ok")),
"rust_fail": read_wrapper_stamp(&wrapper_dir.join("rust-fail")),
},
"codesign_verify": if rust_bin.exists() { Some(run_quick_command(&["codesign", "--verify", "--verbose=2", &rust_bin.display().to_string()])) } else { None },
"spctl_assess": if rust_bin.exists() { Some(run_quick_command(&["spctl", "--assess", "-vv", &rust_bin.display().to_string()])) } else { None },
"xattr_provenance": if rust_bin.exists() { Some(run_quick_command(&["xattr", "-p", "com.apple.provenance", &rust_bin.display().to_string()])) } else { None },
"hint": "If spctl rejects the Rust binary from a normal terminal, allow that terminal in System Settings -> Privacy & Security -> Developer Tools, then rerun with CONSTANT_RUST_RECHECK=1 to force a fresh startup probe.",
"hint": "The public wrapper is Rust-only. If the binary is missing or stale, rerun ./scripts/Constant and it will rebuild and ad-hoc sign the binary before launch.",
})
}
fn read_wrapper_stamp(path: &Path) -> Value {
let Ok(raw) = fs::read_to_string(path) else {
return Value::Null;
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return Value::Null;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 2 {
return json!({ "raw": trimmed });
}
let mut payload = json!({
"signature": parts[0],
"epoch": parts[1],
});
if let Ok(epoch) = parts[1].parse::<u64>() {
if let Some(object) = payload.as_object_mut() {
object.insert("timestamp".to_string(), json!(epoch));
}
}
payload
}
fn read_wrapper_mode(path: &Path) -> Value {
let Ok(raw) = fs::read_to_string(path) else {
return Value::Null;
+121
View File
@@ -707,6 +707,83 @@ pub fn summarize_mission_to_memory(mission: &Mission) -> Result<Value, String> {
}))
}
pub fn sync_qdrant(workspace: Option<&str>) -> Result<Value, String> {
ensure_schema()?;
let config = load_memory_config()?;
let url = config.qdrant_url.trim().to_string();
if url.is_empty() {
return Ok(json!({
"ok": false,
"skipped": true,
"reason": "qdrant_url is not configured",
}));
}
let hits = search_memory("*", workspace, Some(32))?
.get("hits")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let points = hits
.iter()
.map(|hit| {
let kind = hit.get("kind").and_then(Value::as_str).unwrap_or("memory");
let path = hit.get("path").and_then(Value::as_str).unwrap_or("-");
let snippet = hit.get("snippet").and_then(Value::as_str).unwrap_or("");
json!({
"id": stable_point_id(kind, path),
"vector": embed_text(snippet, config.vector_dimensions as usize),
"payload": hit,
})
})
.collect::<Vec<_>>();
let endpoint = format!(
"{}/collections/{}/points?wait=true",
url.trim_end_matches('/'),
config.qdrant_collection
);
let body = json!({ "points": points });
let output = Command::new("curl")
.args([
"-fsS",
"-X",
"PUT",
"-H",
"Content-Type: application/json",
&endpoint,
"--data-binary",
&body.to_string(),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match output {
Ok(output) if output.status.success() => {
let payload: Value = serde_json::from_slice(&output.stdout)
.map_err(|err| format!("cannot decode qdrant response: {err}"))?;
Ok(json!({
"ok": true,
"response": payload,
"points": body["points"].as_array().map(|items| items.len()).unwrap_or(0),
}))
}
Ok(output) => Ok(json!({
"ok": false,
"skipped": false,
"reason": String::from_utf8_lossy(&output.stderr).trim(),
"points": body["points"].as_array().map(|items| items.len()).unwrap_or(0),
})),
Err(err) => Ok(json!({
"ok": false,
"skipped": false,
"reason": err.to_string(),
"points": body["points"].as_array().map(|items| items.len()).unwrap_or(0),
})),
}
}
fn ensure_schema() -> Result<(), String> {
let db = paths::memory_store_path();
if let Some(parent) = db.parent() {
@@ -1185,6 +1262,50 @@ fn tokenize(text: &str) -> Vec<String> {
tokens
}
fn embed_text(text: &str, dims: usize) -> Vec<f64> {
let mut vector = vec![0.0_f64; dims];
if dims == 0 {
return vector;
}
let mut counts = BTreeMap::new();
for token in tokenize(text) {
*counts.entry(token).or_insert(0_u32) += 1;
}
for (token, count) in counts {
let digest = sha256_text(&token);
let bytes = digest.as_bytes();
let mut index_seed = 0_u32;
for byte in bytes.iter().take(4) {
index_seed = (index_seed << 8) | *byte as u32;
}
let index = index_seed as usize % dims;
let sign = if bytes.get(4).copied().unwrap_or(b'0') % 2 == 0 {
1.0
} else {
-1.0
};
vector[index] += sign * (1.0 + f64::from(count).ln_1p());
}
let norm = vector.iter().map(|value| value * value).sum::<f64>().sqrt();
if norm == 0.0 {
return vector;
}
vector
.into_iter()
.map(|value| ((value / norm) * 1_000_000.0).round() / 1_000_000.0)
.collect()
}
fn stable_point_id(kind: &str, path: &str) -> i64 {
let raw = format!("{kind}:{path}");
let digest = sha256_text(&raw);
let mut value = 0_i64;
for byte in digest.as_bytes().iter().take(8) {
value = (value << 8) | i64::from(*byte);
}
value.abs()
}
fn lexical_score(text: &str, query: &str) -> f64 {
let haystack = text.to_lowercase();
tokenize(query)
+485
View File
@@ -0,0 +1,485 @@
use serde_json::{Value, json};
use crate::buddy::buddy_ask;
use crate::capabilities::{
AGENTS, MINIMAL_STACK, SKILLS, WORKFLOW_STACK, match_skill, resolve_skill_and_agent,
skill_by_id,
};
use crate::config::{load_fleet_config, load_models_config};
use crate::memory::{instruction_skill_sources, search_memory};
use crate::state::Mission;
pub const CHAT_ROLES: [&str; 4] = ["claude", "codex", "copilot", "vibe"];
pub fn health_value() -> Result<Value, String> {
let models = load_models_config()?;
let model_rows = [
("planner", &models.planner.model_id),
("buddy", &models.buddy.model_id),
("verify", &models.verify.model_id),
]
.into_iter()
.map(|(role, model_id)| {
(
role.to_string(),
json!({
"role": role,
"model_id": model_id,
"available": false,
"loaded": false,
"backend": "heuristic",
"cached": false,
"cache_path": "",
}),
)
})
.collect::<serde_json::Map<String, Value>>();
Ok(json!({
"mlx_python": false,
"mlx_probe": {
"requested": false,
"package_present": false,
"available": false,
"reason": "disabled in Rust runtime",
},
"models": Value::Object(model_rows),
"local_model_warmup": {
"enabled": false,
"download_required": false,
"missing_model_count": 0,
"missing_models": [],
"startup_message": "",
},
"fallback_mode": models.fallback_mode,
"agents": AGENTS,
"skills": SKILLS,
"recommended_skill_stack": recommended_skill_stack_value(),
}))
}
pub fn models_status_value() -> Result<Value, String> {
Ok(json!({
"config": load_models_config()?,
"runtime": {
"backend": "heuristic",
"python": false,
"service": false,
},
"health": health_value()?,
}))
}
pub fn chat(
message: &str,
mission: Option<&Mission>,
workspace: &str,
selected_machine: Option<&str>,
selected_role: Option<&str>,
_chat_history: &[Value],
) -> Result<Value, String> {
let (prompt, explicit_skill) = extract_explicit_skill(message);
let prompt = prompt.trim().to_string();
let prompt_l = prompt.to_lowercase();
let memory_payload = search_memory(&prompt, Some(workspace), Some(4))?;
let memory_hits = memory_payload
.get("hits")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let skill_sources = instruction_skill_sources(workspace, Some(&prompt), 4)?
.as_array()
.cloned()
.unwrap_or_default();
let memory_lines = memory_hits
.iter()
.take(3)
.map(|hit| {
format!(
"{} {} :: {}",
hit.get("kind").and_then(Value::as_str).unwrap_or("memory"),
hit.get("path").and_then(Value::as_str).unwrap_or("-"),
hit.get("snippet").and_then(Value::as_str).unwrap_or("")
)
})
.collect::<Vec<_>>();
let target_machine = match_machine_label(&prompt)?
.or_else(|| selected_machine.map(ToString::to_string))
.unwrap_or_else(|| fleet_labels().local);
let target_role = match_role(&prompt)
.or_else(|| selected_role.map(ToString::to_string))
.unwrap_or_else(|| "codex".to_string());
let matched_skill = explicit_skill.or_else(|| {
if prompt.is_empty() {
None
} else {
Some(match_skill(&prompt, false))
}
});
let mut intent = "plain_chat".to_string();
let mut cockpit_action = Value::Null;
let mut buddy_note = Value::Null;
let mut reply: String;
let mut mission_goal = Value::Null;
let mut routing_overrides = serde_json::Map::new();
if contains_any(&prompt_l, &["open cockpit", "attach cockpit", "show cockpit"]) {
intent = "cockpit_open".to_string();
cockpit_action = json!({ "type": "open" });
reply = "I can hand off to the full cockpit now.".to_string();
} else if contains_any(&prompt_l, &["restart", "relance", "respawn"]) {
intent = "cockpit_restart".to_string();
cockpit_action = json!({
"type": "restart",
"machine": target_machine,
"pane": target_role,
});
reply = format!("I'll restart {target_machine}:{target_role}.");
} else if contains_any(&prompt_l, &["capture", "log", "logs", "show pane", "see pane"]) {
intent = "cockpit_capture".to_string();
cockpit_action = json!({
"type": "capture",
"machine": target_machine,
"pane": target_role,
});
reply = format!("I'll capture {target_machine}:{target_role}.");
} else if contains_any(&prompt_l, &["focus", "jump", "go to", "ouvre", "open machine"]) {
intent = "cockpit_focus".to_string();
cockpit_action = json!({
"type": "focus",
"machine": target_machine,
"pane": target_role,
});
reply = format!("I'll focus {target_machine}:{target_role}.");
} else if contains_any(
&prompt_l,
&[
"memory",
"remember",
"decision",
"persona",
"what do we know",
"qu'est-ce qu",
"souviens",
],
) {
intent = "memory_lookup".to_string();
reply = if memory_lines.is_empty() {
"No strong memory hits for that query yet.".to_string()
} else {
format!("Memory echoes:\n- {}", memory_lines.join("\n- "))
};
} else if explicit_skill.is_none()
&& prompt.ends_with('?')
&& !contains_any(
&prompt_l,
&[
"fix",
"build",
"implement",
"write",
"create",
"deploy",
"restart",
"capture",
"focus",
],
)
{
let title = mission
.map(|entry| entry.title.as_str())
.unwrap_or("global cockpit");
reply = format!(
"Constant view for {title}. selected={}:{}",
selected_machine.unwrap_or("-"),
selected_role.unwrap_or("-")
);
if !memory_lines.is_empty() {
reply.push_str(&format!("\nMemory echoes:\n- {}", memory_lines[..memory_lines.len().min(2)].join("\n- ")));
}
} else {
intent = "mission_create".to_string();
if let Some(skill) = matched_skill {
routing_overrides.insert("skill".to_string(), json!(skill.id));
routing_overrides.insert("agent".to_string(), json!(skill.preferred_agent));
routing_overrides.insert("cli".to_string(), json!(skill.preferred_cli));
}
let preview = heuristic_plan_preview(&prompt, workspace, &routing_overrides)?;
let step = preview
.get("steps")
.and_then(Value::as_array)
.and_then(|steps| steps.first())
.cloned()
.unwrap_or_else(|| json!({}));
buddy_note = json!({
"answer": format!(
"Route preview agrees with {}/{}/{}.",
step.get("machine").and_then(Value::as_str).unwrap_or("-"),
step.get("cli").and_then(Value::as_str).unwrap_or("-"),
step.get("backend").and_then(Value::as_str).unwrap_or("-"),
),
"mode": "rust-heuristic",
});
reply = format!(
"I turned that into a mission. Route preview: {}/{}/{} skill={} agent={}.",
step.get("machine").and_then(Value::as_str).unwrap_or("-"),
step.get("cli").and_then(Value::as_str).unwrap_or("-"),
step.get("backend").and_then(Value::as_str).unwrap_or("-"),
step.get("skill").and_then(Value::as_str).unwrap_or("-"),
step.get("agent").and_then(Value::as_str).unwrap_or("-"),
);
mission_goal = json!(prompt);
}
if matches!(intent.as_str(), "plain_chat" | "memory_lookup") && buddy_note.is_null() {
if contains_any(
&prompt_l,
&["route", "reroute", "machine", "cli", "pane", "codex", "claude", "vibe", "copilot"],
) {
buddy_note = buddy_ask(mission, &prompt, Some(workspace))?;
}
}
Ok(json!({
"intent": intent,
"reply": reply,
"message": prompt,
"mode": "rust-heuristic",
"cockpit_action": cockpit_action,
"buddy_note": buddy_note,
"memory_hits": memory_hits,
"skill_sources": skill_sources,
"workspace": workspace,
"mission_goal": mission_goal,
"skill": matched_skill.map(skill_value).unwrap_or(Value::Null),
"routing_overrides": Value::Object(routing_overrides),
}))
}
pub fn recommended_skill_stack_value() -> Value {
json!({
"minimal": MINIMAL_STACK,
"workflow": WORKFLOW_STACK,
"layers": {
"reflection": ["spec-planner", "architecture-brainstorm"],
"execution": ["repo-onboarding", "task-decomposer", "pr-review-prep"],
}
})
}
fn skill_value(skill: &crate::capabilities::Skill) -> Value {
json!({
"id": skill.id,
"label": skill.label,
"layer": skill.layer,
"visibility": skill.visibility,
"summary": skill.summary,
"preferred_cli": skill.preferred_cli,
"preferred_agent": skill.preferred_agent,
})
}
fn extract_explicit_skill(message: &str) -> (String, Option<&'static crate::capabilities::Skill>) {
let raw = message.trim();
let lowered = raw.to_lowercase();
if let Some(payload) = lowered.strip_prefix("skill:") {
let offset = raw.len() - payload.len();
if let Some((skill, remainder)) = parse_skill_payload(&raw[offset..]) {
return (remainder, Some(skill));
}
}
if raw.starts_with("/skill ") {
if let Some((skill, remainder)) = parse_skill_payload(&raw["/skill ".len()..]) {
return (remainder, Some(skill));
}
}
if raw.starts_with('/') {
let payload = &raw[1..];
let mut parts = payload.splitn(2, char::is_whitespace);
if let Some(skill_id) = parts.next() {
if let Some(skill) = skill_by_id(skill_id, false) {
let remainder = parts.next().unwrap_or_default().trim().to_string();
return (if remainder.is_empty() { raw.to_string() } else { remainder }, Some(skill));
}
}
}
for skill in SKILLS.iter().filter(|skill| skill.visibility == "public") {
if lowered.contains(skill.id) {
return (raw.to_string(), Some(skill));
}
}
(raw.to_string(), None)
}
fn parse_skill_payload(payload: &str) -> Option<(&'static crate::capabilities::Skill, String)> {
let trimmed = payload.trim();
if trimmed.is_empty() {
return None;
}
let mut parts = trimmed.splitn(2, char::is_whitespace);
let skill_id = parts.next()?;
let skill = skill_by_id(skill_id, false)?;
let remainder = parts.next().unwrap_or_default().trim().to_string();
Some((
skill,
if remainder.is_empty() {
trimmed.to_string()
} else {
remainder
},
))
}
fn heuristic_plan_preview(
goal: &str,
_workspace: &str,
overrides: &serde_json::Map<String, Value>,
) -> Result<Value, String> {
let resolution = resolve_skill_and_agent(
Some(goal),
overrides.get("skill").and_then(Value::as_str),
overrides.get("agent").and_then(Value::as_str),
overrides.get("cli").and_then(Value::as_str),
);
let skill = resolution.skill;
let cli = resolution.cli;
let machine = overrides
.get("machine")
.and_then(Value::as_str)
.map(ToString::to_string)
.unwrap_or_else(|| route_machine(goal, skill.id));
let backend = overrides
.get("backend")
.and_then(Value::as_str)
.map(ToString::to_string)
.unwrap_or_else(|| route_backend(&machine, cli, goal));
Ok(json!({
"title": goal.lines().next().unwrap_or_default().trim().chars().take(80).collect::<String>(),
"summary": format!("Route the mission to {machine} using {cli} via {backend} for skill {}.", skill.id),
"steps": [{
"step_id": "step-1",
"kind": "task",
"title": format!("Execute mission on {machine}"),
"prompt": goal,
"machine": machine,
"backend": backend,
"cli": cli,
"agent": resolution.agent.id,
"agent_role": resolution.agent.role,
"skill": skill.id,
"skill_summary": skill.summary,
"status": "pending",
"attempt": 0,
"depends_on": [],
"artifact_refs": [],
"result_summary": "",
}]
}))
}
fn match_machine_label(message: &str) -> Result<Option<String>, String> {
let message_l = message.to_lowercase();
let fleet = load_fleet_config()?;
Ok(fleet
.machines
.iter()
.find(|machine| message_l.contains(&machine.label.to_lowercase()))
.map(|machine| machine.label.clone()))
}
fn match_role(message: &str) -> Option<String> {
let message_l = message.to_lowercase();
CHAT_ROLES
.iter()
.find(|role| message_l.contains(**role))
.map(|role| (*role).to_string())
}
struct FleetLabels {
local: String,
builder_a: String,
builder_b: String,
edge_a: String,
lab_a: String,
}
fn fleet_labels() -> FleetLabels {
let fleet = load_fleet_config().unwrap_or_default();
let labels = fleet
.machines
.iter()
.map(|machine| machine.label.clone())
.collect::<Vec<_>>();
FleetLabels {
local: fleet
.machines
.iter()
.find(|machine| machine.label == fleet.local_machine)
.map(|machine| machine.label.clone())
.unwrap_or_else(|| labels.first().cloned().unwrap_or_else(|| "command-center".to_string())),
builder_a: labels.get(1).cloned().unwrap_or_else(|| "builder-a".to_string()),
builder_b: labels.get(2).cloned().unwrap_or_else(|| "builder-b".to_string()),
edge_a: labels.get(3).cloned().unwrap_or_else(|| "edge-a".to_string()),
lab_a: labels.get(4).cloned().unwrap_or_else(|| "lab-a".to_string()),
}
}
fn route_machine(goal: &str, skill_id: &str) -> String {
let labels = fleet_labels();
let goal_l = goal.to_lowercase();
if matches!(skill_id, "spec-planner" | "repo-onboarding" | "task-decomposer") {
return labels.local;
}
if skill_id == "architecture-brainstorm" {
return labels.lab_a;
}
if skill_id == "pr-review-prep" {
return labels.builder_a;
}
if skill_id == "ops-deployment" {
return labels.edge_a;
}
if skill_id == "debug-restoration"
&& contains_any(&goal_l, &["performance", "deep", "benchmark", "compiler", "cuda"])
{
return labels.builder_b;
}
if contains_any(&goal_l, &["ssh", "shell", "fleet", "ops", "network", "infra"]) {
return labels.edge_a;
}
if contains_any(&goal_l, &["refactor", "performance", "deep", "cuda", "compiler", "benchmark"]) {
return labels.builder_b;
}
if contains_any(&goal_l, &["review", "audit", "test", "qa", "docs"]) {
return labels.builder_a;
}
if contains_any(&goal_l, &["experiment", "prototype", "sandbox", "branch"]) {
return labels.lab_a;
}
labels.local
}
fn route_backend(machine: &str, cli: &str, goal: &str) -> String {
let local_machine = fleet_labels().local;
let goal_l = goal.to_lowercase();
if machine == local_machine
&& matches!(cli, "claude" | "codex")
&& contains_any(&goal_l, &["parallel", "team", "multi-agent", "compare"])
{
return "omc".to_string();
}
if machine == local_machine {
"cli-local".to_string()
} else {
"cli-ssh".to_string()
}
}
fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| haystack.contains(needle))
}
+35
View File
@@ -31,6 +31,26 @@ pub fn data_root() -> PathBuf {
.unwrap_or_else(|| PathBuf::from(".local/share/constant"))
}
pub fn expand_home_string(value: &str) -> PathBuf {
if value == "~" {
return home_dir().unwrap_or_else(|| PathBuf::from(value));
}
if let Some(rest) = value.strip_prefix("~/") {
return home_dir()
.map(|home| home.join(rest))
.unwrap_or_else(|| PathBuf::from(value));
}
if value == "$HOME" {
return home_dir().unwrap_or_else(|| PathBuf::from(value));
}
if let Some(rest) = value.strip_prefix("$HOME/") {
return home_dir()
.map(|home| home.join(rest))
.unwrap_or_else(|| PathBuf::from(value));
}
PathBuf::from(value)
}
pub fn planner_dir() -> PathBuf {
cache_root().join("planner")
}
@@ -92,6 +112,18 @@ pub fn indexes_dir() -> PathBuf {
data_root().join("indexes")
}
pub fn chat_indexes_dir() -> PathBuf {
indexes_dir().join("chat")
}
pub fn chat_threads_index_dir() -> PathBuf {
chat_indexes_dir().join("threads")
}
pub fn chat_views_dir() -> PathBuf {
chat_indexes_dir().join("views")
}
pub fn memory_sources_dir() -> PathBuf {
data_root().join("sources")
}
@@ -121,6 +153,9 @@ pub fn ensure_runtime_dirs() -> Result<(), String> {
data_root(),
missions_dir(),
indexes_dir(),
chat_indexes_dir(),
chat_threads_index_dir(),
chat_views_dir(),
memory_sources_dir(),
planner_dir(),
] {
+687 -286
View File
File diff suppressed because it is too large Load Diff
+297
View File
@@ -0,0 +1,297 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{Value, json};
fn unique_temp_dir(label: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let path = std::env::temp_dir().join(format!("constant-cli-test-{label}-{unique}"));
fs::create_dir_all(&path).expect("create temp dir");
path
}
fn constant_output(home: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_constant"))
.args(args)
.env("HOME", home)
.output()
.expect("run constant")
}
fn write_json(path: &Path, payload: Value) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dir");
}
fs::write(path, format!("{}\n", serde_json::to_string_pretty(&payload).unwrap()))
.expect("write json");
}
fn write_mission(home: &Path, mission_id: &str, status: &str, workspace: &Path) {
let path = home
.join(".cache/constant/missions")
.join(mission_id)
.join("mission.json");
write_json(
&path,
json!({
"mission_id": mission_id,
"title": format!("Mission {mission_id}"),
"goal": "Ship the migration",
"workspace": workspace.display().to_string(),
"status": status,
"priority": "normal",
"created_at": "2026-04-06T08:00:00Z",
"updated_at": "2026-04-06T08:00:00Z",
"planner_model": "heuristic",
"buddy_model": "heuristic",
"verify_model": "heuristic",
"owner": "Constant",
"routing_overrides": {},
"steps": [],
"artifacts": [],
"meta": {},
}),
);
}
fn stable_hash_hex(value: &str) -> String {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in value.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
fn workspace_slug(workspace: &Path) -> String {
let resolved = workspace
.canonicalize()
.unwrap_or_else(|_| workspace.to_path_buf());
let name = resolved
.file_name()
.and_then(|value| value.to_str())
.filter(|value| !value.is_empty())
.unwrap_or("workspace");
format!("{name}-{}", &stable_hash_hex(&resolved.display().to_string())[..10])
}
#[test]
fn fleet_config_get_reads_existing_config() {
let home = unique_temp_dir("fleet-config-get");
let config_path = home.join(".config/constant/fleet.json");
write_json(
&config_path,
json!({
"version": 1,
"local_machine": "command-center",
"repo_dir": "$HOME/constant",
"machines": [
{ "label": "command-center", "target": "local", "auto_clis": [], "manual_clis": [], "backends": ["cockpit"] },
{ "label": "builder-a", "target": "dev@builder-a", "auto_clis": [], "manual_clis": [], "backends": ["cli-ssh"] }
],
}),
);
let repo_dir = constant_output(&home, &["fleet", "config-get", "repo_dir"]);
assert!(repo_dir.status.success());
assert_eq!(String::from_utf8_lossy(&repo_dir.stdout).trim(), "$HOME/constant");
let machine_specs = constant_output(&home, &["fleet", "config-get", "machine_specs"]);
assert!(machine_specs.status.success());
let lines = String::from_utf8_lossy(&machine_specs.stdout);
assert!(lines.contains("command-center=local"));
assert!(lines.contains("builder-a=dev@builder-a"));
}
#[test]
fn fleet_render_scan_json_emits_candidate_payload() {
let home = unique_temp_dir("fleet-render-scan-json");
let scan_file = home.join("scan.tsv");
fs::write(
&scan_file,
"builder-a\tdev\tbuilder-a\t22\tyes\tbuilder-a\tDarwin\t/Users/dev\t\n",
)
.unwrap();
let output = constant_output(
&home,
&[
"fleet",
"render-scan-json",
scan_file.to_str().expect("scan path"),
],
);
assert!(output.status.success());
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(payload["candidates"][0]["host"], "builder-a");
assert_eq!(payload["candidates"][0]["port"], 22);
assert_eq!(payload["candidates"][0]["reachable"], true);
}
#[test]
fn fleet_write_config_generates_json_file() {
let home = unique_temp_dir("fleet-write-config");
let finalized = home.join("finalized.tsv");
let output_path = home.join("fleet.json");
fs::write(
&finalized,
"command-center\tlocal\tlocal\tlocal\nbuilder-a\tremote\tdev\tbuilder-a\n",
)
.unwrap();
let output = constant_output(
&home,
&[
"fleet",
"write-config",
finalized.to_str().expect("finalized path"),
"--output",
output_path.to_str().expect("output path"),
"--repo-dir",
"$HOME/constant",
],
);
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), output_path.display().to_string());
let payload: Value = serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap();
assert_eq!(payload["local_machine"], "command-center");
assert_eq!(payload["machines"][1]["target"], "dev@builder-a");
}
#[test]
fn memory_sync_qdrant_skips_when_url_is_not_configured() {
let home = unique_temp_dir("memory-sync");
let output = constant_output(&home, &["memory", "sync-qdrant", "--json"]);
assert!(output.status.success());
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(payload["skipped"], true);
assert_eq!(payload["ok"], false);
}
#[test]
fn mission_delete_blocks_active_missions() {
let home = unique_temp_dir("mission-delete-blocked");
let workspace = home.join("workspace");
fs::create_dir_all(&workspace).unwrap();
write_mission(&home, "mission-running", "running", &workspace);
let output = constant_output(
&home,
&["mission", "delete", "mission-running", "--json"],
);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("cannot be deleted yet"));
}
#[test]
fn mission_delete_removes_terminal_thread_files() {
let home = unique_temp_dir("mission-delete-terminal");
let workspace = home.join("workspace");
fs::create_dir_all(&workspace).unwrap();
write_mission(&home, "mission-done", "done", &workspace);
let mission_chat = home.join(".cache/constant/chat/missions/mission-done.ndjson");
if let Some(parent) = mission_chat.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(
&mission_chat,
"{\"timestamp\":\"2026-04-06T08:01:00Z\",\"role\":\"constant\",\"content\":\"Done\"}\n",
)
.unwrap();
let mission_index = home
.join(".local/share/constant/indexes/chat/threads/mission-mission-done.json");
write_json(
&mission_index,
json!({
"thread_key": "mission:mission-done",
"workspace": workspace.display().to_string(),
"mission_id": "mission-done",
"kind": "mission",
"title": "Mission mission-done",
"mission_status": "done",
"message_count": 1,
"last_role": "constant",
"last_preview": "Done",
"last_timestamp": "2026-04-06T08:01:00Z",
"chat_mtime": 1,
"updated_at": "2026-04-06T08:01:00Z",
}),
);
let view_state = home.join(format!(
".local/share/constant/indexes/chat/views/workspace-{}.json",
workspace_slug(&workspace)
));
write_json(
&view_state,
json!({
"seen_counts": {
"mission:mission-done": 1
}
}),
);
let output = constant_output(&home, &["mission", "delete", "mission-done", "--json"]);
assert!(output.status.success());
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(payload["deleted"], true);
assert!(!home.join(".cache/constant/missions/mission-done").exists());
assert!(!mission_chat.exists());
assert!(!mission_index.exists());
let view_payload: Value = serde_json::from_str(&fs::read_to_string(&view_state).unwrap()).unwrap();
assert!(view_payload["seen_counts"]["mission:mission-done"].is_null());
}
#[test]
fn cockpit_status_line_rebuilds_workspace_sidecar_from_chat() {
let home = unique_temp_dir("status-line-sidecar");
let workspace = home.join("workspace");
fs::create_dir_all(&workspace).unwrap();
let slug = workspace_slug(&workspace);
let workspace_chat = home.join(format!(
".cache/constant/chat/workspaces/{slug}.ndjson"
));
if let Some(parent) = workspace_chat.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(
&workspace_chat,
"{\"timestamp\":\"2026-04-06T08:00:00Z\",\"role\":\"user\",\"content\":\"hello from workspace\"}\n",
)
.unwrap();
let output = constant_output(
&home,
&[
"cockpit",
"status-line",
"--workspace",
workspace.to_str().expect("workspace path"),
"--scope-label",
"fleet",
"--max-length",
"120",
],
);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("fleet | workspace"));
assert!(stdout.contains("YOU: hello from workspace"));
let sidecar = home.join(format!(
".local/share/constant/indexes/chat/threads/workspace-{slug}.json"
));
assert!(sidecar.exists());
}