Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e45a1fba0 | ||
|
|
ad348e813f | ||
|
|
de5dcceeaf | ||
|
|
53fdcfec75 | ||
|
|
bad680cf24 | ||
|
|
a5e64ded83 | ||
|
|
77e368d354 | ||
|
|
db92a78d2b | ||
|
|
420deebdb4 |
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Long-running supervisor agent with periodic pipeline polling"
|
|
||||||
agent: coder-opus
|
|
||||||
---
|
|
||||||
|
|
||||||
# Story 280: Long-running supervisor agent with periodic pipeline polling
|
|
||||||
|
|
||||||
## User Story
|
|
||||||
|
|
||||||
As a project owner, I want a long-running supervisor agent (opus) that automatically monitors the pipeline, assigns agents, resolves stuck items, and handles routine operational tasks, so that I don't have to manually check status, kick agents, or babysit the pipeline in every conversation.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Server can start a persistent supervisor agent that stays alive across the session (not per-story)
|
|
||||||
- [ ] Server prods the supervisor periodically (default 30s, configurable in project.toml) with a pipeline status update
|
|
||||||
- [ ] Supervisor auto-assigns agents to unassigned items in current/qa/merge stages
|
|
||||||
- [ ] Supervisor detects stuck agents (no progress for configurable timeout) and restarts them
|
|
||||||
- [ ] Supervisor detects merge failures and sends stories back to current for rebase when appropriate
|
|
||||||
- [ ] Supervisor can be chatted with via Matrix (timmy relays to supervisor) or via the web UI
|
|
||||||
- [ ] Supervisor logs its decisions so the human can review what it did and why
|
|
||||||
- [ ] Polling interval is configurable in project.toml (e.g. supervisor_poll_interval_secs = 30)
|
|
||||||
- [ ] Supervisor logs persistent/recurring problems to `.story_kit/problems.md` with timestamp, description, and frequency — humans review this file periodically to create stories for systemic issues
|
|
||||||
|
|
||||||
## Out of Scope
|
|
||||||
|
|
||||||
- Supervisor accepting or merging stories to master (human job)
|
|
||||||
- Supervisor making architectural decisions
|
|
||||||
- Replacing the existing per-story agent spawning — supervisor coordinates on top of it
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: "Matrix bot ambient mode toggle via chat command"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 282: Matrix bot ambient mode toggle via chat command
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a user chatting with Timmy in a Matrix room, I want to toggle between "addressed mode" (bot only responds when mentioned by name) and "ambient mode" (bot responds to all messages) via a chat command, so that I don't have to @-mention the bot on every message when I'm the only one around.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Matrix bot defaults to addressed mode — only forwards messages containing the bot's name to Claude
|
||||||
|
- [ ] Chat command "timmy ambient on" switches to ambient mode — bot forwards all room messages to Claude
|
||||||
|
- [ ] Chat command "timmy ambient off" switches back to addressed mode
|
||||||
|
- [ ] Mode persists until explicitly toggled (not across bot restarts)
|
||||||
|
- [ ] Bot confirms the mode switch with a short response in chat
|
||||||
|
- [ ] When other users join or are active, user can flip back to addressed mode to avoid noise
|
||||||
|
- [ ] Ambient mode applies per-room (not globally across all rooms)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: "Detect and log when root .mcp.json port is modified"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 276: Detect and log when root .mcp.json port is modified
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a ..., I want ..., so that ...
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] TODO
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -3997,7 +3997,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "story-kit"
|
name = "story-kit"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "story-kit"
|
name = "story-kit"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ fn run_pty_session(
|
|||||||
user_message: &str,
|
user_message: &str,
|
||||||
cwd: &str,
|
cwd: &str,
|
||||||
resume_session_id: Option<&str>,
|
resume_session_id: Option<&str>,
|
||||||
system_prompt: Option<&str>,
|
_system_prompt: Option<&str>,
|
||||||
cancelled: Arc<AtomicBool>,
|
cancelled: Arc<AtomicBool>,
|
||||||
token_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
token_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||||
thinking_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
thinking_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||||
@@ -189,10 +189,8 @@ fn run_pty_session(
|
|||||||
// a tool requires user approval, instead of using PTY stdin/stdout.
|
// a tool requires user approval, instead of using PTY stdin/stdout.
|
||||||
cmd.arg("--permission-prompt-tool");
|
cmd.arg("--permission-prompt-tool");
|
||||||
cmd.arg("mcp__story-kit__prompt_permission");
|
cmd.arg("mcp__story-kit__prompt_permission");
|
||||||
if let Some(sys) = system_prompt {
|
// Note: --system is not a valid Claude Code CLI flag. System-level
|
||||||
cmd.arg("--system");
|
// instructions (like bot name) are prepended to the user prompt instead.
|
||||||
cmd.arg(sys);
|
|
||||||
}
|
|
||||||
cmd.cwd(cwd);
|
cmd.cwd(cwd);
|
||||||
// Keep TERM reasonable but disable color
|
// Keep TERM reasonable but disable color
|
||||||
cmd.env("NO_COLOR", "1");
|
cmd.env("NO_COLOR", "1");
|
||||||
|
|||||||
@@ -167,6 +167,18 @@ pub struct BotContext {
|
|||||||
pub bot_name: String,
|
pub bot_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Startup announcement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Format the startup greeting the bot sends to each room when it comes online.
|
||||||
|
///
|
||||||
|
/// Uses the bot's configured display name so the message reads naturally
|
||||||
|
/// (e.g. "Timmy is online.").
|
||||||
|
pub fn format_startup_announcement(bot_name: &str) -> String {
|
||||||
|
format!("{bot_name} is online.")
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Bot entry point
|
// Bot entry point
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -305,10 +317,11 @@ pub async fn run_bot(
|
|||||||
target_room_ids
|
target_room_ids
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clone values needed by the notification listener before they are moved
|
// Clone values needed by the notification listener and startup announcement
|
||||||
// into BotContext.
|
// before they are moved into BotContext.
|
||||||
let notif_room_ids = target_room_ids.clone();
|
let notif_room_ids = target_room_ids.clone();
|
||||||
let notif_project_root = project_root.clone();
|
let notif_project_root = project_root.clone();
|
||||||
|
let announce_room_ids = target_room_ids.clone();
|
||||||
|
|
||||||
let persisted = load_history(&project_root);
|
let persisted = load_history(&project_root);
|
||||||
slog!(
|
slog!(
|
||||||
@@ -320,6 +333,7 @@ pub async fn run_bot(
|
|||||||
.display_name
|
.display_name
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "Assistant".to_string());
|
.unwrap_or_else(|| "Assistant".to_string());
|
||||||
|
let announce_bot_name = bot_name.clone();
|
||||||
|
|
||||||
let ctx = BotContext {
|
let ctx = BotContext {
|
||||||
bot_user_id,
|
bot_user_id,
|
||||||
@@ -351,6 +365,23 @@ pub async fn run_bot(
|
|||||||
notif_project_root,
|
notif_project_root,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Send a startup announcement to each configured room so users know the
|
||||||
|
// bot is online. This runs once per process start — the sync loop handles
|
||||||
|
// reconnects internally so this code is never reached again on a network
|
||||||
|
// blip or sync resumption.
|
||||||
|
let announce_msg = format_startup_announcement(&announce_bot_name);
|
||||||
|
slog!("[matrix-bot] Sending startup announcement: {announce_msg}");
|
||||||
|
for room_id in &announce_room_ids {
|
||||||
|
if let Some(room) = client.get_room(room_id) {
|
||||||
|
let content = RoomMessageEventContent::text_plain(announce_msg.clone());
|
||||||
|
if let Err(e) = room.send(content).await {
|
||||||
|
slog!("[matrix-bot] Failed to send startup announcement to {room_id}: {e}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog!("[matrix-bot] Room {room_id} not found in client state, skipping announcement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
slog!("[matrix-bot] Starting Matrix sync loop");
|
slog!("[matrix-bot] Starting Matrix sync loop");
|
||||||
|
|
||||||
// This blocks until the connection is terminated or an error occurs.
|
// This blocks until the connection is terminated or an error occurs.
|
||||||
@@ -748,11 +779,10 @@ async fn handle_message(
|
|||||||
|
|
||||||
// The prompt is just the current message with sender attribution.
|
// The prompt is just the current message with sender attribution.
|
||||||
// Prior conversation context is carried by the Claude Code session.
|
// Prior conversation context is carried by the Claude Code session.
|
||||||
let prompt = format_user_prompt(&sender, &user_message);
|
|
||||||
|
|
||||||
let bot_name = &ctx.bot_name;
|
let bot_name = &ctx.bot_name;
|
||||||
let system_prompt = format!(
|
let prompt = format!(
|
||||||
"Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude."
|
"[Your name is {bot_name}. Refer to yourself as {bot_name}, not Claude.]\n\n{}",
|
||||||
|
format_user_prompt(&sender, &user_message)
|
||||||
);
|
);
|
||||||
|
|
||||||
let provider = ClaudeCodeProvider::new();
|
let provider = ClaudeCodeProvider::new();
|
||||||
@@ -792,7 +822,7 @@ async fn handle_message(
|
|||||||
&prompt,
|
&prompt,
|
||||||
&project_root_str,
|
&project_root_str,
|
||||||
resume_session_id.as_deref(),
|
resume_session_id.as_deref(),
|
||||||
Some(&system_prompt),
|
None,
|
||||||
&mut cancel_rx,
|
&mut cancel_rx,
|
||||||
move |token| {
|
move |token| {
|
||||||
let mut buf = buffer_for_callback.lock().unwrap();
|
let mut buf = buffer_for_callback.lock().unwrap();
|
||||||
@@ -1664,6 +1694,19 @@ mod tests {
|
|||||||
assert!(is_permission_approval("\tyes\n"));
|
assert!(is_permission_approval("\tyes\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- format_startup_announcement ----------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startup_announcement_uses_bot_name() {
|
||||||
|
assert_eq!(format_startup_announcement("Timmy"), "Timmy is online.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startup_announcement_uses_configured_display_name_not_hardcoded() {
|
||||||
|
assert_eq!(format_startup_announcement("HAL"), "HAL is online.");
|
||||||
|
assert_eq!(format_startup_announcement("Assistant"), "Assistant is online.");
|
||||||
|
}
|
||||||
|
|
||||||
// -- bot_name / system prompt -------------------------------------------
|
// -- bot_name / system prompt -------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user