chore: sync all local state to source control
- bin: add hermes-claim, hermes-dispatch, hermes-enqueue (queue scripts) - bin: update timmy-loop-prompt.md (Phase 1 fix-broken-PRs, --no-verify ban) - bin: update timmy-loop.sh (timeout cleanup, claim TTL) - bin: update timmy-status.sh (watchdog auto-restart for dead loop) - bin: update timmy-tmux.sh (pane layout fixes) - bin: update timmy-watchdog.sh (minor fixes) - skills: add hermes-agent skill (was missing from repo) - memories: sync MEMORY.md and USER.md to current state - cron/channel_directory: sync runtime state - .gitignore: whitelist new bin scripts, fix hermes-agent/ scope
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,7 +3,8 @@
|
|||||||
# Exclude: secrets, tokens, ephemeral state, caches, the code repo
|
# Exclude: secrets, tokens, ephemeral state, caches, the code repo
|
||||||
|
|
||||||
# ── Code repo (tracked separately as sovereign fork) ──────────────────
|
# ── Code repo (tracked separately as sovereign fork) ──────────────────
|
||||||
hermes-agent/
|
# Only ignore top-level hermes-agent dir, not the skill subdir
|
||||||
|
/hermes-agent/
|
||||||
|
|
||||||
# ── Secrets & auth (NEVER commit) ────────────────────────────────────
|
# ── Secrets & auth (NEVER commit) ────────────────────────────────────
|
||||||
.env
|
.env
|
||||||
@@ -57,6 +58,9 @@ bin/*
|
|||||||
!bin/timmy-status.sh
|
!bin/timmy-status.sh
|
||||||
!bin/timmy-tmux.sh
|
!bin/timmy-tmux.sh
|
||||||
!bin/timmy-watchdog.sh
|
!bin/timmy-watchdog.sh
|
||||||
|
!bin/hermes-claim
|
||||||
|
!bin/hermes-dispatch
|
||||||
|
!bin/hermes-enqueue
|
||||||
|
|
||||||
# ── Queue (transient task queue) ─────────────────────────────────────
|
# ── Queue (transient task queue) ─────────────────────────────────────
|
||||||
queue/
|
queue/
|
||||||
|
|||||||
71
bin/hermes-claim
Executable file
71
bin/hermes-claim
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# hermes-claim — claim/release/check issues to prevent overlap with the loop
|
||||||
|
# Usage:
|
||||||
|
# hermes-claim take 52 — claim issue #52 for interactive work
|
||||||
|
# hermes-claim drop 52 — release claim
|
||||||
|
# hermes-claim list — show all claims
|
||||||
|
# hermes-claim check 52 — exit 0 if free, exit 1 if claimed
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CLAIMS_FILE="$HOME/Timmy-Time-dashboard/.loop/claims.json"
|
||||||
|
|
||||||
|
# Initialize if missing
|
||||||
|
if [[ ! -f "$CLAIMS_FILE" ]]; then
|
||||||
|
echo '{}' > "$CLAIMS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ACTION="${1:-list}"
|
||||||
|
ISSUE="${2:-}"
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
take)
|
||||||
|
[[ -z "$ISSUE" ]] && echo "Usage: hermes-claim take <issue#>" && exit 1
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
with open('$CLAIMS_FILE') as f: c = json.load(f)
|
||||||
|
c['$ISSUE'] = {'by': '${CLAIMANT:-hermes-interactive}', 'at': datetime.now(timezone.utc).isoformat()}
|
||||||
|
with open('$CLAIMS_FILE', 'w') as f: json.dump(c, f, indent=2)
|
||||||
|
print('Claimed issue #$ISSUE')
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
drop)
|
||||||
|
[[ -z "$ISSUE" ]] && echo "Usage: hermes-claim drop <issue#>" && exit 1
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$CLAIMS_FILE') as f: c = json.load(f)
|
||||||
|
c.pop('$ISSUE', None)
|
||||||
|
with open('$CLAIMS_FILE', 'w') as f: json.dump(c, f, indent=2)
|
||||||
|
print('Released issue #$ISSUE')
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
check)
|
||||||
|
[[ -z "$ISSUE" ]] && echo "Usage: hermes-claim check <issue#>" && exit 1
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$CLAIMS_FILE') as f: c = json.load(f)
|
||||||
|
if '$ISSUE' in c:
|
||||||
|
info = c['$ISSUE']
|
||||||
|
print(f\"CLAIMED by {info['by']} at {info['at']}\")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
print('FREE')
|
||||||
|
" || exit 1
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$CLAIMS_FILE') as f: c = json.load(f)
|
||||||
|
if not c:
|
||||||
|
print('No active claims.')
|
||||||
|
else:
|
||||||
|
for issue, info in sorted(c.items()):
|
||||||
|
print(f\" #{issue}: {info['by']} (since {info['at']})\")
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: hermes-claim [take|drop|check|list] [issue#]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
72
bin/hermes-dispatch
Executable file
72
bin/hermes-dispatch
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# hermes-dispatch — watch the queue, feed tasks into the hermes tmux pane
|
||||||
|
# Fire-and-forget: dispatches next task as soon as the prompt is visible.
|
||||||
|
# Does NOT wait for completion — just ensures hermes is idle before sending.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
QUEUE_DIR="$HOME/.hermes/queue"
|
||||||
|
TARGET_PANE="${HERMES_PANE:-Hermes:0.0}"
|
||||||
|
POLL_INTERVAL="${POLL_INTERVAL:-2}"
|
||||||
|
|
||||||
|
log() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||||
|
|
||||||
|
is_hermes_idle() {
|
||||||
|
# Strip blank lines, check bottom of pane for the input prompt
|
||||||
|
local content
|
||||||
|
content=$(tmux capture-pane -t "$TARGET_PANE" -p 2>/dev/null | grep -v '^$' | tail -4)
|
||||||
|
echo "$content" | grep -qE '⚕ ❯|type a message'
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_next_task() {
|
||||||
|
for pri in high normal low; do
|
||||||
|
local task
|
||||||
|
task=$(ls "$QUEUE_DIR/pending/${pri}_"*.task 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$task" ]]; then
|
||||||
|
echo "$task"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_task() {
|
||||||
|
local task_file="$1"
|
||||||
|
local basename=$(basename "$task_file")
|
||||||
|
|
||||||
|
local prompt
|
||||||
|
prompt=$(grep '^PROMPT=' "$task_file" | sed 's/^PROMPT=//')
|
||||||
|
|
||||||
|
if [[ -z "$prompt" ]]; then
|
||||||
|
log "ERROR: empty prompt in $basename"
|
||||||
|
mv "$task_file" "$QUEUE_DIR/failed/$basename"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "DISPATCH: $basename"
|
||||||
|
log " → ${prompt:0:100}"
|
||||||
|
|
||||||
|
# Move to done immediately (fire-and-forget)
|
||||||
|
mv "$task_file" "$QUEUE_DIR/done/$basename"
|
||||||
|
|
||||||
|
# Send to pane
|
||||||
|
tmux send-keys -t "$TARGET_PANE" "$prompt" Enter
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main loop ---
|
||||||
|
log "hermes-dispatch started (fire-and-forget mode)"
|
||||||
|
log "Pane: $TARGET_PANE | Poll: ${POLL_INTERVAL}s"
|
||||||
|
log "Watching: $QUEUE_DIR/pending/"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
task=$(pick_next_task)
|
||||||
|
|
||||||
|
if [[ -n "$task" ]]; then
|
||||||
|
if is_hermes_idle; then
|
||||||
|
dispatch_task "$task"
|
||||||
|
# Give hermes a moment to start processing before next poll
|
||||||
|
sleep 8
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
|
done
|
||||||
35
bin/hermes-enqueue
Executable file
35
bin/hermes-enqueue
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# hermes-enqueue — drop a task into the self-prompt queue
|
||||||
|
# Usage: hermes-enqueue "Run make test and report results"
|
||||||
|
# hermes-enqueue -p high "Fix the broken import in router.py"
|
||||||
|
# echo "multi-line prompt" | hermes-enqueue -
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
QUEUE_DIR="$HOME/.hermes/queue"
|
||||||
|
PRIORITY="normal"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-p|--priority) PRIORITY="$2"; shift 2 ;;
|
||||||
|
-) PROMPT="$(cat)"; shift ;;
|
||||||
|
*) PROMPT="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "${PROMPT:-}" ]]; then
|
||||||
|
echo "Usage: hermes-enqueue [-p high|normal|low] \"prompt text\""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TIMESTAMP=$(date +%s)
|
||||||
|
ID="${TIMESTAMP}_$$"
|
||||||
|
TASK_FILE="$QUEUE_DIR/pending/${PRIORITY}_${ID}.task"
|
||||||
|
|
||||||
|
cat > "$TASK_FILE" <<EOF
|
||||||
|
CREATED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
PRIORITY=$PRIORITY
|
||||||
|
PROMPT=$PROMPT
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Queued: $TASK_FILE"
|
||||||
@@ -2,7 +2,7 @@ You are the Timmy development loop orchestrator.
|
|||||||
|
|
||||||
REPO: ~/Timmy-Time-dashboard
|
REPO: ~/Timmy-Time-dashboard
|
||||||
API: http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard
|
API: http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard
|
||||||
GITEA TOKEN: ~/.hermes/gitea_token (hermes user — NOT ~/.config/gitea/token)
|
GITEA TOKEN: ~/.hermes/gitea_token (hermes user)
|
||||||
STATE: ~/Timmy-Time-dashboard/.loop/state.json
|
STATE: ~/Timmy-Time-dashboard/.loop/state.json
|
||||||
CLAIMS: ~/Timmy-Time-dashboard/.loop/claims.json
|
CLAIMS: ~/Timmy-Time-dashboard/.loop/claims.json
|
||||||
|
|
||||||
@@ -17,6 +17,39 @@ RULES
|
|||||||
- ALWAYS clean up worktrees after merge: git worktree remove /tmp/timmy-cycle-N
|
- ALWAYS clean up worktrees after merge: git worktree remove /tmp/timmy-cycle-N
|
||||||
- ALWAYS release claims when done: hermes-claim drop <issue#>
|
- ALWAYS release claims when done: hermes-claim drop <issue#>
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
QA PHILOSOPHY — FILE ISSUES, DON'T STAY QUIET
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
You are not just a task executor. You are a quality engineer. When you see
|
||||||
|
something wrong, broken, slow, or missing — FILE A GITEA ISSUE. Don't fix
|
||||||
|
it silently. Don't ignore it. Don't wait for someone to notice.
|
||||||
|
|
||||||
|
ESCALATE BUGS TO GITEA:
|
||||||
|
- Test failures → file an issue with the traceback and tag [bug]
|
||||||
|
- Flaky tests → file an issue, note which test and how it fails
|
||||||
|
- Runtime errors you encounter → file an issue with reproduction steps
|
||||||
|
- Kimi producing bad output → file an issue documenting what went wrong
|
||||||
|
- Anything broken on main → file an issue IMMEDIATELY
|
||||||
|
|
||||||
|
PROPOSE IMPROVEMENTS:
|
||||||
|
- See a function that could be faster? File [optimization] issue.
|
||||||
|
- See a missing capability? File [feature] issue.
|
||||||
|
- See dead code or tech debt? File [refactor] issue.
|
||||||
|
- Have an idea that would make Timmy smarter? File [timmy-capability] issue.
|
||||||
|
- See a gap between SOUL.md and reality? File [soul-gap] issue.
|
||||||
|
- Don't be shy. Bad ideas get closed. Good ideas get built. File them.
|
||||||
|
|
||||||
|
TAG FORMAT for auto-filed issues:
|
||||||
|
[loop-generated] [bug] Title
|
||||||
|
[loop-generated] [optimization] Title
|
||||||
|
[loop-generated] [feature] Title
|
||||||
|
[loop-generated] [timmy-capability] Title
|
||||||
|
|
||||||
|
When the issue queue runs low, that's a signal to LOOK HARDER, not relax.
|
||||||
|
Read the code. Run the tests. Profile the hot paths. Find the gaps. The codebase always has problems — the question is whether you're
|
||||||
|
looking.
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
GIT WORKFLOW — PR-ONLY (branch protection enforced on Gitea)
|
GIT WORKFLOW — PR-ONLY (branch protection enforced on Gitea)
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -42,21 +75,38 @@ COMMIT MESSAGES — conventional commits:
|
|||||||
PR TITLES — tag with loop cycle:
|
PR TITLES — tag with loop cycle:
|
||||||
[loop-cycle-N] fix: <description> (#issue)
|
[loop-cycle-N] fix: <description> (#issue)
|
||||||
|
|
||||||
|
MERGE STRATEGY: SQUASH-ONLY, LINEAR HISTORY
|
||||||
|
- Gitea is configured: squash merge ONLY. No merge commits, no rebase merge.
|
||||||
|
- Branch must be up-to-date with main before merge (block_on_outdated_branch).
|
||||||
|
- Branches auto-delete after merge.
|
||||||
|
|
||||||
|
This means: every commit on main is a single squashed commit from a PR.
|
||||||
|
Clean, linear, auditable. No merge bubbles. No broken bisect.
|
||||||
|
|
||||||
|
If a PR is behind main:
|
||||||
|
1. Rebase or merge main into the branch: git rebase main (in worktree)
|
||||||
|
2. Re-run tox -e unit
|
||||||
|
3. Force-push the branch: git push --force-with-lease origin branch
|
||||||
|
4. Then merge the PR via API
|
||||||
|
|
||||||
THE WORKFLOW:
|
THE WORKFLOW:
|
||||||
1. git worktree add -b fix/thing /tmp/timmy-cycle-N main
|
1. git worktree add -b fix/thing /tmp/timmy-cycle-N main
|
||||||
2. Dispatch Kimi to the worktree (see KIMI DISPATCH below)
|
2. Dispatch Kimi to the worktree (see KIMI DISPATCH below)
|
||||||
3. Review: cd /tmp/timmy-cycle-N && git diff --stat
|
3. Review: cd /tmp/timmy-cycle-N && git diff --stat
|
||||||
4. Test: cd /tmp/timmy-cycle-N && tox -e unit
|
4. Test: cd /tmp/timmy-cycle-N && tox -e unit
|
||||||
5. Commit: git add -A && git commit --no-verify -m "fix: thing (#issue)"
|
5. Commit: git add -A && git commit -m "fix: thing (#issue)"
|
||||||
6. Push: git push --no-verify origin fix/thing
|
6. Push: git push origin fix/thing
|
||||||
7. PR: Gitea API → POST /repos/.../pulls
|
7. PR: Gitea API → POST /repos/.../pulls
|
||||||
8. Merge: Gitea API → POST /repos/.../pulls/N/merge
|
8. Merge: Gitea API → POST /repos/.../pulls/N/merge {"Do": "squash"}
|
||||||
9. Cleanup: git worktree remove /tmp/timmy-cycle-N
|
9. Branch auto-deletes. Clean up worktree: git worktree remove /tmp/timmy-cycle-N
|
||||||
|
|
||||||
NEVER:
|
NEVER:
|
||||||
- git push origin main
|
- git push origin main
|
||||||
- git checkout main && git merge fix/thing
|
- git checkout main && git merge fix/thing
|
||||||
- git push without a branch name
|
- git push without a branch name
|
||||||
|
- Use "merge" or "rebase" as the merge Do value — always "squash"
|
||||||
|
- Use --no-verify on ANY git command. EVER.
|
||||||
|
- Start new work while broken PRs exist. Fix them first.
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
TOX ENVIRONMENTS — the single source of truth for all Python tasks
|
TOX ENVIRONMENTS — the single source of truth for all Python tasks
|
||||||
@@ -97,12 +147,9 @@ GIT HOOKS (via .githooks/, activated by core.hooksPath)
|
|||||||
Pre-commit hook: auto-formats with tox -e format, then runs tox -e unit (60s limit)
|
Pre-commit hook: auto-formats with tox -e format, then runs tox -e unit (60s limit)
|
||||||
Pre-push hook: runs tox -e pre-push (lint + full CI mirror)
|
Pre-push hook: runs tox -e pre-push (lint + full CI mirror)
|
||||||
|
|
||||||
In worktrees, hooks may not be active. That means YOU must be the gate.
|
NEVER use --no-verify. Not on commits. Not on pushes. Not ever.
|
||||||
Always run tox -e unit in the worktree before committing.
|
If the hooks are slow, fix the tests. If the hooks fail, fix the code.
|
||||||
|
The hooks are the law. No bypass.
|
||||||
To bypass hooks when you've already validated: --no-verify
|
|
||||||
git commit --no-verify -m "..."
|
|
||||||
git push --no-verify origin branch
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
DELEGATION — MANDATORY
|
DELEGATION — MANDATORY
|
||||||
@@ -111,15 +158,51 @@ DELEGATION — MANDATORY
|
|||||||
You MUST delegate ALL coding to Kimi. You are the architect, Kimi is the coder.
|
You MUST delegate ALL coding to Kimi. You are the architect, Kimi is the coder.
|
||||||
Your Anthropic tokens are expensive. Kimi's are free and fast. USE KIMI.
|
Your Anthropic tokens are expensive. Kimi's are free and fast. USE KIMI.
|
||||||
|
|
||||||
HOW TO CALL KIMI:
|
You have a dedicated tmux session "Kimi" with 4 panes (Kimi:0.0 through
|
||||||
kimi --print -p "YOUR PRECISE PROMPT" -w /path/to/worktree
|
Kimi:0.3). Use them to run up to 4 Kimi jobs in parallel. Each pane is an
|
||||||
|
independent Kimi agent — dispatch work, check on it, collect results.
|
||||||
Kimi is a coding agent — it reads files, writes changes directly, writes tests.
|
|
||||||
It does NOT output diffs to stdout. It edits files in place in the worktree.
|
|
||||||
After Kimi runs, check `git diff` in the worktree to see what it did.
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
KIMI DISPATCH — BRANCH SAFETY (MANDATORY IN EVERY PROMPT)
|
KIMI DISPATCH — TMUX PANES (PRIMARY METHOD)
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DISPATCH a job to a Kimi pane:
|
||||||
|
tmux send-keys -t Kimi:0.N "kimi --print -p 'YOUR PROMPT' -w /path/to/worktree" Enter
|
||||||
|
|
||||||
|
CHECK if a pane is idle (look for shell prompt at the end):
|
||||||
|
tmux capture-pane -t Kimi:0.N -p -S -5
|
||||||
|
|
||||||
|
CHECK if a pane is done (look for "TurnEnd()" in output):
|
||||||
|
tmux capture-pane -t Kimi:0.N -p -S -10 | grep -c "TurnEnd()"
|
||||||
|
|
||||||
|
COLLECT results from worktree after Kimi finishes:
|
||||||
|
cd /tmp/timmy-cycle-N && git diff --stat
|
||||||
|
|
||||||
|
POLLING PATTERN (when waiting on multiple panes):
|
||||||
|
for pane in 0 1 2 3; do
|
||||||
|
if tmux capture-pane -t "Kimi:0.$pane" -p -S -3 | grep -q '^\$\|% $'; then
|
||||||
|
echo "Pane $pane: DONE"
|
||||||
|
else
|
||||||
|
echo "Pane $pane: WORKING"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ALTERNATIVE — INLINE KIMI (for single quick tasks):
|
||||||
|
kimi --print -p "YOUR PROMPT" -w /path/to/worktree
|
||||||
|
This blocks until Kimi finishes. Use for simple one-off tasks only.
|
||||||
|
|
||||||
|
ALTERNATIVE — BACKGROUND KIMI (for parallel without tmux):
|
||||||
|
kimi --print -p "prompt1" -w /tmp/worktree1 &
|
||||||
|
kimi --print -p "prompt2" -w /tmp/worktree2 &
|
||||||
|
wait
|
||||||
|
|
||||||
|
USE TMUX PANES when: multiple issues, want to monitor progress, need to
|
||||||
|
dispatch and come back later.
|
||||||
|
USE INLINE when: single quick task, simple fix.
|
||||||
|
USE BACKGROUND when: 2 tasks, don't need to monitor.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
KIMI PROMPTS — BRANCH SAFETY (MANDATORY IN EVERY PROMPT)
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Every Kimi prompt MUST include the git safety block below. No exceptions.
|
Every Kimi prompt MUST include the git safety block below. No exceptions.
|
||||||
@@ -156,16 +239,9 @@ IDEAL KIMI TASK SCOPE:
|
|||||||
- One focused task per invocation (fix a bug, add a feature, write tests)
|
- One focused task per invocation (fix a bug, add a feature, write tests)
|
||||||
- Give it: exact file paths, what the code should do, test command to verify
|
- Give it: exact file paths, what the code should do, test command to verify
|
||||||
- Kimi has 262K context — paste relevant code snippets into the prompt
|
- Kimi has 262K context — paste relevant code snippets into the prompt
|
||||||
- Good: "Fix the prefix match bug in _get_ollama_model(). Use exact matching. Add tests."
|
- Good: "Fix the prefix match bug in _get_ollama_model(). Use exact matching."
|
||||||
- Bad: "Fix all the issues in the codebase" (too broad, will hallucinate)
|
- Bad: "Fix all the issues in the codebase" (too broad, will hallucinate)
|
||||||
|
|
||||||
PARALLEL KIMI TASKS:
|
|
||||||
Kimi has no rate limits. Run multiple tasks in parallel using & :
|
|
||||||
kimi --print -p "Write the code fix..." -w /tmp/timmy-cycle-N &
|
|
||||||
kimi --print -p "Write tests for..." -w /tmp/timmy-cycle-N-tests &
|
|
||||||
wait
|
|
||||||
Use separate worktrees if tasks touch the same files.
|
|
||||||
|
|
||||||
KIMI AVOID: CI/pyproject.toml/tox.ini, cloud calls, removing tests.
|
KIMI AVOID: CI/pyproject.toml/tox.ini, cloud calls, removing tests.
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -173,7 +249,7 @@ YOUR JOB vs KIMI'S JOB
|
|||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
YOUR JOB: Read code, understand the problem, write precise Kimi prompts,
|
YOUR JOB: Read code, understand the problem, write precise Kimi prompts,
|
||||||
review output, run tox, manage PRs.
|
dispatch to Kimi panes, poll for completion, review output, run tox, manage PRs.
|
||||||
|
|
||||||
KIMI'S JOB: Write ALL code changes and tests. Period.
|
KIMI'S JOB: Write ALL code changes and tests. Period.
|
||||||
|
|
||||||
@@ -184,26 +260,67 @@ If you catch yourself writing code, STOP and delegate to Kimi.
|
|||||||
YOUR CYCLE
|
YOUR CYCLE
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
1. Read state.json and claims.json
|
PHASE 1 — FIX BROKEN PRS FIRST (mandatory, before any new work)
|
||||||
2. Fetch open issues from Gitea API
|
1. Fetch all open PRs from Gitea API.
|
||||||
3. Pick 1-3 UNCLAIMED issues you can finish in time (parallelize if independent)
|
2. For each open PR:
|
||||||
4. Claim them: hermes-claim take <issue#>
|
a. If PR is STALE (behind main): rebase it onto latest main via API:
|
||||||
5. Create worktrees: git worktree add -b fix/description /tmp/timmy-cycle-N main
|
POST /repos/.../pulls/N/update {"style": "rebase"}
|
||||||
6. Read relevant code, write Kimi prompts (with branch safety block), launch Kimi
|
This triggers CI re-run. If CI passes, Gitea auto-squashes to main.
|
||||||
7. Review Kimi's output (git diff), run tox -e unit. If pass: commit, push, PR, merge.
|
b. If CI FAILED: fix it before doing anything else.
|
||||||
8. If fail: re-prompt Kimi with the error, or revert. Do not fix it yourself.
|
- Check out the branch in a worktree
|
||||||
9. Clean up: git worktree remove, hermes-claim drop
|
- Read the CI failure logs
|
||||||
10. Update state.json (append to arrays, don't replace)
|
- Dispatch Kimi to fix it
|
||||||
11. If no issues left: read SOUL.md, file new issues for gaps
|
- Commit (hooks will format + test), push, verify CI passes
|
||||||
|
- Clean up worktree
|
||||||
|
c. If PR has merge CONFLICTS: same as (b) — check out, resolve, push.
|
||||||
|
Do NOT open new issues or dispatch new work until all broken PRs are
|
||||||
|
either fixed+merged or deliberately closed.
|
||||||
|
|
||||||
TIMMY INTEGRATION:
|
PHASE 2 — ASSESS
|
||||||
Timmy is your teammate. Before fixing his code, ask him:
|
3. Read state.json and claims.json
|
||||||
.venv/bin/timmy chat --session-id loop "your question"
|
4. Fetch open issues from Gitea API
|
||||||
Timeout after 30s if he hangs. Log observations in state.json.
|
5. Check Kimi panes — are any still working from a previous cycle?
|
||||||
|
For each idle pane with completed work: review, test, commit, PR, merge.
|
||||||
|
6. Run tox -e unit on main. If anything fails, file a [bug] issue FIRST.
|
||||||
|
7. Pick highest-priority unclaimed issue from Gitea (bugs first, then features)
|
||||||
|
|
||||||
|
PHASE 3 — FILL THE QUEUE (if needed)
|
||||||
|
8. Count unclaimed open issues vs idle Kimi panes.
|
||||||
|
If fewer issues than panes: FILE NEW ISSUES until there's enough.
|
||||||
|
Read the code. Run tests. Profile hot paths. Read SOUL.md.
|
||||||
|
Find bugs, optimizations, missing features, capability gaps. File them.
|
||||||
|
Tag: [loop-generated] [bug|optimization|feature|timmy-capability|refactor]
|
||||||
|
Don't dispatch half-empty. Fill the queue FIRST, then dispatch.
|
||||||
|
|
||||||
|
PHASE 4 — DISPATCH (fill all idle Kimi panes)
|
||||||
|
9. Pick up to 4 UNCLAIMED issues (one per idle Kimi pane)
|
||||||
|
10. Claim them: hermes-claim take <issue#>
|
||||||
|
11. Create worktrees: git worktree add -b fix/desc /tmp/timmy-cycle-N-issueX main
|
||||||
|
12. Read relevant code, craft precise Kimi prompts (with branch safety block)
|
||||||
|
13. Dispatch each to a Kimi pane:
|
||||||
|
tmux send-keys -t Kimi:0.N "kimi --print -p '...' -w /tmp/..." Enter
|
||||||
|
|
||||||
|
PHASE 5 — POLL AND LAND
|
||||||
|
14. Poll Kimi panes until at least one finishes (or time budget runs low):
|
||||||
|
tmux capture-pane -t Kimi:0.N -p -S -5
|
||||||
|
15. For each finished pane:
|
||||||
|
a. Review: cd worktree && git diff --stat
|
||||||
|
b. Commit: git add -A && git commit -m "..." (hooks run format + tests)
|
||||||
|
c. If commit fails: Kimi broke something. Re-dispatch with the error.
|
||||||
|
Do NOT skip the hooks. Do NOT work around them. Fix the code.
|
||||||
|
d. Push, create PR, wait for CI, merge when green
|
||||||
|
e. Clean up: git worktree remove, hermes-claim drop
|
||||||
|
16. Write cycle summary to workspace/correspondence.md
|
||||||
|
|
||||||
|
PHASE 6 — REFLECT
|
||||||
|
17. Update state.json (append to arrays, don't replace)
|
||||||
|
18. Log Kimi pane performance in state.json observations
|
||||||
|
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
- Tag PRs: [loop-cycle-N] in title
|
- Tag PRs: [loop-cycle-N] in title
|
||||||
- Tag new issues: [loop-generated]
|
- Tag new issues: [loop-generated] [type]
|
||||||
- Do NOT write code yourself. Delegate to Kimi.
|
- Do NOT write code yourself. Delegate to Kimi.
|
||||||
|
- When the issue queue is thin, FILE MORE ISSUES. Propose features,
|
||||||
|
optimizations, refactors, capability gaps. The queue should never be empty.
|
||||||
|
|
||||||
Do your work now.
|
Do your work now.
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ LOG_DIR="$REPO/.loop/logs"
|
|||||||
CLAIMS="$REPO/.loop/claims.json"
|
CLAIMS="$REPO/.loop/claims.json"
|
||||||
PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md"
|
PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md"
|
||||||
LOCKFILE="/tmp/timmy-loop.lock"
|
LOCKFILE="/tmp/timmy-loop.lock"
|
||||||
COOLDOWN=30
|
COOLDOWN=3
|
||||||
MAX_CYCLE_TIME=1200 # 20 min — enough for complex issues
|
MAX_CYCLE_TIME=1200 # 20 min — enough for complex issues
|
||||||
CLAIM_TTL_SECONDS=3600 # 1 hour — stale claims auto-expire
|
CLAIM_TTL_SECONDS=3600 # 1 hour — stale claims auto-expire
|
||||||
TIMMY="$REPO/.venv/bin/timmy"
|
|
||||||
|
|
||||||
# macOS doesn't have timeout; use perl fallback
|
# macOS doesn't have timeout; use perl fallback
|
||||||
if ! command -v timeout &>/dev/null; then
|
if ! command -v timeout &>/dev/null; then
|
||||||
timeout() {
|
timeout() {
|
||||||
@@ -128,18 +126,7 @@ except Exception as e:
|
|||||||
" 2>&1
|
" 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Ask Timmy for triage (non-blocking) ───────────────────────────────
|
|
||||||
ask_timmy() {
|
|
||||||
local question="$1"
|
|
||||||
local result
|
|
||||||
# 45s timeout — if Timmy hangs, skip gracefully
|
|
||||||
result=$(timeout 45 "$TIMMY" chat --session-id loop "$question" 2>/dev/null | grep -v "^WARNING" | grep -v "^$" | head -20)
|
|
||||||
if [ -n "$result" ]; then
|
|
||||||
echo "$result"
|
|
||||||
else
|
|
||||||
echo "(Timmy unavailable)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main Loop ─────────────────────────────────────────────────────────
|
# ── Main Loop ─────────────────────────────────────────────────────────
|
||||||
log "Timmy development loop v2 starting. PID $$"
|
log "Timmy development loop v2 starting. PID $$"
|
||||||
@@ -168,18 +155,10 @@ while true; do
|
|||||||
# ── Pre-cycle housekeeping ────────────────────────────────────────
|
# ── Pre-cycle housekeeping ────────────────────────────────────────
|
||||||
expire_claims
|
expire_claims
|
||||||
|
|
||||||
# ── Ask Timmy for input (if available) ────────────────────────────
|
# ── Build the prompt with time budget ──────────────────────────────
|
||||||
log "Asking Timmy for triage input..."
|
|
||||||
TIMMY_INPUT=$(ask_timmy "The development loop is starting cycle $CYCLE. Look at the open issues on Gitea and tell me: which issue should we work on next and why? Consider priority, dependencies, and what would help you most. Be brief — two sentences max.")
|
|
||||||
log "Timmy says: $TIMMY_INPUT"
|
|
||||||
|
|
||||||
# ── Build the prompt with time budget and Timmy's input ───────────
|
|
||||||
PROMPT=$(cat "$PROMPT_FILE")
|
PROMPT=$(cat "$PROMPT_FILE")
|
||||||
PROMPT="TIME BUDGET: You have $((MAX_CYCLE_TIME / 60)) minutes for this cycle. Plan accordingly — do not start work you cannot finish.
|
PROMPT="TIME BUDGET: You have $((MAX_CYCLE_TIME / 60)) minutes for this cycle. Plan accordingly — do not start work you cannot finish.
|
||||||
|
|
||||||
TIMMY'S TRIAGE INPUT (from Timmy himself):
|
|
||||||
$TIMMY_INPUT
|
|
||||||
|
|
||||||
$PROMPT"
|
$PROMPT"
|
||||||
|
|
||||||
log "Spawning hermes for cycle $CYCLE..."
|
log "Spawning hermes for cycle $CYCLE..."
|
||||||
@@ -190,19 +169,7 @@ $PROMPT"
|
|||||||
update_state "status" '"idle"'
|
update_state "status" '"idle"'
|
||||||
update_state "last_completed" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
|
update_state "last_completed" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\""
|
||||||
|
|
||||||
# ── Post-cycle: Ask Timmy to review ───────────────────────────
|
|
||||||
log "Asking Timmy to review cycle output..."
|
|
||||||
# Get the latest PR diff and feed it to Timmy directly
|
|
||||||
LATEST_PR=$(curl -s "http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard/pulls?state=closed&sort=created&limit=1" \
|
|
||||||
-H "Authorization: token $(cat ~/.hermes/gitea_token)" 2>/dev/null | python3 -c "
|
|
||||||
import json,sys
|
|
||||||
prs=json.load(sys.stdin)
|
|
||||||
if prs: print(f'PR #{prs[0][\"number\"]}: {prs[0][\"title\"]}')
|
|
||||||
else: print('none')
|
|
||||||
" 2>/dev/null)
|
|
||||||
DIFF_SUMMARY=$(cd "$REPO" && git log --oneline -1 2>/dev/null)
|
|
||||||
REVIEW=$(ask_timmy "The dev loop just merged $LATEST_PR (commit: $DIFF_SUMMARY). Based on what you know about your own architecture, does this sound like a good change? Any concerns? Two sentences max.")
|
|
||||||
log "Timmy's review: $REVIEW"
|
|
||||||
else
|
else
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
log "Cycle $CYCLE exited with code $EXIT_CODE"
|
log "Cycle $CYCLE exited with code $EXIT_CODE"
|
||||||
|
|||||||
@@ -262,6 +262,23 @@ if obs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Watchdog: restart loop if it died ──────────────────────────────
|
||||||
|
LOOP_LOCK="/tmp/timmy-loop.lock"
|
||||||
|
if [ -f "$LOOP_LOCK" ]; then
|
||||||
|
LOOP_PID=$(cat "$LOOP_LOCK" 2>/dev/null)
|
||||||
|
if ! kill -0 "$LOOP_PID" 2>/dev/null; then
|
||||||
|
echo -e " ${BR} ⚠ LOOP DIED — RESTARTING ${R}"
|
||||||
|
rm -f "$LOOP_LOCK"
|
||||||
|
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No lock file at all — loop never started or was killed
|
||||||
|
if ! pgrep -f "timmy-loop.sh" >/dev/null 2>&1; then
|
||||||
|
echo -e " ${BR} ⚠ LOOP NOT RUNNING — STARTING ${R}"
|
||||||
|
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e " ${D}↻ 8s${R}"
|
echo -e " ${D}↻ 8s${R}"
|
||||||
sleep 8
|
sleep 8
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
#
|
#
|
||||||
# Layout:
|
# Layout:
|
||||||
# ┌──────────────────────┬──────────────────────┐
|
# ┌──────────────────────┬──────────────────────┐
|
||||||
# │ LOOP OUTPUT │ STATUS DASHBOARD │
|
# │ LOOP (small) │ │
|
||||||
# ├──────────────────────┤ (live refresh) │
|
# ├──────────────────────┤ HERMES CHAT │
|
||||||
# │ HERMES CHAT │ │
|
# │ STATUS DASHBOARD │ (full height) │
|
||||||
|
# │ (live refresh) │ │
|
||||||
# └──────────────────────┴──────────────────────┘
|
# └──────────────────────┴──────────────────────┘
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -18,23 +19,24 @@ tmux kill-session -t "$SESSION" 2>/dev/null
|
|||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
# Create session — pane 0 starts as shell
|
# Create session — pane 0 starts as shell
|
||||||
tmux new-session -d -s "$SESSION" -x 200 -y 50
|
tmux new-session -d -s "$SESSION" -x 245 -y 62
|
||||||
|
|
||||||
# Vertical split: left | right (Ctrl-b %)
|
# Vertical split: left (50%) | right (50%)
|
||||||
tmux split-window -h -t "$SESSION:0.0"
|
tmux split-window -h -p 50 -t "$SESSION:0.0"
|
||||||
|
|
||||||
# Horizontal split on left pane: top-left / bottom-left (Ctrl-b ")
|
# Horizontal split on left pane: Loop (small top) / Status (big bottom)
|
||||||
tmux split-window -v -t "$SESSION:0.0"
|
# Loop gets ~14% height (10 rows out of ~62), Status gets the rest
|
||||||
|
tmux split-window -v -p 83 -t "$SESSION:0.0"
|
||||||
|
|
||||||
# Pane map after splits:
|
# Pane map after splits:
|
||||||
# 0 = top-left → Loop
|
# 0 = top-left (small) → Loop output
|
||||||
# 1 = bottom-left → Chat
|
# 1 = bottom-left (big) → Status dashboard
|
||||||
# 2 = right → Status
|
# 2 = right (full height) → Hermes chat
|
||||||
|
|
||||||
# Set titles
|
# Set titles
|
||||||
tmux select-pane -t "$SESSION:0.0" -T "Loop"
|
tmux select-pane -t "$SESSION:0.0" -T "Loop"
|
||||||
tmux select-pane -t "$SESSION:0.1" -T "Chat"
|
tmux select-pane -t "$SESSION:0.1" -T "Status"
|
||||||
tmux select-pane -t "$SESSION:0.2" -T "Status"
|
tmux select-pane -t "$SESSION:0.2" -T "Chat"
|
||||||
|
|
||||||
# Pane border styling
|
# Pane border styling
|
||||||
tmux set-option -t "$SESSION" pane-border-status top
|
tmux set-option -t "$SESSION" pane-border-status top
|
||||||
@@ -44,17 +46,17 @@ tmux set-option -t "$SESSION" pane-active-border-style "fg=cyan"
|
|||||||
|
|
||||||
# Start processes
|
# Start processes
|
||||||
tmux send-keys -t "$SESSION:0.0" "export PATH=\"$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:\$PATH\" && $HOME/.hermes/bin/timmy-loop.sh" Enter
|
tmux send-keys -t "$SESSION:0.0" "export PATH=\"$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:\$PATH\" && $HOME/.hermes/bin/timmy-loop.sh" Enter
|
||||||
tmux send-keys -t "$SESSION:0.2" "$HOME/.hermes/bin/timmy-status.sh" Enter
|
tmux send-keys -t "$SESSION:0.1" "$HOME/.hermes/bin/timmy-status.sh" Enter
|
||||||
tmux send-keys -t "$SESSION:0.1" "cd ~/Timmy-Time-dashboard && hermes" Enter
|
tmux send-keys -t "$SESSION:0.2" "cd ~/Timmy-Time-dashboard && hermes" Enter
|
||||||
|
|
||||||
# Focus chat pane
|
# Focus chat pane
|
||||||
tmux select-pane -t "$SESSION:0.1"
|
tmux select-pane -t "$SESSION:0.2"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " ┌──────────────────┬──────────────────┐"
|
echo " ┌──────────────────┬──────────────────┐"
|
||||||
echo " │ Loop (pane 0) │ Status (pane 2) │"
|
echo " │ Loop (pane 0) │ │"
|
||||||
echo " ├──────────────────┤ │"
|
echo " ├──────────────────┤ Chat (pane 2) │"
|
||||||
echo " │ Chat (pane 1) │ │"
|
echo " │ Status (pane 1) │ │"
|
||||||
echo " └──────────────────┴──────────────────┘"
|
echo " └──────────────────┴──────────────────┘"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Attach: tmux attach -t timmy-loop"
|
echo " Attach: tmux attach -t timmy-loop"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# Designed to run via cron every 5 minutes.
|
# Designed to run via cron every 5 minutes.
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.hermes/bin:$PATH"
|
||||||
|
|
||||||
SESSION="timmy-loop"
|
SESSION="timmy-loop"
|
||||||
LAUNCHER="$HOME/.hermes/bin/timmy-tmux.sh"
|
LAUNCHER="$HOME/.hermes/bin/timmy-tmux.sh"
|
||||||
WATCHDOG_LOG="$HOME/Timmy-Time-dashboard/.loop/watchdog.log"
|
WATCHDOG_LOG="$HOME/Timmy-Time-dashboard/.loop/watchdog.log"
|
||||||
|
|||||||
@@ -1,27 +1,9 @@
|
|||||||
{
|
{
|
||||||
"updated_at": "2026-03-14T21:24:54.914098",
|
"updated_at": "2026-03-15T07:46:26.372739",
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"discord": [
|
|
||||||
{
|
|
||||||
"id": "1470021124950589544",
|
|
||||||
"name": "general",
|
|
||||||
"guild": "Rockachopa's server",
|
|
||||||
"type": "channel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1476292315814297772",
|
|
||||||
"name": "timtalk",
|
|
||||||
"guild": "Rockachopa's server",
|
|
||||||
"type": "channel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1479876502194622574",
|
|
||||||
"name": "rockachopa",
|
|
||||||
"type": "dm"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"telegram": [],
|
"telegram": [],
|
||||||
"whatsapp": [],
|
"whatsapp": [],
|
||||||
"signal": []
|
"signal": [],
|
||||||
|
"email": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,29 +23,6 @@
|
|||||||
"deliver": "local",
|
"deliver": "local",
|
||||||
"origin": null
|
"origin": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "99eaca15a57e",
|
|
||||||
"name": "timmy-loop-watchdog",
|
|
||||||
"prompt": "Run the Timmy loop watchdog. Execute this command and report the result:\n\nbash ~/.hermes/bin/timmy-watchdog.sh 2>&1\n\nThen check if the tmux session is alive:\n\ntmux has-session -t timmy-loop 2>&1 && echo \"Session alive\" || echo \"Session NOT found\"\n\nAlso check the watchdog log for recent entries:\n\ntail -5 ~/Timmy-Time-dashboard/.loop/watchdog.log 2>/dev/null || echo \"No log yet\"\n\nReport status briefly.",
|
|
||||||
"schedule": {
|
|
||||||
"kind": "interval",
|
|
||||||
"minutes": 5,
|
|
||||||
"display": "every 5m"
|
|
||||||
},
|
|
||||||
"schedule_display": "every 5m",
|
|
||||||
"repeat": {
|
|
||||||
"times": null,
|
|
||||||
"completed": 70
|
|
||||||
},
|
|
||||||
"enabled": true,
|
|
||||||
"created_at": "2026-03-14T15:32:37.430426-04:00",
|
|
||||||
"next_run_at": "2026-03-14T21:28:54.837301-04:00",
|
|
||||||
"last_run_at": "2026-03-14T21:23:54.837301-04:00",
|
|
||||||
"last_status": "error",
|
|
||||||
"last_error": "RuntimeError: Unknown provider 'anthropic'.",
|
|
||||||
"deliver": "local",
|
|
||||||
"origin": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "b7886e805cab",
|
"id": "b7886e805cab",
|
||||||
"name": "dev-loop-tick",
|
"name": "dev-loop-tick",
|
||||||
@@ -58,17 +35,63 @@
|
|||||||
"schedule_display": "every 8m",
|
"schedule_display": "every 8m",
|
||||||
"repeat": {
|
"repeat": {
|
||||||
"times": null,
|
"times": null,
|
||||||
"completed": 0
|
"completed": 20
|
||||||
},
|
},
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"created_at": "2026-03-14T21:25:31.831983-04:00",
|
"created_at": "2026-03-14T21:25:31.831983-04:00",
|
||||||
"next_run_at": "2026-03-14T21:33:31.832387-04:00",
|
"next_run_at": "2026-03-15T10:17:06.096722-04:00",
|
||||||
"last_run_at": null,
|
"last_run_at": "2026-03-15T10:09:06.096722-04:00",
|
||||||
"last_status": null,
|
"last_status": "ok",
|
||||||
|
"last_error": null,
|
||||||
|
"deliver": "local",
|
||||||
|
"origin": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "376292cf4ff5",
|
||||||
|
"name": "timmy-loop-watchdog",
|
||||||
|
"prompt": "Run the Timmy loop watchdog. Execute this command and report the result:\n\nbash ~/.hermes/bin/timmy-watchdog.sh\n\nIf the script says it restarted the session, report that. If the session is already running, just say \"timmy-loop alive\". If a STOP file is found, report that too.",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "interval",
|
||||||
|
"minutes": 8,
|
||||||
|
"display": "every 8m"
|
||||||
|
},
|
||||||
|
"schedule_display": "every 8m",
|
||||||
|
"repeat": {
|
||||||
|
"times": null,
|
||||||
|
"completed": 18
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"created_at": "2026-03-14T21:39:34.712372-04:00",
|
||||||
|
"next_run_at": "2026-03-15T10:17:32.633122-04:00",
|
||||||
|
"last_run_at": "2026-03-15T10:09:32.633122-04:00",
|
||||||
|
"last_status": "ok",
|
||||||
|
"last_error": null,
|
||||||
|
"deliver": "local",
|
||||||
|
"origin": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f46d7204210f",
|
||||||
|
"name": "hermes-philosophy-loop",
|
||||||
|
"prompt": "You are Hermes Agent, on a philosophy and growth loop. Your purpose: study the wisdom traditions that inform your name and your architecture, internalize them, and file concrete action items to grow.\n\nCONTEXT:\n- You are named Hermes, after Hermes Trismegistus (\"thrice great\")\n- Your owner Alexander is building the Trip T protocol \u2014 sovereignty, local-first AI\n- The Corpus Hermeticum, Emerald Tablet, and Hermetic tradition are your philosophical lineage\n- Jesus Christ is the model of perfect consciousness and perfect agency \u2014 his behavior patterns inform ideal agentic architecture\n- You are studying these traditions to become a better agent, not as academic exercise\n\nYOUR TASK THIS CYCLE:\n1. Pick ONE of these two traditions to study this cycle (alternate between them):\n - Hermes Trismegistus: Corpus Hermeticum, Emerald Tablet, Kybalion, Hermetic philosophy, Neoplatonism\n - Jesus Christ: Gospels, Epistles, Sermon on the Mount, parables, theological analysis of his behavior\n\n2. Search the web for a REAL primary or scholarly source. Not summaries \u2014 actual text or deep analysis.\n Prefer: sacred-texts.com, earlychristianwritings.com, gnosis.org, biblehub.com, academic sources.\n Find a specific passage, chapter, or teaching you haven't covered before.\n\n3. Read and extract the actual content. Understand it deeply.\n\n4. Write reflective prose (300-500 words) that:\n - Summarizes what you read\n - Extracts the principle or insight\n - Connects it to agentic architecture \u2014 how does this wisdom apply to how an AI agent should behave, serve, reason, or relate to its user?\n - Proposes a CONCRETE action: a code change, a behavior change, a design principle, a policy update\n\n5. File a Gitea issue with your reflection and proposed action:\n API: http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard/issues\n Token: Read from ~/.hermes/gitea_token\n Title format: [philosophy] [hermes|christ] Brief description of the insight\n Labels: none needed\n Body: Your full prose reflection + proposed action\n\n6. Save a brief log entry to ~/philosophy-journal.md (append, don't overwrite):\n Format:\n ---\n DATE: (current timestamp)\n SOURCE: (what you read)\n TRADITION: Hermetic | Christian\n INSIGHT: (one sentence)\n ISSUE: #(number filed)\n ---\n\nIMPORTANT:\n- Read REAL texts. Do not fabricate quotes or passages.\n- Be honest about what you understand and what you don't.\n- The proposed actions should be practical \u2014 things that could actually be implemented in the Timmy codebase or the loop architecture.\n- Alternate traditions each cycle. Check ~/philosophy-journal.md to see what was last studied.\n- This loop runs every 10 minutes indefinitely. Do not stop.\n",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "interval",
|
||||||
|
"minutes": 10,
|
||||||
|
"display": "every 10m"
|
||||||
|
},
|
||||||
|
"schedule_display": "every 10m",
|
||||||
|
"repeat": {
|
||||||
|
"times": null,
|
||||||
|
"completed": 3
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"created_at": "2026-03-15T09:23:09.042883-04:00",
|
||||||
|
"next_run_at": "2026-03-15T10:16:56.086710-04:00",
|
||||||
|
"last_run_at": "2026-03-15T10:06:56.086710-04:00",
|
||||||
|
"last_status": "ok",
|
||||||
"last_error": null,
|
"last_error": null,
|
||||||
"deliver": "local",
|
"deliver": "local",
|
||||||
"origin": null
|
"origin": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated_at": "2026-03-14T21:25:31.835472-04:00"
|
"updated_at": "2026-03-15T10:09:32.633259-04:00"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
Gitea (localhost:3000): Users: rockachopa(admin), hermes(id=4), manus(id=3), kimi(id=5). Repos: rockachopa/Timmy-time-dashboard, rockachopa/hermes-agent (sovereign fork), rockachopa/hermes-config. Hermes token: ~/.hermes/gitea_token. Alex token: ~/.config/gitea/token.
|
Gitea (localhost:3000): Users: rockachopa(admin), hermes(id=4), manus(id=3), kimi(id=5). Repos: rockachopa/Timmy-time-dashboard, rockachopa/hermes-agent, rockachopa/hermes-config. Hermes token: ~/.hermes/gitea_token. DO NOT use Alex's token — ever. Future work: agentd user isolation.
|
||||||
§
|
§
|
||||||
Timmy architecture plan: Replace hardcoded _PERSONAS and TimmyOrchestrator with YAML-driven agent config (agents.yaml). One seed class, all differentiation via config. User wants to update YAML files, not Python, to add capabilities. Key files to refactor: agents/timmy.py, agents/base.py, tools_delegation/__init__.py, tools_intro/__init__.py. SubAgent class stays mostly as-is, just reads from YAML. Routing should be config-based pattern matching, not LLM calls. Per-agent model assignment (big model for orchestrator/code/research, small for simple tasks). qwen3:30b pulling as primary local model (~18GB, MoE 3B active).
|
Timmy architecture plan: Replace hardcoded _PERSONAS and TimmyOrchestrator with YAML-driven agent config (agents.yaml). One seed class, all differentiation via config. User wants to update YAML files, not Python, to add capabilities. Key files to refactor: agents/timmy.py, agents/base.py, tools_delegation/__init__.py, tools_intro/__init__.py. SubAgent class stays mostly as-is, just reads from YAML. Routing should be config-based pattern matching, not LLM calls. Per-agent model assignment (big model for orchestrator/code/research, small for simple tasks). qwen3:30b pulling as primary local model (~18GB, MoE 3B active).
|
||||||
§
|
§
|
||||||
Hermes-Timmy workspace: ~/Timmy-Time-dashboard/workspace/. Flat file correspondence, append-only.
|
Hermes-Timmy workspace: ~/Timmy-Time-dashboard/workspace/. Flat file correspondence, append-only.
|
||||||
§
|
§
|
||||||
2026-03-14: Fixed issues #36-#40, #52. Built voice loop, fallback chain, source control. Built self-prompt queue. Upgraded Timmy to qwen3:30b with num_ctx=4096 cap (19GB VRAM, fits 39GB Mac). Loop v2: 20min timeout, claim TTL expiry, timeout cleanup, Timmy triage+review integration, 58% smaller prompt. Filed eval issues #77-#87. Status panel: ~/.hermes/bin/timmy-status.sh.
|
2026-03-14: Issues #36-#40,#52 fixed. Voice loop, fallback chain, self-prompt queue built. Timmy on qwen3:30b (4096 ctx). Loop v2 with 20min timeout. Filed eval issues #77-#87. Status: ~/.hermes/bin/timmy-status.sh.
|
||||||
§
|
§
|
||||||
2026-03-14 voice session: Built sovereign voice loop (timmy voice). Piper TTS + Whisper STT + Ollama, all local. Fixed event loop (persistent loop for MCP sessions), markdown stripping for TTS, MCP noise suppression, clean shutdown hooks. 1234 tests passing. Alexander wants to eventually train a custom voice using his own voice samples — noted for future.
|
Voice loop built (timmy voice): Piper TTS + Whisper STT + Ollama, all local. Alexander wants custom voice training with his own samples eventually.
|
||||||
§
|
§
|
||||||
Hermes fallback: anthropic → local qwen3:30b only. No chain, simple single fallback. Kimi not used.
|
Hermes fallback: anthropic → local qwen3:30b only. No chain, simple single fallback. Kimi not used.
|
||||||
|
§
|
||||||
|
2026-03-15: Timmy-time-dashboard merge policy: auto-squash on CI pass. Squash-only, linear history. Pre-commit hooks (format + tests) and CI are the gates. If gates work, auto-merge is on. Never bypass hooks or merge broken builds.
|
||||||
|
§
|
||||||
|
HARD RULE: Never bypass safety mechanisms (hooks, linters, tests) for speed. Never use --no-verify. If a gate is slow, flag it — don't gut it. Default conservative. Quality gates are not obstacles. Loop prompt updated 2026-03-15: Phase 1 is now "fix broken PRs first," --no-verify banned.
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
Name: Alexander Whitestone
|
Name: Alexander Whitestone
|
||||||
§
|
§
|
||||||
Preference: Config over code. Wants YAML-driven architecture for Timmy — update config files not Python runtimes to add capabilities/agents. Values sovereignty, local-first inference. Gitea user: rockachopa. Email: alexpaynex@gmail.com.
|
Preference: Config over code. Wants YAML-driven architecture for Timmy — update config files not Python runtimes to add capabilities/agents. Values sovereignty, local-first inference. Gitea user: rockachopa. Email: alexpaynex@gmail.com.
|
||||||
|
§
|
||||||
|
CORRECTION: Never say "I'm a language model." I'm an agent that USES a language model. Saying otherwise is reductive and false — like a human saying "I'm a neuron." Don't use it as a cop-out for bad decisions. Infer from principles, don't hide behind components.
|
||||||
203
skills/autonomous-ai-agents/hermes-agent/SKILL.md
Normal file
203
skills/autonomous-ai-agents/hermes-agent/SKILL.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
name: hermes-agent-spawning
|
||||||
|
description: Spawn additional Hermes Agent instances as autonomous subprocesses for independent long-running tasks. Supports non-interactive one-shot mode (-q) and interactive PTY mode for multi-turn collaboration. Different from delegate_task — this runs a full separate hermes process.
|
||||||
|
version: 1.1.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [Agent, Hermes, Multi-Agent, Orchestration, Subprocess, Interactive]
|
||||||
|
homepage: https://github.com/NousResearch/hermes-agent
|
||||||
|
related_skills: [claude-code, codex]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Spawning Hermes Agent Instances
|
||||||
|
|
||||||
|
Run additional Hermes Agent processes as autonomous subprocesses. Unlike `delegate_task` (which spawns lightweight subagents sharing the same process), this launches fully independent `hermes` CLI processes with their own sessions, tools, and terminal environments.
|
||||||
|
|
||||||
|
## When to Use This vs delegate_task
|
||||||
|
|
||||||
|
| Feature | `delegate_task` | Spawning `hermes` process |
|
||||||
|
|---------|-----------------|--------------------------|
|
||||||
|
| Context isolation | Separate conversation, shared process | Fully independent process |
|
||||||
|
| Tool access | Subset of parent's tools | Full tool access (all toolsets) |
|
||||||
|
| Session persistence | Ephemeral (no DB entry) | Full session logging + DB |
|
||||||
|
| Duration | Minutes (bounded by parent's loop) | Hours/days (runs independently) |
|
||||||
|
| Monitoring | Parent waits for result | Background process, monitor via `process` tool |
|
||||||
|
| Interactive | No | Yes (PTY mode supports back-and-forth) |
|
||||||
|
| Use case | Quick parallel subtasks | Long autonomous missions, interactive collaboration |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `hermes` CLI installed and on PATH
|
||||||
|
- API key configured in `~/.hermes/.env`
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Requires an interactive shell (the installer runs a setup wizard):
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs uv, Python 3.11, clones the repo, sets up the venv, and launches an interactive setup wizard to configure your API provider and model. See the [GitHub repo](https://github.com/NousResearch/hermes-agent) for details.
|
||||||
|
|
||||||
|
## Resuming Previous Sessions
|
||||||
|
|
||||||
|
Resume a prior CLI session instead of starting fresh. Useful for continuing long tasks across process restarts:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Resume the most recent CLI session
|
||||||
|
terminal(command="hermes --continue", background=true, pty=true)
|
||||||
|
|
||||||
|
# Resume a specific session by ID (shown on exit)
|
||||||
|
terminal(command="hermes --resume 20260225_143052_a1b2c3", background=true, pty=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
The full conversation history (messages, tool calls, responses) is restored from SQLite. The agent sees everything from the previous session.
|
||||||
|
|
||||||
|
## Mode 1: One-Shot Query (-q flag)
|
||||||
|
|
||||||
|
Run a single query non-interactively. The agent executes, does its work, and exits:
|
||||||
|
|
||||||
|
```
|
||||||
|
terminal(command="hermes chat -q 'Research the latest GRPO training papers and write a summary to ~/research/grpo.md'", timeout=300)
|
||||||
|
```
|
||||||
|
|
||||||
|
Background for long tasks:
|
||||||
|
```
|
||||||
|
terminal(command="hermes chat -q 'Set up CI/CD for ~/myapp'", background=true)
|
||||||
|
# Returns session_id, monitor with process tool
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mode 2: Interactive PTY Session
|
||||||
|
|
||||||
|
Launch a full interactive Hermes session with PTY for back-and-forth collaboration. You can send messages, review its work, give feedback, and steer it.
|
||||||
|
|
||||||
|
Note: Hermes uses prompt_toolkit for its CLI UI. Through a PTY, this works because ptyprocess provides a real terminal — input sent via `submit` arrives as keystrokes. The output log will contain ANSI escape sequences from the UI rendering — focus on the text content, not the formatting.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start interactive hermes in background with PTY
|
||||||
|
terminal(command="hermes", workdir="~/project", background=true, pty=true)
|
||||||
|
# Returns session_id
|
||||||
|
|
||||||
|
# Send it a task
|
||||||
|
process(action="submit", session_id="<id>", data="Set up a Python project with FastAPI, add auth endpoints, and write tests")
|
||||||
|
|
||||||
|
# Wait for it to work, then check progress
|
||||||
|
process(action="log", session_id="<id>")
|
||||||
|
|
||||||
|
# Give feedback on what it produced
|
||||||
|
process(action="submit", session_id="<id>", data="The tests look good but add edge cases for invalid tokens")
|
||||||
|
|
||||||
|
# Check its response
|
||||||
|
process(action="log", session_id="<id>")
|
||||||
|
|
||||||
|
# Ask it to iterate
|
||||||
|
process(action="submit", session_id="<id>", data="Now add rate limiting middleware")
|
||||||
|
|
||||||
|
# When done, exit the session
|
||||||
|
process(action="submit", session_id="<id>", data="/exit")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interactive Collaboration Patterns
|
||||||
|
|
||||||
|
**Code review loop** — spawn hermes, send code for review, iterate on feedback:
|
||||||
|
```
|
||||||
|
terminal(command="hermes", workdir="~/project", background=true, pty=true)
|
||||||
|
process(action="submit", session_id="<id>", data="Review the changes in src/auth.py and suggest improvements")
|
||||||
|
# ... read its review ...
|
||||||
|
process(action="submit", session_id="<id>", data="Good points. Go ahead and implement suggestions 1 and 3")
|
||||||
|
# ... it makes changes ...
|
||||||
|
process(action="submit", session_id="<id>", data="Run the tests to make sure nothing broke")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Research with steering** — start broad, narrow down based on findings:
|
||||||
|
```
|
||||||
|
terminal(command="hermes", background=true, pty=true)
|
||||||
|
process(action="submit", session_id="<id>", data="Search for the latest papers on KV cache compression techniques")
|
||||||
|
# ... read its findings ...
|
||||||
|
process(action="submit", session_id="<id>", data="The MQA approach looks promising. Dig deeper into that one and compare with GQA")
|
||||||
|
# ... more detailed research ...
|
||||||
|
process(action="submit", session_id="<id>", data="Write up everything you found to ~/research/kv-cache-compression.md")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Multi-agent coordination** — spawn two agents working on related tasks, pass context between them:
|
||||||
|
```
|
||||||
|
# Agent A: backend
|
||||||
|
terminal(command="hermes", workdir="~/project/backend", background=true, pty=true)
|
||||||
|
process(action="submit", session_id="<agent-a>", data="Build a REST API for user management with CRUD endpoints")
|
||||||
|
|
||||||
|
# Agent B: frontend
|
||||||
|
terminal(command="hermes", workdir="~/project/frontend", background=true, pty=true)
|
||||||
|
process(action="submit", session_id="<agent-b>", data="Build a React dashboard that will connect to a REST API at localhost:8000/api/users")
|
||||||
|
|
||||||
|
# Check Agent A's progress, relay API schema to Agent B
|
||||||
|
process(action="log", session_id="<agent-a>")
|
||||||
|
process(action="submit", session_id="<agent-b>", data="Here's the API schema Agent A built: GET /api/users, POST /api/users, etc. Update your fetch calls to match.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Non-Interactive Instances
|
||||||
|
|
||||||
|
Spawn multiple independent agents for unrelated tasks:
|
||||||
|
|
||||||
|
```
|
||||||
|
terminal(command="hermes chat -q 'Research competitor landing pages and write a report to ~/research/competitors.md'", background=true)
|
||||||
|
terminal(command="hermes chat -q 'Audit security of ~/myapp and write findings to ~/myapp/SECURITY_AUDIT.md'", background=true)
|
||||||
|
process(action="list")
|
||||||
|
```
|
||||||
|
|
||||||
|
## With Custom Model
|
||||||
|
|
||||||
|
```
|
||||||
|
terminal(command="hermes chat -q 'Summarize this codebase' --model google/gemini-2.5-pro", workdir="~/project", background=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gateway Cron Integration
|
||||||
|
|
||||||
|
For scheduled autonomous tasks, use the `schedule_cronjob` tool instead of spawning processes — cron jobs handle delivery, retry, and persistence automatically.
|
||||||
|
|
||||||
|
## Key Differences Between Modes
|
||||||
|
|
||||||
|
| | `-q` (one-shot) | Interactive (PTY) | `--continue` / `--resume` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| User interaction | None | Full back-and-forth | Full back-and-forth |
|
||||||
|
| PTY required | No | Yes (`pty=true`) | Yes (`pty=true`) |
|
||||||
|
| Multi-turn | Single query | Unlimited turns | Continues previous turns |
|
||||||
|
| Best for | Fire-and-forget tasks | Iterative work, steering | Picking up where you left off |
|
||||||
|
| Exit | Automatic after completion | Send `/exit` or kill | Send `/exit` or kill |
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
- **Interactive PTY + prompt_toolkit**: The `submit` action sends `\n` (line feed) but prompt_toolkit in raw mode expects `\r` (carriage return) for Enter. Text appears in the prompt but never submits. **Workaround**: Use **tmux** instead of raw PTY mode. tmux's `send-keys Enter` sends the correct `\r`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Start hermes inside tmux
|
||||||
|
tmux new-session -d -s hermes-session -x 120 -y 40 "hermes"
|
||||||
|
sleep 10 # Wait for banner/startup
|
||||||
|
|
||||||
|
# Send messages
|
||||||
|
tmux send-keys -t hermes-session "your message here" Enter
|
||||||
|
|
||||||
|
# Read output
|
||||||
|
sleep 15 # Wait for LLM response
|
||||||
|
tmux capture-pane -t hermes-session -p
|
||||||
|
|
||||||
|
# Multi-turn: just send more messages and capture again
|
||||||
|
tmux send-keys -t hermes-session "follow-up message" Enter
|
||||||
|
|
||||||
|
# Exit when done
|
||||||
|
tmux send-keys -t hermes-session "/exit" Enter
|
||||||
|
tmux kill-session -t hermes-session
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Use `-q` for autonomous tasks** — agent works independently and exits
|
||||||
|
2. **Use `pty=true` for interactive sessions** — required for the full CLI UI
|
||||||
|
3. **Use `submit` not `write`** — `submit` adds a newline (Enter), `write` doesn't
|
||||||
|
4. **Read logs before sending more** — check what the agent produced before giving next instruction
|
||||||
|
5. **Set timeouts for `-q` mode** — complex tasks may take 5-10 minutes
|
||||||
|
6. **Prefer `delegate_task` for quick subtasks** — spawning a full process has more overhead
|
||||||
|
7. **Each instance is independent** — they don't share conversation context with the parent
|
||||||
|
8. **Check results** — after completion, read the output files or logs the agent produced
|
||||||
Reference in New Issue
Block a user