add matrix orchestrator rail and repo scaffolding
This commit is contained in:
16
README.md
16
README.md
@@ -127,6 +127,22 @@ Templates currently supported:
|
|||||||
- `typescript-nextjs`
|
- `typescript-nextjs`
|
||||||
- `monorepo`
|
- `monorepo`
|
||||||
|
|
||||||
|
## Matrix Orchestrator Rail
|
||||||
|
|
||||||
|
Mosaic includes a runtime-agnostic orchestrator rail at:
|
||||||
|
|
||||||
|
- `~/.mosaic/rails/orchestrator-matrix/`
|
||||||
|
|
||||||
|
Run from a bootstrapped repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --once
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --poll-sec 10
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller reads/writes repo-local state in `.mosaic/orchestrator/` and emits
|
||||||
|
structured events to `.mosaic/orchestrator/events.ndjson` for Matrix bridge consumption.
|
||||||
|
|
||||||
## Bootstrap Any Repo (Slave Linkage)
|
## Bootstrap Any Repo (Slave Linkage)
|
||||||
|
|
||||||
Attach any repository/workspace to the master layer:
|
Attach any repository/workspace to the master layer:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Master/slave model:
|
|||||||
- Run validation checks before claiming completion.
|
- Run validation checks before claiming completion.
|
||||||
- Apply quality rails from `~/.mosaic/rails/` when relevant (review, QA, git workflow).
|
- Apply quality rails from `~/.mosaic/rails/` when relevant (review, QA, git workflow).
|
||||||
- For project-level mechanical enforcement templates, use `~/.mosaic/rails/quality/` via `~/.mosaic/bin/mosaic-quality-apply`.
|
- For project-level mechanical enforcement templates, use `~/.mosaic/rails/quality/` via `~/.mosaic/bin/mosaic-quality-apply`.
|
||||||
|
- For runtime-agnostic delegation/orchestration, use `~/.mosaic/rails/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state.
|
||||||
- Avoid hardcoded secrets and token leakage in remotes/commits.
|
- Avoid hardcoded secrets and token leakage in remotes/commits.
|
||||||
- Do not perform destructive git/file actions without explicit instruction.
|
- Do not perform destructive git/file actions without explicit instruction.
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ if [[ ! -d "$TEMPLATE_ROOT" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$TARGET_DIR/.mosaic" "$TARGET_DIR/scripts/agent"
|
mkdir -p "$TARGET_DIR/.mosaic" "$TARGET_DIR/scripts/agent"
|
||||||
|
mkdir -p "$TARGET_DIR/.mosaic/orchestrator" "$TARGET_DIR/.mosaic/orchestrator/logs" "$TARGET_DIR/.mosaic/orchestrator/results"
|
||||||
|
|
||||||
copy_file() {
|
copy_file() {
|
||||||
local src="$1"
|
local src="$1"
|
||||||
@@ -54,6 +55,11 @@ copy_file() {
|
|||||||
copy_file "$TEMPLATE_ROOT/.mosaic/README.md" "$TARGET_DIR/.mosaic/README.md"
|
copy_file "$TEMPLATE_ROOT/.mosaic/README.md" "$TARGET_DIR/.mosaic/README.md"
|
||||||
copy_file "$TEMPLATE_ROOT/.mosaic/repo-hooks.sh" "$TARGET_DIR/.mosaic/repo-hooks.sh"
|
copy_file "$TEMPLATE_ROOT/.mosaic/repo-hooks.sh" "$TARGET_DIR/.mosaic/repo-hooks.sh"
|
||||||
copy_file "$TEMPLATE_ROOT/.mosaic/quality-rails.yml" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
copy_file "$TEMPLATE_ROOT/.mosaic/quality-rails.yml" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||||
|
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/config.json" "$TARGET_DIR/.mosaic/orchestrator/config.json"
|
||||||
|
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/tasks.json" "$TARGET_DIR/.mosaic/orchestrator/tasks.json"
|
||||||
|
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/state.json" "$TARGET_DIR/.mosaic/orchestrator/state.json"
|
||||||
|
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/logs/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/logs/.gitkeep"
|
||||||
|
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/results/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/results/.gitkeep"
|
||||||
|
|
||||||
for file in "$TEMPLATE_ROOT"/scripts/agent/*.sh; do
|
for file in "$TEMPLATE_ROOT"/scripts/agent/*.sh; do
|
||||||
base="$(basename "$file")"
|
base="$(basename "$file")"
|
||||||
@@ -97,6 +103,7 @@ fi
|
|||||||
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
|
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
|
||||||
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
|
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
|
||||||
echo "[mosaic] Optional: apply quality rails via ~/.mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
echo "[mosaic] Optional: apply quality rails via ~/.mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
||||||
|
echo "[mosaic] Optional: run orchestrator rail via ~/.mosaic/bin/mosaic-orchestrator-run --once"
|
||||||
|
|
||||||
if [[ -n "$QUALITY_TEMPLATE" ]]; then
|
if [[ -n "$QUALITY_TEMPLATE" ]]; then
|
||||||
if [[ -x "$MOSAIC_HOME/bin/mosaic-quality-apply" ]]; then
|
if [[ -x "$MOSAIC_HOME/bin/mosaic-quality-apply" ]]; then
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ expect_file "$MOSAIC_HOME/STANDARDS.md"
|
|||||||
expect_dir "$MOSAIC_HOME/guides"
|
expect_dir "$MOSAIC_HOME/guides"
|
||||||
expect_dir "$MOSAIC_HOME/rails"
|
expect_dir "$MOSAIC_HOME/rails"
|
||||||
expect_dir "$MOSAIC_HOME/rails/quality"
|
expect_dir "$MOSAIC_HOME/rails/quality"
|
||||||
|
expect_dir "$MOSAIC_HOME/rails/orchestrator-matrix"
|
||||||
expect_dir "$MOSAIC_HOME/profiles"
|
expect_dir "$MOSAIC_HOME/profiles"
|
||||||
expect_dir "$MOSAIC_HOME/templates/agent"
|
expect_dir "$MOSAIC_HOME/templates/agent"
|
||||||
expect_dir "$MOSAIC_HOME/skills"
|
expect_dir "$MOSAIC_HOME/skills"
|
||||||
@@ -124,6 +125,7 @@ expect_file "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
|
|||||||
expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills"
|
expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills"
|
||||||
expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply"
|
expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply"
|
||||||
expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify"
|
expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify"
|
||||||
|
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run"
|
||||||
|
|
||||||
# Claude runtime file checks (copied, non-symlink).
|
# Claude runtime file checks (copied, non-symlink).
|
||||||
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
|
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
|
||||||
|
|||||||
12
bin/mosaic-orchestrator-run
Executable file
12
bin/mosaic-orchestrator-run
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.mosaic}"
|
||||||
|
CTRL="$MOSAIC_HOME/rails/orchestrator-matrix/controller/mosaic_orchestrator.py"
|
||||||
|
|
||||||
|
if [[ ! -f "$CTRL" ]]; then
|
||||||
|
echo "[mosaic-orchestrator] missing controller: $CTRL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python3 "$CTRL" --repo "$(pwd)" "$@"
|
||||||
50
rails/orchestrator-matrix/README.md
Normal file
50
rails/orchestrator-matrix/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Mosaic Matrix Orchestrator Rail
|
||||||
|
|
||||||
|
Runtime-agnostic orchestration rail for delegating work to worker agents and enforcing
|
||||||
|
mechanical quality gates.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
- Decouple orchestration from any single agent runtime feature set
|
||||||
|
- Persist state in repo-local `.mosaic/orchestrator/` files
|
||||||
|
- Emit structured events for Matrix transport and audit trails
|
||||||
|
- Enforce rails before marking tasks complete
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- `protocol/` - JSON schemas for task/event payloads
|
||||||
|
- `controller/mosaic_orchestrator.py` - deterministic controller loop
|
||||||
|
- `adapters/` - runtime adapter guidance
|
||||||
|
|
||||||
|
## Repo Contract
|
||||||
|
|
||||||
|
The controller expects this layout in each bootstrapped repo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.mosaic/orchestrator/
|
||||||
|
config.json
|
||||||
|
tasks.json
|
||||||
|
state.json
|
||||||
|
events.ndjson
|
||||||
|
logs/
|
||||||
|
results/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
From a bootstrapped repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --once
|
||||||
|
```
|
||||||
|
|
||||||
|
Continuous loop:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --poll-sec 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Matrix Note
|
||||||
|
|
||||||
|
This rail writes canonical events to `.mosaic/orchestrator/events.ndjson`.
|
||||||
|
Matrix bridge services can consume and relay these events to Matrix rooms.
|
||||||
52
rails/orchestrator-matrix/adapters/README.md
Normal file
52
rails/orchestrator-matrix/adapters/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Adapter Contract
|
||||||
|
|
||||||
|
Runtime adapters translate task commands into concrete worker invocations.
|
||||||
|
|
||||||
|
## Minimal Contract
|
||||||
|
|
||||||
|
Each task should define either:
|
||||||
|
|
||||||
|
1. `command` directly in `tasks.json`, or
|
||||||
|
2. controller-level `worker.command_template` in `.mosaic/orchestrator/config.json`
|
||||||
|
|
||||||
|
`command_template` may use:
|
||||||
|
|
||||||
|
- `{task_id}`
|
||||||
|
- `{task_title}`
|
||||||
|
- `{task_file}`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Codex:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"worker": {
|
||||||
|
"command_template": "codex \"run task {task_id}: {task_title}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"worker": {
|
||||||
|
"command_template": "claude -p \"Execute task {task_id}: {task_title}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenCode:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"worker": {
|
||||||
|
"command_template": "opencode \"execute task {task_id}: {task_title}\""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Prefer explicit per-task `command` for deterministic execution and auditability.
|
||||||
Binary file not shown.
259
rails/orchestrator-matrix/controller/mosaic_orchestrator.py
Executable file
259
rails/orchestrator-matrix/controller/mosaic_orchestrator.py
Executable file
@@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Deterministic orchestrator controller for Mosaic task delegation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def now_iso() -> str:
|
||||||
|
return dt.datetime.now(dt.timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: pathlib.Path, default: Any) -> Any:
|
||||||
|
if not path.exists():
|
||||||
|
return default
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def save_json(path: pathlib.Path, data: Any) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||||
|
with tmp.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
tmp.replace(path)
|
||||||
|
|
||||||
|
|
||||||
|
def append_event(events_path: pathlib.Path, event: dict[str, Any]) -> None:
|
||||||
|
events_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with events_path.open("a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(event, ensure_ascii=True) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def emit_event(
|
||||||
|
events_path: pathlib.Path,
|
||||||
|
event_type: str,
|
||||||
|
task_id: str,
|
||||||
|
status: str,
|
||||||
|
source: str,
|
||||||
|
message: str,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
append_event(
|
||||||
|
events_path,
|
||||||
|
{
|
||||||
|
"event_id": str(uuid.uuid4()),
|
||||||
|
"event_type": event_type,
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": status,
|
||||||
|
"timestamp": now_iso(),
|
||||||
|
"source": source,
|
||||||
|
"message": message,
|
||||||
|
"metadata": metadata or {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_shell(command: str, cwd: pathlib.Path, log_path: pathlib.Path) -> tuple[int, str]:
|
||||||
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with log_path.open("a", encoding="utf-8") as log:
|
||||||
|
log.write(f"\n[{now_iso()}] COMMAND: {command}\n")
|
||||||
|
log.flush()
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["bash", "-lc", command],
|
||||||
|
cwd=str(cwd),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
output_chunks: list[str] = []
|
||||||
|
assert proc.stdout is not None
|
||||||
|
for line in proc.stdout:
|
||||||
|
output_chunks.append(line)
|
||||||
|
log.write(line)
|
||||||
|
code = proc.wait()
|
||||||
|
log.write(f"[{now_iso()}] EXIT: {code}\n")
|
||||||
|
return code, "".join(output_chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def render_command_template(template: str, task: dict[str, Any], task_file: pathlib.Path) -> str:
|
||||||
|
return (
|
||||||
|
template.replace("{task_id}", str(task.get("id", "")))
|
||||||
|
.replace("{task_title}", str(task.get("title", "")))
|
||||||
|
.replace("{task_file}", str(task_file))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_next_task(tasks: list[dict[str, Any]]) -> dict[str, Any] | None:
|
||||||
|
for task in tasks:
|
||||||
|
if task.get("status", "pending") == "pending":
|
||||||
|
return task
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def run_single_task(repo_root: pathlib.Path, orch_dir: pathlib.Path, config: dict[str, Any]) -> bool:
|
||||||
|
tasks_path = orch_dir / "tasks.json"
|
||||||
|
state_path = orch_dir / "state.json"
|
||||||
|
events_path = orch_dir / "events.ndjson"
|
||||||
|
logs_dir = orch_dir / "logs"
|
||||||
|
results_dir = orch_dir / "results"
|
||||||
|
|
||||||
|
tasks = load_json(tasks_path, {"tasks": []})
|
||||||
|
task_items = tasks.get("tasks", [])
|
||||||
|
if not isinstance(task_items, list):
|
||||||
|
raise ValueError("tasks.json must contain {'tasks': [...]} structure")
|
||||||
|
|
||||||
|
task = pick_next_task(task_items)
|
||||||
|
if not task:
|
||||||
|
return False
|
||||||
|
|
||||||
|
task_id = str(task.get("id", "unknown-task"))
|
||||||
|
task["status"] = "running"
|
||||||
|
task["started_at"] = now_iso()
|
||||||
|
save_json(tasks_path, {"tasks": task_items})
|
||||||
|
|
||||||
|
state = load_json(state_path, {"running_task_id": None, "updated_at": None})
|
||||||
|
state["running_task_id"] = task_id
|
||||||
|
state["updated_at"] = now_iso()
|
||||||
|
save_json(state_path, state)
|
||||||
|
|
||||||
|
emit_event(events_path, "task.assigned", task_id, "running", "controller", "Task assigned")
|
||||||
|
emit_event(events_path, "task.started", task_id, "running", "worker", "Worker execution started")
|
||||||
|
|
||||||
|
log_path = logs_dir / f"{task_id}.log"
|
||||||
|
task_file = orch_dir / f"task-{task_id}.json"
|
||||||
|
save_json(task_file, task)
|
||||||
|
|
||||||
|
cmd = str(task.get("command", "")).strip()
|
||||||
|
if not cmd:
|
||||||
|
template = str(config.get("worker", {}).get("command_template", "")).strip()
|
||||||
|
if template:
|
||||||
|
cmd = render_command_template(template, task, task_file)
|
||||||
|
|
||||||
|
if not cmd:
|
||||||
|
task["status"] = "failed"
|
||||||
|
task["failed_at"] = now_iso()
|
||||||
|
task["error"] = "No task command or worker command_template configured."
|
||||||
|
save_json(tasks_path, {"tasks": task_items})
|
||||||
|
emit_event(events_path, "task.failed", task_id, "failed", "controller", task["error"])
|
||||||
|
state["running_task_id"] = None
|
||||||
|
state["updated_at"] = now_iso()
|
||||||
|
save_json(state_path, state)
|
||||||
|
return True
|
||||||
|
|
||||||
|
rc, _ = run_shell(cmd, repo_root, log_path)
|
||||||
|
if rc != 0:
|
||||||
|
task["status"] = "failed"
|
||||||
|
task["failed_at"] = now_iso()
|
||||||
|
task["error"] = f"Worker command failed with exit code {rc}"
|
||||||
|
save_json(tasks_path, {"tasks": task_items})
|
||||||
|
emit_event(events_path, "task.failed", task_id, "failed", "worker", task["error"])
|
||||||
|
state["running_task_id"] = None
|
||||||
|
state["updated_at"] = now_iso()
|
||||||
|
save_json(state_path, state)
|
||||||
|
save_json(results_dir / f"{task_id}.json", {"task_id": task_id, "status": "failed", "exit_code": rc})
|
||||||
|
return True
|
||||||
|
|
||||||
|
gates = task.get("quality_gates") or config.get("quality_gates") or []
|
||||||
|
all_passed = True
|
||||||
|
gate_results: list[dict[str, Any]] = []
|
||||||
|
for gate in gates:
|
||||||
|
gate_cmd = str(gate).strip()
|
||||||
|
if not gate_cmd:
|
||||||
|
continue
|
||||||
|
emit_event(events_path, "rail.check.started", task_id, "running", "quality-gate", f"Running gate: {gate_cmd}")
|
||||||
|
gate_rc, _ = run_shell(gate_cmd, repo_root, log_path)
|
||||||
|
if gate_rc == 0:
|
||||||
|
emit_event(events_path, "rail.check.passed", task_id, "running", "quality-gate", f"Gate passed: {gate_cmd}")
|
||||||
|
else:
|
||||||
|
all_passed = False
|
||||||
|
emit_event(
|
||||||
|
events_path,
|
||||||
|
"rail.check.failed",
|
||||||
|
task_id,
|
||||||
|
"failed",
|
||||||
|
"quality-gate",
|
||||||
|
f"Gate failed ({gate_rc}): {gate_cmd}",
|
||||||
|
)
|
||||||
|
gate_results.append({"command": gate_cmd, "exit_code": gate_rc})
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
task["status"] = "completed"
|
||||||
|
task["completed_at"] = now_iso()
|
||||||
|
emit_event(events_path, "task.completed", task_id, "completed", "controller", "Task completed")
|
||||||
|
else:
|
||||||
|
task["status"] = "failed"
|
||||||
|
task["failed_at"] = now_iso()
|
||||||
|
task["error"] = "One or more quality gates failed"
|
||||||
|
emit_event(events_path, "task.failed", task_id, "failed", "controller", task["error"])
|
||||||
|
|
||||||
|
save_json(tasks_path, {"tasks": task_items})
|
||||||
|
state["running_task_id"] = None
|
||||||
|
state["updated_at"] = now_iso()
|
||||||
|
save_json(state_path, state)
|
||||||
|
save_json(
|
||||||
|
results_dir / f"{task_id}.json",
|
||||||
|
{
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": task["status"],
|
||||||
|
"completed_at": task.get("completed_at"),
|
||||||
|
"failed_at": task.get("failed_at"),
|
||||||
|
"gate_results": gate_results,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Mosaic deterministic orchestrator controller")
|
||||||
|
parser.add_argument("--repo", default=os.getcwd(), help="Repository root (default: cwd)")
|
||||||
|
parser.add_argument("--once", action="store_true", help="Process at most one pending task and exit")
|
||||||
|
parser.add_argument("--poll-sec", type=int, default=10, help="Polling interval for continuous mode")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
repo_root = pathlib.Path(args.repo).resolve()
|
||||||
|
orch_dir = repo_root / ".mosaic" / "orchestrator"
|
||||||
|
config_path = orch_dir / "config.json"
|
||||||
|
if not config_path.exists():
|
||||||
|
print(f"[mosaic-orchestrator] missing config: {config_path}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
config = load_json(config_path, {})
|
||||||
|
if not config.get("enabled", False):
|
||||||
|
print("[mosaic-orchestrator] disabled in .mosaic/orchestrator/config.json (enabled=false)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.once:
|
||||||
|
processed = run_single_task(repo_root, orch_dir, config)
|
||||||
|
if not processed:
|
||||||
|
print("[mosaic-orchestrator] no pending tasks")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"[mosaic-orchestrator] loop start repo={repo_root} poll={args.poll_sec}s")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
processed = run_single_task(repo_root, orch_dir, config)
|
||||||
|
if not processed:
|
||||||
|
time.sleep(max(1, args.poll_sec))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n[mosaic-orchestrator] stopping")
|
||||||
|
return 0
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print(f"[mosaic-orchestrator] error: {exc}", file=sys.stderr)
|
||||||
|
time.sleep(max(1, args.poll_sec))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
64
rails/orchestrator-matrix/protocol/event.schema.json
Normal file
64
rails/orchestrator-matrix/protocol/event.schema.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://mosaicstack.dev/schemas/orchestrator/event.schema.json",
|
||||||
|
"title": "Mosaic Orchestrator Event",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"event_id",
|
||||||
|
"event_type",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"timestamp",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"event_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "UUID string"
|
||||||
|
},
|
||||||
|
"event_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"task.assigned",
|
||||||
|
"task.started",
|
||||||
|
"task.progress",
|
||||||
|
"task.completed",
|
||||||
|
"task.failed",
|
||||||
|
"rail.check.started",
|
||||||
|
"rail.check.passed",
|
||||||
|
"rail.check.failed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"task_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"running",
|
||||||
|
"completed",
|
||||||
|
"failed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"controller",
|
||||||
|
"worker",
|
||||||
|
"quality-gate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
49
rails/orchestrator-matrix/protocol/task.schema.json
Normal file
49
rails/orchestrator-matrix/protocol/task.schema.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://mosaicstack.dev/schemas/orchestrator/task.schema.json",
|
||||||
|
"title": "Mosaic Orchestrator Task",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"running",
|
||||||
|
"completed",
|
||||||
|
"failed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Preferred worker runtime, e.g. codex, claude, opencode"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Worker command to execute for this task"
|
||||||
|
},
|
||||||
|
"quality_gates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
@@ -29,3 +29,19 @@ Verify enforcement:
|
|||||||
```bash
|
```bash
|
||||||
~/.mosaic/bin/mosaic-quality-verify --target .
|
~/.mosaic/bin/mosaic-quality-verify --target .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Optional Matrix Orchestrator Rail
|
||||||
|
|
||||||
|
Repo-local orchestrator state lives in `.mosaic/orchestrator/`.
|
||||||
|
|
||||||
|
Run one cycle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --once
|
||||||
|
```
|
||||||
|
|
||||||
|
Run continuously:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.mosaic/bin/mosaic-orchestrator-run --poll-sec 10
|
||||||
|
```
|
||||||
|
|||||||
19
templates/repo/.mosaic/orchestrator/config.json
Normal file
19
templates/repo/.mosaic/orchestrator/config.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"transport": "matrix",
|
||||||
|
"matrix": {
|
||||||
|
"control_room_id": "",
|
||||||
|
"workspace_id": "",
|
||||||
|
"homeserver_url": ""
|
||||||
|
},
|
||||||
|
"worker": {
|
||||||
|
"runtime": "codex",
|
||||||
|
"command_template": "",
|
||||||
|
"timeout_seconds": 7200
|
||||||
|
},
|
||||||
|
"quality_gates": [
|
||||||
|
"pnpm lint",
|
||||||
|
"pnpm typecheck",
|
||||||
|
"pnpm test"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
templates/repo/.mosaic/orchestrator/logs/.gitkeep
Normal file
1
templates/repo/.mosaic/orchestrator/logs/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
templates/repo/.mosaic/orchestrator/results/.gitkeep
Normal file
1
templates/repo/.mosaic/orchestrator/results/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
4
templates/repo/.mosaic/orchestrator/state.json
Normal file
4
templates/repo/.mosaic/orchestrator/state.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"running_task_id": null,
|
||||||
|
"updated_at": null
|
||||||
|
}
|
||||||
16
templates/repo/.mosaic/orchestrator/tasks.json
Normal file
16
templates/repo/.mosaic/orchestrator/tasks.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "EXAMPLE-001",
|
||||||
|
"title": "Example orchestrator task",
|
||||||
|
"description": "Replace this with a real task and command",
|
||||||
|
"status": "pending",
|
||||||
|
"runtime": "codex",
|
||||||
|
"command": "",
|
||||||
|
"quality_gates": [],
|
||||||
|
"metadata": {
|
||||||
|
"source": "bootstrap-template"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user