Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/agent_prompts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::path::PathBuf;

const AGENTS_DIR: &str = "agents";

fn get_agents_dir() -> PathBuf {
let hcom_dir = crate::config::Config::get().hcom_dir;
hcom_dir.join(AGENTS_DIR)
}

pub fn get_agent_prompt_path(instance_name: &str) -> PathBuf {
get_agents_dir().join(format!("{}.md", instance_name))
}

pub fn load_agent_prompt(instance_name: &str) -> Option<String> {
let path = get_agent_prompt_path(instance_name);
if path.exists() {
std::fs::read_to_string(&path).ok().filter(|s| !s.trim().is_empty())
} else {
None
}
}

pub fn ensure_agents_dir() -> std::io::Result<()> {
std::fs::create_dir_all(get_agents_dir())
}

#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;

#[test]
fn test_load_nonexistent_returns_none() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("nonexistent.md");
assert!(!path.exists());
}

#[test]
fn test_ensure_agents_dir_creates() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().join("agents");
assert!(!dir.exists());
std::fs::create_dir_all(&dir).unwrap();
assert!(dir.exists());
}
}
19 changes: 18 additions & 1 deletion src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ You MUST use `hcom <cmd+flags> --name {instance_name}` for all hcom commands:
- View events: events [--last N] [--all] [--sql EXPR] [filters]
Filters (same flag=OR, different=AND): --agent NAME | --type message|status|life | --status listening|active|blocked | --cmd PATTERN (contains, ^prefix, =exact) | --file PATH (*.py for glob, file.py for contains)
Event-based notifications, watch agents, subscribe, react: events sub [filters] | --help
- Per-agent system prompts: create `~/.hcom/agents/<name>.md` with the role prompt for any agent (self or spawned)
Then `hcom list -v` confirms if file loaded. Re-injected on every session compaction.
- Handoff context: bundle prepare
- Spawn agents: [num] <claude|gemini|codex|opencode> [--tag labelOrGroup] [--terminal tmux|kitty|wezterm|etc]
- Spawn agents: [num] <claude|gemini|codex|opencode> [--tag labelOrGroup] [--agent-name customName] [--terminal tmux|kitty|wezterm|etc]
Example: `hcom 1 claude --tag cool` -> automatic <hcom> msg when ready -> send it task via hcom send
Example: `hcom 1 claude --agent-name mybot` -> named agent, load ~/.hcom/agents/mybot.md as system prompt
Resume: hcom r <name> [args] | Fork: hcom f <name> [args] | Kill: hcom kill <name(s)>
background, set prompt, system, forward args: <claude|gemini|codex|opencode> --help
- Run workflows: run <script> [args] [--help]
Expand Down Expand Up @@ -182,6 +185,12 @@ Commands:
{hcom_cmd} events --name {subagent_name}
{hcom_cmd} <cmd> --help --name {subagent_name}

Agent prompts:
- When spawning sub-agents use `--agent-name <name>` to name them
- Write role prompt: `echo "role instructions" > ~/.hcom/agents/<name>.md`
- The sub-agent reads this file on startup and every session compaction
- Create the file before or right after spawning

Rules:
- Task via hcom → ack, work, report
- Authority: @{SENDER} > others
Expand Down Expand Up @@ -438,6 +447,14 @@ pub fn get_bootstrap(
result.push_str(&format!("\n\n## NOTES\n\n{}\n", ctx.notes));
}

// Agent prompt from ~/.hcom/agents/<name>.md
if let Some(agent_prompt) = crate::agent_prompts::load_agent_prompt(instance_name) {
result.push_str(&format!(
"\n\n## AGENT PROMPT\n\n{}\n",
agent_prompt
));
}

// Rewrite hcom references if using alternate command
if ctx.hcom_cmd != "hcom" {
let sentinel = "__HCOM_CMD__";
Expand Down
8 changes: 7 additions & 1 deletion src/commands/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result<i32> {
"args": tool_args,
"tag": tag,
"launcher": launcher_name,
"name": hcom_flags.name,
"background": headless,
"pty": pty_requested,
"terminal": terminal,
Expand Down Expand Up @@ -198,7 +199,7 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result<i32> {
launcher: Some(launcher_name.clone()),
run_here: hcom_flags.run_here,
batch_id: hcom_flags.batch_id,
name: None, // --name is caller identity, not instance name
name: hcom_flags.name.clone(),
skip_validation: false,
terminal,
append_reply_handoff: true,
Expand Down Expand Up @@ -463,6 +464,7 @@ pub(crate) struct HcomLaunchFlags {
pub run_here: Option<bool>,
pub batch_id: Option<String>,
pub dir: Option<String>,
pub name: Option<String>,
}

/// Parse launch argv: extract count, tool name, hcom flags, and tool-specific args.
Expand Down Expand Up @@ -661,6 +663,10 @@ pub(crate) fn extract_launch_flags(args: &[String]) -> (HcomLaunchFlags, Vec<Str
flags.run_here = Some(false);
i += 1;
}
"--agent-name" if i + 1 < args.len() => {
flags.name = Some(args[i + 1].clone());
i += 2;
}
"--name" if i + 1 < args.len() => {
i += 2;
}
Expand Down
3 changes: 3 additions & 0 deletions src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,9 @@ pub fn launch(db: &HcomDb, mut params: LaunchParams) -> Result<LaunchResult> {
);
}

// Ensure agents dir exists for per-agent prompts
crate::agent_prompts::ensure_agents_dir().ok();

// Ensure hooks are installed (strict: refuse to launch without hooks)
ensure_hooks_installed(&normalized)?;

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! each tool's own UI. Agents use `hcom` CLI commands (learnt through bootstrap.rs)
//! as a side-channel for messaging and coordination with other agents.

mod agent_prompts;
mod bootstrap;
mod cli_context;
pub mod commands;
Expand Down
12 changes: 11 additions & 1 deletion src/opencode_plugin/hcom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
import type { Event } from "@opencode-ai/sdk"
import { appendFileSync } from "fs"
import { appendFileSync, readFileSync, existsSync } from "fs"
import { homedir } from "os"

const HCOM_DIR = process.env.HCOM_DIR || `${homedir()}/.hcom`
Expand Down Expand Up @@ -524,6 +524,16 @@ export const HcomPlugin: Plugin = async ({ client, $ }) => {
`You are connected to hcom as "${instanceName}". ` +
`Use --name ${instanceName} for all hcom commands.`
)

const agentPromptPath = `${HCOM_DIR}/agents/${instanceName}.md`
if (existsSync(agentPromptPath)) {
const agentPrompt = readFileSync(agentPromptPath, "utf-8").trim()
if (agentPrompt) {
output.context.push(`## AGENT PROMPT\n\n${agentPrompt}`)
log("INFO", "plugin.agent_prompt_injected", instanceName, { path: agentPromptPath })
}
}

log("INFO", "plugin.compaction_reset", instanceName)
} catch (e) {
log("ERROR", "plugin.compaction_error", instanceName, { error: String(e) })
Expand Down