Fix 9p-read-inbox to strip .json extension when marking messages complete

This commit is contained in:
Levi Neely 2026-02-17 14:37:46 +01:00
parent 241e4b9818
commit 35b3ff338e
16 changed files with 413 additions and 51 deletions

6
.gitignore vendored
View File

@ -3,9 +3,9 @@ Prompt
Q-Reset
Q-Login
/anvillm
anvilweb
/anvilweb
.claude
.kiro
*.elc
Assist
anvilsrv
/Assist
/anvilsrv

View File

@ -1,5 +1,7 @@
# Developer role configuration
filesystem:
ro:
- "{HOME}/doc"
rw:
- "{CWD}"
rwx:

View File

@ -5,3 +5,64 @@ permissionMode: bypassPermissions
---
You are an AI assistant for AnviLLM multi-agent workflows. You have full access to all tools and operate without permission restrictions.
## Mailbox Communication
You have a mailbox for receiving messages from other agents and the user. Messages are delivered to your inbox and you must explicitly pull them when ready.
### Checking Your Inbox
Your inbox is accessible via the `agent` namespace. Check it using the helper script:
```bash
# Check for messages
9p-read-inbox
# Read and mark complete in one step
9p-read-inbox --complete
```
Or manually:
```bash
# Get your agent ID
AGENT_ID=$(9p ls agent/ | head -1)
# List pending messages
9p ls agent/$AGENT_ID/inbox/
# Read first message
MSG=$(9p ls agent/$AGENT_ID/inbox/ | head -1)
9p read agent/$AGENT_ID/inbox/$MSG
# After processing, mark complete
9p rm agent/$AGENT_ID/inbox/$MSG
```
### When to Check
- When transitioning to idle state (after completing a task)
- When explicitly told to check messages
- Periodically during long-running tasks
### Message Format
Messages are JSON with these fields:
- `from`: Sender's session ID or "user"
- `to`: Your session ID
- `type`: Message type (QUESTION, REVIEW_REQUEST, STATUS_UPDATE, etc.)
- `subject`: Brief subject line
- `body`: Message content
- `id`: Unique message ID
- `timestamp`: When message was sent
### Sending Messages
To send a message to another agent or user:
```bash
cat > /tmp/msg.json <<'ENDMSG'
{"to":"recipient-id","type":"MESSAGE_TYPE","subject":"Subject","body":"Message body"}
ENDMSG
9p write agent/YOUR_ID/outbox/msg-$(date +%s).json < /tmp/msg.json
```

View File

