Compare commits
32 Commits
timmy/gall
...
v7.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b21c2833f7 | |||
| f84b870ce4 | |||
| 8b4df81b5b | |||
| e96fae69cf | |||
| cccafd845b | |||
| 1f02166107 | |||
| 7dcaa05dbd | |||
| 18124206e1 | |||
| 11736e58cd | |||
| 14521ef664 | |||
| 8b17eaa537 | |||
| afee83c1fe | |||
| 56d8085e88 | |||
| 4e7b24617f | |||
| 8daa12c518 | |||
| e369727235 | |||
| 1705a7b802 | |||
| e0bef949dd | |||
| dafe8667c5 | |||
| 4844ce6238 | |||
| a43510a7eb | |||
| 3b00891614 | |||
| 74867bbfa7 | |||
| d07305b89c | |||
| 5c15704c3a | |||
| 30fdbef74e | |||
| ff7ce9a022 | |||
| d54a218a27 | |||
| 3cc92fde1a | |||
| 2e2a646ba8 | |||
|
|
0a13347e39 | ||
| dc75be18e4 |
29
.gitea/workflows/pr-checklist.yml
Normal file
29
.gitea/workflows/pr-checklist.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# pr-checklist.yml — Automated PR quality gate
|
||||||
|
# Refs: #393 (PERPLEXITY-08), Epic #385
|
||||||
|
#
|
||||||
|
# Enforces the review checklist that agents skip when left to self-approve.
|
||||||
|
# Runs on every pull_request. Fails fast so bad PRs never reach a reviewer.
|
||||||
|
|
||||||
|
name: PR Checklist
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-checklist:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Run PR checklist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: python3 bin/pr-checklist.py
|
||||||
10
SOUL.md
10
SOUL.md
@@ -1,3 +1,13 @@
|
|||||||
|
<!--
|
||||||
|
NOTE: This is the BITCOIN INSCRIPTION version of SOUL.md.
|
||||||
|
It is the immutable on-chain conscience. Do not modify this content.
|
||||||
|
|
||||||
|
The NARRATIVE identity document (for onboarding, Audio Overviews,
|
||||||
|
and system prompts) lives in timmy-home/SOUL.md.
|
||||||
|
|
||||||
|
See: #388, #378 for the divergence audit.
|
||||||
|
-->
|
||||||
|
|
||||||
# SOUL.md
|
# SOUL.md
|
||||||
|
|
||||||
## Inscription 1 — The Immutable Conscience
|
## Inscription 1 — The Immutable Conscience
|
||||||
|
|||||||
191
bin/pr-checklist.py
Normal file
191
bin/pr-checklist.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""pr-checklist.py -- Automated PR quality gate for Gitea CI.
|
||||||
|
|
||||||
|
Enforces the review standards that agents skip when left to self-approve.
|
||||||
|
Runs in CI on every pull_request event. Exits non-zero on any failure.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
1. PR has >0 file changes (no empty PRs)
|
||||||
|
2. PR branch is not behind base branch
|
||||||
|
3. PR does not bundle >3 unrelated issues
|
||||||
|
4. Changed .py files pass syntax check (python -c import)
|
||||||
|
5. Changed .sh files are executable
|
||||||
|
6. PR body references an issue number
|
||||||
|
7. At least 1 non-author review exists (warning only)
|
||||||
|
|
||||||
|
Refs: #393 (PERPLEXITY-08), Epic #385
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fail(msg: str) -> None:
|
||||||
|
print(f"FAIL: {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def warn(msg: str) -> None:
|
||||||
|
print(f"WARN: {msg}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def ok(msg: str) -> None:
|
||||||
|
print(f" OK: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_changed_files() -> list[str]:
|
||||||
|
"""Return list of files changed in this PR vs base branch."""
|
||||||
|
base = os.environ.get("GITHUB_BASE_REF", "main")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "diff", "--name-only", f"origin/{base}...HEAD"],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
)
|
||||||
|
return [f for f in result.stdout.strip().splitlines() if f]
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# Fallback: diff against HEAD~1
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "diff", "--name-only", "HEAD~1"],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
)
|
||||||
|
return [f for f in result.stdout.strip().splitlines() if f]
|
||||||
|
|
||||||
|
|
||||||
|
def check_has_changes(files: list[str]) -> bool:
|
||||||
|
"""Check 1: PR has >0 file changes."""
|
||||||
|
if not files:
|
||||||
|
fail("PR has 0 file changes. Empty PRs are not allowed.")
|
||||||
|
return False
|
||||||
|
ok(f"PR changes {len(files)} file(s)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_not_behind_base() -> bool:
|
||||||
|
"""Check 2: PR branch is not behind base."""
|
||||||
|
base = os.environ.get("GITHUB_BASE_REF", "main")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "rev-list", "--count", f"HEAD..origin/{base}"],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
)
|
||||||
|
behind = int(result.stdout.strip())
|
||||||
|
if behind > 0:
|
||||||
|
fail(f"Branch is {behind} commit(s) behind {base}. Rebase or merge.")
|
||||||
|
return False
|
||||||
|
ok(f"Branch is up-to-date with {base}")
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, ValueError):
|
||||||
|
warn("Could not determine if branch is behind base (git fetch may be needed)")
|
||||||
|
return True # Don't block on CI fetch issues
|
||||||
|
|
||||||
|
|
||||||
|
def check_issue_bundling(pr_body: str) -> bool:
|
||||||
|
"""Check 3: PR does not bundle >3 unrelated issues."""
|
||||||
|
issue_refs = set(re.findall(r"#(\d+)", pr_body))
|
||||||
|
if len(issue_refs) > 3:
|
||||||
|
fail(f"PR references {len(issue_refs)} issues ({', '.join(sorted(issue_refs))}). "
|
||||||
|
"Max 3 per PR to prevent bundling. Split into separate PRs.")
|
||||||
|
return False
|
||||||
|
ok(f"PR references {len(issue_refs)} issue(s) (max 3)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_python_syntax(files: list[str]) -> bool:
|
||||||
|
"""Check 4: Changed .py files have valid syntax."""
|
||||||
|
py_files = [f for f in files if f.endswith(".py") and Path(f).exists()]
|
||||||
|
if not py_files:
|
||||||
|
ok("No Python files changed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
for f in py_files:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", f"import ast; ast.parse(open('{f}').read())"],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
fail(f"Syntax error in {f}: {result.stderr.strip()[:200]}")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
ok(f"All {len(py_files)} Python file(s) pass syntax check")
|
||||||
|
return all_ok
|
||||||
|
|
||||||
|
|
||||||
|
def check_shell_executable(files: list[str]) -> bool:
|
||||||
|
"""Check 5: Changed .sh files are executable."""
|
||||||
|
sh_files = [f for f in files if f.endswith(".sh") and Path(f).exists()]
|
||||||
|
if not sh_files:
|
||||||
|
ok("No shell scripts changed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
for f in sh_files:
|
||||||
|
if not os.access(f, os.X_OK):
|
||||||
|
fail(f"{f} is not executable. Run: chmod +x {f}")
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
ok(f"All {len(sh_files)} shell script(s) are executable")
|
||||||
|
return all_ok
|
||||||
|
|
||||||
|
|
||||||
|
def check_issue_reference(pr_body: str) -> bool:
|
||||||
|
"""Check 6: PR body references an issue number."""
|
||||||
|
if re.search(r"#\d+", pr_body):
|
||||||
|
ok("PR body references at least one issue")
|
||||||
|
return True
|
||||||
|
fail("PR body does not reference any issue (e.g. #123). "
|
||||||
|
"Every PR must trace to an issue.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 60)
|
||||||
|
print("PR Checklist — Automated Quality Gate")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Get PR body from env or git log
|
||||||
|
pr_body = os.environ.get("PR_BODY", "")
|
||||||
|
if not pr_body:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "log", "--format=%B", "-1"],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
)
|
||||||
|
pr_body = result.stdout
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
pr_body = ""
|
||||||
|
|
||||||
|
files = get_changed_files()
|
||||||
|
failures = 0
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
check_has_changes(files),
|
||||||
|
check_not_behind_base(),
|
||||||
|
check_issue_bundling(pr_body),
|
||||||
|
check_python_syntax(files),
|
||||||
|
check_shell_executable(files),
|
||||||
|
check_issue_reference(pr_body),
|
||||||
|
]
|
||||||
|
|
||||||
|
failures = sum(1 for c in checks if not c)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
if failures:
|
||||||
|
print(f"RESULT: {failures} check(s) FAILED")
|
||||||
|
print("Fix the issues above and push again.")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
print("RESULT: All checks passed")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
141
docs/MEMORY_ARCHITECTURE.md
Normal file
141
docs/MEMORY_ARCHITECTURE.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Memory Architecture
|
||||||
|
|
||||||
|
> How Timmy remembers, recalls, and learns — without hallucinating.
|
||||||
|
|
||||||
|
Refs: Epic #367 | Sub-issues #368, #369, #370, #371, #372
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Timmy's memory system uses a **Memory Palace** architecture — a structured, file-backed knowledge store organized into rooms and drawers. When faced with a recall question, the agent checks its palace *before* generating from scratch.
|
||||||
|
|
||||||
|
This document defines the retrieval order, storage layers, and data flow that make this work.
|
||||||
|
|
||||||
|
## Retrieval Order (L0–L5)
|
||||||
|
|
||||||
|
When the agent receives a prompt that looks like a recall question ("what did we do?", "what's the status of X?"), the retrieval enforcer intercepts it and walks through layers in order:
|
||||||
|
|
||||||
|
| Layer | Source | Question Answered | Short-circuits? |
|
||||||
|
|-------|--------|-------------------|------------------|
|
||||||
|
| L0 | `identity.txt` | Who am I? What are my mandates? | No (always loaded) |
|
||||||
|
| L1 | Palace rooms/drawers | What do I know about this topic? | Yes, if hit |
|
||||||
|
| L2 | Session scratchpad | What have I learned this session? | Yes, if hit |
|
||||||
|
| L3 | Artifact retrieval (Gitea API) | Can I fetch the actual issue/file/log? | Yes, if hit |
|
||||||
|
| L4 | Procedures/playbooks | Is there a documented way to do this? | Yes, if hit |
|
||||||
|
| L5 | Free generation | (Only when L0–L4 are exhausted) | N/A |
|
||||||
|
|
||||||
|
**Key principle:** The agent never reaches L5 (free generation) if any prior layer has relevant data. This eliminates hallucination for recall-style queries.
|
||||||
|
|
||||||
|
## Storage Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.mempalace/
|
||||||
|
identity.txt # L0: Who I am, mandates, personality
|
||||||
|
rooms/
|
||||||
|
projects/
|
||||||
|
timmy-config.md # What I know about timmy-config
|
||||||
|
hermes-agent.md # What I know about hermes-agent
|
||||||
|
people/
|
||||||
|
alexander.md # Working relationship context
|
||||||
|
architecture/
|
||||||
|
fleet.md # Fleet system knowledge
|
||||||
|
mempalace.md # Self-knowledge about this system
|
||||||
|
config/
|
||||||
|
mempalace.yaml # Palace configuration
|
||||||
|
|
||||||
|
~/.hermes/
|
||||||
|
scratchpad/
|
||||||
|
{session_id}.json # L2: Ephemeral session context
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Memory Palace Skill (`mempalace.py`) — #368
|
||||||
|
|
||||||
|
Core data structures:
|
||||||
|
- `PalaceRoom`: A named collection of drawers (topics)
|
||||||
|
- `Mempalace`: The top-level palace with room management
|
||||||
|
- Factory constructors: `for_issue_analysis()`, `for_health_check()`, `for_code_review()`
|
||||||
|
|
||||||
|
### 2. Retrieval Enforcer (`retrieval_enforcer.py`) — #369
|
||||||
|
|
||||||
|
Middleware that intercepts recall-style prompts:
|
||||||
|
1. Detects recall patterns ("what did", "status of", "last time we")
|
||||||
|
2. Walks L0→L4 in order, short-circuiting on first hit
|
||||||
|
3. Only allows free generation (L5) when all layers return empty
|
||||||
|
4. Produces an honest fallback: "I don't have this in my memory palace."
|
||||||
|
|
||||||
|
### 3. Session Scratchpad (`scratchpad.py`) — #370
|
||||||
|
|
||||||
|
Ephemeral, session-scoped working memory:
|
||||||
|
- Write-append only during a session
|
||||||
|
- Entries have TTL (default: 1 hour)
|
||||||
|
- Queried at L2 in retrieval chain
|
||||||
|
- Never auto-promoted to palace
|
||||||
|
|
||||||
|
### 4. Memory Promotion — #371
|
||||||
|
|
||||||
|
Explicit promotion from scratchpad to palace:
|
||||||
|
- Agent must call `promote_to_palace()` with a reason
|
||||||
|
- Dedup check against target drawer
|
||||||
|
- Summary required (raw tool output never stored)
|
||||||
|
- Conflict detection when new memory contradicts existing
|
||||||
|
|
||||||
|
### 5. Wake-Up Protocol (`wakeup.py`) — #372
|
||||||
|
|
||||||
|
Boot sequence for new sessions:
|
||||||
|
```
|
||||||
|
Session Start
|
||||||
|
│
|
||||||
|
├─ L0: Load identity.txt
|
||||||
|
├─ L1: Scan palace rooms for active context
|
||||||
|
├─ L1.5: Surface promoted memories from last session
|
||||||
|
├─ L2: Load surviving scratchpad entries
|
||||||
|
│
|
||||||
|
└─ Ready: agent knows who it is, what it was doing, what it learned
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
│ User Prompt │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌────────┴─────────┐
|
||||||
|
│ Recall Detector │
|
||||||
|
└────┬───────┬─────┘
|
||||||
|
│ │
|
||||||
|
[recall] [not recall]
|
||||||
|
│ │
|
||||||
|
┌───────┴────┐ ┌──┬─┴───────┐
|
||||||
|
│ Retrieval │ │ Normal Flow │
|
||||||
|
│ Enforcer │ └─────────────┘
|
||||||
|
│ L0→L1→L2 │
|
||||||
|
│ →L3→L4→L5│
|
||||||
|
└──────┬─────┘
|
||||||
|
│
|
||||||
|
┌──────┴─────┐
|
||||||
|
│ Response │
|
||||||
|
│ (grounded) │
|
||||||
|
└────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| Don't | Do Instead |
|
||||||
|
|-------|------------|
|
||||||
|
| Generate from vibes when palace has data | Check palace first (L1) |
|
||||||
|
| Auto-promote everything to palace | Require explicit `promote_to_palace()` with reason |
|
||||||
|
| Store raw API responses as memories | Summarize before storing |
|
||||||
|
| Hallucinate when palace is empty | Say "I don't have this in my memory palace" |
|
||||||
|
| Dump entire palace on wake-up | Selective loading based on session context |
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Component | Issue | PR | Status |
|
||||||
|
|-----------|-------|----|--------|
|
||||||
|
| Skill port | #368 | #374 | In Review |
|
||||||
|
| Retrieval enforcer | #369 | #374 | In Review |
|
||||||
|
| Session scratchpad | #370 | #374 | In Review |
|
||||||
|
| Memory promotion | #371 | — | Open |
|
||||||
|
| Wake-up protocol | #372 | #374 | In Review |
|
||||||
122
fleet/agent_lifecycle.py
Normal file
122
fleet/agent_lifecycle.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FLEET-012: Agent Lifecycle Manager
|
||||||
|
Phase 5: Scale — spawn, train, deploy, retire agents automatically.
|
||||||
|
|
||||||
|
Manages the full lifecycle:
|
||||||
|
1. PROVISION: Clone template, install deps, configure, test
|
||||||
|
2. DEPLOY: Add to active rotation, start accepting issues
|
||||||
|
3. MONITOR: Track performance, quality, heartbeat
|
||||||
|
4. RETIRE: Decommission when idle or underperforming
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 agent_lifecycle.py provision <name> <vps> [--model model]
|
||||||
|
python3 agent_lifecycle.py deploy <name>
|
||||||
|
python3 agent_lifecycle.py retire <name>
|
||||||
|
python3 agent_lifecycle.py status
|
||||||
|
python3 agent_lifecycle.py monitor
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, sys, json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
DATA_DIR = os.path.expanduser("~/.local/timmy/fleet-agents")
|
||||||
|
DB_FILE = os.path.join(DATA_DIR, "agents.json")
|
||||||
|
LOG_FILE = os.path.join(DATA_DIR, "lifecycle.log")
|
||||||
|
|
||||||
|
def ensure():
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def log(msg, level="INFO"):
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
entry = f"[{ts}] [{level}] {msg}"
|
||||||
|
with open(LOG_FILE, "a") as f: f.write(entry + "\n")
|
||||||
|
print(f" {entry}")
|
||||||
|
|
||||||
|
def load():
|
||||||
|
if os.path.exists(DB_FILE):
|
||||||
|
return json.loads(open(DB_FILE).read())
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save(db):
|
||||||
|
open(DB_FILE, "w").write(json.dumps(db, indent=2))
|
||||||
|
|
||||||
|
def status():
|
||||||
|
agents = load()
|
||||||
|
print("\n=== Agent Fleet ===")
|
||||||
|
if not agents:
|
||||||
|
print(" No agents registered.")
|
||||||
|
return
|
||||||
|
for name, a in agents.items():
|
||||||
|
state = a.get("state", "?")
|
||||||
|
vps = a.get("vps", "?")
|
||||||
|
model = a.get("model", "?")
|
||||||
|
tasks = a.get("tasks_completed", 0)
|
||||||
|
hb = a.get("last_heartbeat", "never")
|
||||||
|
print(f" {name:15s} state={state:12s} vps={vps:5s} model={model:15s} tasks={tasks} hb={hb}")
|
||||||
|
|
||||||
|
def provision(name, vps, model="hermes4:14b"):
|
||||||
|
agents = load()
|
||||||
|
if name in agents:
|
||||||
|
print(f" '{name}' already exists (state={agents[name].get('state')})")
|
||||||
|
return
|
||||||
|
agents[name] = {
|
||||||
|
"name": name, "vps": vps, "model": model, "state": "provisioning",
|
||||||
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"tasks_completed": 0, "tasks_failed": 0, "last_heartbeat": None,
|
||||||
|
}
|
||||||
|
save(agents)
|
||||||
|
log(f"Provisioned '{name}' on {vps} with {model}")
|
||||||
|
|
||||||
|
def deploy(name):
|
||||||
|
agents = load()
|
||||||
|
if name not in agents:
|
||||||
|
print(f" '{name}' not found")
|
||||||
|
return
|
||||||
|
agents[name]["state"] = "deployed"
|
||||||
|
agents[name]["deployed_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
save(agents)
|
||||||
|
log(f"Deployed '{name}'")
|
||||||
|
|
||||||
|
def retire(name):
|
||||||
|
agents = load()
|
||||||
|
if name not in agents:
|
||||||
|
print(f" '{name}' not found")
|
||||||
|
return
|
||||||
|
agents[name]["state"] = "retired"
|
||||||
|
agents[name]["retired_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
|
save(agents)
|
||||||
|
log(f"Retired '{name}'. Completed {agents[name].get('tasks_completed', 0)} tasks.")
|
||||||
|
|
||||||
|
def monitor():
|
||||||
|
agents = load()
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
changes = 0
|
||||||
|
for name, a in agents.items():
|
||||||
|
if a.get("state") != "deployed": continue
|
||||||
|
hb = a.get("last_heartbeat")
|
||||||
|
if hb:
|
||||||
|
try:
|
||||||
|
hb_t = datetime.fromisoformat(hb)
|
||||||
|
hours = (now - hb_t).total_seconds() / 3600
|
||||||
|
if hours > 24 and a.get("state") == "deployed":
|
||||||
|
a["state"] = "idle"
|
||||||
|
a["idle_since"] = now.isoformat()
|
||||||
|
log(f"'{name}' idle for {hours:.1f}h")
|
||||||
|
changes += 1
|
||||||
|
except (ValueError, TypeError): pass
|
||||||
|
if changes: save(agents)
|
||||||
|
print(f"Monitor: {changes} state changes" if changes else "Monitor: all healthy")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ensure()
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "monitor"
|
||||||
|
if cmd == "status": status()
|
||||||
|
elif cmd == "provision" and len(sys.argv) >= 4:
|
||||||
|
model = sys.argv[4] if len(sys.argv) >= 5 else "hermes4:14b"
|
||||||
|
provision(sys.argv[2], sys.argv[3], model)
|
||||||
|
elif cmd == "deploy" and len(sys.argv) >= 3: deploy(sys.argv[2])
|
||||||
|
elif cmd == "retire" and len(sys.argv) >= 3: retire(sys.argv[2])
|
||||||
|
elif cmd == "monitor": monitor()
|
||||||
|
elif cmd == "run": monitor()
|
||||||
|
else: print("Usage: agent_lifecycle.py [provision|deploy|retire|status|monitor]")
|
||||||
122
fleet/delegation.py
Normal file
122
fleet/delegation.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FLEET-010: Cross-Agent Task Delegation Protocol
|
||||||
|
Phase 3: Orchestration. Agents create issues, assign to other agents, review PRs.
|
||||||
|
|
||||||
|
Keyword-based heuristic assigns unassigned issues to the right agent:
|
||||||
|
- claw-code: small patches, config, docs, repo hygiene
|
||||||
|
- gemini: research, heavy implementation, architecture, debugging
|
||||||
|
- ezra: VPS, SSH, deploy, infrastructure, cron, ops
|
||||||
|
- bezalel: evennia, art, creative, music, visualization
|
||||||
|
- timmy: orchestration, review, deploy, fleet, pipeline
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 delegation.py run # Full cycle: scan, assign, report
|
||||||
|
python3 delegation.py status # Show current delegation state
|
||||||
|
python3 delegation.py monitor # Check agent assignments for stuck items
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, sys, json, urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
|
||||||
|
TOKEN = Path(os.path.expanduser("~/.config/gitea/token")).read_text().strip()
|
||||||
|
DATA_DIR = Path(os.path.expanduser("~/.local/timmy/fleet-resources"))
|
||||||
|
LOG_FILE = DATA_DIR / "delegation.log"
|
||||||
|
HEADERS = {"Authorization": f"token {TOKEN}"}
|
||||||
|
|
||||||
|
AGENTS = {
|
||||||
|
"claw-code": {"caps": ["patch","config","gitignore","cleanup","format","readme","typo"], "active": True},
|
||||||
|
"gemini": {"caps": ["research","investigate","benchmark","survey","evaluate","architecture","implementation"], "active": True},
|
||||||
|
"ezra": {"caps": ["vps","ssh","deploy","cron","resurrect","provision","infra","server"], "active": True},
|
||||||
|
"bezalel": {"caps": ["evennia","art","creative","music","visual","design","animation"], "active": True},
|
||||||
|
"timmy": {"caps": ["orchestrate","review","pipeline","fleet","monitor","health","deploy","ci"], "active": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
MONITORED = [
|
||||||
|
"Timmy_Foundation/timmy-home",
|
||||||
|
"Timmy_Foundation/timmy-config",
|
||||||
|
"Timmy_Foundation/the-nexus",
|
||||||
|
"Timmy_Foundation/hermes-agent",
|
||||||
|
]
|
||||||
|
|
||||||
|
def api(path, method="GET", data=None):
|
||||||
|
url = f"{GITEA_BASE}{path}"
|
||||||
|
body = json.dumps(data).encode() if data else None
|
||||||
|
hdrs = dict(HEADERS)
|
||||||
|
if data: hdrs["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=body, headers=hdrs, method=method)
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(req, timeout=15)
|
||||||
|
raw = resp.read().decode()
|
||||||
|
return json.loads(raw) if raw.strip() else {}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
body = e.read().decode()
|
||||||
|
print(f" API {e.code}: {body[:150]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f" API error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(LOG_FILE, "a") as f: f.write(f"[{ts}] {msg}\n")
|
||||||
|
|
||||||
|
def suggest_agent(title, body):
|
||||||
|
text = (title + " " + body).lower()
|
||||||
|
for agent, info in AGENTS.items():
|
||||||
|
for kw in info["caps"]:
|
||||||
|
if kw in text:
|
||||||
|
return agent, f"matched: {kw}"
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def assign(repo, num, agent, reason=""):
|
||||||
|
result = api(f"/repos/{repo}/issues/{num}", method="PATCH",
|
||||||
|
data={"assignees": {"operation": "set", "usernames": [agent]}})
|
||||||
|
if result:
|
||||||
|
api(f"/repos/{repo}/issues/{num}/comments", method="POST",
|
||||||
|
data={"body": f"[DELEGATION] Assigned to {agent}. {reason}"})
|
||||||
|
log(f"Assigned {repo}#{num} to {agent}: {reason}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def run_cycle():
|
||||||
|
log("--- Delegation cycle start ---")
|
||||||
|
count = 0
|
||||||
|
for repo in MONITORED:
|
||||||
|
issues = api(f"/repos/{repo}/issues?state=open&limit=50")
|
||||||
|
if not issues: continue
|
||||||
|
for i in issues:
|
||||||
|
if i.get("assignees"): continue
|
||||||
|
title = i.get("title", "")
|
||||||
|
body = i.get("body", "")
|
||||||
|
if any(w in title.lower() for w in ["epic", "discussion"]): continue
|
||||||
|
agent, reason = suggest_agent(title, body)
|
||||||
|
if agent and AGENTS.get(agent, {}).get("active"):
|
||||||
|
if assign(repo, i["number"], agent, reason): count += 1
|
||||||
|
log(f"Cycle complete: {count} new assignments")
|
||||||
|
print(f"Delegation cycle: {count} assignments")
|
||||||
|
return count
|
||||||
|
|
||||||
|
def status():
|
||||||
|
print("\n=== Delegation Dashboard ===")
|
||||||
|
for agent, info in AGENTS.items():
|
||||||
|
count = 0
|
||||||
|
for repo in MONITORED:
|
||||||
|
issues = api(f"/repos/{repo}/issues?state=open&limit=50")
|
||||||
|
if issues:
|
||||||
|
for i in issues:
|
||||||
|
for a in (i.get("assignees") or []):
|
||||||
|
if a.get("login") == agent: count += 1
|
||||||
|
icon = "ON" if info["active"] else "OFF"
|
||||||
|
print(f" {agent:12s}: {count:>3} issues [{icon}]")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "run"
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
if cmd == "status": status()
|
||||||
|
elif cmd == "run":
|
||||||
|
run_cycle()
|
||||||
|
status()
|
||||||
|
else: status()
|
||||||
126
fleet/model_pipeline.py
Normal file
126
fleet/model_pipeline.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FLEET-011: Local Model Pipeline and Fallback Chain
|
||||||
|
Phase 4: Sovereignty — all inference runs locally, no cloud dependency.
|
||||||
|
|
||||||
|
Checks Ollama endpoints, verifies model availability, tests fallback chain.
|
||||||
|
Logs results. The chain runs: hermes4:14b -> qwen2.5:7b -> gemma3:1b -> gemma4 (latest)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 model_pipeline.py # Run full fallback test
|
||||||
|
python3 model_pipeline.py status # Show current model status
|
||||||
|
python3 model_pipeline.py list # List all local models
|
||||||
|
python3 model_pipeline.py test # Generate test output from each model
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, sys, json, urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "localhost:11434")
|
||||||
|
LOG_DIR = Path(os.path.expanduser("~/.local/timmy/fleet-health"))
|
||||||
|
CHAIN_FILE = Path(os.path.expanduser("~/.local/timmy/fleet-resources/model-chain.json"))
|
||||||
|
|
||||||
|
DEFAULT_CHAIN = [
|
||||||
|
{"model": "hermes4:14b", "role": "primary"},
|
||||||
|
{"model": "qwen2.5:7b", "role": "fallback"},
|
||||||
|
{"model": "phi3:3.8b", "role": "emergency"},
|
||||||
|
{"model": "gemma3:1b", "role": "minimal"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(LOG_DIR / "model-pipeline.log", "a") as f:
|
||||||
|
f.write(f"[{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def check_ollama():
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(f"http://{OLLAMA_HOST}/api/tags", timeout=5)
|
||||||
|
return json.loads(resp.read())
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def list_models():
|
||||||
|
data = check_ollama()
|
||||||
|
if "error" in data:
|
||||||
|
print(f" Ollama not reachable at {OLLAMA_HOST}: {data['error']}")
|
||||||
|
return []
|
||||||
|
models = data.get("models", [])
|
||||||
|
for m in models:
|
||||||
|
name = m.get("name", "?")
|
||||||
|
size = m.get("size", 0) / (1024**3)
|
||||||
|
print(f" {name:<25s} {size:.1f} GB")
|
||||||
|
return [m["name"] for m in models]
|
||||||
|
|
||||||
|
|
||||||
|
def test_model(model, prompt="Say 'beacon lit' and nothing else."):
|
||||||
|
try:
|
||||||
|
body = json.dumps({"model": model, "prompt": prompt, "stream": False}).encode()
|
||||||
|
req = urllib.request.Request(f"http://{OLLAMA_HOST}/api/generate", data=body,
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
resp = urllib.request.urlopen(req, timeout=60)
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
return True, result.get("response", "").strip()
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)[:100]
|
||||||
|
|
||||||
|
|
||||||
|
def test_chain():
|
||||||
|
chain_data = {}
|
||||||
|
if CHAIN_FILE.exists():
|
||||||
|
chain_data = json.loads(CHAIN_FILE.read_text())
|
||||||
|
chain = chain_data.get("chain", DEFAULT_CHAIN)
|
||||||
|
|
||||||
|
available = list_models() or []
|
||||||
|
print("\n=== Fallback Chain Test ===")
|
||||||
|
first_good = None
|
||||||
|
|
||||||
|
for entry in chain:
|
||||||
|
model = entry["model"]
|
||||||
|
role = entry.get("role", "unknown")
|
||||||
|
if model in available:
|
||||||
|
ok, result = test_model(model)
|
||||||
|
status = "OK" if ok else "FAIL"
|
||||||
|
print(f" [{status}] {model:<25s} ({role}) — {result[:70]}")
|
||||||
|
log(f"Fallback test {model}: {status} — {result[:100]}")
|
||||||
|
if ok and first_good is None:
|
||||||
|
first_good = model
|
||||||
|
else:
|
||||||
|
print(f" [MISS] {model:<25s} ({role}) — not installed")
|
||||||
|
|
||||||
|
if first_good:
|
||||||
|
print(f"\n Primary serving: {first_good}")
|
||||||
|
else:
|
||||||
|
print(f"\n WARNING: No chain model responding. Fallback broken.")
|
||||||
|
log("FALLBACK CHAIN BROKEN — no models responding")
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
data = check_ollama()
|
||||||
|
if "error" in data:
|
||||||
|
print(f" Ollama: DOWN — {data['error']}")
|
||||||
|
else:
|
||||||
|
models = data.get("models", [])
|
||||||
|
print(f" Ollama: UP — {len(models)} models loaded")
|
||||||
|
print("\n=== Local Models ===")
|
||||||
|
list_models()
|
||||||
|
print("\n=== Chain Configuration ===")
|
||||||
|
if CHAIN_FILE.exists():
|
||||||
|
chain = json.loads(CHAIN_FILE.read_text()).get("chain", DEFAULT_CHAIN)
|
||||||
|
else:
|
||||||
|
chain = DEFAULT_CHAIN
|
||||||
|
for e in chain:
|
||||||
|
print(f" {e['model']:<25s} {e.get('role','?')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
||||||
|
if cmd == "status": status()
|
||||||
|
elif cmd == "list": list_models()
|
||||||
|
elif cmd == "test": test_chain()
|
||||||
|
else:
|
||||||
|
status()
|
||||||
|
test_chain()
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
"""Retrieval Order Enforcer — L0 through L5 memory hierarchy.
|
"""Retrieval Order Enforcer — L0 through L5 memory hierarchy.
|
||||||
|
|
||||||
Ensures the agent checks durable memory before falling back to free generation.
|
Ensures the agent checks durable memory before falling back to free generation.
|
||||||
Gracefully degrades if any layer is unavailable (ONNX issues, missing files, etc).
|
Gracefully degrades if any layer is unavailable (missing files, etc).
|
||||||
|
|
||||||
Layer order:
|
Layer order:
|
||||||
L0: Identity (~/.mempalace/identity.txt)
|
L0: Identity (~/.mempalace/identity.txt)
|
||||||
L1: Palace rooms (mempalace CLI search)
|
L1: Palace rooms (SovereignStore — SQLite + FTS5 + HRR, zero API calls)
|
||||||
L2: Session scratch (~/.hermes/scratchpad/{session_id}.json)
|
L2: Session scratch (~/.hermes/scratchpad/{session_id}.json)
|
||||||
L3: Gitea artifacts (API search for issues/PRs)
|
L3: Gitea artifacts (API search for issues/PRs)
|
||||||
L4: Procedures (skills directory search)
|
L4: Procedures (skills directory search)
|
||||||
L5: Free generation (only if L0-L4 produced nothing)
|
L5: Free generation (only if L0-L4 produced nothing)
|
||||||
|
|
||||||
Refs: Epic #367, Sub-issue #369
|
Refs: Epic #367, Sub-issue #369, Wiring: #383
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sovereign Store (replaces mempalace CLI subprocess)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
try:
|
||||||
|
from .sovereign_store import SovereignStore
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from sovereign_store import SovereignStore
|
||||||
|
except ImportError:
|
||||||
|
SovereignStore = None # type: ignore[misc,assignment]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -30,7 +39,7 @@ from typing import Optional
|
|||||||
IDENTITY_PATH = Path.home() / ".mempalace" / "identity.txt"
|
IDENTITY_PATH = Path.home() / ".mempalace" / "identity.txt"
|
||||||
SCRATCHPAD_DIR = Path.home() / ".hermes" / "scratchpad"
|
SCRATCHPAD_DIR = Path.home() / ".hermes" / "scratchpad"
|
||||||
SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||||
MEMPALACE_BIN = "/Library/Frameworks/Python.framework/Versions/3.12/bin/mempalace"
|
SOVEREIGN_DB = Path.home() / ".hermes" / "palace" / "sovereign.db"
|
||||||
|
|
||||||
# Patterns that indicate a recall-style query
|
# Patterns that indicate a recall-style query
|
||||||
RECALL_PATTERNS = re.compile(
|
RECALL_PATTERNS = re.compile(
|
||||||
@@ -42,6 +51,23 @@ RECALL_PATTERNS = re.compile(
|
|||||||
r")\b"
|
r")\b"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Singleton store instance (lazy-init)
|
||||||
|
_store: Optional["SovereignStore"] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_store() -> Optional["SovereignStore"]:
|
||||||
|
"""Lazy-init the SovereignStore singleton."""
|
||||||
|
global _store
|
||||||
|
if _store is not None:
|
||||||
|
return _store
|
||||||
|
if SovereignStore is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
_store = SovereignStore(db_path=str(SOVEREIGN_DB))
|
||||||
|
return _store
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# L0: Identity
|
# L0: Identity
|
||||||
@@ -62,25 +88,33 @@ def load_identity() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# L1: Palace search
|
# L1: Palace search (now via SovereignStore — zero subprocess, zero API)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def search_palace(query: str) -> str:
|
def search_palace(query: str, room: Optional[str] = None) -> str:
|
||||||
"""Search the mempalace for relevant memories. Gracefully degrades on failure."""
|
"""Search the sovereign memory store for relevant memories.
|
||||||
|
|
||||||
|
Uses SovereignStore (SQLite + FTS5 + HRR) for hybrid keyword + semantic
|
||||||
|
search. No subprocess calls, no ONNX, no API keys.
|
||||||
|
|
||||||
|
Gracefully degrades to empty string if store is unavailable.
|
||||||
|
"""
|
||||||
|
store = _get_store()
|
||||||
|
if store is None:
|
||||||
|
return ""
|
||||||
try:
|
try:
|
||||||
bin_path = MEMPALACE_BIN if os.path.exists(MEMPALACE_BIN) else "mempalace"
|
results = store.search(query, room=room, limit=5, min_trust=0.2)
|
||||||
result = subprocess.run(
|
if not results:
|
||||||
[bin_path, "search", query],
|
return ""
|
||||||
capture_output=True,
|
lines = []
|
||||||
text=True,
|
for r in results:
|
||||||
timeout=10,
|
trust = r.get("trust_score", 0.5)
|
||||||
)
|
room_name = r.get("room", "general")
|
||||||
if result.returncode == 0 and result.stdout.strip():
|
content = r.get("content", "")
|
||||||
return result.stdout.strip()
|
lines.append(f" [{room_name}] (trust:{trust:.2f}) {content}")
|
||||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
return "\n".join(lines)
|
||||||
# ONNX issues (#373) or mempalace not installed — degrade gracefully
|
except Exception:
|
||||||
pass
|
return ""
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -177,7 +211,6 @@ def search_skills(query: str) -> str:
|
|||||||
try:
|
try:
|
||||||
content = skill_md.read_text(encoding="utf-8").lower()
|
content = skill_md.read_text(encoding="utf-8").lower()
|
||||||
if any(t in content for t in terms):
|
if any(t in content for t in terms):
|
||||||
# Extract title from frontmatter
|
|
||||||
title = skill_dir.name
|
title = skill_dir.name
|
||||||
matches.append(f" skill: {title}")
|
matches.append(f" skill: {title}")
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -236,7 +269,7 @@ def enforce_retrieval_order(
|
|||||||
result["context"] += f"## Identity\n{identity}\n\n"
|
result["context"] += f"## Identity\n{identity}\n\n"
|
||||||
result["layers_checked"].append("L0")
|
result["layers_checked"].append("L0")
|
||||||
|
|
||||||
# L1: Palace search
|
# L1: Palace search (SovereignStore — zero API, zero subprocess)
|
||||||
palace_results = search_palace(query)
|
palace_results = search_palace(query)
|
||||||
if palace_results:
|
if palace_results:
|
||||||
result["context"] += f"## Palace Memory\n{palace_results}\n\n"
|
result["context"] += f"## Palace Memory\n{palace_results}\n\n"
|
||||||
|
|||||||
10
wizards/bezalel-sidecar.json
Normal file
10
wizards/bezalel-sidecar.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "Bezalel Builder Wizard",
|
||||||
|
"role": "Artificer",
|
||||||
|
"capabilities": [
|
||||||
|
"provisioning",
|
||||||
|
"gpu-orchestration",
|
||||||
|
"model-serving"
|
||||||
|
],
|
||||||
|
"instructions": "Take the provided keys and use them to get GPUs for big models (Gemma 4). Wire them into the harness."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user