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 #!/bin/bash
# acme-ollie-ensure - Ensure acme session and agent exist # acme-ollie-ensure - Ensure acme session and agent exist
# #
# One session per project root (git worktree aware), one agent "assistant". # One session "acme", one agent per project directory.
# Session name: acme-{hash of project root} # 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) # Usage: eval $(acme-ollie-ensure)
# Then use $OLLIE_SESSION and $OLLIE_AGENT # Then use $OLLIE_SESSION and $OLLIE_AGENT
set -euo pipefail 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. # 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). # Fall back to tag parsing if $winid is set (e.g. for pipe commands).
cwd="$PWD" cwd="$PWD"
@ -28,19 +46,17 @@ if wt_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null); then
project_root="$wt_root" project_root="$wt_root"
fi fi
# Session name from project root hash SESSION="acme"
session_hash=$(printf '%s' "$project_root" | md5sum | cut -c1-8) AGENT=$(agent_name_from_cwd "$project_root")
SESSION="acme-$session_hash"
AGENT="assistant"
# Ensure session exists (with project root as cwd) # Ensure session exists
if ! ollie-9p read "session/$SESSION/env" >/dev/null 2>&1; then 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 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 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 fi
# Output for eval # Output for eval

View File

@ -2,7 +2,8 @@
# ollie-session-here: start a session in the given directory # ollie-session-here: start a session in the given directory
# Works from both Dolphin (passes %f) and terminal (uses $PWD). # 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. # Session/agent creation is idempotent.
TARGET="$1" TARGET="$1"
@ -10,12 +11,27 @@ TARGET="$1"
[ -z "$TARGET" ] && TARGET="$PWD" [ -z "$TARGET" ] && TARGET="$PWD"
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD" TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD"
# Agent name is base directory name # Compute agent name: parent:prefix (prefix = basename up to first _)
AGENT_NAME="$(basename "$TARGET")" 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 # Create default session (idempotent)
printf 'name=default\ncwd=%s\n' "$TARGET" | ollie-9p write session/new printf 'name=default\ncwd=%s\n' "$TARGET" | ollie-9p rdwr session/new >/dev/null 2>&1
printf 'name=%s\n' "$AGENT_NAME" | ollie-9p write session/default/agent/new
# 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 if [ -t 1 ]; then
# Terminal: print session info # 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 --- // --- Agent Management ---
void OllieKateView::ensureAgent() 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 // Ensure kate session exists
if (m_sessionId.isEmpty()) { 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()); QByteArray resp = m_9p->rdwr("session/new", ("name=kate\ncwd=" + cwd + "\n").toUtf8());
if (resp.isEmpty()) { if (resp.isEmpty()) {
qWarning() << "Ollie Kate: failed to create session"; qWarning() << "Ollie Kate: failed to create session";
@ -198,22 +228,18 @@ void OllieKateView::ensureAgent()
m_sessionId = "kate"; m_sessionId = "kate";
} }
// Look for an existing agent in the kate session // Check if agent with this name already exists
QStringList agents = m_9p->ls("session/kate/agent"); QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg");
agents.removeAll("new"); if (!check.isEmpty()) {
agents.removeAll("idx"); m_agentId = agentName;
// Ensure cwd is up to date
if (!agents.isEmpty()) { m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8());
m_agentId = agents.first();
} else { } else {
// Create one // Create new agent with computed name
QString cwd = projectBaseDir(); QString agentCfg = "name=" + agentName + "\ncwd=" + cwd + "\n";
if (cwd.isEmpty()) cwd = QDir::homePath();
QString agentCfg = "cwd=" + cwd + "\n";
QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8()); QByteArray resp = m_9p->rdwr("session/kate/agent/new", agentCfg.toUtf8());
if (!resp.isEmpty()) { if (!resp.isEmpty()) {
m_agentId = QString::fromUtf8(resp).trimmed(); m_agentId = agentName;
} }
} }
updateStatusLabel(); updateStatusLabel();
@ -226,26 +252,25 @@ bool OllieKateView::selectAgentForProject()
if (sessCheck.isEmpty()) return false; if (sessCheck.isEmpty()) return false;
m_sessionId = "kate"; m_sessionId = "kate";
// Find first agent in the session QString cwd = projectBaseDir();
QStringList agents = m_9p->ls("session/kate/agent"); if (cwd.isEmpty()) return false;
agents.removeAll("new");
agents.removeAll("idx"); QString agentName = agentNameFromCwd(cwd);
if (agents.isEmpty()) return false; // Check if agent exists
QByteArray check = m_9p->read("session/kate/agent/" + agentName + "/cfg");
QString agentId = agents.first(); if (!check.isEmpty()) {
if (agentId != m_agentId) { m_agentId = agentName;
m_agentId = agentId; // Ensure cwd is up to date
} m_9p->rdwr("session/kate/agent/" + agentName + "/ctl", ("cwd " + cwd).toUtf8());
updateStatusLabel();
// Update cwd to match current project return true;
QString baseDir = projectBaseDir();
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
} }
// Agent doesn't exist yet — will be created on first use
m_agentId.clear();
updateStatusLabel(); updateStatusLabel();
return true; return false;
} }
void OllieKateView::switchToAgent(const QString &agentId) void OllieKateView::switchToAgent(const QString &agentId)
@ -257,25 +282,35 @@ void OllieKateView::switchToAgent(const QString &agentId)
void OllieKateView::onProjectChanged() void OllieKateView::onProjectChanged()
{ {
// Update the agent's cwd to the new project directory // Switch to the agent for the new project (or create if needed)
QString baseDir = projectBaseDir(); selectAgentForProject();
if (!baseDir.isEmpty() && !m_agentId.isEmpty()) {
m_9p->write(agentPath() + "/ctl", ("cwd " + baseDir).toUtf8());
}
updateStatusLabel();
} }
QString OllieKateView::projectBaseDir() QString OllieKateView::projectBaseDir()
{ {
// Try Kate's project plugin first
QObject *projectView = m_mainWindow->pluginView("kateprojectplugin"); QObject *projectView = m_mainWindow->pluginView("kateprojectplugin");
if (projectView) { if (projectView) {
QString dir = projectView->property("projectBaseDir").toString(); QString dir = projectView->property("projectBaseDir").toString();
if (!dir.isEmpty()) return dir; if (!dir.isEmpty()) return dir;
} }
// Fall back to git root detection from active document
auto *view = m_mainWindow->activeView(); auto *view = m_mainWindow->activeView();
if (view && view->document() && !view->document()->url().toLocalFile().isEmpty()) { 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(); return QString();
} }

View File

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