Compare commits

..

30 Commits

Author SHA1 Message Date
44ff1411a3
docs: record env cleanup direction 2026-05-31 18:02:59 +09:00
d06bb14e64
ticket: plan env var cleanup 2026-05-31 18:02:59 +09:00
f186994c83
docs: clarify env path categories 2026-05-31 17:55:21 +09:00
d1bc1d41b9
close: document env var policy 2026-05-31 17:49:34 +09:00
9bc7920ab5
docs: document environment variable policy 2026-05-31 17:29:29 +09:00
69cc4d38f0
docs: create env policy ticket 2026-05-31 17:29:23 +09:00
9b4b24c7ae
ticket: plan tui module layout cleanup 2026-05-31 16:43:49 +09:00
d56d3951d3
close: rename pod-command crate 2026-05-31 15:49:50 +09:00
9e886bc73e
cli: rename pod command crate to insomnia 2026-05-31 15:49:14 +09:00
3a58ab7bb3
cli: create insomnia crate rename ticket 2026-05-31 15:49:03 +09:00
b977bacb29
close: remove insomnia-pod binary 2026-05-31 15:10:45 +09:00
8560bd88a7
merge: remove insomnia-pod binary 2026-05-31 15:08:40 +09:00
7181afab40
review: approve insomnia-pod removal 2026-05-31 15:08:38 +09:00
0d7d3c7bf1
cli: remove insomnia-pod binary output 2026-05-31 14:59:22 +09:00
fd32805c30
cli: create insomnia-pod removal ticket 2026-05-31 14:48:34 +09:00
bd959ce25c
close: spawn through insomnia pod 2026-05-31 14:27:15 +09:00
cfbafbd67f
merge: spawn through insomnia pod 2026-05-31 14:25:41 +09:00
582a2cd31f
review: spawn through insomnia pod 2026-05-31 14:25:41 +09:00
4f622b8e32
cli: spawn pods through insomnia pod 2026-05-31 14:20:00 +09:00
de3f64f41d
cli: create insomnia pod spawn ticket 2026-05-31 13:51:24 +09:00
c9b7e7f462
close: insomnia pod runtime entrypoint 2026-05-31 13:50:25 +09:00
898ff98761
merge: insomnia pod runtime entrypoint 2026-05-31 13:47:04 +09:00
07827f56ce
review: insomnia pod runtime entrypoint 2026-05-31 13:47:04 +09:00
544257bfdd
cli: add insomnia pod runtime entrypoint 2026-05-31 13:42:29 +09:00
0dd48f6152
cli: plan insomnia pod runtime entrypoint 2026-05-31 13:33:37 +09:00
74ac3b73cd
chore: TODO.mdの削除 2026-05-31 12:30:23 +09:00
ae6eb52f35
close: pod tool surface cleanup 2026-05-31 12:00:25 +09:00
97484091b6
merge: pod tool surface cleanup 2026-05-31 11:59:27 +09:00
bca464cc73
review: pod tool surface cleanup 2026-05-31 11:59:27 +09:00
5472ceca48
pod: simplify pod tool surface 2026-05-31 11:50:41 +09:00
78 changed files with 1770 additions and 620 deletions

View File

@ -2,7 +2,7 @@
description: worktree と sibling の coder / reviewer Pod を使い、下位 orchestrator が複数 ticket の実装・外部レビュー・修正・完了準備を管理する orchestration フロー
model_invokation: true
user_invocable: true
requires: ["ticket-preflight-workflow", "worktree-workflow"]
requires: []
---
# Multi-agent Worktree Workflow

View File

@ -1,75 +0,0 @@
全体設計が概ね固まり、随所の細かい仕様を詰めながら実装を進めている。
## このシステムに置ける設計要旨
- プロンプトはすべて resources/promptsに集約している。管理効率の工場と同時に、ユーザーがオーバーライドする形式でもある。
- E2E(実プロセスをスポーンさせてのテスト)は未設計。
- 変更量を最小にするために設計を歪めたり、設計問題に対して不必要な後方互換性を作らない。長期的なメンテナンスと型安全性を追求すること。
### LLM コンテキストの加工原則
LLM に投げる context への割り込みは、大きく2種類に分かれる。**前者は許されるが、後者は禁止**。
Podの状態から純粋に再現可能で、且つ揮発性の無い操作であることが望ましい。pruning、tool result の content 切り詰め、prompt cache anchor の付与等)。
原則として、コンテキストは積み重ねるものであり、一時的にメッセージを差し込むことや、過去のメッセージを改ざんすることはKVキャッシュのヒット率を下げる。
**禁止**: ターンを跨ぐことができない情報に基づいて、history に記録せずに context だけにコンテンツを差し込むこと。これをやると LLM はそれに反応して生成を行う一方、次以降のターンでhistoryに残らないため、「自分がなぜその発言/tool call をしたか」の根拠が消えるうえ、prompt cache のヒット率も低下させることになる。
新しい input を context に乗せたいなら、必ず先に `worker.history` に append して commit すること。`history.json` への永続化はそこから自動的についてくる。Notify / PodEvent / `<system-reminder>` 系はこの原則で扱う(→ `tickets/notify-history-persist.md`)。
また、キャッシュを破壊するタイミングは正確にコントロールされる必要があり、キャッシュ破壊とトークン消費のトレードオフに基づいて慎重に設計されるべきである。
---
## 実際のセッションを読んでデバッグする
`~/.insomnia/sessions`にすべてのセッションがある。jsonlなので、いい感じにBashで読むこと。
---
## Git操作
workflowで明示されない限り、読み取り以外の操作は控えること。
基本はworktree上の一時的なブランチでコミットを重ね、メインブランチに取り込む運用をしている。
コミットメッセージは適当に`<prefix>: *簡潔な1行*`で書いている。
外部の参考プロジェクトは必要に応じてローカルの外部 checkout からReadすること。
---
## Ticketの運用について
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
### TODO.md
- 1チケット = 1行。未完了のみ記載し、完了したら行ごと削除する履歴はgitで追える
- ネストは同一領域のグルーピング(表示用)にのみ使う。実装上の依存関係はネストで表現しない
- 完了した子は削除し、親は未完了の子がある限り残す。最後の子が完了したら親ごと削除
- Ticketを追加する際は、合わせてTODOも書くこと
### Ticket の粒度
- 1チケット = 完了時点で、実装が仕様又は機能として説明できる粒度。
- 作成時、背景や要件を前提として書き、実装の方針やコードの詳細は不必要に増やさない。
- チケット内のステップPhase 1, 2, ...は実装順序であり、TODO等、外に出さない
- ビルドが通り、その機能に限り,まだ動作できないと明示出来ている場合を除いて全体を通して動作させられる状態である必要がある。
### Ticket のライフサイクル
gitがタイムラインの単一の情報源。ファイル操作とcommitで状態遷移を表現する。
a. 作成: `tickets/foo.md` を作成してcommit
b. 詳細化や前提の変化: `tickets/foo.md` を更新してcommit
c. レビュー: `tickets/foo.md` にレビュー状態を追記 + `tickets/foo.review.md` を作成してcommit
d. 完了: `tickets/foo.md``tickets/foo.review.md` を両方削除してcommit
worktreeと併用して作業を進める場合、必ずブランチを切る前に対象のチケットをコミットしてから切ること。
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
`.review.md` にはレビューの指摘事項と判断結果を記載する。
レビューはdiffの確認だけでなく、チケットはどのような前提・要件であり、それが達成されたかの確認まで含めて行う。
常に、提出された実装で良いのか、コードベースを歪めていないか、不必要な実装ではないかを確認すること。
---
insomniaでinsomniaを開発している際、AI自身のフィードバックを元に改善を回すために `docs/report/`ディレクトリに感じた障壁や改善案等を書き残す形にした。 明確に力不足な点/ツールの問題があった場合や、ユーザーからの指示があった際に作ること。

7
Cargo.lock generated
View File