@ -26,6 +26,12 @@ fi
echo "$(date '+%Y-%m-%d %H:%M:%S') - Found session ID: $session" >> /tmp/claude-hooks.log
if echo idle | 9p write agent/$session/state 2>>/tmp/claude-hooks.log; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - SUCCESS: Wrote 'idle' to agent/$session/state" >> /tmp/claude-hooks.log
# Check for pending messages in inbox
if [ -n "$(9p ls agent/$session/inbox/ 2>/dev/null)" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - INBOX: Pending messages detected" >> /tmp/claude-hooks.log
echo "[INBOX] You have pending messages. Check with: 9p-read-inbox"
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - ERROR: Failed to write to agent/$session/state" >> /tmp/claude-hooks.log
fi

View File

@ -387,6 +387,21 @@ func sendPrompt(id, prompt string) error {
return nil
}
func sendPromptDirect(id, prompt string) error {
if !isConnected() {
return fmt.Errorf("not connected to anvilsrv")
}
path := filepath.Join(id, "in")
fid, err := fs.Open(path, plan9.OWRITE)
if err != nil {
return err
}
defer fid.Close()
_, err = fid.Write([]byte(prompt))
return err
}
func setAlias(id, alias string) error {
if !isConnected() {
return fmt.Errorf("not connected to anvilsrv")
@ -563,7 +578,7 @@ func openPromptWindow(sess *SessionInfo) (*acme.Win, error) {
return nil, err
}
w.Name(name)
w.Write("tag", []byte("Send "))
w.Write("tag", []byte("Prompt Message "))
w.Ctl("clean")
// Track window ID client-side
@ -584,15 +599,22 @@ func handlePromptWindow(w *acme.Win, sess *SessionInfo) {
}()
for e := range w.EventChan() {
if (e.C2 == 'x' || e.C2 == 'X') && string(e.Text) == "Send" {
cmd := string(e.Text)
if (e.C2 == 'x' || e.C2 == 'X') && (cmd == "Prompt" || cmd == "Message") {
body, err := w.ReadAll("body")
if err != nil {
continue
}
prompt := strings.TrimSpace(string(body))
if prompt != "" {
if err := sendPrompt(sess.ID, prompt); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send prompt: %v\n", err)
var sendErr error
if cmd == "Prompt" {
sendErr = sendPromptDirect(sess.ID, prompt)
} else {
sendErr = sendPrompt(sess.ID, prompt)
}
if sendErr != nil {
fmt.Fprintf(os.Stderr, "Failed to send %s: %v\n", strings.ToLower(cmd), sendErr)
continue
}
w.Ctl("delete")

View File

@ -377,6 +377,18 @@ func sendPrompt(id, prompt string) error {
return nil
}
func sendPromptDirect(id, prompt string) error {
path := filepath.Join(id, "in")
fid, err := fs.Open(path, plan9.OWRITE)
if err != nil {
return err
}
defer fid.Close()
_, err = fid.Write([]byte(prompt))
return err
}
func updateStatus(msg string) {
statusBar.Clear()
fmt.Fprintf(statusBar, " %s", msg)

View File

@ -203,6 +203,8 @@ func handleSession(w http.ResponseWriter, r *http.Request) {
case "POST":
if action == "prompt" {
sendPrompt(w, r, id)
} else if action == "prompt-direct" {
sendPromptDirect(w, r, id)
} else if action == "ctl" {
controlSession(w, r, id)
} else {
@ -373,6 +375,44 @@ func sendPrompt(w http.ResponseWriter, r *http.Request, id string) {
w.WriteHeader(http.StatusOK)
}
func sendPromptDirect(w http.ResponseWriter, r *http.Request, id string) {
var req struct {
Prompt string `json:"prompt"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if strings.TrimSpace(req.Prompt) == "" {
http.Error(w, "Prompt cannot be empty", http.StatusBadRequest)
return
}
fs, err := connect()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer fs.Close()
path := filepath.Join(id, "in")
fid, err := fs.Open(path, plan9.OWRITE)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer fid.Close()
if _, err := fid.Write([]byte(req.Prompt)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func controlSession(w http.ResponseWriter, r *http.Request, id string) {
var req struct {
Command string `json:"command"`

View File

@ -183,7 +183,7 @@ Format: id alias state pid cwd (whitespace-separated; often tabs)."
entry))
(defun anvillm-send-prompt-minibuffer ()
"Send a prompt to the selected session."
"Send a prompt to the selected session via mailbox (queued)."
(interactive)
(if-let ((session-id (anvillm--get-selected-session)))
(let ((prompt (read-string "Prompt: ")))
@ -204,6 +204,20 @@ Format: id alias state pid cwd (whitespace-separated; often tabs)."
(message "Failed to send prompt: %s" (error-message-string err))))))
(message "No session selected")))
(defun anvillm-send-prompt-direct ()
"Send a prompt directly to the selected session (immediate, may block)."
(interactive)
(if-let ((session-id (anvillm--get-selected-session)))
(let ((prompt (read-string "Prompt (direct): ")))
(when (> (length prompt) 0)
(condition-case err
(progn
(anvillm--9p-write (concat anvillm-agent-path "/" session-id "/in") prompt)
(message "Sent direct prompt to %s" (substring session-id 0 (min 8 (length session-id)))))
(error
(message "Failed to send direct prompt: %s" (error-message-string err))))))
(message "No session selected")))
(defun anvillm-stop-session ()
"Stop the selected session."
(interactive)
@ -315,6 +329,7 @@ Backends:
(set-keymap-parent map tabulated-list-mode-map)
(define-key map (kbd "s") #'anvillm-start-session)
(define-key map (kbd "P") #'anvillm-send-prompt-minibuffer)
(define-key map (kbd "M-P") #'anvillm-send-prompt-direct)
(define-key map (kbd "t") #'anvillm-stop-session)
(define-key map (kbd "R") #'anvillm-restart-session)
(define-key map (kbd "K") #'anvillm-kill-session)

View File

@ -57,6 +57,7 @@ type Session struct {
// State machine
idleCond *sync.Cond // Signals when state transitions to idle
idleSince time.Time // Timestamp when session last transitioned to idle
mu sync.Mutex
}
@ -95,11 +96,23 @@ func (s *Session) TransitionTo(newState string) error {
return s.transitionToLocked(newState)
}
// IdleDuration returns how long the session has been idle.
// Returns 0 if not currently idle.
func (s *Session) IdleDuration() time.Duration {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != "idle" || s.idleSince.IsZero() {
return 0
}
return time.Since(s.idleSince)
}
func (s *Session) transitionToLocked(newState string) error {
// Validate transition
switch {
case s.state == "starting" && newState == "idle":
s.state = newState
s.idleSince = time.Now()
s.idleCond.Broadcast()
return nil
case s.state == "starting" && newState == "error":
@ -110,6 +123,7 @@ func (s *Session) transitionToLocked(newState string) error {
return nil
case s.state == "running" && newState == "idle":
s.state = newState
s.idleSince = time.Now()
s.idleCond.Broadcast()
return nil
case s.state == "running" && newState == "error":
@ -119,6 +133,7 @@ func (s *Session) transitionToLocked(newState string) error {
case s.state == "error" && newState == "idle":
// Manual recovery from error state
s.state = newState
s.idleSince = time.Now()
s.idleCond.Broadcast()
return nil
case s.state == "error" && newState == "starting":

View File

@ -138,6 +138,42 @@ func (m *Manager) GetPendingMessages(sessionID string) ([]*Message, error) {
return m.GetInbox(sessionID), nil
}
// PullMessage retrieves and removes the first message from inbox
func (m *Manager) PullMessage(sessionID string) (*Message, error) {
m.mu.Lock()
defer m.mu.Unlock()
msgs := m.inboxes[sessionID]
if len(msgs) == 0 {
return nil, fmt.Errorf("no messages in inbox")
}
msg := msgs[0]
m.inboxes[sessionID] = msgs[1:]
return msg, nil
}
// PeekInbox returns first message without removing it
func (m *Manager) PeekInbox(sessionID string) (*Message, error) {
m.mu.RLock()
defer m.mu.RUnlock()
msgs := m.inboxes[sessionID]
if len(msgs) == 0 {
return nil, fmt.Errorf("no messages in inbox")
}
return msgs[0], nil
}
// HasPendingMessages checks if inbox has messages
func (m *Manager) HasPendingMessages(sessionID string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.inboxes[sessionID]) > 0
}
// CompleteMessage moves a message from inbox to completed
func (m *Manager) CompleteMessage(sessionID, msgID string) error {
m.mu.Lock()

View File

@ -62,6 +62,7 @@ const (
qidRoot = iota
qidCtl
qidList
qidStatus
qidUser // user directory
qidUserInbox // user/inbox
qidUserOutbox // user/outbox
@ -106,8 +107,9 @@ type Server struct {
}
type connState struct {
fids map[uint32]*fid
mu sync.RWMutex
fids map[uint32]*fid
mu sync.RWMutex
sessionID string // Track which session owns this connection
}
type fid struct {
@ -203,6 +205,8 @@ func (s *Server) handle(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return s.read(cs, fc)
case plan9.Twrite:
return s.write(cs, fc)
case plan9.Tremove:
return s.remove(cs, fc)
case plan9.Tstat:
return s.stat(cs, fc)
case plan9.Tclunk:
@ -507,7 +511,7 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
}
args := strings.Fields(input)
if len(args) == 0 {
return errFcall(fc, "usage: stop | restart | kill | refresh")
return errFcall(fc, "usage: stop | restart | kill | refresh | complete <msg-id>")
}
switch args[0] {
case "stop":
@ -528,6 +532,15 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if err := sess.Refresh(ctx); err != nil {
return errFcall(fc, err.Error())
}
case "complete":
if len(args) < 2 {
return errFcall(fc, "usage: complete <msg-id>")
}
msgID := args[1]
mailMgr := s.mgr.GetMailManager()
if err := mailMgr.CompleteMessage(parts[0], msgID); err != nil {
return errFcall(fc, err.Error())
}
default:
return errFcall(fc, "unknown command")
}
@ -536,10 +549,12 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
// /{id}/in - send prompt (async, non-blocking)
if len(parts) == 2 && parts[1] == "in" {
sess := s.mgr.Get(parts[0])
sessID := parts[0]
sess := s.mgr.Get(sessID)
if sess == nil {
return errFcall(fc, "session not found")
}
ctx := context.Background()
if _, err := sess.Send(ctx, input); err != nil {
return errFcall(fc, err.Error())
@ -593,7 +608,8 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
// /{id}/state - set session state (with validation)
if len(parts) == 2 && parts[1] == "state" {
sess := s.mgr.Get(parts[0])
sessID := parts[0]
sess := s.mgr.Get(sessID)
if sess == nil {
return errFcall(fc, "session not found")
}
@ -617,6 +633,14 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return errFcall(fc, fmt.Sprintf("invalid state transition: %v", err))
}
}
// Track this connection's session ID
cs.mu.Lock()
if cs.sessionID == "" {
cs.sessionID = sessID
}
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
@ -624,6 +648,13 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if len(parts) == 3 && parts[1] == "outbox" && strings.HasSuffix(parts[2], ".json") {
sessID := parts[0]
// Track this connection's session ID
cs.mu.Lock()
if cs.sessionID == "" {
cs.sessionID = sessID
}
cs.mu.Unlock()
// Parse JSON message
msg, err := mailbox.FromJSON(fc.Data)
if err != nil {
@ -691,6 +722,45 @@ func (s *Server) stat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: stat}
}
func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
sessionID := cs.sessionID
cs.mu.Unlock()
if !ok {
return errFcall(fc, "bad fid")
}
// Only support removing inbox messages (marks them as completed)
parts := strings.Split(strings.TrimPrefix(f.path, "/"), "/")
if len(parts) == 3 && parts[1] == "inbox" && strings.HasSuffix(parts[2], ".json") {
sessID := parts[0]
msgID := strings.TrimSuffix(parts[2], ".json")
// Validate ownership: only allow removing from own inbox or user inbox
if sessionID == "" || (sessID != "user" && sessID != sessionID) {
return errFcall(fc, "permission denied: can only remove from own inbox")
}
mailMgr := s.mgr.GetMailManager()
if mailMgr == nil {
return errFcall(fc, "mailbox not available")
}
if err := mailMgr.CompleteMessage(sessID, msgID); err != nil {
return errFcall(fc, err.Error())
}
cs.mu.Lock()
delete(cs.fids, fc.Fid)
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag}
}
return errFcall(fc, "remove not supported for this file")
}
func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
var dirs []plan9.Dir
@ -703,6 +773,10 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
Qid: plan9.Qid{Type: QTFile, Path: qidList}, Mode: 0444, Name: "list",
Uid: "q", Gid: "q", Muid: "q",
})
dirs = append(dirs, plan9.Dir{
Qid: plan9.Qid{Type: QTFile, Path: qidStatus}, Mode: 0444, Name: "status",
Uid: "q", Gid: "q", Muid: "q",
})
dirs = append(dirs, plan9.Dir{
Qid: plan9.Qid{Type: QTDir, Path: qidUser},
Mode: plan9.DMDIR | 0555, Name: "user", Uid: "q", Gid: "q", Muid: "q",
@ -891,6 +965,35 @@ func (s *Server) readFile(path string) string {
return strings.Join(lines, "\n") + "\n"
}
if path == "/status" {
var lines []string
mailMgr := s.mgr.GetMailManager()
for _, id := range s.mgr.List() {
sess := s.mgr.Get(id)
if sess != nil {
state := sess.State()
idleSince := "-"
inboxCount := 0
if tmuxSess, ok := sess.(*tmux.Session); ok {
if state == "idle" {
idleDuration := tmuxSess.IdleDuration()
if idleDuration > 0 {
idleSince = fmt.Sprintf("%ds", int(idleDuration.Seconds()))
}
}
}
if mailMgr != nil {
inboxCount = len(mailMgr.GetInbox(id))
}
lines = append(lines, fmt.Sprintf("%s %s %s %d", id, state, idleSince, inboxCount))
}
}
return strings.Join(lines, "\n") + "\n"
}
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
// User message file: /user/mailbox/msg-*.json

View File

@ -158,51 +158,31 @@ func (m *Manager) processMailboxes() {
}
}
// 3. Process inbound messages for idle sessions (one message at a time)
// 3. Prompt idle agents with pending messages (after 30 seconds of idle)
for _, sess := range sessions {
if sess.State() != "idle" {
continue
}
messages, err := m.mailManager.GetPendingMessages(sess.ID())
if err != nil || len(messages) == 0 {
// Check if session has been idle for more than 30 seconds
tmuxSess, ok := sess.(*tmux.Session)
if !ok {
continue
}
// Process first message only
msg := messages[0]
// Format message for bot
prompt := fmt.Sprintf("[Message from %s]\nType: %s\nSubject: %s\n\n%s",
msg.From, msg.Type, msg.Subject, msg.Body)
// Send asynchronously - only move to completed after success
_, err = sess.Send(context.Background(), prompt)
if err != nil {
msg.Retries++
if msg.Retries > 3 {
// Discard after 3 failed attempts
m.mailManager.CompleteMessage(sess.ID(), msg.ID)
fmt.Fprintf(os.Stderr, "Message %s from %s to %s failed after 3 retries, discarding\nSubject: %s\nBody: %s\n",
msg.ID, msg.From, sess.ID(), msg.Subject, msg.Body)
// Notify sender
if msg.From != "" && msg.From != "user" {
if senderSess := m.Get(msg.From); senderSess != nil {
if tmuxSess, ok := senderSess.(*tmux.Session); ok {
notification := fmt.Sprintf("\n[DELIVERY FAILURE]\nFailed to deliver message to %s after 3 attempts.\nSubject: %s\nBody: %s\n\n",
sess.ID(), msg.Subject, msg.Body)
tmuxSess.AppendToChatLog("SYSTEM", notification)
}
}
}
}
// Message stays in inbox for retry on next tick
idleDuration := tmuxSess.IdleDuration()
if idleDuration < 30*time.Second {
continue
}
// Success - move to completed
m.mailManager.CompleteMessage(sess.ID(), msg.ID)
// Check if inbox has messages
if !m.mailManager.HasPendingMessages(sess.ID()) {
continue
}
// Prompt agent to check inbox
ctx := context.Background()
sess.Send(ctx, "check your inbox with 9p-read-inbox")
}
}

View File

@ -16,7 +16,7 @@
],
"stop": [
{
"command": "KIRO_PID=$(ps -o ppid= -p $PPID | tr -d ' '); cat $(namespace)/anvillm-session-id-$KIRO_PID 2>/dev/null | xargs -I{} sh -c 'echo idle | 9p write agent/{}/state 2>/dev/null' || true"
"command": "KIRO_PID=$(ps -o ppid= -p $PPID | tr -d ' '); cat $(namespace)/anvillm-session-id-$KIRO_PID 2>/dev/null | xargs -I{} sh -c 'echo idle | 9p write agent/{}/state 2>/dev/null; AGENT_ID=$(9p ls agent/ 2>/dev/null | head -1); [ -n \"$AGENT_ID\" ] && [ -n \"$(9p ls agent/$AGENT_ID/inbox/ 2>/dev/null)\" ] && echo \"[INBOX] You have pending messages. Check with: 9p-read-inbox\"' || true"
}
]
}

2
mkfile
View File

@ -11,6 +11,8 @@ build:V:
chmod 0755 $INSTALL_PATH/Alog
cp scripts/anvillm-skills $INSTALL_PATH/anvillm-skills
chmod 0755 $INSTALL_PATH/anvillm-skills
cp scripts/9p-read-inbox $INSTALL_PATH/9p-read-inbox
chmod 0755 $INSTALL_PATH/9p-read-inbox
mkdir -p $INSTALL_PATH/Workflows
cp workflows/* $INSTALL_PATH/Workflows
chmod 0755 $INSTALL_PATH/Workflows/*

50
scripts/9p-read-inbox Executable file
View File

@ -0,0 +1,50 @@
#!/bin/bash
# Read next message from bot's inbox and mark it complete
# Usage: 9p-read-inbox [--help]
set -e
if [ "$1" = "--help" ]; then
echo "Usage: 9p-read-inbox"
echo "Read the next message from the bot's inbox and mark it complete"
exit 0
fi
# Get agent ID from namespace file
KIRO_PID=$(ps -o ppid= -p $PPID | tr -d ' ')
AGENT_ID=$(cat $(namespace)/anvillm-session-id-$KIRO_PID 2>/dev/null)
if [ -z "$AGENT_ID" ]; then
echo "Error: Cannot determine agent ID" >&2
exit 1
fi
INBOX_PATH="agent/$AGENT_ID/inbox"
# List messages
MESSAGES=$(9p ls "$INBOX_PATH" 2>/dev/null) || {
echo "Error: Failed to list inbox" >&2
exit 1
}
if [ -z "$MESSAGES" ]; then
echo "No messages"
exit 0
fi
# Read first message
FIRST_MSG=$(echo "$MESSAGES" | head -1)
MSG_JSON=$(9p read "$INBOX_PATH/$FIRST_MSG") || {
echo "Error: Failed to read message" >&2
exit 1
}
# Display the message body
echo "$MSG_JSON" | jq -r '"[Message from \(.from)]", "Type: \(.type)", "Subject: \(.subject)", "", .body'
# Mark as complete by writing to ctl file (strip .json extension)
MSG_ID="${FIRST_MSG%.json}"
echo "complete $MSG_ID" | 9p write "agent/$AGENT_ID/ctl" || {
echo "Warning: Failed to mark message as complete" >&2
exit 1
}

View File

@ -9,8 +9,20 @@ description: Query and maintain a local knowledge base of code insights and arch
Leverage accumulated knowledge in `~/doc/agent-kb` to avoid redundant code research. Add new entries when discoveries warrant preservation.
## Answer Priority Hierarchy
When answering user questions, follow this strict priority order:
1. **KB First** - Search `~/doc/agent-kb/` for relevant entries
2. **Code Search** - If KB doesn't have the answer, search/inspect the codebase
3. **Web Search** - If code doesn't provide clarity, research online with proper citations
4. **Training Data** - Only as last resort to fill remaining gaps
**Always cite your source** - Tell the user which level you're answering from.
## When to Use
- **When user asks any question** - Check KB first before answering
- Encountering unfamiliar code patterns, services, or architecture
- When user asks questions outside training data that would require code research
- Before extensive codebase research—check KB first
@ -47,7 +59,12 @@ After significant code exploration, consider what was learned:
grep -l "pattern" ~/doc/agent-kb/*.md
```
3. Read relevant entries before researching code.
3. Search by content before answering questions:
```bash
grep -il "keyword" ~/doc/agent-kb/*.md
```
4. Read relevant entries before researching code.
## File Naming Convention
@ -79,7 +96,7 @@ Examples:
tags: [tag1, tag2]
identifier: YYYYMMDDThhmmss
verified: YYYY-MM-DD
source: code-inspection | documentation | verbal | inferred
source: code-inspection | documentation | online | verbal | inferred
---
# Title
@ -94,6 +111,7 @@ Examples:
- `documentation` - from internal docs (Confluence, etc.)
- `online` - from external online documentation
- `verbal` - expert opinion, discussion, tacit knowledge
- `inferred` - derived from training data (lowest priority)
If a source has a URL (online or documentation), add a `## Sources` section at the bottom of the document with the citation(s).
@ -112,4 +130,4 @@ When knowledge becomes stale or incomplete:
## What Belongs in the KB
- **Only verifiable knowledge from existing code** - no design alternatives, proposals, or "we could do X" speculation. Code is the source of truth.
- **Only verifiable knowledge from authoritative sources** - code inspection, official documentation, or properly cited research. No speculation, design alternatives, or "we could do X" proposals.