consistent agent naming: parent:prefix convention

Agent names derived from cwd using parent:prefix format:
- /home/user/src/ollie -> src:ollie
- /home/user/proj/ABC-123_feature/repo -> abc-123:repo

Updates:
- Kate plugin: one agent per project, git root fallback if no project plugin
- acme-ollie-ensure: single 'acme' session, per-project agents
- ollie-session-here: single 'default' session, per-project agents

Also fixed write -> rdwr for session/agent creation endpoints
This commit is contained in:
Levi Neely 2026-10-07 14:39:47 +02:00
parent 1c02b50add
commit d5efe575ce
4 changed files with 125 additions and 55 deletions

View File

@ -1,14 +1,32 @@
#!/bin/bash
# acme-ollie-ensure - Ensure acme session and agent exist
#
# One session per project root (git worktree aware), one agent "assistant".
# Session name: acme-{hash of project root}
# One session "acme", one agent per project directory.
# Agent name: parent:basename (e.g., src:ollie, ABC-123:repo)
# If basename contains _, use prefix before first _ (e.g., ABC-123_feature -> ABC-123)
#
# Usage: eval $(acme-ollie-ensure)
# Then use $OLLIE_SESSION and $OLLIE_AGENT
set -euo pipefail
# Compute agent name from directory path
# Format: parent:basename (basename split at first _ if present)
agent_name_from_cwd() {
local cwd="$1"
local parent basename prefix
parent=$(basename "$(dirname "$cwd")")
basename=$(basename "$cwd")
# Split basename at first underscore if present
if [[ "$basename" == *_* ]]; then
prefix="${basename%%_*}"
else
prefix="$basename"
fi
# Slugify: lowercase, replace non-alnum with -
echo "${parent}:${prefix}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9:-]/-/g'
}
# Acme sets $PWD to the window's directory for executed commands.
# Fall back to tag parsing if $winid is set (e.g. for pipe commands).
cwd="$PWD"
@ -28,19 +46,17 @@ if wt_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null); then
project_root="$wt_root"
fi
# Session name from project root hash
session_hash=$(printf '%s' "$project_root" | md5sum | cut -c1-8)
SESSION="acme-$session_hash"
AGENT="assistant"
SESSION="acme"
AGENT=$(agent_name_from_cwd "$project_root")
# Ensure session exists (with project root as cwd)
# Ensure session exists
if ! ollie-9p read "session/$SESSION/env" >/dev/null 2>&1; then
printf 'name=%s\ncwd=%s\n' "$SESSION" "$project_root" | ollie-9p write "session/new" 2>/dev/null || true
printf 'name=%s\ncwd=%s\n' "$SESSION" "$project_root" | ollie-9p rdwr "session/new" >/dev/null 2>&1 || true
fi
# Ensure agent exists
# Ensure agent exists (with project root as cwd)
if ! ollie-9p read "session/$SESSION/agent/$AGENT/cfg" >/dev/null 2>&1; then
printf 'name=%s\n' "$AGENT" | ollie-9p write "session/$SESSION/agent/new" 2>/dev/null || true
printf 'name=%s\ncwd=%s\n' "$AGENT" "$project_root" | ollie-9p rdwr "session/$SESSION/agent/new" >/dev/null 2>&1 || true
fi
# Output for eval

View File

@ -2,7 +2,8 @@
# ollie-session-here: start a session in the given directory
# Works from both Dolphin (passes %f) and terminal (uses $PWD).
#
# Session is always "default". Agent name is the base directory name.
# Session is always "default". Agent name follows convention: parent:prefix
# where prefix is basename split at first _ (e.g., src:ollie, ABC-123:repo)
# Session/agent creation is idempotent.
TARGET="$1"
@ -10,12 +11,27 @@ TARGET="$1"
[ -z "$TARGET" ] && TARGET="$PWD"
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD"
# Agent name is base directory name
AGENT_NAME="$(basename "$TARGET")"
# Compute agent name: parent:prefix (prefix = basename up to first _)
PARENT="$(basename "$(dirname "$TARGET")")"
BASENAME="$(basename "$TARGET")"
case "$BASENAME" in
*_*) PREFIX="${BASENAME%%_*}" ;;
*) PREFIX="$BASENAME" ;;
esac
# Slugify: lowercase, replace non-alnum with -
AGENT_NAME="$(printf '%s:%s' "$PARENT" "$PREFIX" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9:-]/-/g')"
# Create default session (idempotent) and named agent
printf 'name=default\ncwd=%s\n' "$TARGET" | ollie-9p write session/new
printf 'name=%s\n' "$AGENT_NAME" | ollie-9p write session/default/agent/new
# Create default session (idempotent)
printf 'name=default\ncwd=%s\n' "$TARGET" | ollie-9p rdwr session/new >/dev/null 2>&1
# Create or update agent with computed name
if ollie-9p read "session/default/agent/$AGENT_NAME/cfg" >/dev/null 2>&1; then
# Agent exists, update cwd
printf 'cwd %s' "$TARGET" | ollie-9p rdwr "session/default/agent/$AGENT_NAME/ctl" >/dev/null 2>&1
else
# Create new agent
printf 'name=%s\ncwd=%s\n' "$AGENT_NAME" "$TARGET" | ollie-9p rdwr session/default/agent/new >/dev/null 2>&1
fi
if [ -t 1 ]; then
# Terminal: print session info