@ -332,6 +332,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
name = "client"
version = "0.1.0"
dependencies = [
"insomnia",
"manifest",
"protocol",
"serde_json",
@ -1479,6 +1480,10 @@ dependencies = [
"rustversion",
]
[[package]]
name = "insomnia"
version = "0.1.0"
[[package]]
name = "instability"
version = "0.3.12"
@ -2333,6 +2338,7 @@ dependencies = [
"fs4",
"futures",
"include_dir",
"insomnia",
"libc",
"llm-worker",
"manifest",
@ -3912,6 +3918,7 @@ dependencies = [
"llm-worker",
"manifest",
"memory",
"pod",
"pod-registry",
"pod-store",
"protocol",

View File

@ -8,6 +8,7 @@ members = [
"crates/session-store",
"crates/manifest",
"crates/pod",
"crates/insomnia",
"crates/pod-store",
"crates/protocol",
"crates/provider",
@ -32,6 +33,8 @@ llm-worker-macros = { path = "crates/llm-worker-macros", version = "0.2" }
manifest = { path = "crates/manifest" }
lint-common = { path = "crates/lint-common" }
memory = { path = "crates/memory" }
pod = { path = "crates/pod" }
insomnia = { path = "crates/insomnia" }
pod-registry = { path = "crates/pod-registry" }
pod-store = { path = "crates/pod-store" }
protocol = { path = "crates/protocol" }

View File

@ -1,5 +0,0 @@
# TODO legacy notice
Active repository work items have been migrated to `work-items/`.
Use `./tickets.sh list --status all` for the generated/current view and `./tickets.sh doctor` to validate the migration state.

View File

@ -7,6 +7,7 @@ license.workspace = true
[dependencies]
protocol = { workspace = true }
manifest = { workspace = true }
insomnia = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync", "time", "process", "fs"] }
uuid = { workspace = true }

View File

@ -1,4 +1,4 @@
//! `insomnia-pod` バイナリをサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
//! Pod runtime command をサブプロセスとして立ち上げ、`INSOMNIA-READY` を待つ
//! ハンドシェイク。
//!
//! - 親プロセス (TUI / GUI / E2E) は profile/default/typed restore flags を
@ -15,6 +15,7 @@ use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use insomnia::PodRuntimeCommand;
use tokio::process::Command;
use uuid::Uuid;
@ -99,7 +100,7 @@ pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnR
where
F: FnMut(&str),
{
let pod_bin = resolve_pod_command();
let runtime_command = PodRuntimeCommand::resolve().map_err(SpawnError::Io)?;
let pod_runtime_dir = manifest::paths::pod_runtime_dir(&config.pod_name)
.ok_or(SpawnError::RuntimeDirUnavailable)?;
@ -107,8 +108,9 @@ where
let stderr_path = pod_runtime_dir.join("stderr.log");
let stderr_file = std::fs::File::create(&stderr_path).map_err(SpawnError::Io)?;
let mut command = Command::new(&pod_bin);
let mut command = Command::new(runtime_command.program());
command
.args(runtime_command.prefix_args())
.current_dir(&config.cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
@ -268,23 +270,6 @@ async fn drain_stderr_into_tail(stderr_path: &Path, tail: &mut StderrTail, offse
*offset = content.len();
}
/// Resolves the binary used to launch a child Pod. Must point at a
/// `insomnia-pod`-compatible executable — the parent reads the child's stderr
/// directly looking for `INSOMNIA-READY`, so any wrapper that emits
/// extra lines on stderr will pollute that handshake.
///
/// `INSOMNIA_POD_COMMAND` overrides the lookup (used by tests to inject
/// a mock binary). Otherwise we defer to `PATH` — missing binary
/// surfaces as the spawn `io::Error`.
fn resolve_pod_command() -> PathBuf {
if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND")
&& !cmd.is_empty()
{
return PathBuf::from(cmd);
}
PathBuf::from("insomnia-pod")
}
struct StderrTail {
lines: std::collections::VecDeque<String>,
}

View File

@ -0,0 +1,7 @@
[package]
name = "insomnia"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]

156
crates/insomnia/src/lib.rs Normal file
View File

@ -0,0 +1,156 @@
use std::ffi::OsString;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
pub const POD_COMMAND_OVERRIDE_ENV: &str = "INSOMNIA_POD_COMMAND";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PodRuntimeCommand {
pub program: PathBuf,
pub prefix_args: Vec<OsString>,
}
impl PodRuntimeCommand {
pub fn new(program: impl Into<PathBuf>, prefix_args: Vec<OsString>) -> Self {
Self {
program: program.into(),
prefix_args,
}
}
pub fn executable_only(program: impl Into<PathBuf>) -> Self {
Self::new(program, Vec::new())
}
pub fn for_current_exe() -> io::Result<Self> {
Ok(Self::for_executable(std::env::current_exe()?))
}
pub fn for_executable(program: impl Into<PathBuf>) -> Self {
Self::new(program, vec![OsString::from("pod")])
}
/// Resolve the Pod runtime command used for subprocess launches.
///
/// `INSOMNIA_POD_COMMAND` is intentionally executable-only: its value is
/// used as the program path without shell parsing and without the unified
/// `pod` prefix arg. That keeps development/test overrides safe while the
/// default path is always `current_exe() + ["pod"]`.
pub fn resolve() -> io::Result<Self> {
if let Some(command) = Self::from_override_env() {
return Ok(command);
}
Self::for_current_exe()
}
pub fn from_override_env() -> Option<Self> {
let raw = std::env::var_os(POD_COMMAND_OVERRIDE_ENV)?;
if raw.is_empty() {
return None;
}
Some(Self::executable_only(raw))
}
pub fn program(&self) -> &Path {
&self.program
}
pub fn prefix_args(&self) -> &[OsString] {
&self.prefix_args
}
pub fn argv_with<I, S>(&self, args: I) -> Vec<OsString>
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
let mut argv = self.prefix_args.clone();
argv.extend(args.into_iter().map(Into::into));
argv
}
}
impl fmt::Display for PodRuntimeCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.program.display())?;
for arg in &self.prefix_args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvRestore(Option<OsString>);
impl EnvRestore {
fn capture() -> Self {
Self(std::env::var_os(POD_COMMAND_OVERRIDE_ENV))
}
}
impl Drop for EnvRestore {
fn drop(&mut self) {
unsafe {
match &self.0 {
Some(value) => std::env::set_var(POD_COMMAND_OVERRIDE_ENV, value),
None => std::env::remove_var(POD_COMMAND_OVERRIDE_ENV),
}
}
}
}
#[test]
fn insomnia_binary_defaults_to_pod_prefix() {
let command = PodRuntimeCommand::for_executable("/opt/insomnia/bin/insomnia");
assert_eq!(command.program(), Path::new("/opt/insomnia/bin/insomnia"));
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
assert_eq!(
command.argv_with(["--pod", "agent"]),
vec!["pod", "--pod", "agent"]
.into_iter()
.map(OsString::from)
.collect::<Vec<_>>()
);
}
#[test]
fn any_runtime_executable_gets_pod_prefix() {
let command = PodRuntimeCommand::for_executable("/opt/insomnia/bin/custom-runtime");
assert_eq!(
command.program(),
Path::new("/opt/insomnia/bin/custom-runtime")
);
assert_eq!(command.prefix_args(), [OsString::from("pod")]);
assert_eq!(
command.argv_with(["--pod", "agent"]),
vec!["pod", "--pod", "agent"]
.into_iter()
.map(OsString::from)
.collect::<Vec<_>>()
);
}
#[test]
fn env_override_is_executable_only_and_not_shell_parsed() {
let _guard = ENV_LOCK.lock().unwrap();
let _restore = EnvRestore::capture();
unsafe {
std::env::set_var(POD_COMMAND_OVERRIDE_ENV, "/tmp/mock pod --flag");
}
let command = PodRuntimeCommand::resolve().unwrap();
assert_eq!(command.program(), Path::new("/tmp/mock pod --flag"));
assert!(command.prefix_args().is_empty());
}
}

View File

@ -3,10 +3,7 @@ name = "pod"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[[bin]]
name = "insomnia-pod"
path = "src/main.rs"
autobins = false
[dependencies]
async-trait = { workspace = true }
@ -17,6 +14,7 @@ pod-store = { workspace = true }
manifest = { workspace = true }
protocol = { workspace = true }
provider = { workspace = true }
insomnia = { workspace = true }
pod-registry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@ -8,9 +8,7 @@ use pod_store::PodMetadataStore;
use session_store::Store;
use tokio::sync::{broadcast, mpsc, oneshot};
use crate::discovery::{
PodDiscovery, attach_or_restore_pod_tool, inspect_pod_tool, list_visible_pods_tool,
};
use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool};
use crate::ipc::alerter::Alerter;
use crate::ipc::notify_buffer::NotifyBuffer;
use crate::ipc::server::SocketServer;
@ -18,9 +16,7 @@ use crate::pod::{Pod, PodError, PodRunResult, SystemItemCommitter};
use crate::runtime::dir::RuntimeDir;
use crate::segment_log_sink::SegmentLogSink;
use crate::shared_state::PodSharedState;
use crate::spawn::comm_tools::{
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
};
use crate::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
use crate::spawn::registry::SpawnedPodRegistry;
use crate::spawn::tool::spawn_pod_tool;
use protocol::{
@ -563,12 +559,9 @@ where
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
worker.register_tool(stop_pod_tool(spawned_registry.clone()));
worker.register_tool(list_pods_tool(spawned_registry.clone()));
let discovery = PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry);
worker.register_tool(list_visible_pods_tool(discovery.clone()));
worker.register_tool(inspect_pod_tool(discovery.clone()));
worker.register_tool(attach_or_restore_pod_tool(discovery));
worker.register_tool(list_pods_tool(discovery.clone()));
worker.register_tool(restore_pod_tool(discovery));
pod.attach_tracker(tracker);
fs_for_view
}
@ -835,10 +828,10 @@ async fn controller_loop<C, St>(
break;
}
Method::ListVisiblePods => match discovery.list_visible().await {
Method::ListPods => match discovery.list_visible().await {
Ok(pods) => match serde_json::to_value(pods) {
Ok(pods) => {
let _ = event_tx.send(Event::VisiblePods { pods });
let _ = event_tx.send(Event::PodsListed { pods });
}
Err(error) => {
let _ = event_tx.send(Event::Error {
@ -855,35 +848,15 @@ async fn controller_loop<C, St>(
}
},
Method::InspectPod { name } => match discovery.inspect(&name).await {
Ok(pod) => match serde_json::to_value(pod) {
Ok(pod) => {
let _ = event_tx.send(Event::PodInspection { pod });
}
Err(error) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::Internal,
message: format!("serialize pod inspection: {error}"),
});
}
},
Err(error) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::InvalidRequest,
message: error.to_string(),
});
}
},
Method::AttachOrRestorePod { name } => match discovery.attach_or_restore(&name).await {
Method::RestorePod { name } => match discovery.restore(&name).await {
Ok(result) => match serde_json::to_value(result) {
Ok(result) => {
let _ = event_tx.send(Event::PodAttachRestore { result });
let _ = event_tx.send(Event::PodRestored { result });
}
Err(error) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::Internal,
message: format!("serialize pod attach/restore result: {error}"),
message: format!("serialize pod restore result: {error}"),
});
}
},
@ -1096,11 +1069,7 @@ where
notify_buffer.push_notify(message);
}
Some(Method::ListCompletions { .. }) => {}
Some(
Method::ListVisiblePods
| Method::InspectPod { .. }
| Method::AttachOrRestorePod { .. },
) => {
Some(Method::ListPods | Method::RestorePod { .. }) => {
let _ = event_tx.send(Event::Error {
code: ErrorCode::AlreadyRunning,
message: "Pod discovery requests are only handled while the Pod is idle or paused"

View File

@ -1,4 +1,4 @@
//! Pod-state-backed discovery and restore/attach tools.
//! Pod-state-backed discovery and restore tools.
//!
//! This surface deliberately does not enumerate every Pod on the host. The
//! listing path starts from the caller's visibility set (the caller itself and
@ -14,7 +14,9 @@ use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use insomnia::PodRuntimeCommand;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{Permission, ScopeRule};
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
use protocol::stream::JsonLineReader;
use protocol::{Event, PodStatus};
@ -24,6 +26,7 @@ use session_store::{SegmentId, SessionId};
use tokio::net::UnixStream;
use tokio::process::Command;
use crate::runtime::dir::SpawnedPodRecord;
use crate::runtime::pod_registry;
use crate::spawn::registry::SpawnedPodRegistry;
@ -97,26 +100,23 @@ where
}
}
pub async fn attach_or_restore(
&self,
pod_name: &str,
) -> Result<AttachRestoreResult, PodDiscoveryError> {
match self.plan_attach_or_restore(pod_name).await? {
AttachRestorePlan::Attach {
pub async fn restore(&self, pod_name: &str) -> Result<RestoreResult, PodDiscoveryError> {
match self.plan_restore(pod_name).await? {
RestorePlan::AlreadyLive {
pod_name,
socket_path,
status,
} => Ok(AttachRestoreResult::Attached {
} => Ok(RestoreResult::AlreadyLive {
pod_name,
socket_path,
status,
}),
AttachRestorePlan::Restore {
RestorePlan::Restore {
pod_name,
socket_path,
} => {
self.spawn_restore_process(&pod_name, &socket_path).await?;
Ok(AttachRestoreResult::Restored {
Ok(RestoreResult::Restored {
pod_name,
socket_path,
})
@ -124,13 +124,10 @@ where
}
}
pub async fn plan_attach_or_restore(
&self,
pod_name: &str,
) -> Result<AttachRestorePlan, PodDiscoveryError> {
pub async fn plan_restore(&self, pod_name: &str) -> Result<RestorePlan, PodDiscoveryError> {
let detail = self.inspect(pod_name).await?;
if detail.live.reachable {
return Ok(AttachRestorePlan::Attach {
return Ok(RestorePlan::AlreadyLive {
pod_name: pod_name.to_string(),
socket_path: detail.live.socket_path,
status: detail.live.status,
@ -153,7 +150,7 @@ where
if let Some(lock) = lookup_segment_lock(segment_id)? {
let lock_live = probe_socket(&lock.socket).await;
return if lock_live.reachable {
Ok(AttachRestorePlan::Attach {
Ok(RestorePlan::AlreadyLive {
pod_name: lock.pod_name,
socket_path: lock.socket,
status: lock_live.status,
@ -169,7 +166,7 @@ where
};
}
Ok(AttachRestorePlan::Restore {
Ok(RestorePlan::Restore {
pod_name: pod_name.to_string(),
socket_path: self.default_socket_path(pod_name),
})
@ -178,6 +175,7 @@ where
async fn visibility(&self) -> Result<VisibilitySet, PodDiscoveryError> {
let mut visible = BTreeMap::new();
let mut child_sockets = BTreeMap::new();
let mut comm_registry = BTreeMap::new();
visible.insert(self.self_pod_name.clone(), VisibilityReason::SelfPod);
// Durable parent -> child state is the primary visibility source.
@ -186,7 +184,8 @@ where
visible
.entry(child.pod_name.clone())
.or_insert(VisibilityReason::SpawnedChild);
child_sockets.insert(child.pod_name, child.socket_path);
child_sockets.insert(child.pod_name.clone(), child.socket_path.clone());
comm_registry.insert(child.pod_name.clone(), comm_info_from_spawned_child(&child));
}
}
@ -197,12 +196,17 @@ where
visible
.entry(record.pod_name.clone())
.or_insert(VisibilityReason::SpawnedChild);
child_sockets.insert(record.pod_name, record.socket_path);
child_sockets.insert(record.pod_name.clone(), record.socket_path.clone());
comm_registry.insert(
record.pod_name.clone(),
CommRegistryInfo::from_record(&record),
);
}
Ok(VisibilitySet {
visible,
child_sockets,
comm_registry,
})
}
@ -222,6 +226,7 @@ where
active: detail.active,
live: detail.live,
restore: detail.restore,
comm_registry: detail.comm_registry,
spawned_children: detail.spawned_children,
error: None,
}
@ -233,6 +238,7 @@ where
active: None,
live: self.live_for_name(pod_name, None).await,
restore: RestoreInfo::not_possible("pod state missing"),
comm_registry: visibility.comm_info_for(pod_name),
spawned_children: SpawnedChildrenSummary::default(),
error: None,
},
@ -243,6 +249,7 @@ where
active: None,
live: self.live_for_name(pod_name, None).await,
restore: RestoreInfo::not_possible("pod state is unreadable"),
comm_registry: visibility.comm_info_for(pod_name),
spawned_children: SpawnedChildrenSummary::default(),
error: Some(error.to_string()),
},
@ -268,6 +275,7 @@ where
active: metadata.active.map(ActivePointer::from),
live,
restore,
comm_registry: visibility.comm_info_for(&metadata.pod_name),
spawned_children,
}
}
@ -321,8 +329,11 @@ where
pod_name: &str,
socket_path: &Path,
) -> Result<(), PodDiscoveryError> {
let mut command = Command::new(resolve_pod_command());
let runtime_command =
PodRuntimeCommand::resolve().map_err(PodDiscoveryError::RestoreSpawn)?;
let mut command = Command::new(runtime_command.program());
command
.args(runtime_command.prefix_args())
.arg("--pod")
.arg(pod_name)
.arg("--require-pod-state")
@ -424,6 +435,33 @@ pub struct SpawnedChildrenSummary {
pub unreachable: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommRegistryInfo {
pub registered: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub socket_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scope_delegated: Vec<ScopeRule>,
}
impl CommRegistryInfo {
fn missing() -> Self {
Self {
registered: false,
socket_path: None,
scope_delegated: Vec::new(),
}
}
fn from_record(record: &SpawnedPodRecord) -> Self {
Self {
registered: true,
socket_path: Some(record.socket_path.clone()),
scope_delegated: record.scope_delegated.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VisiblePodItem {
pub pod_name: String,
@ -433,6 +471,7 @@ pub struct VisiblePodItem {
pub active: Option<ActivePointer>,
pub live: LiveInfo,
pub restore: RestoreInfo,
pub comm_registry: CommRegistryInfo,
pub spawned_children: SpawnedChildrenSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
@ -446,13 +485,14 @@ pub struct PodDetail {
pub active: Option<ActivePointer>,
pub live: LiveInfo,
pub restore: RestoreInfo,
pub comm_registry: CommRegistryInfo,
pub spawned_children: SpawnedChildrenSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum AttachRestorePlan {
Attach {
pub enum RestorePlan {
AlreadyLive {
pod_name: String,
socket_path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -466,8 +506,8 @@ pub enum AttachRestorePlan {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum AttachRestoreResult {
Attached {
pub enum RestoreResult {
AlreadyLive {
pod_name: String,
socket_path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -514,6 +554,7 @@ pub enum PodDiscoveryError {
struct VisibilitySet {
visible: BTreeMap<String, VisibilityReason>,
child_sockets: BTreeMap<String, PathBuf>,
comm_registry: BTreeMap<String, CommRegistryInfo>,
}
impl VisibilitySet {
@ -527,6 +568,37 @@ impl VisibilitySet {
fn child_socket_for(&self, pod_name: &str) -> Option<PathBuf> {
self.child_sockets.get(pod_name).cloned()
}
fn comm_info_for(&self, pod_name: &str) -> CommRegistryInfo {
self.comm_registry
.get(pod_name)
.cloned()
.unwrap_or_else(CommRegistryInfo::missing)
}
}
fn comm_info_from_spawned_child(child: &pod_store::PodSpawnedChild) -> CommRegistryInfo {
let scope_delegated = child
.scope_delegated
.iter()
.filter_map(|rule| {
let permission = match rule.permission.as_str() {
"read" => Permission::Read,
"write" => Permission::Write,
_ => return None,
};
Some(ScopeRule {
target: rule.target.clone(),
permission,
recursive: rule.recursive,
})
})
.collect();
CommRegistryInfo {
registered: true,
socket_path: Some(child.socket_path.clone()),
scope_delegated,
}
}
async fn summarize_spawned_children(
@ -593,27 +665,18 @@ fn lookup_segment_lock(
pod_registry::lookup_segment(segment_id)
}
fn resolve_pod_command() -> PathBuf {
if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND")
&& !cmd.is_empty()
{
return PathBuf::from(cmd);
}
PathBuf::from("insomnia-pod")
}
#[derive(Debug, Deserialize, JsonSchema)]
struct PodNameInput {
/// Pod name to inspect, attach, or restore.
/// Pod name to restore.
name: String,
}
struct ListVisiblePodsTool<St> {
struct ListPodsTool<St> {
discovery: PodDiscovery<St>,
}
#[async_trait]
impl<St> Tool for ListVisiblePodsTool<St>
impl<St> Tool for ListPodsTool<St>
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
@ -631,53 +694,28 @@ where
}
}
struct InspectPodTool<St> {
struct RestorePodTool<St> {
discovery: PodDiscovery<St>,
}
#[async_trait]
impl<St> Tool for InspectPodTool<St>
impl<St> Tool for RestorePodTool<St>
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
let input: PodNameInput = serde_json::from_str(input_json)
.map_err(|e| ToolError::InvalidArgument(format!("invalid InspectPod input: {e}")))?;
let detail = self
.discovery
.inspect(&input.name)
.await
.map_err(discovery_error_to_tool_error)?;
Ok(ToolOutput {
summary: format!("pod `{}` inspected", detail.pod_name),
content: Some(json_content(&detail)?),
})
}
}
struct AttachOrRestorePodTool<St> {
discovery: PodDiscovery<St>,
}
#[async_trait]
impl<St> Tool for AttachOrRestorePodTool<St>
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
async fn execute(&self, input_json: &str) -> Result<ToolOutput, ToolError> {
let input: PodNameInput = serde_json::from_str(input_json).map_err(|e| {
ToolError::InvalidArgument(format!("invalid AttachOrRestorePod input: {e}"))
})?;
.map_err(|e| ToolError::InvalidArgument(format!("invalid RestorePod input: {e}")))?;
let result = self
.discovery
.attach_or_restore(&input.name)
.restore(&input.name)
.await
.map_err(discovery_error_to_tool_error)?;
let summary = match &result {
AttachRestoreResult::Attached { pod_name, .. } => {
format!("pod `{pod_name}` is live; attached to existing socket")
RestoreResult::AlreadyLive { pod_name, .. } => {
format!("pod `{pod_name}` is already live")
}
AttachRestoreResult::Restored { pod_name, .. } => {
RestoreResult::Restored { pod_name, .. } => {
format!("pod `{pod_name}` restored from pod state")
}
};
@ -688,55 +726,38 @@ where
}
}
pub fn list_visible_pods_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
pub fn list_pods_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
Arc::new(move || {
let meta = ToolMeta::new("ListVisiblePods")
let meta = ToolMeta::new("ListPods")
.description(
"List Pod state entries visible to this Pod. This is state-backed and does not expose the host-wide Pod universe.",
"List Pods visible to this Pod from durable Pod state and the spawned-child registry. This does not expose the host-wide Pod universe.",
)
.input_schema(serde_json::json!({
"type": "object",
"properties": {},
"additionalProperties": false,
}));
let tool: Arc<dyn Tool> = Arc::new(ListVisiblePodsTool {
let tool: Arc<dyn Tool> = Arc::new(ListPodsTool {
discovery: discovery.clone(),
});
(meta, tool)
})
}
pub fn inspect_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
pub fn restore_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
Arc::new(move || {
let meta = ToolMeta::new("InspectPod")
let meta = ToolMeta::new("RestorePod")
.description(
"Inspect one visible Pod by name from durable Pod state, distinguishing missing state from not-visible Pods.",
"Restore a visible stopped/restorable Pod, or report that a visible Pod is already live. Missing state is an error.",
)
.input_schema(serde_json::to_value(schemars::schema_for!(PodNameInput)).unwrap());
let tool: Arc<dyn Tool> = Arc::new(InspectPodTool {
discovery: discovery.clone(),
});
(meta, tool)
})
}
pub fn attach_or_restore_pod_tool<St>(discovery: PodDiscovery<St>) -> ToolDefinition
where
St: PodMetadataStore + Clone + Send + Sync + 'static,
{
Arc::new(move || {
let meta = ToolMeta::new("AttachOrRestorePod")
.description(
"Attach to a visible live Pod, or restore it from Pod state when no live socket is reachable. Missing state is an error.",
)
.input_schema(serde_json::to_value(schemars::schema_for!(PodNameInput)).unwrap());
let tool: Arc<dyn Tool> = Arc::new(AttachOrRestorePodTool {
let tool: Arc<dyn Tool> = Arc::new(RestorePodTool {
discovery: discovery.clone(),
});
(meta, tool)
@ -781,7 +802,7 @@ mod tests {
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[tokio::test(flavor = "current_thread")]
async fn state_backed_visibility_and_attach_restore_planning() {
async fn state_backed_visibility_and_restore_planning() {
let _env = ENV_LOCK.lock().unwrap();
let root = TempDir::new().unwrap();
let store_dir = root.path().join("store");
@ -873,6 +894,13 @@ mod tests {
registry,
);
let list_tool_def = list_pods_tool(discovery.clone());
let (list_meta, _) = list_tool_def();
assert_eq!(list_meta.name, "ListPods");
let restore_tool_def = restore_pod_tool(discovery.clone());
let (restore_meta, _) = restore_tool_def();
assert_eq!(restore_meta.name, "RestorePod");
let list = discovery.list_visible().await.unwrap();
let names: Vec<_> = list.iter().map(|p| p.pod_name.as_str()).collect();
assert_eq!(
@ -912,25 +940,16 @@ mod tests {
missing_err,
PodDiscoveryError::StateMissing { .. }
));
let hidden_restore_err = discovery
.plan_attach_or_restore("hidden")
.await
.unwrap_err();
let hidden_restore_err = discovery.plan_restore("hidden").await.unwrap_err();
assert!(matches!(
hidden_restore_err,
PodDiscoveryError::NotVisible { .. }
));
let attach_plan = discovery
.plan_attach_or_restore("child-live")
.await
.unwrap();
assert!(matches!(attach_plan, AttachRestorePlan::Attach { .. }));
let restore_plan = discovery
.plan_attach_or_restore("child-stale")
.await
.unwrap();
assert!(matches!(restore_plan, AttachRestorePlan::Restore { .. }));
let live_plan = discovery.plan_restore("child-live").await.unwrap();
assert!(matches!(live_plan, RestorePlan::AlreadyLive { .. }));
let restore_plan = discovery.plan_restore("child-stale").await.unwrap();
assert!(matches!(restore_plan, RestorePlan::Restore { .. }));
let lock_socket = runtime_base.join("lock-owner.sock");
let _guard = pod_registry::install_top_level(
@ -945,10 +964,7 @@ mod tests {
active_child_segment,
)
.unwrap();
let locked_err = discovery
.plan_attach_or_restore("child-stale")
.await
.unwrap_err();
let locked_err = discovery.plan_restore("child-stale").await.unwrap_err();
assert!(matches!(locked_err, PodDiscoveryError::LockConflict { .. }));
live_listener.abort();

View File

@ -1,19 +1,17 @@
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::Parser;
use crate::{Pod, PodController, PromptLoader};
use clap::{CommandFactory, FromArgMatches, Parser};
use manifest::{
PodManifest, PodManifestConfig, ProfileResolveOptions, ProfileResolver, ProfileSelector, paths,
};
use pod::{Pod, PodController, PromptLoader};
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
use session_store::{FsStore, SegmentId, Store};
#[derive(Debug, Parser)]
#[command(
name = "insomnia-pod",
about = "Spawn a Pod process from a profile or a single manifest file"
)]
#[command(about = "Spawn a Pod process from a profile or a single manifest file")]
struct Cli {
/// Profile to evaluate. Accepts an explicit path, `path:<path>`, a
/// discovered profile name, `default`, or a source-qualified name such as
@ -192,10 +190,53 @@ fn load_single_manifest(
Ok((manifest, PromptLoader::builtins_only()))
}
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
pub async fn run_cli() -> ExitCode {
run_cli_from("insomnia pod", std::env::args_os().skip(1)).await
}
pub async fn run_cli_from<I, T>(bin_name: &'static str, args: I) -> ExitCode
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let cli = match parse_cli_from(bin_name, args) {
Ok(cli) => cli,
Err(err) => {
let code = err.exit_code();
if let Err(print_err) = err.print() {
eprintln!("error: failed to write CLI error: {print_err}");
}
return exit_code_from_i32(code);
}
};
run_cli_inner(cli).await
}
fn parse_cli_from<I, T>(bin_name: &'static str, args: I) -> Result<Cli, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let argv = std::iter::once(OsString::from(bin_name))
.chain(args.into_iter().map(Into::into))
.collect::<Vec<_>>();
let matches = Cli::command()
.name(bin_name)
.bin_name(bin_name)
.try_get_matches_from(argv)?;
Cli::from_arg_matches(&matches)
}
fn exit_code_from_i32(code: i32) -> ExitCode {
match code {
0 => ExitCode::SUCCESS,
1 => ExitCode::FAILURE,
code => ExitCode::from(code.clamp(0, u8::MAX as i32) as u8),
}
}
async fn run_cli_inner(cli: Cli) -> ExitCode {
let (mut manifest, loader) = match resolve_manifest(&cli) {
Ok(pair) => pair,
Err(e) => {
@ -403,14 +444,23 @@ permission = "write"
#[test]
fn user_manifest_flag_is_not_accepted() {
let err =
Cli::try_parse_from(["insomnia-pod", "--user-manifest", "manifest.toml"]).unwrap_err();
Cli::try_parse_from(["insomnia pod", "--user-manifest", "manifest.toml"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
#[test]
fn subcommand_help_uses_insomnia_pod_invocation() {
let err = parse_cli_from("insomnia pod", ["--help"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
let help = err.to_string();
assert!(help.contains("Usage: insomnia pod"), "{help}");
assert!(help.contains("--pod <NAME>"), "{help}");
}
#[test]
fn manifest_conflicts_with_project() {
let project_err = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--manifest",
"manifest.toml",
"--project",
@ -422,7 +472,7 @@ permission = "write"
#[test]
fn overlay_flag_is_not_accepted() {
let err = Cli::try_parse_from(["insomnia-pod", "--overlay", "pod.name = 'x'"]).unwrap_err();
let err = Cli::try_parse_from(["insomnia pod", "--overlay", "pod.name = 'x'"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
}
@ -431,7 +481,7 @@ permission = "write"
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("single", tmp.path()));
let cli = Cli::try_parse_from(["insomnia-pod", "--manifest", manifest.to_str().unwrap()])
let cli = Cli::try_parse_from(["insomnia pod", "--manifest", manifest.to_str().unwrap()])
.unwrap();
let (manifest, loader) = resolve_manifest(&cli).unwrap();
@ -446,7 +496,7 @@ permission = "write"
let tmp = TempDir::new().unwrap();
let profile = tmp.path().join("profile.lua");
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--profile",
profile.to_str().unwrap(),
"--profile-pod-name",
@ -479,7 +529,7 @@ permission = "write"
fn profile_accepts_source_qualified_discovered_name() {
let tmp = TempDir::new().unwrap();
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--profile",
"project:coder",
"--profile-pod-name",
@ -514,7 +564,7 @@ permission = "write"
#[test]
fn normal_startup_uses_default_profile() {
let tmp = TempDir::new().unwrap();
let cli = Cli::try_parse_from(["insomnia-pod"]).unwrap();
let cli = Cli::try_parse_from(["insomnia pod"]).unwrap();
let mut called = false;
let (manifest, _loader) =
@ -535,7 +585,7 @@ permission = "write"
#[test]
fn project_flag_no_longer_enables_ambient_manifest_cascade() {
let cli = Cli::try_parse_from(["insomnia-pod", "--project", "."]).unwrap();
let cli = Cli::try_parse_from(["insomnia pod", "--project", "."]).unwrap();
let err = resolve_manifest_with_profile_loader(&cli, |_, _| {
panic!("default profile loader must not run when deprecated --project is present")
})
@ -547,7 +597,7 @@ permission = "write"
fn pod_flag_conflicts_with_session() {
let segment_id = session_store::new_segment_id();
let segment_id = segment_id.to_string();
let err = Cli::try_parse_from(["insomnia-pod", "--pod", "agent", "--session", &segment_id])
let err = Cli::try_parse_from(["insomnia pod", "--pod", "agent", "--session", &segment_id])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
@ -558,7 +608,7 @@ permission = "write"
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("from-file", tmp.path()));
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
@ -590,7 +640,7 @@ permission = "write"
"#,
);
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--manifest",
manifest.to_str().unwrap(),
"--pod",
@ -607,7 +657,7 @@ permission = "write"
#[test]
fn pod_flag_with_no_manifest_creates_from_default_profile_with_typed_name() {
let tmp = TempDir::new().unwrap();
let cli = Cli::try_parse_from(["insomnia-pod", "--pod", "agent"]).unwrap();
let cli = Cli::try_parse_from(["insomnia pod", "--pod", "agent"]).unwrap();
let mut called = false;
let (manifest, _loader) =
@ -633,10 +683,10 @@ permission = "write"
fn profile_conflicts_with_manifest_and_restore_modes() {
let segment_id = session_store::new_segment_id().to_string();
for args in [
vec!["insomnia-pod", "--profile", "p.lua", "--manifest", "m.toml"],
vec!["insomnia-pod", "--profile", "p.lua", "--pod", "agent"],
vec!["insomnia pod", "--profile", "p.lua", "--manifest", "m.toml"],
vec!["insomnia pod", "--profile", "p.lua", "--pod", "agent"],
vec![
"insomnia-pod",
"insomnia pod",
"--profile",
"p.lua",
"--session",
@ -650,14 +700,14 @@ permission = "write"
#[test]
fn profile_pod_name_requires_profile() {
let err = Cli::try_parse_from(["insomnia-pod", "--profile-pod-name", "agent"]).unwrap_err();
let err = Cli::try_parse_from(["insomnia pod", "--profile-pod-name", "agent"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
}
#[test]
fn profile_pod_name_is_not_restore_pod_flag() {
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--profile",
"p.lua",
"--profile-pod-name",
@ -676,7 +726,7 @@ permission = "write"
std::fs::create_dir_all(tmp.path().join("prompts")).unwrap();
std::fs::create_dir_all(tmp.path().join(".insomnia").join("prompts")).unwrap();
let cli = Cli::try_parse_from([
"insomnia-pod",
"insomnia pod",
"--manifest",
single_manifest.to_str().unwrap(),
])

View File

@ -1,6 +1,7 @@
pub mod compact;
pub mod controller;
pub mod discovery;
pub mod entrypoint;
pub mod fs_view;
pub mod hook;
pub mod ipc;

View File

@ -1,7 +1,7 @@
//! Pod-to-Pod communication tools.
//!
//! Four tools in one module — `SendToPod`, `ReadPodOutput`, `StopPod`,
//! `ListPods` — all built on the same `SpawnedPodRegistry` handed in by
//! Three tools in one module: `SendToPod`, `ReadPodOutput`, `StopPod`,
//! all built on the same `SpawnedPodRegistry` handed in by
//! the controller. Each operation is request-response: connect to the
//! target's Unix socket, perform one method exchange, disconnect.
//!
@ -23,7 +23,6 @@ use session_store::LogEntry;
use tokio::net::UnixStream;
use crate::runtime::dir::SpawnedPodRecord;
use crate::runtime::pod_registry::{self, LockFileGuard};
use crate::spawn::registry::SpawnedPodRegistry;
/// Timeout applied to each socket-level operation — connect, write,
@ -244,76 +243,6 @@ pub fn stop_pod_tool(registry: Arc<SpawnedPodRegistry>) -> ToolDefinition {
})
}
// ---------------------------------------------------------------------------
// ListPods
// ---------------------------------------------------------------------------
const LIST_PODS_DESCRIPTION: &str = "List all Pods spawned by this Pod along with their reachability \
status (`alive` / `stopped`) and the scope each was granted.";
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct EmptyInput {}
struct ListPodsTool {
registry: Arc<SpawnedPodRegistry>,
}
#[async_trait]
impl Tool for ListPodsTool {
async fn execute(&self, _input_json: &str) -> Result<ToolOutput, ToolError> {
let records = self.registry.list().await;
if records.is_empty() {
return Ok(ToolOutput {
summary: "no spawned pods".into(),
content: None,
});
}
let mut lines: Vec<String> = Vec::with_capacity(records.len());
let mut stale_names: Vec<String> = Vec::new();
for record in &records {
let alive = is_reachable(&record.socket_path).await;
let status = if alive { "alive" } else { "stopped" };
let scope = summarize_scope(record);
lines.push(format!("{} [{status}] scope={scope}", record.pod_name));
if !alive {
stale_names.push(record.pod_name.clone());
}
}
// Trigger stale reclaim on unreachable pods so the lock file's
// allocation table doesn't keep growing indefinitely when
// children crash without a clean exit path.
if !stale_names.is_empty() {
if let Ok(lock_path) = pod_registry::default_registry_path()
&& let Ok(mut guard) = LockFileGuard::open(&lock_path)
{
pod_registry::reclaim_stale(&mut guard);
}
}
let summary = format!("{} pod(s) known", records.len());
Ok(ToolOutput {
summary,
content: Some(lines.join("\n")),
})
}
}
pub fn list_pods_tool(registry: Arc<SpawnedPodRegistry>) -> ToolDefinition {
Arc::new(move || {
let schema = schemars::schema_for!(EmptyInput);
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
let meta = ToolMeta::new("ListPods")
.description(LIST_PODS_DESCRIPTION)
.input_schema(schema_value);
let tool: Arc<dyn Tool> = Arc::new(ListPodsTool {
registry: registry.clone(),
});
(meta, tool)
})
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@ -322,6 +251,29 @@ fn unknown_pod_err(name: &str) -> ToolError {
ToolError::InvalidArgument(format!("no spawned pod named `{name}`"))
}
fn summarize_scope(record: &SpawnedPodRecord) -> String {
if record.scope_delegated.is_empty() {
return "(none)".into();
}
let parts: Vec<String> = record
.scope_delegated
.iter()
.map(|rule| {
let perm = match rule.permission {
manifest::Permission::Read => "read",
manifest::Permission::Write => "write",
};
let recursive = if rule.recursive {
""
} else {
" [non-recursive]"
};
format!("{perm}:{}{recursive}", rule.target.display())
})
.collect();
parts.join(", ")
}
/// Connect with a timeout, drain the server's connect-time snapshot,
/// write one `Method` line, flush, and close.
///
@ -487,14 +439,6 @@ async fn fetch_history(socket: &Path) -> std::io::Result<Vec<serde_json::Value>>
}
}
/// Probe-connect test. Connection accepted within timeout → alive.
async fn is_reachable(socket: &Path) -> bool {
tokio::time::timeout(SOCKET_OP_TIMEOUT, UnixStream::connect(socket))
.await
.map(|r| r.is_ok())
.unwrap_or(false)
}
fn extract_assistant_text(entries: &[serde_json::Value]) -> String {
let mut out = String::new();
for value in entries {
@ -536,25 +480,6 @@ fn push_assistant_text(out: &mut String, logged: session_store::LoggedItem) {
}
}
fn summarize_scope(record: &SpawnedPodRecord) -> String {
if record.scope_delegated.is_empty() {
return "(none)".into();
}
let parts: Vec<String> = record
.scope_delegated
.iter()
.map(|r| {
let perm = match r.permission {
manifest::Permission::Read => "read",
manifest::Permission::Write => "write",
};
let tag = if r.recursive { "" } else { " [non-recursive]" };
format!("{perm}:{}{tag}", r.target.display())
})
.collect();
parts.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,9 +1,10 @@
//! Shared registry of Pods spawned by this Pod.
//!
//! `SpawnPod` writes here; the pod-comm tools (`SendToPod`,
//! `ReadPodOutput`, `StopPod`, `ListPods`) read and mutate the same
//! instance. Runtime write-through still materialises `spawned_pods.json`,
//! but durable state lives in the spawner's Pod metadata.
//! `ReadPodOutput`, `StopPod`) read and mutate the same instance. Discovery
//! tools consult this registry together with durable Pod state. Runtime
//! write-through still materialises `spawned_pods.json`, but durable state lives
//! in the spawner's Pod metadata.
//!
//! `ReadPodOutput` additionally owns a per-spawned-pod cursor here so
//! two consecutive reads yield only new assistant text. The cursor is

View File

@ -2,7 +2,7 @@
//!
//! Wires pod-registry delegation, child manifest-config construction, subprocess
//! launch, and socket handoff into a single `Tool` implementation. When
//! the LLM calls `SpawnPod`, a fresh `insomnia-pod` binary is exec'd in its own
//! the LLM calls `SpawnPod`, a fresh Pod runtime command is exec'd in its own
//! process group, the pod-registry is updated atomically, and the child's
//! first turn is kicked off by handing its socket a `Method::Run`.
@ -12,6 +12,7 @@ use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use insomnia::PodRuntimeCommand;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{
CompactionConfigPartial, FileUploadLimitsPartial, Permission, PermissionConfigPartial,
@ -220,8 +221,8 @@ pub struct SpawnPodTool {
/// override it. Defaults to the spawner's pwd — see module docs.
spawner_pwd: PathBuf,
/// Shared registry of spawned children, also used by the
/// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod` /
/// `ListPods`). Writes the list to runtime and durable Pod state on
/// pod-comm tools (`SendToPod` / `ReadPodOutput` / `StopPod`) and by
/// Pod discovery. Writes the list to runtime and durable Pod state on
/// each add.
registry: Arc<SpawnedPodRegistry>,
/// THIS Pod's own parent-callback socket, if any. After a
@ -408,8 +409,9 @@ impl SpawnPodTool {
spawn_config_json: &str,
predicted_socket: &Path,
) -> Result<(), ToolError> {
let pod_command =
std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "insomnia-pod".into());
let runtime_command = PodRuntimeCommand::resolve().map_err(|error| {
ToolError::ExecutionFailed(format!("failed to resolve Pod runtime command: {error}"))
})?;
// Pre-create the child's runtime dir so we have a stable place to
// capture its stderr before it has had a chance to bind anything.
@ -430,8 +432,9 @@ impl SpawnPodTool {
ToolError::ExecutionFailed(format!("open {}: {e}", stderr_path.display()))
})?;
let mut cmd = Command::new(&pod_command);
cmd.arg("--adopt")
let mut cmd = Command::new(runtime_command.program());
cmd.args(runtime_command.prefix_args())
.arg("--adopt")
.arg("--callback")
.arg(&self.callback_socket)
.arg("--spawn-config-json")
@ -443,7 +446,7 @@ impl SpawnPodTool {
.process_group(0);
let child = cmd.spawn().map_err(|e| {
ToolError::ExecutionFailed(format!("failed to spawn `{pod_command}`: {e}"))
ToolError::ExecutionFailed(format!("failed to spawn `{runtime_command}`: {e}"))
})?;
// Default `kill_on_drop = false` keeps the process alive after
@ -488,7 +491,7 @@ fn parse_scope(rules: &[ScopeRuleInput]) -> Result<Vec<ScopeRule>, ToolError> {
}
/// Serialise the internal manifest config that gets handed to the child
/// `insomnia-pod` binary via the hidden `--spawn-config-json` flag.
/// Pod runtime process via the hidden `--spawn-config-json` flag.
/// `PodManifestConfig`'s `Serialize` impl is the single source of truth for the
/// internal handoff shape.
///

View File

@ -1,5 +1,5 @@
//! Integration tests for the pod-comm tools (`SendToPod`,
//! `ReadPodOutput`, `StopPod`, `ListPods`).
//! `ReadPodOutput`, `StopPod`).
//!
//! The real child Pod binary is not started. Instead each test stands
//! up a mock `UnixListener` that speaks the socket protocol directly:
@ -16,9 +16,7 @@ use llm_worker::tool::ToolOutput;
use manifest::{Permission, Scope, ScopeRule, SharedScope};
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::pod_registry::{self, LockFileGuard};
use pod::spawn::comm_tools::{
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
};
use pod::spawn::comm_tools::{read_pod_output_tool, send_to_pod_tool, stop_pod_tool};
use pod::spawn::registry::SpawnedPodRegistry;
use pod_store::{CombinedStore, FsPodStore, PodMetadataStore};
use protocol::stream::{JsonLineReader, JsonLineWriter};
@ -544,13 +542,6 @@ async fn restored_registry_uses_pod_state_without_runtime_file() {
.await
.unwrap();
let def = list_pods_tool(restored.clone());
let (_meta, tool) = def();
let output: ToolOutput = tool.execute("{}").await.unwrap();
assert!(output.summary.contains("1 pod"), "{}", output.summary);
let body = output.content.expect("restored ListPods should list child");
assert!(body.contains("child [alive]"), "body: {body}");
let def = send_to_pod_tool(restored.clone());
let (_meta, tool) = def();
let input = json!({ "name": "child", "message": "after restart" }).to_string();
@ -718,51 +709,3 @@ async fn load_from_pod_state_reclaims_missing_child_scope_and_records_history()
let runtime_records: Vec<SpawnedPodRecord> = serde_json::from_str(&runtime_contents).unwrap();
assert!(runtime_records.is_empty());
}
// ---------------------------------------------------------------------------
// ListPods
// ---------------------------------------------------------------------------
#[tokio::test]
async fn list_pods_reports_alive_and_stopped() {
let (tmp, registry, _rd) = setup_registry().await;
// One child is reachable…
let (live_socket, listener) = bind_mock_socket(tmp.path(), "alive").await;
// Keep the listener alive by moving it into a task that never exits.
let _accept = tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
return;
};
drop(stream);
}
});
register_child(&registry, "alive", &live_socket, tmp.path()).await;
// …the other is not.
let dead_socket = tmp.path().join("dead.sock");
register_child(&registry, "dead", &dead_socket, tmp.path()).await;
let def = list_pods_tool(registry);
let (_meta, tool) = def();
let output: ToolOutput = tool.execute("{}").await.unwrap();
assert!(output.summary.contains("2 pod"), "{}", output.summary);
let body = output.content.expect("list_pods should populate content");
assert!(body.contains("alive [alive]"), "body: {body}");
assert!(body.contains("dead [stopped]"), "body: {body}");
}
#[tokio::test]
async fn list_pods_empty_when_nothing_registered() {
let (_tmp, registry, _rd) = setup_registry().await;
let def = list_pods_tool(registry);
let (_meta, tool) = def();
let output: ToolOutput = tool.execute("{}").await.unwrap();
assert!(
output.summary.contains("no spawned pods"),
"{}",
output.summary
);
assert!(output.content.is_none());
}

View File

@ -2,7 +2,7 @@
//!
//! These tests exercise the tool's pod-registry delegation, subprocess
//! launch, socket handoff, and `spawned_pods.json` write without relying
//! on the real `insomnia-pod` binary. `INSOMNIA_POD_COMMAND` is pointed at
//! on the real Pod runtime executable. `INSOMNIA_POD_COMMAND` is pointed at
//! `/bin/true` (which exits immediately) while a test-owned Unix
//! listener pre-binds the predicted socket path, so the tool sees the
//! "child" as live.
@ -141,7 +141,7 @@ fn accept_one_method(listener: UnixListener) -> tokio::task::JoinHandle<Option<M
})
}
fn point_pod_command_at_true() {
fn point_runtime_command_at_true() {
let path = which_true();
unsafe {
std::env::set_var("INSOMNIA_POD_COMMAND", &path);
@ -224,7 +224,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
let allow_root = TempDir::new().unwrap();
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
setup_spawner("root", allow_root.path()).await;
point_pod_command_at_true();
point_runtime_command_at_true();
let (_predicted_socket, listener) = bind_mock_pod_socket(&runtime_base, "child").await;
let received = accept_one_method(listener);
@ -317,7 +317,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
let outside = TempDir::new().unwrap();
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
setup_spawner("root", allow_root.path()).await;
point_pod_command_at_true();
point_runtime_command_at_true();
let registry = SpawnedPodRegistry::new(spawner_rd);
let spawner_scope = shared_scope_for(allow_root.path());
@ -379,7 +379,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
let allow_root = TempDir::new().unwrap();
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
setup_spawner("root", allow_root.path()).await;
point_pod_command_at_true();
point_runtime_command_at_true();
// Deliberately do NOT bind a socket at the predicted path. The
// tool's wait_for_socket should time out, triggering rollback.

View File

@ -56,16 +56,12 @@ pub enum Method {
kind: CompletionKind,
prefix: String,
},
/// List Pods visible to this Pod from durable Pod state. This is not a
/// host-wide Pod universe query.
ListVisiblePods,
/// Inspect one Pod by name if its state exists and it is visible to this Pod.
InspectPod {
name: String,
},
/// Attach to a visible live Pod, or restore it from durable Pod state when
/// it is not live. Missing state and not-visible state are distinct errors.
AttachOrRestorePod {
/// List Pods visible to this Pod from durable Pod state and the spawned-child
/// registry. This is not a host-wide Pod universe query.
ListPods,
/// Restore a visible stopped/restorable Pod, or report that it is already
/// live. Missing state and not-visible state are distinct errors.
RestorePod {
name: String,
},
}
@ -474,18 +470,14 @@ pub enum Event {
input: Vec<Segment>,
summary: RewindSummary,
},
/// Reply to `Method::ListVisiblePods`. Payload is a stable JSON value so
/// the Pod crate can evolve discovery fields without introducing a protocol
/// Reply to `Method::ListPods`. Payload is a stable JSON value so the Pod
/// crate can evolve discovery fields without introducing a protocol
/// dependency on session-store.
VisiblePods {
PodsListed {
pods: serde_json::Value,
},
/// Reply to `Method::InspectPod`.
PodInspection {
pod: serde_json::Value,
},
/// Reply to `Method::AttachOrRestorePod`.
PodAttachRestore {
/// Reply to `Method::RestorePod`.
PodRestored {
result: serde_json::Value,
},
Alert(Alert),
@ -1469,11 +1461,8 @@ mod tests {
#[test]
fn pod_discovery_methods_roundtrip() {
let methods = [
Method::ListVisiblePods,
Method::InspectPod {
name: "child".into(),
},
Method::AttachOrRestorePod {
Method::ListPods,
Method::RestorePod {
name: "child".into(),
},
];
@ -1481,9 +1470,8 @@ mod tests {
let json = serde_json::to_string(&method).unwrap();
let decoded: Method = serde_json::from_str(&json).unwrap();
match (decoded, method) {
(Method::ListVisiblePods, Method::ListVisiblePods)
| (Method::InspectPod { .. }, Method::InspectPod { .. })
| (Method::AttachOrRestorePod { .. }, Method::AttachOrRestorePod { .. }) => {}
(Method::ListPods, Method::ListPods)
| (Method::RestorePod { .. }, Method::RestorePod { .. }) => {}
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
}
}
@ -1492,30 +1480,23 @@ mod tests {
#[test]
fn pod_discovery_events_roundtrip() {
let events = [
Event::VisiblePods {
Event::PodsListed {
pods: serde_json::json!([{ "pod_name": "child" }]),
},
Event::PodInspection {
pod: serde_json::json!({ "pod_name": "child" }),
},
Event::PodAttachRestore {
result: serde_json::json!({ "action": "attach" }),
Event::PodRestored {
result: serde_json::json!({ "action": "already_live" }),
},
];
for event in events {
let json = serde_json::to_string(&event).unwrap();
let decoded: Event = serde_json::from_str(&json).unwrap();
match (decoded, event) {
(Event::VisiblePods { pods }, Event::VisiblePods { pods: expected }) => {
(Event::PodsListed { pods }, Event::PodsListed { pods: expected }) => {
assert_eq!(pods, expected)
}
(Event::PodInspection { pod }, Event::PodInspection { pod: expected }) => {
assert_eq!(pod, expected)
(Event::PodRestored { result }, Event::PodRestored { result: expected }) => {
assert_eq!(result, expected)
}
(
Event::PodAttachRestore { result },
Event::PodAttachRestore { result: expected },
) => assert_eq!(result, expected),
(decoded, expected) => panic!("decoded {decoded:?}, expected {expected:?}"),
}
}

View File

@ -22,6 +22,7 @@ manifest = { workspace = true }
memory = { workspace = true }
session-store = { workspace = true }
pod-store = { workspace = true }
pod = { workspace = true }
pod-registry = { workspace = true }
serde = { workspace = true, features = ["derive"] }
pulldown-cmark = { version = "0.13.3", default-features = false }

View File

@ -1204,9 +1204,7 @@ impl App {
message,
});
}
Event::VisiblePods { .. }
| Event::PodInspection { .. }
| Event::PodAttachRestore { .. } => {}
Event::PodsListed { .. } | Event::PodRestored { .. } => {}
Event::Shutdown => {
self.mark_orphan_compacts_incomplete();
self.quit = true;

View File

@ -65,7 +65,7 @@ enum Mode {
profile: Option<String>,
},
/// `insomnia <name>` / `insomnia --pod <name>`: attach to a live Pod by name if
/// possible; otherwise launch `insomnia-pod --pod <name>` so the pod process
/// possible; otherwise launch the Pod runtime command with `--pod <name>` so it
/// resumes from name-keyed state or creates a fresh same-name Pod.
PodName {
pod_name: String,
@ -83,6 +83,8 @@ enum Mode {
Multi,
/// `insomnia memory lint`: headless lint for workspace memory and knowledge files.
MemoryLint(memory_lint::LintCliOptions),
/// `insomnia pod ...`: run the Pod runtime parser/entrypoint without TUI side effects.
PodRuntime(Vec<String>),
}
#[derive(Debug)]
@ -120,6 +122,9 @@ where
let options = memory_lint::parse_lint_args(&args[2..]).map_err(ParseError::MemoryLint)?;
return Ok(Mode::MemoryLint(options));
}
if args.first().map(String::as_str) == Some("pod") {
return Ok(Mode::PodRuntime(args[1..].to_vec()));
}
let mut resume = false;
let mut multi = false;
@ -285,6 +290,10 @@ async fn main() -> ExitCode {
};
}
if let Mode::PodRuntime(args) = mode {
return pod::entrypoint::run_cli_from("insomnia pod", args).await;
}
if let Err(e) = enable_raw_mode() {
eprintln!("insomnia: failed to enter raw mode: {e}");
return ExitCode::FAILURE;
@ -305,6 +314,7 @@ async fn main() -> ExitCode {
Mode::ResumeWithSession(id) => run_spawn(Some(id), None).await,
Mode::Multi => run_multi().await,
Mode::MemoryLint(_) => unreachable!("memory lint returns before terminal setup"),
Mode::PodRuntime(_) => unreachable!("pod runtime returns before terminal setup"),
};
// Always restore the terminal first so any pending eprintln below
@ -1210,6 +1220,28 @@ mod tests {
}
}
#[test]
fn parse_pod_subcommand_uses_runtime_mode() {
match parse_args_from(["pod", "--pod", "agent", "--profile", "default"]).unwrap() {
Mode::PodRuntime(args) => assert_eq!(args, ["--pod", "agent", "--profile", "default"]),
_ => panic!("expected PodRuntime mode"),
}
}
#[test]
fn parse_literal_pod_name_still_available_with_flag() {
match parse_args_from(["--pod", "pod"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "pod");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_lint_mode() {
match parse_args_from([

View File

@ -2,7 +2,7 @@
//!
//! Reads live Pod allocations from the runtime registry and stopped Pod state
//! from the pod-store name-keyed metadata. Picking a live row attaches to
//! its socket; picking a stopped row restores via `insomnia-pod --pod <name>`.
//! its socket; picking a stopped row restores via the Pod runtime command.
use std::io;
use std::path::PathBuf;
@ -65,7 +65,7 @@ impl From<session_store::StoreError> for PickerError {
pub enum PickerOutcome {
/// User picked a Pod. `socket_override` is set for live rows when the
/// runtime registry knows the exact socket path; stopped rows leave it
/// empty so the caller restores with `insomnia-pod --pod <name>`.
/// empty so the caller restores by spawning the Pod runtime command.
Picked {
pod_name: String,
socket_override: Option<PathBuf>,
@ -262,7 +262,7 @@ fn draw(f: &mut Frame<'_>, list: &PodList) {
Span::styled("[↑/↓]", Style::default().fg(Color::DarkGray)),
Span::raw(" select "),
Span::styled("[enter]", Style::default().fg(Color::Green)),
Span::raw(" attach/restore "),
Span::raw(" open/restore "),
Span::styled("[esc]", Style::default().fg(Color::Yellow)),
Span::raw(" cancel"),
])),

View File

@ -3,7 +3,7 @@
//! Rendered at the user's current cursor position when `insomnia` is invoked
//! with no positional argument. Discovers `.insomnia/profiles.toml` profile
//! choices plus bundled profiles, defaults to the builtin profile, prompts for
//! the Pod's name, and on confirmation launches the `insomnia-pod` binary as an
//! the Pod's name, and on confirmation launches the Pod runtime command as an
//! independent process. Once the process reports its socket via the
//! `INSOMNIA-READY` stderr line, the dialog hands control back so main can
//! switch the terminal to alternate-screen mode.
@ -72,7 +72,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Source session for a resume run. `None` = fresh spawn (current
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
/// passes `--session <id>` to the spawned `insomnia-pod` child.
/// passes `--session <id>` to the spawned Pod runtime child.
pub async fn run(
resume_from: Option<SegmentId>,
profile: Option<String>,
@ -162,7 +162,7 @@ pub async fn run(
}
}
/// Launch `insomnia-pod --pod <name>` without opening the name dialog. The child Pod
/// Launch a Pod runtime command with `--pod <name>` without opening the name dialog. The child Pod
/// resolves persisted Pod metadata if present, or creates a fresh same-name Pod
/// from the default profile.
pub async fn run_pod_name(pod_name: String) -> Result<SpawnOutcome, SpawnError> {
@ -415,7 +415,7 @@ struct Form {
/// When true, launch the child with `--pod <name>` so the pod process
/// resolves name-keyed state before falling back to fresh creation.
resume_by_pod_name: bool,
/// Optional profile choices passed to `insomnia-pod --profile` for
/// Optional profile choices passed with `--profile` for
/// fresh spawns. This is not used for resume/attach flows because those must
/// restore Pod state rather than re-evaluate a profile source.
profile_choices: Vec<ProfileChoice>,

View File

@ -1,23 +1,4 @@
{ pkgs }:
let
# Dev-only wrapper. tui の spawn 経路は `insomnia-pod` バイナリを直に exec し、
# stderr の `INSOMNIA-READY` 行で握手するので、cargo の進捗や rustc の
# warning が混ざると tail に余計な行が積もり本当のエラーが押し出される。
# ここで一度ビルドを切り離し、成功時はビルド出力を一切捨てて素のバイナリ
# を exec、失敗時のみ build log を stderr に流して exit する。
pod-dev = pkgs.writeShellScriptBin "insomnia-pod" ''
set -u
buildlog=$(mktemp)
trap 'rm -f "$buildlog"' EXIT
if ! cargo build --quiet -p pod 2>"$buildlog"; then
cat "$buildlog" >&2
exit 1
fi
manifest=$(cargo locate-project --workspace --message-format plain 2>/dev/null)
target_dir=''${CARGO_TARGET_DIR:-$(dirname "$manifest")/target}
exec "$target_dir/debug/insomnia-pod" "$@"
'';
in
pkgs.mkShell {
packages = with pkgs; [
nixfmt
@ -25,7 +6,6 @@ pkgs.mkShell {
git
rustc
cargo
pod-dev
];
buildInputs = with pkgs; [
pkg-config

View File

@ -64,11 +64,11 @@ Pod の制御・監視に使う JSONL ベースのメッセージプロトコル
- context/session 制御: `Compact`, `ListRewindTargets`, `RewindTo`
- typed injection / child lifecycle: `Notify`, `PodEvent`
- client 補助: `ListCompletions`
- Pod visibility / attach: `ListVisiblePods`, `InspectPod`, `AttachOrRestorePod`
- Pod visibility / restore: `ListPods`, `RestorePod`
- **Pod → Client (`Event`)**
- accepted input / history seed: `Snapshot`, `UserMessage`, `SystemItem`, `SegmentRotated`
- generation stream: `TurnStart`, `TurnEnd`, `LlmCallStart`, `LlmCallEnd`, retry/continuation events, `Text*`, `Thinking*`, `ToolCall*`, `ToolResult`, `Usage`, `RunEnd`
- control replies: completions, rewind, visible Pod / inspect / attach results
- control replies: completions, rewind, visible Pod list / restore results
- operational status: `Status`, `Alert`, `MemoryWorker`, `Compact*`, `Error`, `Shutdown`
- リクエストとレスポンスの紐付けを一般化した RPC にはしない。多くの状態は broadcast event と Pod status で観測する
- 一部の reply例: completionsは要求 socket にだけ返る。broadcast event と request-local reply の違いは enum variant のコメントを正とする
@ -103,7 +103,7 @@ permission = "write"
通常の Pod 起動は Lua profile discovery/default から `PodManifest` を生成する。bundled `builtin:default` が fallback default で、user/project `profiles.toml` は profile registry と default selection だけを担う。user/project `manifest.toml` の ambient cascade は通常起動では使わない。
`insomnia-pod --manifest <PATH>` は explicit one-file compatibility/debug input で、指定 TOML 1 枚だけに builtin defaults を merge し、`PodManifestConfig -> PodManifest` の required validation を通す。
`insomnia pod --manifest <PATH>` は explicit one-file compatibility/debug input で、指定 TOML 1 枚だけに builtin defaults を merge し、`PodManifestConfig -> PodManifest` の required validation を通す。
`PodFactory` の user/project/overlay API は低レベル構成部品として残るが、CLI の通常起動 path では generic TOML overlay を公開しない。
@ -146,8 +146,7 @@ Pod が操作できるファイルパスの制御。
| File / shell | `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Bash` | workspace ファイル操作と shell 実行。file tools は `ScopedFs` と read-before-edit tracker を通る。`Bash` は permission policy と出力退避で制御する |
| Task | `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet` | セッション内の短期 task 状態管理 |
| Memory / Knowledge | `MemoryQuery`, `MemoryRead`, `MemoryWrite`, `MemoryEdit`, `MemoryDelete`, `KnowledgeQuery` | manifest の memory 設定が有効な時に登録される durable memory / knowledge 操作 |
| Pod orchestration | `SpawnPod`, `SendToPod`, `ReadPodOutput`, `StopPod`, `ListPods` | child Pod の起動・通信・停止・一覧 |
| Visible Pod state | `ListVisiblePods`, `InspectPod`, `AttachOrRestorePod` | durable Pod state と visibility に基づく Pod inspection / attach / restore |
| Pod orchestration | `SpawnPod`, `SendToPod`, `ReadPodOutput`, `StopPod`, `ListPods`, `RestorePod` | child / visible Pod の起動・通信・停止・一覧・復元 |
| Web | `WebSearch`, `WebFetch` | manifest/env で明示設定された provider 経由の bounded web access |
すべての tool call は manifest tool permission と scope/policy のチェックを通る。ファイル write scope、Pod delegation、memory layout、web provider 設定はそれぞれ別の authority を持ち、UI 表示だけで権限を広げない。

95
docs/environment.md Normal file
View File

@ -0,0 +1,95 @@
# 環境変数ポリシー
INSOMNIA では、プロセス境界で本当に必要な場合を除き、環境変数の利用を避ける。新しい ambient な入力を増やすより、明示的な profile / manifest / config file / typed secret reference / CLI argument を優先する。
それでも、path discovery、runtime directory、package resource lookup、外部 provider の credential 慣習との移行互換のために、一部の環境変数はまだサポートしている。この文書に載せた環境変数は公開 surface として扱う。ただし、fallback 変数は独立した設定項目ではなく、対応する main key の解決順の一部として扱う。開発・テスト都合だけの環境変数は原則として追加せず、既存のものも削除する。
## 原則
- 同じ情報を profile、manifest、config、CLI input で明示的に渡せるなら、便利ショートカットとして新しい環境変数を追加しない。
- 永続的な project state を環境変数に置かない。解決済みの状態は適切な store に保存する。
- 通常の TUI / Pod runtime は `.env` ファイルを暗黙に load しない。example が局所的に `dotenv` を使うことはあっても、application startup が project の `.env` を勝手に読むべきではない。
- 生の secret value を generated log、work item、trace output、docs に出さない。環境変数名の記述はよいが、値は書かない。
- path/location 用の環境変数、provider credential 用の移行互換、test/build-only の環境変数を混ぜない。
- test が process environment を変更する必要がある場合は、guard で直列化し、元の値を復元する。Rust の process environment は global state である。
## Path / resource discovery
Path 系の環境変数は論理的な key ごとに立項する。`XDG_*` や `HOME` は単独の設定 surface ではなく、該当 key の fallback として読む。
| 論理 key | Main env | Fallback / 解決順 | 用途と位置付け |
| --- | --- | --- | --- |
| `home` | `INSOMNIA_HOME` | なし | config / data / runtime をまとめて sandbox する root override。設定を細かく分けるより、test や isolated run ではまずこれを使う。 |
| `config_dir` | `INSOMNIA_CONFIG_DIR` | `$INSOMNIA_HOME/config``$XDG_CONFIG_HOME/insomnia``$HOME/.config/insomnia` | 人が書く設定・override の置き場。`profiles.toml`、prompt override、model/provider override など。 |
| `data_dir` | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME``$HOME/.insomnia` | プログラムが書く永続データの置き場。session log、Pod metadata など、再起動後も restore / replay の根拠になるもの。通常ユーザー向けの primary knob ではなく、migration、test、isolated data store 用の advanced override。 |
| `runtime_dir` | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run``$XDG_RUNTIME_DIR/insomnia``$HOME/.insomnia/run` | socket、pid/status file、live registry mirror など、再起動で捨ててよい runtime state の置き場。 |
| `resource_dir` | `INSOMNIA_RESOURCE_DIR` | installed executable から見た `share/insomnia/resources` → build tree の `resources/` | bundled prompts、builtin profiles、provider/model catalog など、package が所有する immutable-ish な builtin asset の置き場。通常 user configuration ではなく、packaging / development / debug 用の override。 |
空の path 環境変数は、`manifest::paths` では原則として unset 相当に扱う。
### `data_dir``runtime_dir` の違い
`data_dir` は durable data のための場所である。消すと session history、Pod metadata、restore/replay の根拠が失われる。
`runtime_dir` は process coordination のための場所である。socket、pid/status file、live Pod registry mirror などを置き、プロセス終了や再起動で stale になり得る。runtime state は durable authority ではない。session log と Pod metadata が durable source であり、runtime file は mirror / hint である。
このため、socket や pid file を `data_dir` に置かない。永続データと揮発 runtime state は分ける。
### `resource_dir``config_dir` の違い
`resource_dir` は package-owned builtin asset の場所である。installed package では `share/insomnia/resources` に置かれ、binary version と対応する。ユーザーが普段編集する場所ではない。
`config_dir` は user/project-owned override の場所である。`profiles.toml` や prompt/model/provider override はここに置き、package update で上書きされない。
つまり、builtin fallback は `resource_dir`、user override は `config_dir` で扱う。`INSOMNIA_RESOURCE_DIR` を user configuration の代わりに使わない。
## Credential と外部 auth
Provider credential は、現在は manifest / profile / catalog の設定から env var 名を明示的に参照できる。ただし、これは移行互換のための現状であり、長期的な supported configuration path ではない。`manifest-profile-encrypted-secrets` で encrypted secret store と typed secret reference を導入し、credential env var 依存は削除する方針である。
これは「ambient な provider 自動発見」ではなく、設定で選んだ環境変数名を読む仕組みである。通常 runtime が `.env` を暗黙に load することもない。
| 変数 / pattern | 用途 | 備考 |
| --- | --- | --- |
| `INSOMNIA_API_KEY_ANTHROPIC` | custom env が指定されていない場合の Anthropic API key の default env 名。 | provider auth resolution で使う。 |
| `INSOMNIA_API_KEY_OPENAI` | OpenAI / OpenAI Responses API key の default env 名。 | provider auth resolution で使う。 |
| `INSOMNIA_API_KEY_GEMINI` | Gemini API key の default env 名。 | provider auth resolution で使う。 |
| `INSOMNIA_API_KEY_OPENROUTER` | builtin OpenRouter provider の auth hint。 | bundled provider catalog 由来。 |
| custom `model.auth.env` value | manifest / profile ごとの API key env 名。 | 明示的な config が変数名を選ぶ。`auth.env` と `auth.file` が両方ある場合は env が優先される。 |
| `BRAVE_SEARCH_API_KEY` または custom `web.search.api_key_env` | Brave WebSearch key。 | WebSearch は configured env 名だけを読み、missing / empty の場合は fail closed する。 |
| `CODEX_HOME` | Codex OAuth `auth.json` の場所。 | 外部互換用の入力。fallback は `$HOME/.codex`。 |
Credential env var は interoperability のために現時点では残っているが、長期的に望ましい secret mechanism ではない。現時点では適切なら `auth.file` を優先し、今後は typed secret reference へ寄せる。credential UX のために implicit `.env` loading を追加しないこと。project secret を漏らしやすく、profile ごとの credential model とも相性が悪い。
## Build / example / test 変数
これらは通常の application configuration ではない。test-only の user-facing env var は supported surface として立てず、既存の `INSOMNIA_TEST_*` も削除する。test が public env behavior を検証する必要がある場合だけ、shared guard / test-support crate で process environment mutation を閉じ込める。
| 変数 | Context | 備考 |
| --- | --- | --- |
| `CARGO_MANIFEST_DIR`, `OUT_DIR` | build / test resource lookup。 | build script や fixture/resource lookup で使う。 |
| `PATH` | test / dev command lookup。 | helper executable を探す場合だけ使う。 |
| `TMPDIR` | shell script / test。 | `tickets.sh` が temporary file に使う。 |
| `RUST_LOG` | example / dev diagnostics。 | example CLI が tracing setup 経由で読む場合がある。 |
| `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY` などの provider example vars | `llm-worker` や Pod example / fixture recorder。 | example code が `dotenv::dotenv().ok()` を呼ぶことがある。通常の `insomnia` runtime startup には適用されない。 |
## deprecated または意図的に存在しない surface
- `INSOMNIA_USER_MANIFEST` は通常の profile-based Pod/TUI startup の一部ではない。one-file manifest の debug / compatibility path には `insomnia pod --manifest <PATH>` を使う。
- ambient `.insomnia/manifest.toml` discovery は通常の fresh startup の一部ではない。
- `INSOMNIA_POD_COMMAND` は single-binary 化に伴って削除する。Pod runtime は `insomnia pod ...` の typed command として起動する。
- `INSOMNIA_TEST_*` のような test-only 環境変数は supported surface にしない。既存利用も削除する。
- `insomnia-pod` は installed command ではない。Pod runtime は `insomnia pod ...` から起動する。
- 通常 runtime は `.env` ファイルを load しない。
## 整理方針
環境変数に関わるコードを触る場合は、以下を優先する。
1. test-only env var を削除し、public env behavior を検証する test だけを shared guard / test-support crate に集約する。
2. path resolution は `manifest::paths` に集約し、path precedence rule を別の場所で重複実装しない。
3. credential source は resolved config 上で明示し、process-env convention を増やすより typed secret reference へ寄せる。encrypted secret store 導入時に credential env var 依存を削除する。
4. `INSOMNIA_POD_COMMAND` は削除し、Pod runtime 起動は `current_exe() + ["pod"]` の typed command に一本化する。
5. fallback env は独立した設定項目として増やさず、対応する main key の解決順として文書化する。
6. 空の env value は、変数 category に応じて unset / invalid のどちらとして扱うかを一貫させ、新しい supported variable を追加する場合は挙動を文書化する。
7. 外部 process integration が env inheritance / filtering を必要とする場合は、ambient な inherited process state に頼らず、明示的な policy boundary として設計する。

View File

@ -26,14 +26,14 @@ return profile {
Run an explicit path with:
```sh
insomnia-pod --profile ./coder.lua
insomnia pod --profile ./coder.lua
# or through the TUI fresh-spawn dialog
insomnia --profile ./coder.lua
```
`--profile` accepts an explicit path, `path:<path>`, a discovered profile name, `default`, or a source-qualified name such as `project:coder`, `user:coder`, or `builtin:coder`. Path-like values containing `/`, starting with `.`, or ending in `.lua` are explicit paths. ``.nix` paths are no longer supported as profiles and fail with a diagnostic that points users at Lua profiles or `--manifest`.
`--profile` conflicts with `insomnia-pod --manifest` and with restore/session/adopt modes. Use `--profile-pod-name <name>` when a launcher needs a creation-time Pod name override without invoking `--pod` restore semantics. Profile evaluation is a creation-time path; Pod resume restores saved Pod state/resolved snapshots rather than re-evaluating the profile source.
`--profile` conflicts with `insomnia pod --manifest` and with restore/session/adopt modes. Use `--profile-pod-name <name>` when a launcher needs a creation-time Pod name override without invoking `--pod` restore semantics. Profile evaluation is a creation-time path; Pod resume restores saved Pod state/resolved snapshots rather than re-evaluating the profile source.
## Controlled Lua environment
@ -81,7 +81,7 @@ The fresh-spawn TUI also uses discovery. The new Pod dialog defaults to the sele
## One-file manifests
`insomnia-pod --manifest <PATH>` remains as an explicit compatibility/debug path. It reads exactly that TOML file, resolves relative paths against the file's parent directory, merges builtin defaults, and validates through the same `PodManifestConfig -> PodManifest` boundary as profile artifacts. It does not load user or project `manifest.toml` files and conflicts with `--profile`.
`insomnia pod --manifest <PATH>` remains as an explicit compatibility/debug path. It reads exactly that TOML file, resolves relative paths against the file's parent directory, merges builtin defaults, and validates through the same `PodManifestConfig -> PodManifest` boundary as profile artifacts. It does not load user or project `manifest.toml` files and conflicts with `--profile`.
Ambient user/project `manifest.toml` cascade startup has been removed. Normal fresh spawns use profile discovery/default selection, with `profiles.toml` acting only as a profile registry/default selector.

View File

@ -5,7 +5,7 @@
#
# このファイル形式は低レベル runtime manifest。通常起動は profile discovery/default
# (`profiles.toml` と bundled builtin profile) から manifest を生成する。
# `insomnia-pod --manifest <path>` の one-file compatibility/debug mode では、
# `insomnia pod --manifest <path>` の one-file compatibility/debug mode では、
# 指定した TOML 1 枚に builtin defaults を merge し、required validation を行う。
# user/project `manifest.toml` を暗黙に merge する通常起動 cascade は使わない。
#

View File

@ -1,6 +1,6 @@
# Nix package
INSOMNIA provides a flake package for installing the user-facing Pod CLI and TUI binaries without relying on a source checkout at runtime.
INSOMNIA provides a flake package for installing the user-facing `insomnia` command without relying on a source checkout at runtime. The Pod runtime still runs as a separate process through `insomnia pod ...`; the installed package does not expose a separate `insomnia-pod` command.
## Build
@ -10,21 +10,21 @@ From the repository root:
nix build .#
```
The default package is implemented by `package.nix` and builds the Cargo packages `pod` and `tui` as installed binaries `insomnia-pod` and `insomnia`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
The default package is implemented by `package.nix` and builds the Cargo package `tui` as the installed binary `insomnia`. The `pod` crate remains a library dependency that provides the Pod runtime entrypoint used by `insomnia pod ...`. The derivation uses the checked-in `Cargo.lock`, so Cargo dependencies are fetched by the normal Nix Rust packaging path instead of by network access during the build.
The package output contains:
- `bin/insomnia-pod` — Pod CLI / runtime process.
- `bin/insomnia` — terminal UI.
- `bin/insomnia` — terminal UI and `insomnia pod ...` runtime entrypoint.
- `share/insomnia/resources/` — bundled runtime resources, including `resources/prompts/`.
- `share/doc/insomnia/nix.md` — this document.
- `share/doc/insomnia/environment.md` — environment-variable policy and supported variables.
## Run
After `nix build`:
```sh
./result/bin/insomnia-pod --help
./result/bin/insomnia pod --help
./result/bin/insomnia
```
@ -32,14 +32,14 @@ With flakes:
```sh
nix run .#insomnia
nix run .#insomnia-pod -- --help
nix run .#insomnia -- pod --help
```
`nix run .#` defaults to the TUI.
## Configuration discovery
The Nix package does not put user configuration, sessions, sockets, or other mutable state in the Nix store. The installed binaries keep the same path semantics as non-Nix builds:
The Nix package does not put user configuration, sessions, sockets, or other mutable state in the Nix store. The installed binary keeps the same path semantics as non-Nix builds:
| Purpose | Override | `INSOMNIA_HOME` fallback | XDG / default fallback |
| --- | --- | --- | --- |
@ -47,14 +47,15 @@ The Nix package does not put user configuration, sessions, sockets, or other mut
| Persistent data (`sessions/`, Pod metadata) | `INSOMNIA_DATA_DIR` | `$INSOMNIA_HOME` | `$HOME/.insomnia` |
| Runtime state (sockets, lock files, live registry) | `INSOMNIA_RUNTIME_DIR` | `$INSOMNIA_HOME/run` | `$XDG_RUNTIME_DIR/insomnia`, then `$HOME/.insomnia/run` |
Normal fresh startup is profile-based. The package ships a builtin default profile, user/project `profiles.toml` files may select or define profiles, and `insomnia-pod --manifest <PATH>` remains a one-file compatibility/debug input. `INSOMNIA_USER_MANIFEST` and ambient `.insomnia/manifest.toml` discovery are not part of normal Pod/TUI startup.
Normal fresh startup is profile-based. The package ships a builtin default profile, user/project `profiles.toml` files may select or define profiles, and `insomnia pod --manifest <PATH>` remains a one-file compatibility/debug input. `INSOMNIA_USER_MANIFEST` and ambient `.insomnia/manifest.toml` discovery are not part of normal Pod/TUI startup. See [`environment.md`](environment.md) for the environment-variable policy; new configuration should prefer profiles/manifests/config files over additional environment variables.
## Validation
The package derivation has a credential-free install check that verifies:
- `insomnia-pod --help` starts successfully.
- `insomnia pod --help` starts successfully.
- `insomnia` is installed and reaches argument parsing.
- `bin/insomnia-pod` is not installed.
- bundled prompt resources and this Nix usage document are present in the output.
For full validation before handing changes to review, run:
@ -63,12 +64,13 @@ For full validation before handing changes to review, run:
nix build .#
nix flake check
cargo fmt --check
cargo check -p tui -p pod -p client
```
This packaging change does not require provider credentials. A Rust `cargo check` is only needed if Rust source or runtime path semantics are changed.
These checks do not require provider credentials.
## Known limitations
- The package currently installs the TUI and Pod CLI only; development-only wrappers from `devshell.nix` are not part of the installable package.
- The package currently installs only the `insomnia` command; development-only wrappers from `devshell.nix` are not part of the installable package.
- The TUI does not currently expose a conventional `--help` / `--version` CLI path, so the package smoke check uses an argument-parse failure path for the TUI rather than launching an interactive session.
- Bundled resources are installed under `share/insomnia/resources/` for packaging completeness and inspection. Built-in prompt/resource loading remains governed by the existing application code and user/project override rules.

View File

@ -172,7 +172,7 @@ unique であれば workspace を指定しなくて済む。
## Daemon-less リモート Pod 生成SSH-only モデル)
リモートホスト上の Pod 生成は **daemon 無しで SSH だけで成立する**
remote 側に必要なのは `insomnia-pod` バイナリと SSH アクセスのみ。
remote 側に必要なのは `insomnia` バイナリと SSH アクセスのみ。
### 前提
@ -182,7 +182,7 @@ remote 側に必要なのは `insomnia-pod` バイナリと SSH アクセスの
- insomnia が転送するのは**セッション(会話履歴)と manifest overlay**
だけ。コードベースの同期は外部に委ねる
- コンテナ内で動かすか bare metal で動かすかも insomnia は問わない。
`insomnia-pod` バイナリが動くホストの fs 上で活動する主体がある、
`insomnia` バイナリが動くホストの fs 上で活動する主体がある、
それだけが前提
### フロー
@ -193,7 +193,7 @@ host_a (spawner) host_b (remote)
├── ssh: session データを転送 ────────→ ファイル書き込み
├── ssh: profile / one-file manifest 入力を転送 ─→ 必要ならファイル書き込み
├── ssh: `insomnia-pod --profile ... &` ───────→ Pod プロセス起動、socket 作成
├── ssh: `insomnia pod --profile ... &` ───────→ Pod プロセス起動、socket 作成
├── ssh -L: socket を tunnel ─────────→ Pod B の unix socket
└── localhost:tunnel に接続 ──────────→ Method::Run / Event stream
@ -209,7 +209,7 @@ tar cz session/ | ssh insomnia@host-b "tar xz -C ~/workspaces/task-123/store"
scp profile.lua insomnia@host-b:~/workspaces/task-123/profile.lua
# 2. Pod を起動detach
ssh insomnia@host-b "insomnia-pod --store ~/workspaces/task-123/store \
ssh insomnia@host-b "insomnia pod --store ~/workspaces/task-123/store \
--profile ~/workspaces/task-123/profile.lua &"
# 3. socket を tunnel で引っ張る

View File

@ -156,12 +156,12 @@ Profile and one-file Manifest CLI paths currently use builtin prompt assets only
The rendered instruction body is followed by fixed Rust-provided sections for working boundaries and, when present, `AGENTS.md`. User templates cannot remove the scope section.
## `insomnia-pod` CLI
## `insomnia pod` CLI
Normal fresh startup uses profile discovery/default selection:
```text
insomnia-pod [--profile <selector>] [--profile-pod-name <name>] [-s/--store <path>]
insomnia pod [--profile <selector>] [--profile-pod-name <name>] [-s/--store <path>]
```
| Flag | Description |
@ -173,8 +173,8 @@ insomnia-pod [--profile <selector>] [--profile-pod-name <name>] [-s/--store <pat
Restore/attach uses Pod/session state and does not re-evaluate profile sources.
```text
insomnia-pod --pod <name>
insomnia-pod --session <uuid>
insomnia pod --pod <name>
insomnia pod --session <uuid>
```
Spawn children use hidden `--spawn-config-json`, `--adopt`, and `--callback <path>` flags. These are internal handoff details used by `SpawnPod` after the parent has allocated scope and prepared the child config.

View File

@ -29,7 +29,6 @@
apps.default = mkApp "insomnia" "Run the INSOMNIA terminal UI";
apps.insomnia = mkApp "insomnia" "Run the INSOMNIA terminal UI";
apps.insomnia-pod = mkApp "insomnia-pod" "Run the INSOMNIA Pod CLI";
checks.default = insomnia;

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-8TAJLV7+7Th4o5Jpsyqz+n9kiuB0FO6qxGi559otfko=";
cargoHash = "sha256-fisV77ZqAPsI0eLZIqw06HTj1CfmnL3NBHhjruZPZUE=";
depsExtraArgs = {
# nixpkgs 25.11's fetchCargoVendor still uses crates.io's API
@ -82,8 +82,6 @@ rustPlatform.buildRustPackage rec {
);
cargoBuildFlags = [
"-p"
"pod"
"-p"
"tui"
];
@ -95,6 +93,7 @@ rustPlatform.buildRustPackage rec {
postInstall = ''
install -Dm644 docs/nix.md "$out/share/doc/insomnia/nix.md"
install -Dm644 docs/environment.md "$out/share/doc/insomnia/environment.md"
mkdir -p "$out/share/insomnia"
cp -R resources "$out/share/insomnia/resources"
'';
@ -103,8 +102,9 @@ rustPlatform.buildRustPackage rec {
installCheckPhase = ''
runHook preInstallCheck
"$out/bin/insomnia-pod" --help >/dev/null
"$out/bin/insomnia" pod --help >/dev/null
test -x "$out/bin/insomnia"
test ! -e "$out/bin/insomnia-pod"
if "$out/bin/insomnia" --session not-a-uuid 2>insomnia.err; then
echo "insomnia unexpectedly accepted an invalid --session value" >&2
exit 1
@ -113,6 +113,7 @@ rustPlatform.buildRustPackage rec {
test -d "$out/share/insomnia/resources/prompts"
test -f "$out/share/doc/insomnia/nix.md"
test -f "$out/share/doc/insomnia/environment.md"
runHook postInstallCheck
'';

View File

@ -2,12 +2,12 @@
id: 20260531-022821-pod-tool-surface-restore-list
slug: pod-tool-surface-restore-list
title: Pod tools: unify pod listing and rename restore operation
status: open
status: closed
kind: task
priority: P2
labels: [pod, tools, orchestration]
created_at: 2026-05-31T02:28:21Z
updated_at: 2026-05-31T02:29:17Z
updated_at: 2026-05-31T03:00:15Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1 @@
Simplified the LLM-facing Pod tool surface: `ListPods` now uses visibility/state-backed listing semantics, `ListVisiblePods` and `InspectPod` are removed from the active tool/protocol surface, and `AttachOrRestorePod` is renamed to `RestorePod`. TUI/protocol/docs/tests were updated, external review approved, and main workspace validation passed.

View File

@ -0,0 +1,105 @@
<!-- event: create author: tickets.sh at: 2026-05-31T02:28:21Z -->
## Created
Created by tickets.sh create.
---
<!-- event: decision author: hare at: 2026-05-31T02:29:17Z -->
## Decision
Decision note:
- `ListVisiblePods` should be folded into `ListPods` rather than kept as a separate tool.
- `InspectPod` is not needed as an LLM-facing tool; `ListPods` should provide enough state/detail, and action should go through restore/send/read/stop tools.
- `AttachOrRestorePod` should become `RestorePod`. The current "attach" branch only observes an already-live socket and returns status/socket data; it does not mean a persistent attachment or comm-registry materialization, so the name is misleading.
- If deeper semantics are needed later, define them explicitly as comm-registration/materialization rather than using "attach" implicitly.
---
<!-- event: implementation_report author: hare at: 2026-05-31T02:59:12Z -->
## Implementation report
Implementation report from coder Pod `pod-tool-surface-coder-20260531`:
- Branch: `pod-tool-surface-restore-list`
- Commit: `5472cec` (`pod: simplify pod tool surface`)
- Changed files: `crates/pod/src/controller.rs`, `crates/pod/src/discovery.rs`, `crates/pod/src/spawn/comm_tools.rs`, `crates/pod/src/spawn/registry.rs`, `crates/pod/src/spawn/tool.rs`, `crates/pod/tests/pod_comm_tools_test.rs`, `crates/protocol/src/lib.rs`, `crates/tui/src/app.rs`, `crates/tui/src/picker.rs`, `docs/architecture.md`.
- Simplified LLM-facing Pod tools to `ListPods` / `RestorePod`.
- Removed `ListVisiblePods`, `InspectPod`, and `AttachOrRestorePod` from active tool/protocol identifiers.
- Moved old `ListVisiblePods` state-backed visibility semantics into `ListPods`.
- Removed old spawned-registry-only `ListPods` comm tool and added `comm_registry` information to state-backed Pod listing output.
- `RestorePod` now reports already-live Pods or restores visible stopped/restorable Pods without using attach terminology.
- TUI protocol handling and architecture docs were updated.
Validation reported by coder:
- `cargo fmt --check` passed
- `cargo check -p pod -p protocol` passed with existing `llm-worker` warning only
- `cargo test -p protocol pod_discovery` passed
- `cargo test -p pod state_backed_visibility_and_restore_planning` passed
- `cargo test -p pod --test pod_comm_tools_test` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
Unresolved issues: none.
---
<!-- event: review author: hare at: 2026-05-31T02:59:12Z status: approve -->
## Review: approve
External review by reviewer Pod `pod-tool-surface-reviewer-20260531`: approve.
Reviewer summary:
- `ListPods` is now the state/visibility-aware listing tool.
- `RestorePod` replaces old attach/restore naming.
- Active Rust source no longer contains `ListVisiblePods`, `InspectPod`, or `AttachOrRestorePod` tool/protocol identifiers.
- Visibility remains self + durable spawned children + live in-memory spawned registry, not host-wide enumeration.
- `RestorePod` rejects non-visible/missing/non-restorable Pods, reports already-live visible Pods, and only restores when state is restorable and unlocked.
- Existing `SpawnPod`, `SendToPod`, `ReadPodOutput`, and `StopPod` remain coherent; the old comm-registry-only `ListPods` tool was removed rather than retained as alias.
- TUI/protocol/docs were updated consistently.
Blockers: none.
Non-blocking follow-ups:
- `crates/pod/src/controller.rs` had a stale comment saying “four communication tools” after the comm tool count changed; not behavior-affecting.
- Run `cargo check -p tui` before merge because TUI files changed.
- A small assertion that removed tool names are absent from the registry would satisfy the ticket wording more literally, but reviewer did not consider it blocking.
---
<!-- event: implementation_report author: hare at: 2026-05-31T03:00:14Z -->
## Implementation report
Main workspace validation after merge:
- `cargo fmt --check` passed
- `cargo check -p pod -p protocol -p tui` passed with pre-existing dead-code warnings in `llm-worker` and `tui`
- `cargo test -p protocol pod_discovery` passed
- `cargo test -p pod state_backed_visibility_and_restore_planning` passed
- `cargo test -p pod --test pod_comm_tools_test` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
---
<!-- event: close author: hare at: 2026-05-31T03:00:15Z status: closed -->
## Closed
Simplified the LLM-facing Pod tool surface: `ListPods` now uses visibility/state-backed listing semantics, `ListVisiblePods` and `InspectPod` are removed from the active tool/protocol surface, and `AttachOrRestorePod` is renamed to `RestorePod`. TUI/protocol/docs/tests were updated, external review approved, and main workspace validation passed.
---

View File

@ -0,0 +1,63 @@
---
id: 20260531-043239-insomnia-pod-subcommand-runtime
slug: insomnia-pod-subcommand-runtime
title: CLI: add insomnia pod runtime entrypoint
status: closed
kind: task
priority: P2
labels: [cli, pod, nix]
created_at: 2026-05-31T04:32:39Z
updated_at: 2026-05-31T04:50:14Z
assignee: null
legacy_ticket: null
---
## Background
Parent/umbrella ticket: `single-binary-insomnia-cli`.
The project currently has two installed command names:
- `insomnia` from the `tui` package for user-facing CLI/TUI/headless commands;
- `insomnia-pod` from the `pod` package for Pod runtime processes.
The target direction is one primary executable, `insomnia`, with `insomnia pod ...` as the Pod runtime entrypoint. Pod runtime remains a separate process when spawned; only the binary/entrypoint is unified.
This ticket is the first implementation step. It should add the new `insomnia pod ...` runtime entrypoint and share Pod runtime startup code, without yet requiring all internal spawn paths or packaging to drop `insomnia-pod` in the same diff.
## Requirements
- Extract the current `crates/pod/src/main.rs` runtime startup into a library-callable entrypoint in the `pod` crate.
- Keep existing Pod runtime flags/semantics.
- Preserve `INSOMNIA-READY` stderr handshake and detached process behavior.
- Keep the existing `insomnia-pod` binary only as a temporary transition wrapper if needed for tests/current spawn defaults.
- Do not present it as the long-term compatibility alias.
- Do not change Nix installed command set in this ticket unless it is mechanically necessary.
- Add `insomnia pod ...` to the existing `insomnia` binary in the `tui` crate.
- `insomnia pod --help` should reach the Pod runtime parser/help.
- `insomnia pod <runtime flags>` should invoke the same library entrypoint as the old runtime path.
- This means `insomnia pod` becomes reserved as a subcommand; a Pod literally named `pod` can still be addressed through explicit `--pod pod` if needed.
- Preserve existing user-facing TUI/CLI behavior for non-`pod` commands:
- `insomnia memory lint` remains headless and returns before terminal/TUI/Pod side effects;
- existing `insomnia <pod-name>` / `--pod` / `--multi` / resume behavior remains unchanged except for the reserved `pod` subcommand.
- Do not yet switch `client::spawn_pod`, `SpawnPod`, or `RestorePod` default command resolution to `insomnia pod` unless doing so is small and fully tested.
- If not switched, create/fill a follow-up note for command resolution `program + prefix_args` migration.
- Do not rename the `tui` package/crate in this ticket.
## Non-goals
- Removing `insomnia-pod` from packaging in this first step.
- Keeping `insomnia-pod` as a long-term alias.
- Merging Pod controller into the TUI process.
- Reworking Pod protocol or CLI UX beyond the new runtime subcommand.
- Feature-gating TUI dependencies.
## Acceptance criteria
- `insomnia pod --help` works and uses the Pod runtime parser/help.
- `insomnia-pod --help` still works if the temporary binary remains.
- `insomnia memory lint` still runs headlessly without TUI/raw-terminal/Pod startup side effects.
- Existing TUI parser tests are updated for the reserved `pod` subcommand and preserve other positional Pod-name behavior.
- Pod runtime startup code has one shared library path rather than duplicated main logic.
- Follow-up work for switching internal spawn defaults and removing `insomnia-pod` packaging is recorded if not completed here.
- `cargo fmt --check`, focused `cargo test` for affected TUI/Pod CLI parsing, `cargo check -p tui -p pod -p client`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1 @@
Added the first single-binary migration step: Pod runtime startup is now available as a shared `pod::entrypoint` library path, the existing `insomnia-pod` binary is a temporary thin wrapper, and the user-facing `insomnia` binary now supports `insomnia pod ...` with the same Pod runtime parser/help. Existing internal spawn defaults and Nix packaging are intentionally unchanged for this step. External review approved and validation passed.

View File

@ -0,0 +1,112 @@
<!-- event: create author: tickets.sh at: 2026-05-31T04:32:39Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T04:33:28Z -->
## Plan
Implementation plan:
1. Move current `crates/pod/src/main.rs` startup logic behind a `pod` crate library entrypoint, leaving the existing binary as a thin temporary wrapper.
2. Add a reserved `pod` subcommand to the `insomnia` binary that delegates to the same Pod runtime entrypoint.
3. Keep internal spawn defaults and Nix installed commands unchanged in this first step unless the change is smaller than expected; the long-term plan is still to remove `insomnia-pod` after internal callers use `insomnia pod`.
4. Add/update parser tests so `insomnia pod` is reserved and `insomnia --pod pod` remains available for a Pod literally named `pod`.
5. Validate focused CLI/parser/runtime help behavior and affected crates.
---
<!-- event: implementation_report author: hare at: 2026-05-31T04:46:52Z -->
## Implementation report
Implementation report from coder Pod `insomnia-pod-subcommand-coder-20260531`:
- Branch: `insomnia-pod-subcommand-runtime`
- Commit: `544257b` (`cli: add insomnia pod runtime entrypoint`)
- Extracted current Pod runtime startup into `pod::entrypoint` with `run_cli()` / `run_cli_from(...)` sharing one `run_cli_inner(...)` path.
- Kept existing `insomnia-pod` binary as a thin temporary wrapper around the shared entrypoint.
- Added reserved `insomnia pod ...` dispatch in the user-facing `insomnia` binary.
- `insomnia pod --help` reaches the Pod runtime parser with `Usage: insomnia pod ...`.
- `insomnia memory lint` remains a headless path before TUI/raw-terminal/Pod side effects.
- Pod named `pod` remains addressable with explicit `--pod pod`.
- Internal spawn default migration and Nix removal of `insomnia-pod` were intentionally left for follow-up phases.
Validation reported by coder:
- `cargo fmt --check` passed
- `cargo test -p tui parse_` passed
- `cargo test -p pod subcommand_help_uses_insomnia_pod_invocation` passed
- `cargo run -q -p tui -- pod --help` + grep passed
- `cargo run -q -p pod --bin insomnia-pod -- --help` + grep passed
- `cargo run -q -p tui -- memory lint` passed
- `cargo check -p tui -p pod -p client` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
Unresolved follow-ups:
- Switch internal spawn/restore defaults to `insomnia pod` via typed `program + prefix_args` command resolution.
- Remove `insomnia-pod` from packaging once internal callers no longer depend on it.
---
<!-- event: review author: hare at: 2026-05-31T04:46:53Z status: approve -->
## Review: approve
External review by reviewer Pod `insomnia-pod-subcommand-reviewer-20260531`: approve.
Reviewer summary:
- The existing `insomnia-pod` runtime startup is now a library-callable `pod::entrypoint` path.
- Both the temporary `insomnia-pod` wrapper and the new `insomnia pod ...` path call the same runtime entrypoint.
- `insomnia pod --help` reaches the Pod runtime parser/help with the appropriate invocation name.
- Existing Pod runtime flags, semantics, and `INSOMNIA-READY` stderr handshake are preserved.
- `insomnia memory lint` remains headless and returns before terminal/TUI setup.
- Existing CLI behavior is preserved except that leading positional `pod` is now a reserved subcommand; explicit `--pod pod` remains available.
- Spawn/Restore default migration and Nix removal were correctly left for later phases.
Blockers: none.
Non-blocking follow-ups:
- Migrate `client::spawn_pod`, `SpawnPod`, and restore/discovery paths to typed `program + prefix_args` so default runtime startup can become `insomnia pod ...`.
- Add installed-binary smoke coverage once CLI E2E exists.
---
<!-- event: implementation_report author: hare at: 2026-05-31T04:50:13Z -->
## Implementation report
Main workspace validation after merge:
- `cargo fmt --check` passed
- `cargo test -p tui parse_` passed
- `cargo test -p pod subcommand_help_uses_insomnia_pod_invocation` passed
- `cargo run -q -p tui -- pod --help` smoke passed (`Usage: insomnia pod`, Pod runtime flags present)
- `cargo run -q -p pod --bin insomnia-pod -- --help` smoke passed (`Usage: insomnia-pod`, Pod runtime flags present)
- `cargo run -q -p tui -- memory lint` reached the headless command path. It exited `1` because the current generated `.insomnia/memory` contains existing lint errors, which is the expected lint-failure exit code; no TUI/Pod startup side effects were observed.
- `cargo check -p tui -p pod -p client` passed with pre-existing dead-code warnings
- `./tickets.sh doctor` passed
- `git diff --check` passed
---
<!-- event: close author: hare at: 2026-05-31T04:50:14Z status: closed -->
## Closed
Added the first single-binary migration step: Pod runtime startup is now available as a shared `pod::entrypoint` library path, the existing `insomnia-pod` binary is a temporary thin wrapper, and the user-facing `insomnia` binary now supports `insomnia pod ...` with the same Pod runtime parser/help. Existing internal spawn defaults and Nix packaging are intentionally unchanged for this step. External review approved and validation passed.
---

View File

@ -0,0 +1,55 @@
---
id: 20260531-045034-spawn-through-insomnia-pod-subcommand
slug: spawn-through-insomnia-pod-subcommand
title: CLI: spawn Pods through insomnia pod runtime
status: closed
kind: task
priority: P2
labels: [cli, pod, client, nix]
created_at: 2026-05-31T04:50:34Z
updated_at: 2026-05-31T05:27:04Z
assignee: null
legacy_ticket: null
---
## Background
Parent/umbrella ticket: `single-binary-insomnia-cli`.
`insomnia-pod-subcommand-runtime` added `insomnia pod ...` as a Pod runtime entrypoint while keeping the old `insomnia-pod` binary as a temporary thin wrapper. Internal Pod spawning and restore paths still default to invoking `insomnia-pod` as an executable-only command.
The next single-binary migration step is to make internal callers able to spawn Pod runtime processes via `program + prefix_args`, and then switch the default runtime command to the current `insomnia` executable with `pod` as the prefix argument.
## Requirements
- Introduce a typed Pod runtime command representation instead of shell-string parsing.
- Example shape: `{ program: PathBuf, prefix_args: Vec<OsString> }`.
- The default command from the `insomnia` binary should be `current_exe()` + `pod` prefix args.
- Preserve an explicit override mechanism for development/debugging, but do not parse arbitrary shell command strings.
- Switch internal spawn/restore paths that currently call `insomnia-pod` to use the typed runtime command:
- TUI/create/restore paths in `client`/`tui` if applicable;
- `SpawnPod` child creation;
- `RestorePod` / discovery restore flows.
- Preserve detached process behavior and the `INSOMNIA-READY` stderr handshake.
- Preserve devshell/Nix behavior enough that local development still works.
- If `insomnia-pod` is still needed for devshell wrapping in this step, document exactly why and leave removal to the next ticket.
- Keep `insomnia-pod` installed output/package removal out of this ticket unless the migration is already complete and validation is straightforward.
- Do not rename the `tui` package/crate.
- Do not merge Pod controller into the TUI process.
## Non-goals
- Removing `insomnia-pod` from Nix/package outputs as the primary goal. That is the next cleanup once runtime spawning no longer depends on it.
- Changing Pod runtime flags or profile/manifest semantics.
- Changing Pod protocol.
- Large CLI UX redesign.
## Acceptance criteria
- Internal default Pod runtime spawn/restore commands use `insomnia pod ...` through a typed `program + prefix_args` representation.
- `INSOMNIA_POD_COMMAND` or any equivalent override remains safe and documented, or is replaced by a typed override with clear behavior.
- Existing Pod spawning/restore tests pass, with focused tests proving prefix args are included in the spawned command.
- `insomnia pod --help` and existing `insomnia-pod --help` continue to work while the temporary wrapper remains.
- `SpawnPod` and `RestorePod` behavior is unchanged from the user/tool perspective.
- Follow-up for removing `insomnia-pod` packaging is recorded if not completed here.
- `cargo fmt --check`, focused client/pod/tui tests, `cargo check -p client -p pod -p tui`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1 @@
Switched internal Pod runtime spawning/restoration to typed `program + prefix_args` command resolution. Default runtime command now uses the current executable plus `pod`, while `INSOMNIA_POD_COMMAND` remains executable-only and not shell-parsed for development/debug overrides. `client::spawn_pod`, `SpawnPod`, and `RestorePod`/discovery restore paths use the typed command; detached process behavior and `INSOMNIA-READY` handshake are preserved. External review approved and validation passed. `insomnia-pod` packaging removal remains the next phase.

View File

@ -0,0 +1,114 @@
<!-- event: create author: tickets.sh at: 2026-05-31T04:50:34Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T04:51:14Z -->
## Plan
Implementation plan:
1. Find all internal `insomnia-pod` executable resolution paths (`client::spawn_pod`, `SpawnPod`, `RestorePod`/discovery, tests/dev helpers).
2. Introduce a typed runtime command structure with `program` and `prefix_args`, avoiding shell-string parsing.
3. Make the default runtime command for callers running from `insomnia` use `current_exe()` + `pod` prefix args.
4. Preserve explicit override behavior for development/debugging; if the existing env var cannot safely support prefix args, keep it as executable-only and document that it bypasses the unified default.
5. Update tests to assert prefix args are included and existing spawn/restore behavior is unchanged.
6. Leave Nix/package removal of `insomnia-pod` to a follow-up unless this change proves complete and low-risk.
---
<!-- event: implementation_report author: hare at: 2026-05-31T05:25:12Z -->
## Implementation report
Implementation report from coder Pod `spawn-through-insomnia-pod-coder-20260531`:
- Branch: `spawn-through-insomnia-pod-subcommand`
- Commit: `4f622b8` (`cli: spawn pods through insomnia pod`)
- Added new `pod-command` crate with typed `PodRuntimeCommand { program, prefix_args }`.
- Default runtime command now resolves to `current_exe()` + `pod` prefix args, except when running through the temporary legacy `insomnia-pod` wrapper.
- `INSOMNIA_POD_COMMAND` remains executable-only, is not shell-parsed, and bypasses prefix args for development/debug override behavior.
- Migrated internal Pod runtime launches to the typed command:
- `client::spawn_pod`
- `SpawnPod` child process creation
- `PodDiscovery::restore` / `RestorePod`
- TUI spawn/restore callers through `client::spawn_pod`
- Preserved detached process behavior, process groups, socket prediction/probing, and `INSOMNIA-READY` stderr handshake.
- Left `insomnia-pod` package/output removal for the next phase.
Validation reported by coder:
- `cargo fmt --check` passed
- `cargo test -p pod-command` passed
- `cargo test -p client -p pod-command` passed
- `cargo test -p pod --lib discovery::tests` passed
- `cargo test -p pod --test spawn_pod_test` passed
- `cargo test -p tui parse_pod_subcommand_uses_runtime_mode` passed
- `cargo check -p client -p pod -p tui` passed with existing dead-code warnings
- `./tickets.sh doctor` passed
- `git diff --check` passed
Unresolved follow-ups:
- Remove/demote `insomnia-pod` package/output once packaging/devshell/docs are updated.
- Optional stronger future test: capture actual argv of a mock spawned executable in a default-command call site.
---
<!-- event: review author: hare at: 2026-05-31T05:25:12Z status: approve -->
## Review: approve
External review by reviewer Pod `spawn-through-insomnia-pod-reviewer-20260531`: approve.
Reviewer summary:
- The implementation adds typed `PodRuntimeCommand { program, prefix_args }` command resolution.
- Default runtime command is now current executable plus `pod` prefix args, except for the temporary legacy `insomnia-pod` wrapper path.
- `INSOMNIA_POD_COMMAND` remains executable-only and is not shell-parsed.
- `client::spawn_pod`, `SpawnPod`, and `RestorePod`/discovery restore paths now use the typed command.
- Detached process behavior and `INSOMNIA-READY` handshake are preserved.
- Nix/package removal and crate rename are not mixed into this ticket.
Blockers: none.
Non-blocking follow-up:
- Future test could capture actual argv of a mock spawned executable in a default-command call site. Current tests are adequate for this ticket because the typed command composition and existing spawn behavior are covered.
---
<!-- event: implementation_report author: hare at: 2026-05-31T05:27:03Z -->
## Implementation report
Main workspace validation after merge:
- `cargo fmt --check` passed
- `cargo test -p pod-command` passed
- `cargo test -p client -p pod-command` passed
- `cargo test -p pod --lib discovery::tests` passed
- `cargo test -p pod --test spawn_pod_test` passed
- `cargo test -p tui parse_pod_subcommand_uses_runtime_mode` passed
- `cargo check -p client -p pod -p tui` passed with pre-existing dead-code warnings
- `./tickets.sh doctor` passed
- `git diff --check` passed
---
<!-- event: close author: hare at: 2026-05-31T05:27:04Z status: closed -->
## Closed
Switched internal Pod runtime spawning/restoration to typed `program + prefix_args` command resolution. Default runtime command now uses the current executable plus `pod`, while `INSOMNIA_POD_COMMAND` remains executable-only and not shell-parsed for development/debug overrides. `client::spawn_pod`, `SpawnPod`, and `RestorePod`/discovery restore paths use the typed command; detached process behavior and `INSOMNIA-READY` handshake are preserved. External review approved and validation passed. `insomnia-pod` packaging removal remains the next phase.
---

View File

@ -0,0 +1,68 @@
---
id: 20260531-054728-remove-insomnia-pod-binary
slug: remove-insomnia-pod-binary
title: CLI: remove insomnia-pod installed/runtime alias
status: closed
kind: task
priority: P2
labels: [cli, pod, nix, docs]
created_at: 2026-05-31T05:47:28Z
updated_at: 2026-05-31T06:10:39Z
assignee: null
legacy_ticket: null
---
## Background
Parent/umbrella ticket: `single-binary-insomnia-cli`.
Previous phases have already:
- added `insomnia pod ...` as the Pod runtime entrypoint;
- moved internal spawn/restore defaults to typed `current_exe() + ["pod"]` command resolution;
- left `insomnia-pod` as a temporary old binary/package output.
The user decision is that `insomnia-pod` does not need to remain as a compatibility alias. It was never designed as a human-facing command. The next phase is to remove/demote it so the installed package exposes one primary runtime executable: `insomnia`.
## Requirements
- Remove the long-term `insomnia-pod` installed/runtime alias.
- Prefer removing the `insomnia-pod` binary target from the `pod` crate if no active tests/build paths require it.
- The `pod` crate should remain as a library crate providing the Pod runtime entrypoint used by `insomnia pod ...`.
- If a temporary binary target must remain for a narrow reason, document exactly why and do not install/expose it.
- Update Nix packaging:
- build/install only the user-facing `insomnia` binary as the package command;
- remove `apps.insomnia-pod` from `flake.nix`;
- update install checks to use `insomnia pod --help` instead of `insomnia-pod --help`.
- Update devshell behavior:
- remove the `insomnia-pod` wrapper if internal callers no longer need it;
- preserve the “do not pollute `INSOMNIA-READY` stderr handshake with cargo output” property for dev workflows if a wrapper remains necessary;
- document any remaining dev override path (`INSOMNIA_POD_COMMAND`) if still relevant.
- Update docs that describe installed commands / Nix usage / profile manifest CLI examples:
- replace user-facing `insomnia-pod ...` examples with `insomnia pod ...` where current behavior is intended;
- avoid rewriting historical work-item artifacts;
- leave clearly historical docs alone unless they are current user docs.
- Preserve runtime behavior:
- `insomnia pod --help` works;
- Pod spawning/restoration still uses `insomnia pod ...` by default;
- `INSOMNIA_POD_COMMAND` override behavior remains executable-only if still supported, or is intentionally removed with tests/docs updated.
- Do not rename the `tui` package/crate in this ticket.
- Do not merge Pod controller into the TUI process.
## Non-goals
- Crate/package rename from `tui` to `insomnia`.
- Removing the `pod` library crate.
- Changing Pod runtime flags/profile/manifest semantics.
- Changing Pod protocol.
- Large CLI UX redesign.
## Acceptance criteria
- Installed/Nix package exposes `bin/insomnia` as the primary command and no longer exposes `bin/insomnia-pod`.
- `nix build .#insomnia` and install checks pass using `insomnia pod --help`.
- `flake.nix` no longer advertises `apps.insomnia-pod`.
- `insomnia pod --help` and relevant Pod runtime parser tests pass.
- Internal spawn/restore tests still pass after removing the old binary output.
- Current docs no longer instruct users to run `insomnia-pod` as the normal runtime command; use `insomnia pod` instead.
- `cargo fmt --check`, focused pod/tui/client/nix tests or checks, `cargo check -p tui -p pod -p client`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,26 @@
Removed the old `insomnia-pod` installed/runtime alias and completed the next single-binary CLI phase.
Implementation:
- Removed the `insomnia-pod` binary target from the `pod` crate and made it library-only with `autobins = false`.
- Deleted the old `crates/pod/src/main.rs` wrapper while keeping `pod::entrypoint` for `insomnia pod ...`.
- Updated runtime command resolution to default to the current `insomnia` executable plus the `pod` prefix argument, while preserving executable-only `INSOMNIA_POD_COMMAND` override behavior.
- Updated Nix packaging so `.#insomnia` installs `bin/insomnia` and asserts that `bin/insomnia-pod` is absent.
- Removed `apps.insomnia-pod` from `flake.nix` and removed the devshell `insomnia-pod` wrapper.
- Updated current docs to use `insomnia pod ...` instead of the old user-facing `insomnia-pod ...` command.
Review:
- External reviewer `remove-insomnia-pod-reviewer-20260531` approved implementation commit `0d7d3c7bf1e1becb85ca8322e4b72730a91e3513`.
- Review was recorded in `thread.md` before merge.
Validation on develop after merge:
- `cargo fmt --check`
- `cargo test -p tui parse_pod_subcommand_uses_runtime_mode`
- `cargo test -p pod-command`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client`
- `nix build .#insomnia`
- `./result/bin/insomnia pod --help`
- `test ! -e ./result/bin/insomnia-pod`
- `./tickets.sh doctor`
- `git diff --check`

View File

@ -0,0 +1,87 @@
<!-- event: create author: tickets.sh at: 2026-05-31T05:47:28Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T05:48:20Z -->
## Plan
Implementation plan:
1. Remove or demote the `insomnia-pod` Cargo binary target if active code/tests no longer require it; keep `pod` as a library crate.
2. Update Nix packaging/install checks and flake app outputs so the package exposes only `insomnia`; install checks should call `insomnia pod --help`.
3. Update devshell wrapper behavior. Since internal default spawning now uses `current_exe() + ["pod"]`, the old `insomnia-pod` wrapper should be removed unless a focused dev reason remains.
4. Update current user docs (`docs/nix.md`, `docs/pod-factory.md`, manifest/profile docs, architecture docs) from `insomnia-pod` to `insomnia pod`. Do not rewrite historical work-item artifacts.
5. Validate runtime parser/help, internal spawn/restore tests, Nix build, doctor, and diff check.
---
<!-- event: review author: hare at: 2026-05-31T06:08:34Z status: approve -->
## Review: approve
External reviewer: `remove-insomnia-pod-reviewer-20260531`
Reviewed implementation commit: `0d7d3c7bf1e1becb85ca8322e4b72730a91e3513` (`cli: remove insomnia-pod binary output`)
Verdict: approve
Summary:
- The implementation removes the old `insomnia-pod` installed/runtime alias and keeps Pod runtime process separation via `insomnia pod ...`.
- `pod` is now library-only (`autobins = false`), `pod::entrypoint` remains available, and the old `crates/pod/src/main.rs` binary target is gone.
- Nix/package/devshell/docs were updated so the primary installed command is `insomnia`, `apps.insomnia-pod` is no longer advertised, and install checks use `insomnia pod --help` plus a negative check for `bin/insomnia-pod`.
- Internal spawn/restore paths use `PodRuntimeCommand::resolve()` and `prefix_args()` with default `current_exe() + ["pod"]`; `INSOMNIA_POD_COMMAND` remains executable-only.
Requirements mapping:
- Acceptance criteria are satisfied: installed output no longer exposes `bin/insomnia-pod`; `flake.nix` no longer advertises `apps.insomnia-pod`; `insomnia pod --help` remains the runtime entrypoint; relevant tests and docs were updated.
- No hidden non-goal changes were found: no `tui` crate rename, no controller/runtime process merge, and no Pod protocol or runtime flag semantic change.
Blockers: none.
Non-blocking follow-ups:
- A test name in `crates/pod/src/entrypoint.rs` still contains old-ish wording (`subcommand_help_uses_insomnia_pod_invocation`) even though the test contents use `insomnia pod`; renaming it later would improve readability.
- `docs/plan/network-peering.md` still has generic wording around a "pod binary"; this is not a user-facing `insomnia-pod` command example and is not blocking.
Validation adequacy:
- Coder-reported validation covers fmt, parser/unit/integration tests, `cargo check`, Nix build/install smoke, `nix flake check`, ticket doctor, and diff whitespace check.
- Reviewer performed read-only static review, clean status/diff inspection, residual `insomnia-pod` search, and `git diff --check`.
---
<!-- event: close author: hare at: 2026-05-31T06:10:39Z status: closed -->
## Closed
Removed the old `insomnia-pod` installed/runtime alias and completed the next single-binary CLI phase.
Implementation:
- Removed the `insomnia-pod` binary target from the `pod` crate and made it library-only with `autobins = false`.
- Deleted the old `crates/pod/src/main.rs` wrapper while keeping `pod::entrypoint` for `insomnia pod ...`.
- Updated runtime command resolution to default to the current `insomnia` executable plus the `pod` prefix argument, while preserving executable-only `INSOMNIA_POD_COMMAND` override behavior.
- Updated Nix packaging so `.#insomnia` installs `bin/insomnia` and asserts that `bin/insomnia-pod` is absent.
- Removed `apps.insomnia-pod` from `flake.nix` and removed the devshell `insomnia-pod` wrapper.
- Updated current docs to use `insomnia pod ...` instead of the old user-facing `insomnia-pod ...` command.
Review:
- External reviewer `remove-insomnia-pod-reviewer-20260531` approved implementation commit `0d7d3c7bf1e1becb85ca8322e4b72730a91e3513`.
- Review was recorded in `thread.md` before merge.
Validation on develop after merge:
- `cargo fmt --check`
- `cargo test -p tui parse_pod_subcommand_uses_runtime_mode`
- `cargo test -p pod-command`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client`
- `nix build .#insomnia`
- `./result/bin/insomnia pod --help`
- `test ! -e ./result/bin/insomnia-pod`
- `./tickets.sh doctor`
- `git diff --check`
---

View File

@ -0,0 +1,38 @@
---
id: 20260531-064550-rename-pod-command-crate-to-insomnia
slug: rename-pod-command-crate-to-insomnia
title: CLI: rename pod-command crate to insomnia
status: closed
kind: task
priority: P2
labels: [cli, pod, cargo]
created_at: 2026-05-31T06:45:50Z
updated_at: 2026-05-31T06:49:44Z
assignee: null
legacy_ticket: null
---
## Background
The single-binary CLI migration introduced a small helper crate named `pod-command` for typed Pod runtime command resolution (`current_exe() + ["pod"]`, plus executable-only `INSOMNIA_POD_COMMAND` override).
The user decided this helper crate should be named after the installed/runtime binary surface, `insomnia`, rather than `pod-command`.
## Requirements
- Rename the `pod-command` crate/package to `insomnia`.
- Keep the existing typed runtime command behavior intact:
- default runtime command is current executable plus `pod` prefix arg;
- `INSOMNIA_POD_COMMAND` remains executable-only and is not shell parsed.
- Update workspace membership, workspace dependencies, crate dependencies, imports, lockfile, and tests.
- Do not rename the `tui` package/crate in this ticket.
- Do not reintroduce an `insomnia-pod` binary/alias.
- Do not change Pod runtime process model, flags, or protocol.
## Acceptance criteria
- No active code or Cargo metadata references the `pod-command` crate/package.
- The helper crate is available as package/crate `insomnia`.
- Existing spawn/restore code uses the renamed crate without behavior changes.
- Focused tests for the renamed helper crate pass.
- `cargo fmt --check`, relevant `cargo test`/`cargo check`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,20 @@
Renamed the runtime command helper crate/package from `pod-command` to `insomnia` to align the shared helper crate with the installed binary name.
Implementation:
- Renamed `crates/pod-command` to `crates/insomnia`.
- Changed package name from `pod-command` to `insomnia`.
- Updated workspace membership and workspace dependencies.
- Updated `client` and `pod` crate dependencies/imports from `pod_command` to `insomnia`.
- Kept `PodRuntimeCommand` behavior unchanged: default runtime command remains current executable plus `pod`, and `INSOMNIA_POD_COMMAND` remains executable-only.
- Updated test helper names to avoid stale `pod_command` wording.
Validation:
- `cargo fmt --check`
- `cargo test -p insomnia`
- `cargo test -p client -p insomnia`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client -p insomnia`
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "pod-command\|pod_command" -- ':!work-items' ':!docs' ':!Cargo.lock' || true`

View File

@ -0,0 +1,35 @@
<!-- event: create author: tickets.sh at: 2026-05-31T06:45:50Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-31T06:49:44Z status: closed -->
## Closed
Renamed the runtime command helper crate/package from `pod-command` to `insomnia` to align the shared helper crate with the installed binary name.
Implementation:
- Renamed `crates/pod-command` to `crates/insomnia`.
- Changed package name from `pod-command` to `insomnia`.
- Updated workspace membership and workspace dependencies.
- Updated `client` and `pod` crate dependencies/imports from `pod_command` to `insomnia`.
- Kept `PodRuntimeCommand` behavior unchanged: default runtime command remains current executable plus `pod`, and `INSOMNIA_POD_COMMAND` remains executable-only.
- Updated test helper names to avoid stale `pod_command` wording.
Validation:
- `cargo fmt --check`
- `cargo test -p insomnia`
- `cargo test -p client -p insomnia`
- `cargo test -p pod --lib discovery::tests`
- `cargo test -p pod --test spawn_pod_test`
- `cargo check -p tui -p pod -p client -p insomnia`
- `./tickets.sh doctor`
- `git diff --check`
- `git grep -n "pod-command\|pod_command" -- ':!work-items' ':!docs' ':!Cargo.lock' || true`
---

View File

@ -0,0 +1,40 @@
---
id: 20260531-082646-document-env-var-policy
slug: document-env-var-policy
title: Docs: document environment variable policy
status: closed
kind: task
priority: P2
labels: [docs, config, security]
created_at: 2026-05-31T08:26:46Z
updated_at: 2026-05-31T08:29:40Z
assignee: null
legacy_ticket: null
---
## Background
Environment variables are currently used for a few practical boundaries: XDG-style path discovery, runtime/socket directories, development overrides, and legacy/provider secret inputs. The user's preference is that this project should avoid environment variables where possible and make any remaining environment-variable surface explicit.
A short investigation found that path resolution is mostly centralized in `manifest::paths`, while auth/web secret envs and test-only env mutation are more scattered. Normal runtime intentionally does not implicitly load `.env` files.
## Requirements
- Add current documentation for environment-variable policy and supported variables.
- State the design preference clearly: avoid new environment variables when manifest/profile/config/typed secret references are better.
- Document the currently supported categories:
- core path/resource discovery;
- runtime/socket/registry discovery;
- Pod runtime command development override;
- provider/WebSearch credential references;
- external compatibility variables such as Codex home;
- test/build/example-only environment variables.
- Clarify that normal runtime must not implicitly load `.env` files.
- Identify cleanup direction without implementing unrelated refactors in this ticket.
## Acceptance criteria
- A user/developer-facing docs page explains environment-variable policy and current variables.
- Existing Nix/config docs link to the new policy page where relevant.
- Documentation does not expose secret values or read ignored secret-like files.
- `./tickets.sh doctor` and `git diff --check` pass.

View File

@ -0,0 +1,13 @@
Documented the environment-variable policy and current supported surface.
Implementation:
- Added `docs/environment.md`.
- Stated the preferred design rule: avoid new environment variables when profile/manifest/config/CLI/typed secret references are better.
- Documented core path variables, runtime/socket variables, the Pod runtime command override, credential env references, external compatibility variables, and build/test/example-only variables.
- Clarified that normal runtime does not implicitly load `.env` files.
- Added cleanup direction for shared test env guards, keeping path resolution centralized, and moving toward typed secret references.
- Linked the new page from `docs/nix.md` and installed it in the Nix package docs.
Validation:
- `./tickets.sh doctor`
- `git diff --check`

View File

@ -0,0 +1,28 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:26:46Z -->
## Created
Created by tickets.sh create.
---
<!-- event: close author: hare at: 2026-05-31T08:29:40Z status: closed -->
## Closed
Documented the environment-variable policy and current supported surface.
Implementation:
- Added `docs/environment.md`.
- Stated the preferred design rule: avoid new environment variables when profile/manifest/config/CLI/typed secret references are better.
- Documented core path variables, runtime/socket variables, the Pod runtime command override, credential env references, external compatibility variables, and build/test/example-only variables.
- Clarified that normal runtime does not implicitly load `.env` files.
- Added cleanup direction for shared test env guards, keeping path resolution centralized, and moving toward typed secret references.
- Linked the new page from `docs/nix.md` and installed it in the Nix package docs.
Validation:
- `./tickets.sh doctor`
- `git diff --check`
---

View File

@ -23,8 +23,8 @@ Related work item: `work-items/open/20260527-000022-manifest-profiles/item.md`.
## Requirements
- Design a typed secret reference format for manifest/profile fields that need credentials.
- Existing env references such as `api_key_env = "BRAVE_SEARCH_API_KEY"` should keep working.
- Add a new encrypted-store reference form, e.g. `api_key_secret = "brave.search.default"` or a more general `SecretRef` enum.
- Existing env references such as `api_key_env = "BRAVE_SEARCH_API_KEY"` may be supported only as a migration/compatibility input during the transition; the target state is to remove credential environment-variable configuration rather than keep it as a normal fallback.
- Secret references must be explicit in resolved config; do not silently read arbitrary `.env` files.
- Add an encrypted local secret store suitable for API keys/tokens.
- Store secrets outside tracked project files by default, under the user data/config directory.
@ -41,9 +41,9 @@ Related work item: `work-items/open/20260527-000022-manifest-profiles/item.md`.
- Show references and metadata, not secret values.
- Consider migration helpers from existing env-var based configuration, but keep migration optional.
- Update credential consumers.
- WebSearch should support encrypted secret refs in addition to env vars.
- Provider API keys/tokens and future hosted/search credentials should be able to use the same mechanism.
- Existing env-var behavior remains as a fallback/compatibility path.
- WebSearch should use encrypted secret refs instead of requiring env vars.
- Provider API keys/tokens and future hosted/search credentials should use the same mechanism.
- Remove env-var credential configuration from the normal supported path once encrypted secret refs and migration diagnostics exist.
- Security and UX constraints.
- Fail closed when a referenced secret is missing or cannot be decrypted.
- Diagnostics should name the missing reference, not the secret value.
@ -52,13 +52,13 @@ Related work item: `work-items/open/20260527-000022-manifest-profiles/item.md`.
## Acceptance criteria
- Manifest/profile schema has a typed credential reference that can point either to an env var or encrypted secret-store entry.
- Manifest/profile schema has a typed credential reference for encrypted secret-store entries; env-var credential inputs are at most transitional migration inputs, not the final supported configuration path.
- Encrypted secret-store files are created outside the repository by default and use authenticated encryption with atomic update behavior.
- A user can add/list/delete a Brave Search API key in the secret store and configure `WebSearch` to use it without exporting an environment variable.
- Resolved configuration and diagnostics never display plaintext secrets.
- Missing/decryption-failed secrets produce clear fail-closed errors.
- Existing env-var based configuration continues to work.
- Documentation explains how profiles reference secrets and how to manage them.
- Existing env-var based credential configuration is either removed or produces an explicit migration diagnostic after encrypted secret references are available.
- Documentation explains how profiles reference secrets, how to manage them, and why credential env vars are no longer the normal path.
- Focused tests cover config parsing/resolution, missing secret diagnostics, no-plaintext serialization/logging paths, and WebSearch secret resolution.
- `cargo fmt --check`
- Relevant manifest/provider/tools/pod tests pass.

View File

@ -1,50 +1,65 @@
---
id: 20260531-005557-single-binary-insomnia-cli
slug: single-binary-insomnia-cli
title: CLI: clarify single-binary insomnia architecture
title: CLI: migrate toward a single insomnia binary
status: open
kind: task
priority: P2
labels: [cli, architecture, nix]
created_at: 2026-05-31T00:55:57Z
updated_at: 2026-05-31T00:56:38Z
updated_at: 2026-05-31T04:32:30Z
assignee: null
legacy_ticket: null
---
## Background
The installed user-facing command is already `insomnia`, while the Cargo package/crate that owns it is still named `tui`. Headless CLI commands such as `insomnia memory lint` are starting to live in that same binary. This is acceptable short-term, but the architecture should be made explicit before more commands accumulate.
The repository currently installs two command names:
The preferred product direction is a single user-facing `insomnia` binary that can run TUI screens and headless commands. The main downside is that headless commands inherit ratatui/crossterm and related dependencies, but binary size is not expected to dominate runtime memory compared with LLM clients, HTTP/TLS, JSON/session/memory processing, and spawned process orchestration. A single binary also improves distribution and “minimal standalone install” ergonomics.
- `insomnia` from the `tui` package: user-facing TUI/CLI entry point, including headless subcommands such as `insomnia memory lint`.
- `insomnia-pod` from the `pod` package: Pod runtime process entry point used by the TUI, restore flows, and `SpawnPod` to start detached Pod controller processes.
The desired direction is one primary installed executable, `insomnia`, that can act as both the user-facing CLI/TUI and the Pod runtime process entry point. Pod runtime should remain a separate process; the unification is about packaging/entrypoint, not merging TUI and Pod controller into one process.
The Pod runtime does not need a separate human-friendly `insomnia-pod` alias. It was never intended as a user-facing command. Prefer an explicit subcommand such as `insomnia pod ...` over a hidden `__pod-runtime` command.
## Requirements
- Decide and document the intended command architecture:
- one installed `insomnia` binary as the primary user-facing entry point;
- headless subcommands under that binary;
- TUI/interactive paths as subcommands or default modes;
- relationship to `insomnia-pod`.
- Refactor only if necessary to keep headless commands cleanly separated from terminal/TUI initialization.
- Ensure headless commands do not initialize ratatui, enter raw mode, connect Pod sockets, or spawn Pod processes unless explicitly requested by that command.
- Evaluate whether to rename the Cargo package/crate from `tui` to `insomnia`.
- If the rename is straightforward, implement it with scoped Nix/docs updates.
- If it is broad/risky, leave the rename as a follow-up and document the boundary.
- Keep installed command names stable unless a separate explicit decision says otherwise.
- Preserve Nix/Home Manager packaging expectations: installing the flake package should put the intended commands on `PATH`.
- Avoid introducing separate binaries for every headless command unless there is a measured startup/binary-size/runtime-memory reason.
- Plan and implement a staged migration toward a single primary installed `insomnia` executable.
- Target CLI shape:
- `insomnia` / `insomnia <pod>` / `insomnia --pod ...` / `insomnia --multi` keep current user-facing behavior;
- `insomnia memory lint ...` remains a headless command;
- `insomnia pod ...` invokes the Pod runtime using the existing Pod runtime flags/semantics.
- Pod runtime remains a detached child process when spawned by TUI/RestorePod/SpawnPod.
- `insomnia-pod` is not kept as a long-term compatibility alias. If a transition step leaves it temporarily, the follow-up/removal boundary must be explicit.
- Refactor Pod runtime startup so both old and new entrypoints, if temporarily present, share one library entrypoint.
- Update internal spawn/restore paths to support executable + prefix args, so `insomnia pod ...` can replace executable-only `insomnia-pod ...` invocations.
- Preserve the `INSOMNIA-READY` stderr handshake and detached process behavior.
- Preserve headless command invariants: headless subcommands must not initialize ratatui/raw terminal, connect Pod sockets, or spawn Pod processes unless requested by that command.
- Preserve Nix/Home Manager packaging expectations and make the intended installed command set explicit.
- Evaluate `tui` package rename separately. It is not required for runtime unification and should not be mixed into early migration phases unless it becomes mechanically unavoidable.
## Suggested phases
1. Extract Pod runtime startup into a library entrypoint and add `insomnia pod ...` dispatch.
2. Extend Pod runtime command resolution to support `program + prefix_args` and switch TUI/RestorePod/SpawnPod defaults to `current_exe() + ["pod"]` where appropriate.
3. Update Nix/devshell/docs so the canonical installed runtime path is `insomnia pod ...`; remove `insomnia-pod` from installed package outputs when internal callers no longer need it.
4. Consider `tui` package/crate rename as a later cleanup only after binary/process unification is stable.
## Non-goals
- Reworking the Pod daemon binary protocol.
- Removing `insomnia-pod` unless a separate architecture decision covers Pod process lifecycle.
- Running Pod controller inside the TUI process.
- Keeping `insomnia-pod` as a long-term public compatibility command.
- Feature-gating ratatui/crossterm for a headless-only build before there is a measured need.
- Large CLI UX redesign beyond clarifying the single-binary structure.
- Large CLI UX redesign beyond the runtime entrypoint migration.
- Renaming `tui` package/crate in the first implementation step.
## Acceptance criteria
- The repository has a clear documented decision or implementation path for the single-binary `insomnia` CLI architecture.
- Existing TUI flows and headless command flows remain tested.
- If crate/package rename is implemented, Cargo/Nix/docs references are updated consistently.
- If crate/package rename is deferred, a precise follow-up or documented rationale explains why.
- `cargo fmt --check`, relevant `cargo test`/`cargo check`, `./tickets.sh doctor`, and `git diff --check` pass.
- The repository has a concrete staged plan and implementation tickets for moving Pod runtime startup under `insomnia pod`.
- First implementation step lands without changing unrelated CLI behavior.
- `insomnia pod --help` reaches the Pod runtime parser.
- Existing headless command behavior (`insomnia memory lint`) remains headless.
- Internal callers can be migrated from `insomnia-pod` to `insomnia pod` through typed command resolution rather than shell-string parsing.
- Follow-up/removal of `insomnia-pod` is explicit if not completed in the first implementation step.
- `cargo fmt --check`, relevant `cargo test`/`cargo check`, `./tickets.sh doctor`, and `git diff --check` pass for each step.

View File

@ -19,4 +19,21 @@ Decision note from discussion:
- Future cleanup: consider renaming the Cargo package/crate from `tui` to `insomnia`; treat it as part of this ticket only if the scope remains contained.
---
<!-- event: decision author: hare at: 2026-05-31T04:32:30Z -->
## Decision
Revised decision from user discussion:
- The intended single-binary work is not merely “put headless subcommands in the existing `insomnia` binary”; it is to migrate the current `insomnia` + `insomnia-pod` two-binary architecture toward one primary executable.
- Pod runtime should remain a separate process. The unification is at the executable/entrypoint/packaging level.
- `insomnia-pod` does not need to remain as a long-term alias. It was not designed as a human-facing command.
- Prefer a normal subcommand `insomnia pod ...` for Pod runtime startup instead of a hidden `__pod-runtime` command.
- `tui` package/crate rename remains separate from binary unification unless it becomes necessary.
Initial implementation should start by extracting the Pod runtime into a library entrypoint and adding `insomnia pod ...`; subsequent steps can migrate spawn defaults and remove `insomnia-pod` from packaging.
---

View File

@ -1,21 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T02:28:21Z -->
## Created
Created by tickets.sh create.
---
<!-- event: decision author: hare at: 2026-05-31T02:29:17Z -->
## Decision
Decision note:
- `ListVisiblePods` should be folded into `ListPods` rather than kept as a separate tool.
- `InspectPod` is not needed as an LLM-facing tool; `ListPods` should provide enough state/detail, and action should go through restore/send/read/stop tools.
- `AttachOrRestorePod` should become `RestorePod`. The current "attach" branch only observes an already-live socket and returns status/socket data; it does not mean a persistent attachment or comm-registry materialization, so the name is misleading.
- If deeper semantics are needed later, define them explicitly as comm-registration/materialization rather than using "attach" implicitly.
---

View File

@ -0,0 +1,45 @@
---
id: 20260531-074258-tui-extract-cli-parsing
slug: tui-extract-cli-parsing
title: TUI: extract CLI parsing from main.rs
status: open
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
assignee: null
legacy_ticket: null
---
## Background
A read-only investigation of `crates/tui` found that the TUI crate's flat module list is not only cosmetic: `main.rs` has accumulated entrypoint, CLI parsing, mode dispatch, terminal setup, Pod connection, single-Pod event loop, key/mouse handling, and tests.
This ticket is the first, lowest-risk cleanup step: move command-line parsing types/functions/tests out of `main.rs` so `main.rs` starts becoming a thin entrypoint without changing runtime behavior.
## Requirements
- Extract the TUI CLI parsing code from `crates/tui/src/main.rs` into a focused module, likely `crates/tui/src/cli.rs`.
- Move the related parse tests with the parsing implementation.
- Keep external CLI behavior unchanged, including:
- `insomnia pod ...` runtime dispatch;
- `--pod`, `--session`, `-r`, `--multi`, profile/manifest-related arguments;
- memory lint and other headless subcommands.
- Keep this ticket to mechanical extraction plus names needed for clarity; do not reorganize the broader TUI module tree.
- Do not rename the `tui` crate/package.
## Non-goals
- Moving terminal runtime/event-loop code.
- Moving `ui::Mode` / view state.
- Reworking CLI UX or flag semantics.
- Introducing compatibility aliases for removed commands.
## Acceptance criteria
- `main.rs` no longer owns the core CLI parse type/function/test definitions.
- `main.rs` uses the extracted CLI module for dispatch.
- Behavior remains unchanged for normal TUI launch, Pod runtime launch, restore/attach options, multi-Pod launch, and memory lint.
- Focused CLI parser tests pass.
- `cargo fmt --check`, relevant `cargo test -p tui`, `cargo check -p tui`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,7 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -0,0 +1,49 @@
---
id: 20260531-074258-tui-extract-single-pod-runtime
slug: tui-extract-single-pod-runtime
title: TUI: extract single-Pod runtime loop from main.rs
status: open
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
assignee: null
legacy_ticket: null
---
## Background
A read-only investigation of `crates/tui` found that `main.rs` remains large even after CLI parsing because it owns terminal setup, Pod connection orchestration, single-Pod event loop, key/mouse handling, stream drain behavior, and many tests.
After CLI parsing is extracted, this ticket should move the single-Pod runtime/event-loop responsibilities out of `main.rs` so the entrypoint mostly chooses a mode and delegates execution.
## Requirements
- Extract single-Pod TUI runtime/event-loop code from `main.rs` into a focused module, likely `single_pod.rs` or `runtime.rs`.
- Include the related key/mouse handling and stream/event drain helpers only where doing so keeps behavior-preserving extraction clear.
- Keep mode dispatch in `main.rs` thin: parse CLI, choose runtime mode, delegate.
- Preserve terminal setup/restore behavior, input queueing behavior, interruption handling, task reminders, manual rewind/compact handling, and Pod socket behavior.
- Keep the change behavior-preserving; do not redesign keybindings or event handling.
- Avoid broad module reorganization in the same ticket.
## Dependencies / sequencing
- Prefer doing this after `tui-extract-cli-parsing` so `main.rs` is less entangled.
- This can be done before or after `tui-move-view-mode-state`, but implementation should avoid increasing `app <-> ui` coupling.
## Non-goals
- Moving render helpers into `render/`.
- Splitting `multi_pod.rs`.
- Reworking `App` state ownership.
- Changing Pod protocol or TUI UX.
- Renaming the `tui` crate/package.
## Acceptance criteria
- `main.rs` no longer owns the bulk of the single-Pod event loop implementation.
- Normal single-Pod TUI launch behavior is unchanged.
- Existing queueing, interrupt, compact, rewind, and stream handling tests still pass.
- `main.rs` remains the binary entrypoint and high-level dispatcher.
- `cargo fmt --check`, relevant `cargo test -p tui`, `cargo check -p tui`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,7 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -0,0 +1,44 @@
---
id: 20260531-074258-tui-move-view-mode-state
slug: tui-move-view-mode-state
title: TUI: move view mode state out of ui module
status: open
kind: task
priority: P2
labels: [tui, cleanup]
created_at: 2026-05-31T07:42:58Z
updated_at: 2026-05-31T07:42:58Z
assignee: null
legacy_ticket: null
---
## Background
A read-only investigation of `crates/tui` found that `app.rs` imports `crate::ui::Mode` while `ui.rs` imports and renders `App`. This is not a Rust module cycle, but it makes the state/render boundary conceptually circular: application state depends on a type owned by the rendering module.
This ticket separates the single-Pod display/history mode state from the render module before larger module grouping work.
## Requirements
- Move the single-Pod display/history mode type currently owned by `ui.rs` to a state-oriented location.
- Acceptable destinations include `app.rs` or a focused module such as `view_mode.rs`.
- If renaming, prefer a clearer name such as `ViewMode` or `HistoryMode`; keep the diff small if a rename would become noisy.
- Update `app.rs`, `ui.rs`, `tool.rs`, and tests to use the new location/name.
- Remove the `app.rs -> ui.rs` dependency caused only by this mode type.
- Preserve rendering behavior and keyboard/command behavior.
- Keep visibility changes minimal; do not combine this with broad `App` field privatization.
## Non-goals
- Moving the whole render tree into `render/`.
- Splitting `app.rs` broadly.
- Reworking mode semantics or UI layout.
- Renaming the `tui` crate/package.
## Acceptance criteria
- `app.rs` no longer imports `crate::ui` solely to access display/history mode state.
- Render code still consumes the mode state from the state-oriented module/type.
- Existing mode-related behavior is unchanged.
- Focused tests covering mode/key/render behavior pass.
- `cargo fmt --check`, relevant `cargo test -p tui`, `cargo check -p tui`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,7 @@
<!-- event: create author: tickets.sh at: 2026-05-31T07:42:58Z -->
## Created
Created by tickets.sh create.
---

View File

@ -0,0 +1,43 @@
---
id: 20260531-085959-eliminate-test-only-env-vars
slug: eliminate-test-only-env-vars
title: Tests: eliminate test-only environment variables
status: open
kind: task
priority: P2
labels: [test, env, cleanup]
created_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T08:59:59Z
assignee: null
legacy_ticket: null
---
## Background
The environment-variable policy now treats process environment as an undesirable ambient input. Tests currently still use several test-only or test-generated env names, such as `INSOMNIA_TEST_*`, and many tests mutate process environment directly with local guards.
The user decision is to eliminate test-only environment-variable surfaces rather than documenting them as supported configuration. A shared test utility crate may be added if it helps remove duplicated unsafe env mutation and replace test-only env channels with typed fixtures.
## Requirements
- Remove test-only environment variables from active code/tests, including `INSOMNIA_TEST_*` patterns.
- Do not add new test-only user-facing env vars.
- Where tests need to exercise real supported env behavior, keep those mutations isolated behind a shared guard rather than ad-hoc per-test `set_var`/`remove_var` code.
- Prefer typed fixtures, temporary files, explicit config structs, or dependency injection over process-global env channels.
- It is acceptable to introduce a small `test-support` crate if it reduces duplication and keeps env mutation serialized/restored.
- Update docs so test-only env vars are not listed as a supported surface.
## Non-goals
- Removing tests that intentionally verify public path/env fallback behavior such as `INSOMNIA_HOME`, `XDG_CONFIG_HOME`, or `INSOMNIA_RUNTIME_DIR`.
- Removing credential env vars in this ticket; those belong with `manifest-profile-encrypted-secrets`.
- Removing `INSOMNIA_POD_COMMAND`; that is tracked by `remove-insomnia-pod-command-env`.
## Acceptance criteria
- No active code/tests generate or depend on `INSOMNIA_TEST_*` env names.
- Test-only env vars are absent from `docs/environment.md`.
- Any remaining test env mutation is for documented public env behavior or unavoidable external compatibility and is guarded/serialized/restored.
- If a `test-support` crate is added, it is test-only/dev-only and does not become runtime dependency surface.
- Relevant test suites pass, including tools/provider/manifest/pod tests that previously mutated env.
- `cargo fmt --check`, relevant `cargo test`/`cargo check`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,7 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---

View File

@ -0,0 +1,43 @@
---
id: 20260531-085959-remove-insomnia-pod-command-env
slug: remove-insomnia-pod-command-env
title: CLI: remove INSOMNIA_POD_COMMAND override
status: open
kind: task
priority: P2
labels: [cli, pod, env]
created_at: 2026-05-31T08:59:59Z
updated_at: 2026-05-31T08:59:59Z
assignee: null
legacy_ticket: null
---
## Background
The single-binary migration changed the normal Pod runtime command to the current `insomnia` executable plus the `pod` prefix argument. During the transition, `INSOMNIA_POD_COMMAND` remained as an executable-only development/test override.
The user decision is to remove this override now that runtime launch is aligned with the single binary. Keeping a process-wide environment override is no longer worth the configuration surface area.
## Requirements
- Remove `INSOMNIA_POD_COMMAND` support from the `insomnia` helper crate and any callers/tests.
- Keep default Pod runtime command behavior unchanged: current executable plus `pod` prefix argument.
- Update spawn/restore tests so they no longer depend on a process-wide command override.
- Prefer a typed test injection path or direct unit tests of `PodRuntimeCommand` construction.
- Do not introduce a replacement environment variable.
- Update docs to remove `INSOMNIA_POD_COMMAND` from supported environment variables.
- Preserve detached process behavior and `INSOMNIA-READY` handshake behavior.
## Non-goals
- Reintroducing an `insomnia-pod` binary or alias.
- Changing Pod runtime flags/profile/manifest semantics.
- Changing the Pod protocol.
- Renaming the `tui` package/crate.
## Acceptance criteria
- `git grep INSOMNIA_POD_COMMAND` finds no active code/docs references outside historical work-item records.
- Pod spawn/restore still defaults to `insomnia pod ...`.
- Focused tests cover runtime command construction without environment-variable mutation.
- `cargo fmt --check`, relevant `cargo test`/`cargo check`, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,7 @@
<!-- event: create author: tickets.sh at: 2026-05-31T08:59:59Z -->
## Created
Created by tickets.sh create.
---