View File

@ -180,16 +180,46 @@ OllieKateView::~OllieKateView()
}
}
// --- Agent Naming ---
// Compute agent name from directory path
// Format: parent:basename (basename split at first _ if present)
// Examples: /home/user/src/ollie -> src:ollie
// /home/user/proj/ABC-123_feature/repo -> ABC-123:repo
QString OllieKateView::agentNameFromCwd(const QString &cwd)
{
QFileInfo fi(cwd);
QString parent = QFileInfo(fi.path()).fileName(); // parent directory name
QString basename = fi.fileName();
// Split basename at first underscore if present
int underscoreIdx = basename.indexOf('_');
QString prefix = (underscoreIdx > 0) ? basename.left(underscoreIdx) : basename;
// Slugify: lowercase, replace non-alnum with -
QString slug = (parent + ":" + prefix).toLower();
QString result;
for (QChar c : slug) {
if (c.isLetterOrNumber() || c == ':' || c == '-') {
result += c;
} else {
result += '-';
}
}
return result;
}
// --- Agent Management ---
void OllieKateView::ensureAgent()
{
if (!m_agentId.isEmpty()) return;
QString cwd = projectBaseDir();
if (cwd.isEmpty()) cwd = QDir::homePath();
QString agentName = agentNameFromCwd(cwd);
// Ensure kate session exists
if (m_sessionId.isEmpty()) {
QString cwd = projectBaseDir();
if (cwd.isEmpty()) cwd = QDir::homePath();
QByteArray resp = m_9p->rdwr("session/new", ("name=kate\ncwd=" + cwd + "\n").toUtf8());
if (resp.isEmpty()) {
qWarning() << "Ollie Kate: failed to create session";
@ -198,22 +228,18 @@ void OllieKateView::ensureAgent()
m_sessionId = "kate";
}
// Look for an existing agent in the kate session
QStringList agents = m_9p->ls("session/kate/agent");
agents.removeAll("new");
agents.removeAll("idx");
if (!agents.isEmpty()) {
m_agentId = agents.first();
// Check if agent with this name already exists
QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg");
if (!check.isEmpty()) {
m_agentId = agentName;
// Ensure cwd is up to date
m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8());
} else {
// Create one
QString cwd = projectBaseDir();
if (cwd.isEmpty()) cwd = QDir::homePath();
QString agentCfg = "cwd=" + cwd + "\n";
// Create new agent with computed name
QString agentCfg = "name=" + agentName + "\ncwd=" + cwd + "\n";
QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8());
if (!resp.isEmpty()) {
m_agentId = QString::fromUtf8(resp).trimmed();
m_agentId = agentName;
}
}
updateStatusLabel();
@ -226,26 +252,25 @@ bool OllieKateView::selectAgentForProject()
if (sessCheck.isEmpty()) return false;
m_sessionId = "kate";
// Find first agent in the session
QStringList agents = m_9p->ls("session/kate/agent");
agents.removeAll("new");
agents.removeAll("idx");
QString cwd = projectBaseDir();
if (cwd.isEmpty()) return false;
QString agentName = agentNameFromCwd(cwd);
if (agents.isEmpty()) return false;
QString agentId = agents.first();
if (agentId != m_agentId) {
m_agentId = agentId;
}
// Update cwd to match current project
QString baseDir = projectBaseDir();
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
// Check if agent exists
QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg");
if (!check.isEmpty()) {
m_agentId = agentName;
// Ensure cwd is up to date
m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8());
updateStatusLabel();
return true;
}
// Agent doesn't exist yet — will be created on first use
m_agentId.clear();
updateStatusLabel();
return true;
return false;
}
void OllieKateView::switchToAgent(const QString &agentId)
@ -257,25 +282,35 @@ void OllieKateView::switchToAgent(const QString &agentId)
void OllieKateView::onProjectChanged()
{
// Update the agent's cwd to the new project directory
QString baseDir = projectBaseDir();
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
}
updateStatusLabel();
// Switch to the agent for the new project (or create if needed)
selectAgentForProject();
}
QString OllieKateView::projectBaseDir()
{
// Try Kate's project plugin first
QObject *projectView = m_mainWindow->pluginView("kateprojectplugin");
if (projectView) {
QString dir = projectView->property("projectBaseDir").toString();
if (!dir.isEmpty()) return dir;
}
// Fall back to git root detection from active document
auto *view = m_mainWindow->activeView();
if (view && view->document() && !view->document()->url().toLocalFile().isEmpty()) {
return QFileInfo(view->document()->url().toLocalFile()).absolutePath();
QString docDir = QFileInfo(view->document()->url().toLocalFile()).absolutePath();
// Walk up to find .git directory
QDir dir(docDir);
while (!dir.isRoot()) {
if (QFileInfo(dir.filePath(".git")).exists()) {
return dir.absolutePath();
}
if (!dir.cdUp()) break;
}
// No git root found, use document directory
return docDir;
}
return QString();
}

View File

@ -71,6 +71,9 @@ private:
void reviewDiff();
void startSession();
// Agent naming
QString agentNameFromCwd(const QString &cwd);
// Agent management
void ensureAgent();
bool selectAgentForProject();