Compare commits

..

11 Commits

26 changed files with 1163 additions and 54 deletions

1
Cargo.lock generated
View File

@ -3911,6 +3911,7 @@ dependencies = [
"crossterm 0.28.1",
"llm-worker",
"manifest",
"memory",
"pod-registry",
"pod-store",
"protocol",

View File

@ -442,17 +442,16 @@ mod tests {
}
#[test]
fn context_window_is_clamped_by_catalog_backend_max() {
fn codex_gpt55_catalog_records_effective_context_window() {
let providers = load_builtin_providers().unwrap();
let models = load_builtin_models().unwrap();
let manifest = ModelManifest {
ref_: Some("codex-oauth/gpt-5.5".into()),
context_window: Some(1_000_000),
..Default::default()
};
let cfg = resolve_with_catalogs(&manifest, &providers, &models).unwrap();
assert_eq!(cfg.context_window, 272_000);
assert_eq!(cfg.max_context_window, Some(272_000));
assert_eq!(cfg.max_context_window, None);
}
#[test]
@ -473,7 +472,7 @@ mod tests {
}
#[test]
fn manifest_backend_max_overrides_catalog_backend_max() {
fn manifest_backend_max_clamps_ref_context_override() {
let providers = load_builtin_providers().unwrap();
let models = load_builtin_models().unwrap();
let manifest = ModelManifest {

View File

@ -19,6 +19,7 @@ unicode-width = "0.2.2"
uuid = { workspace = true }
toml = { workspace = true }
manifest = { workspace = true }
memory = { workspace = true }
session-store = { workspace = true }
pod-store = { workspace = true }
pod-registry = { workspace = true }

View File

@ -4,6 +4,7 @@ mod cache;
mod command;
mod input;
mod markdown;
mod memory_lint;
mod multi_pod;
mod picker;
mod pod_list;
@ -80,12 +81,15 @@ enum Mode {
/// separate from `-r`/`--resume`, which keeps its single-Pod picker
/// meaning.
Multi,
/// `insomnia memory lint`: headless lint for workspace memory and knowledge files.
MemoryLint(memory_lint::LintCliOptions),
}
#[derive(Debug)]
enum ParseError {
Conflict(&'static str),
InvalidSession(String),
MemoryLint(memory_lint::UsageError),
MissingValue(&'static str),
}
@ -94,6 +98,7 @@ impl std::fmt::Display for ParseError {
match self {
Self::Conflict(message) => write!(f, "{message}"),
Self::InvalidSession(s) => write!(f, "invalid --session UUID: {s}"),
Self::MemoryLint(err) => write!(f, "{err}"),
Self::MissingValue(flag) => write!(f, "{flag} requires a value"),
}
}
@ -109,6 +114,13 @@ where
S: Into<String>,
{
let args: Vec<String> = args.into_iter().map(Into::into).collect();
if args.first().map(String::as_str) == Some("memory")
&& args.get(1).map(String::as_str) == Some("lint")
{
let options = memory_lint::parse_lint_args(&args[2..]).map_err(ParseError::MemoryLint)?;
return Ok(Mode::MemoryLint(options));
}
let mut resume = false;
let mut multi = false;
let mut session: Option<SegmentId> = None;
@ -255,10 +267,24 @@ async fn main() -> ExitCode {
Ok(m) => m,
Err(e) => {
eprintln!("insomnia: {e}");
return ExitCode::FAILURE;
return match e {
ParseError::MemoryLint(_) => ExitCode::from(2),
_ => ExitCode::FAILURE,
};
}
};
if let Mode::MemoryLint(ref options) = mode {
return match memory_lint::run(options) {
Ok(memory_lint::LintStatus::Clean) => ExitCode::SUCCESS,
Ok(memory_lint::LintStatus::Failed) => ExitCode::FAILURE,
Err(err) => {
eprintln!("insomnia: {err}");
ExitCode::from(2)
}
};
}
if let Err(e) = enable_raw_mode() {
eprintln!("insomnia: failed to enter raw mode: {e}");
return ExitCode::FAILURE;
@ -278,6 +304,7 @@ async fn main() -> ExitCode {
Mode::Resume => run_resume().await,
Mode::ResumeWithSession(id) => run_spawn(Some(id), None).await,
Mode::Multi => run_multi().await,
Mode::MemoryLint(_) => unreachable!("memory lint returns before terminal setup"),
};
// Always restore the terminal first so any pending eprintln below
@ -1169,6 +1196,67 @@ mod tests {
}
}
#[test]
fn parse_memory_alone_remains_positional_pod_name() {
match parse_args_from(["memory"]).unwrap() {
Mode::PodName {
pod_name,
socket_override,
} => {
assert_eq!(pod_name, "memory");
assert_eq!(socket_override, None);
}
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_memory_lint_mode() {
match parse_args_from([
"memory",
"lint",
"--workspace",
"/tmp/ws",
"--json",
"--warnings-as-errors",
])
.unwrap()
{
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(options.json);
assert!(options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn parse_memory_lint_rejects_usage_errors() {
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
assert_eq!(err.to_string(), "--workspace requires a value");
}
#[test]
fn parse_memory_lint_workspace_equals() {
match parse_args_from(["memory", "lint", "--workspace=/tmp/ws"]).unwrap() {
Mode::MemoryLint(options) => {
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(!options.json);
assert!(!options.warnings_as_errors);
}
_ => panic!("expected MemoryLint mode"),
}
}
#[test]
fn memory_lint_with_other_second_word_remains_positional_pod_name() {
match parse_args_from(["memory", "other"]).unwrap() {
Mode::PodName { pod_name, .. } => assert_eq!(pod_name, "memory"),
_ => panic!("expected PodName mode"),
}
}
#[test]
fn parse_rejects_pod_and_session() {
let segment_id = session_store::new_segment_id().to_string();

View File

@ -0,0 +1,515 @@
use std::fmt;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use memory::linter::WriteMode;
use memory::{Linter, WorkspaceLayout};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LintCliOptions {
pub workspace: Option<PathBuf>,
pub json: bool,
pub warnings_as_errors: bool,
}
impl LintCliOptions {
fn workspace_root(&self) -> Result<PathBuf, LintCliError> {
let cwd = std::env::current_dir().map_err(LintCliError::CurrentDir)?;
Ok(match &self.workspace {
Some(path) if path.is_absolute() => path.clone(),
Some(path) => cwd.join(path),
None => cwd,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UsageError {
MissingValue(&'static str),
UnknownArgument(String),
UnexpectedArgument(String),
}
impl fmt::Display for UsageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingValue(flag) => write!(f, "{flag} requires a value"),
Self::UnknownArgument(arg) => write!(f, "unknown memory lint argument: {arg}"),
Self::UnexpectedArgument(arg) => write!(f, "unexpected memory lint argument: {arg}"),
}
}
}
pub fn parse_lint_args(args: &[String]) -> Result<LintCliOptions, UsageError> {
let mut options = LintCliOptions {
workspace: None,
json: false,
warnings_as_errors: false,
};
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--json" => {
options.json = true;
i += 1;
}
"--warnings-as-errors" => {
options.warnings_as_errors = true;
i += 1;
}
"--workspace" => {
let raw = args
.get(i + 1)
.ok_or(UsageError::MissingValue("--workspace"))?;
if raw.starts_with('-') {
return Err(UsageError::MissingValue("--workspace"));
}
options.workspace = Some(PathBuf::from(raw));
i += 2;
}
arg if arg.starts_with("--workspace=") => {
let value = arg.trim_start_matches("--workspace=");
if value.is_empty() {
return Err(UsageError::MissingValue("--workspace"));
}
options.workspace = Some(PathBuf::from(value));
i += 1;
}
arg if arg.starts_with('-') => {
return Err(UsageError::UnknownArgument(arg.to_string()));
}
arg => return Err(UsageError::UnexpectedArgument(arg.to_string())),
}
}
Ok(options)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintStatus {
Clean,
Failed,
}
#[derive(Debug)]
pub enum LintCliError {
CurrentDir(io::Error),
Io { path: PathBuf, source: io::Error },
Output(io::Error),
}
impl fmt::Display for LintCliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CurrentDir(source) => write!(f, "failed to resolve current directory: {source}"),
Self::Io { path, source } => write!(f, "io error at {}: {source}", path.display()),
Self::Output(source) => write!(f, "failed to write lint output: {source}"),
}
}
}
impl std::error::Error for LintCliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CurrentDir(source) | Self::Io { source, .. } | Self::Output(source) => {
Some(source)
}
}
}
}
pub fn run(options: &LintCliOptions) -> Result<LintStatus, LintCliError> {
let stdout = io::stdout();
run_with_writer(options, stdout.lock())
}
pub fn run_with_writer<W: Write>(
options: &LintCliOptions,
mut writer: W,
) -> Result<LintStatus, LintCliError> {
let workspace = options.workspace_root()?;
let report = lint_workspace(&workspace)?;
if options.json {
write_json_report(&mut writer, &report)?;
} else {
write_human_report(&mut writer, &report)?;
}
if report.counts.errors > 0 || (options.warnings_as_errors && report.counts.warnings > 0) {
Ok(LintStatus::Failed)
} else {
Ok(LintStatus::Clean)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WorkspaceLintReport {
pub workspace: String,
pub files: Vec<FileLintReport>,
pub errors: Vec<LintDiagnostic>,
pub warnings: Vec<LintDiagnostic>,
pub counts: LintCounts,
}
#[derive(Debug, Clone, Serialize)]
pub struct FileLintReport {
pub path: String,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct LintDiagnostic {
pub path: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, Default, Serialize)]
pub struct LintCounts {
pub files: usize,
pub errors: usize,
pub warnings: usize,
pub failing_files: usize,
}
pub fn lint_workspace(workspace: &Path) -> Result<WorkspaceLintReport, LintCliError> {
let layout = WorkspaceLayout::new(workspace.to_path_buf());
let linter = Linter::new(layout.clone());
let mut paths = collect_record_paths(&layout)?;
paths.sort();
let mut files = Vec::with_capacity(paths.len());
for path in paths {
let content = std::fs::read_to_string(&path).map_err(|source| LintCliError::Io {
path: path.clone(),
source,
})?;
let lint = linter.lint(&path, &content, WriteMode::Update);
files.push(FileLintReport {
path: display_path(workspace, &path),
errors: lint.errors.iter().map(ToString::to_string).collect(),
warnings: lint.warnings.iter().map(ToString::to_string).collect(),
});
}
let counts = count_files(&files);
let errors = diagnostics_for(&files, DiagnosticKind::Error);
let warnings = diagnostics_for(&files, DiagnosticKind::Warning);
Ok(WorkspaceLintReport {
workspace: workspace.display().to_string(),
files,
errors,
warnings,
counts,
})
}
fn collect_record_paths(layout: &WorkspaceLayout) -> Result<Vec<PathBuf>, LintCliError> {
let mut paths = Vec::new();
let summary = layout.summary_path();
if is_file(&summary)? {
paths.push(summary);
}
collect_md_files(&layout.decisions_dir(), &mut paths)?;
collect_md_files(&layout.requests_dir(), &mut paths)?;
collect_md_files(&layout.knowledge_dir(), &mut paths)?;
Ok(paths)
}
fn is_file(path: &Path) -> Result<bool, LintCliError> {
match std::fs::metadata(path) {
Ok(meta) => Ok(meta.is_file()),
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(LintCliError::Io {
path: path.to_path_buf(),
source,
}),
}
}
fn collect_md_files(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), LintCliError> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(source) => {
return Err(LintCliError::Io {
path: dir.to_path_buf(),
source,
});
}
};
for entry in entries {
let entry = entry.map_err(|source| LintCliError::Io {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
let file_type = entry.file_type().map_err(|source| LintCliError::Io {
path: path.clone(),
source,
})?;
if file_type.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("md") {
paths.push(path);
}
}
Ok(())
}
fn diagnostics_for(files: &[FileLintReport], kind: DiagnosticKind) -> Vec<LintDiagnostic> {
files
.iter()
.flat_map(|file| {
let messages = match kind {
DiagnosticKind::Error => &file.errors,
DiagnosticKind::Warning => &file.warnings,
};
messages.iter().map(|message| LintDiagnostic {
path: file.path.clone(),
message: message.clone(),
})
})
.collect()
}
#[derive(Debug, Clone, Copy)]
enum DiagnosticKind {
Error,
Warning,
}
fn count_files(files: &[FileLintReport]) -> LintCounts {
files.iter().fold(
LintCounts {
files: files.len(),
..LintCounts::default()
},
|mut counts, file| {
counts.errors += file.errors.len();
counts.warnings += file.warnings.len();
if !file.errors.is_empty() {
counts.failing_files += 1;
}
counts
},
)
}
fn display_path(workspace: &Path, path: &Path) -> String {
path.strip_prefix(workspace)
.unwrap_or(path)
.display()
.to_string()
}
fn write_human_report<W: Write>(
writer: &mut W,
report: &WorkspaceLintReport,
) -> Result<(), LintCliError> {
writeln!(writer, "Workspace: {}", report.workspace).map_err(LintCliError::Output)?;
for file in &report.files {
let status = if file.errors.is_empty() && file.warnings.is_empty() {
"ok"
} else if file.errors.is_empty() {
"warning"
} else {
"error"
};
writeln!(writer, "{status}: {}", file.path).map_err(LintCliError::Output)?;
for error in &file.errors {
writeln!(writer, " error: {error}").map_err(LintCliError::Output)?;
}
for warning in &file.warnings {
writeln!(writer, " warning: {warning}").map_err(LintCliError::Output)?;
}
}
writeln!(
writer,
"Summary: files={} errors={} warnings={} failing_files={}",
report.counts.files,
report.counts.errors,
report.counts.warnings,
report.counts.failing_files
)
.map_err(LintCliError::Output)
}
fn write_json_report<W: Write>(
writer: &mut W,
report: &WorkspaceLintReport,
) -> Result<(), LintCliError> {
serde_json::to_writer_pretty(&mut *writer, report)
.map_err(|source| LintCliError::Output(io::Error::other(source)))?;
writeln!(writer).map_err(LintCliError::Output)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use tempfile::TempDir;
fn write(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
fn valid_summary() -> &'static str {
"---\nupdated_at: 2026-05-31T00:00:00Z\n---\nsummary body\n"
}
fn valid_request() -> &'static str {
"---\ncreated_at: 2026-05-31T00:00:00Z\nupdated_at: 2026-05-31T00:00:00Z\nsources: []\n---\nrequest body\n"
}
fn warning_request() -> String {
format!(
"---\ncreated_at: 2026-05-31T00:00:00Z\nupdated_at: 2026-05-31T00:00:00Z\nsources:\n - segment_id: seg\n range: [0, 1]\n---\n{}\n",
"x".repeat(1500)
)
}
#[test]
fn parses_lint_options() {
let args = vec![
"--workspace".to_string(),
"/tmp/ws".to_string(),
"--json".to_string(),
"--warnings-as-errors".to_string(),
];
let parsed = parse_lint_args(&args).unwrap();
assert_eq!(parsed.workspace, Some(PathBuf::from("/tmp/ws")));
assert!(parsed.json);
assert!(parsed.warnings_as_errors);
}
#[test]
fn rejects_lint_usage_errors() {
assert_eq!(
parse_lint_args(&["--workspace".to_string()]).unwrap_err(),
UsageError::MissingValue("--workspace")
);
assert_eq!(
parse_lint_args(&["--workspace".to_string(), "--json".to_string()]).unwrap_err(),
UsageError::MissingValue("--workspace")
);
assert_eq!(
parse_lint_args(&["--bogus".to_string()]).unwrap_err(),
UsageError::UnknownArgument("--bogus".to_string())
);
assert_eq!(
parse_lint_args(&["extra".to_string()]).unwrap_err(),
UsageError::UnexpectedArgument("extra".to_string())
);
}
#[test]
fn lints_only_workspace_memory_and_knowledge_records() {
let dir = TempDir::new().unwrap();
let root = dir.path();
write(&root.join(".insomnia/memory/summary.md"), valid_summary());
write(
&root.join(".insomnia/memory/requests/request-one.md"),
valid_request(),
);
write(
&root.join(".insomnia/memory/_logs/ignored.md"),
"not frontmatter",
);
write(
&root.join(".insomnia/workflow/ignored.md"),
"not frontmatter",
);
let report = lint_workspace(root).unwrap();
assert_eq!(
report
.files
.iter()
.map(|file| file.path.as_str())
.collect::<Vec<_>>(),
vec![
".insomnia/memory/requests/request-one.md",
".insomnia/memory/summary.md",
]
);
assert_eq!(report.counts.files, 2);
assert_eq!(report.counts.errors, 0);
}
#[test]
fn invalid_records_count_as_lint_failures() {
let dir = TempDir::new().unwrap();
let root = dir.path();
write(
&root.join(".insomnia/memory/summary.md"),
"missing frontmatter",
);
let report = lint_workspace(root).unwrap();
assert_eq!(report.counts.files, 1);
assert_eq!(report.counts.errors, 1);
assert_eq!(report.counts.failing_files, 1);
assert!(report.files[0].errors[0].contains("frontmatter"));
}
#[test]
fn warnings_as_errors_changes_status_without_changing_report() {
let dir = TempDir::new().unwrap();
let root = dir.path();
write(
&root.join(".insomnia/memory/requests/large-record.md"),
&warning_request(),
);
let mut output = Vec::new();
let status = run_with_writer(
&LintCliOptions {
workspace: Some(root.to_path_buf()),
json: false,
warnings_as_errors: true,
},
&mut output,
)
.unwrap();
assert_eq!(status, LintStatus::Failed);
let text = String::from_utf8(output).unwrap();
assert!(text.contains("warnings=1"));
assert!(text.contains("errors=0"));
}
#[test]
fn json_output_is_machine_readable() {
let dir = TempDir::new().unwrap();
let root = dir.path();
write(&root.join(".insomnia/memory/summary.md"), valid_summary());
let mut output = Vec::new();
let status = run_with_writer(
&LintCliOptions {
workspace: Some(root.to_path_buf()),
json: true,
warnings_as_errors: false,
},
&mut output,
)
.unwrap();
assert_eq!(status, LintStatus::Clean);
let parsed: Value = serde_json::from_slice(&output).unwrap();
assert_eq!(parsed["workspace"], root.display().to_string());
assert_eq!(parsed["counts"]["files"], 1);
assert_eq!(parsed["files"][0]["path"], ".insomnia/memory/summary.md");
assert!(parsed["files"][0]["errors"].as_array().unwrap().is_empty());
}
}

View File

@ -0,0 +1,65 @@
# Child Pod visibility/restore loss during review flow
Date: 2026-05-31
## Summary
During the `workspace-memory-lint-cli` review flow, a spawned reviewer Pod appeared to stop producing notifications/output and then became impossible to attach/restore from the parent Pod. The parent later saw no spawned Pods at all, while a restore/prune notification reported that missing or unreachable delegated child Pods had been reclaimed.
This looks like a control-plane visibility/restore issue rather than an implementation-review issue. The lost Pod was read-only and the review was safely re-run in a new reviewer Pod, but the incident is worth recording because it undermines long-running multi-agent workflows.
## Observed sequence
1. `workspace-memory-lint-coder-20260531` completed implementation and reported commit `7a717f2 cli: add workspace memory lint`.
2. A read-only reviewer Pod was spawned:
- `workspace-memory-lint-reviewer-20260531`
- read scope: main workspace and `.worktree/workspace-memory-lint-cli`
3. Repeated `ReadPodOutput` calls returned:
- `running; no new assistant text`
4. `InspectPod` still saw the reviewer as live/reachable/running at one point:
- socket: `/run/user/1000/insomnia/workspace-memory-lint-reviewer-20260531/sock`
- restore impossible only because the segment was locked by that live Pod
5. Later, after the user asked to restore it, `AttachOrRestorePod` failed:
- `pod workspace-memory-lint-reviewer-20260531 is not visible to this Pod`
6. `ListPods` then reported no spawned Pods, and `ListVisiblePods` only showed the self Pod `insomnia`.
7. A notification appeared:
- `Restored Pod state contained missing or unreachable delegated child Pods; their delegated write scopes were reclaimed before resume.`
8. The review had to be re-run by spawning a new read-only reviewer:
- `workspace-memory-lint-reviewer-rerun-20260531`
## Impact
- Parent-side orchestration lost track of a child reviewer Pod that had previously been visible.
- The parent could not attach/restore by name because the child was no longer visible to the parent Pod.
- Any review result already produced by the lost child would have been hard to recover through normal parent tools.
- Multi-agent workflows that rely on long-running reviewer/coder Pods become less reliable if spawned-child visibility can disappear during parent resume/restore/prune.
In this instance the practical impact was low because the reviewer had read-only scope and the review could be re-run. The incident would be more serious for implementation Pods with unmerged write-scope work or for expensive/long review tasks.
## Why this matters
The current design intent is that Pod metadata is durable current state and spawned child registry persistence reuses Pod metadata. Parent-side tools should be able to inspect/attach/restore visible spawned children where durable state still records them, and pruning should be conservative enough not to erase reachable or recoverable child work prematurely.
This incident suggests at least one of these paths needs inspection:
- parent spawned-child registry persistence/restoration;
- pruning of unreachable children during parent restore;
- visibility rules for previously spawned child Pods after parent resume;
- distinction between live socket reachability, durable pod-store metadata, and parent-visible child registry;
- notification/read-output cursor behavior when a child is still running but no output arrives.
## Notes for follow-up
- The failure mode was not simply “child stopped”; the parent tool reported “not visible to this Pod,” which is different from stopped/unreachable.
- `InspectPod` had previously seen the child as live and locked; later `ListPods` returned no spawned Pods.
- The prune/reclaim notification may have happened after parent restore and may have removed child visibility state.
- A useful regression test would simulate parent restore with a child that is pending/running/unreachable at different phases and assert whether it remains visible, attachable, or intentionally pruned with a recoverable diagnostic.
- A workflow-level mitigation is to write important reviewer/coder outputs into ticket threads/artifacts promptly after reading them, and to re-run read-only reviewers if child visibility is lost.
## Current workaround
For `workspace-memory-lint-cli`, a replacement reviewer Pod was spawned with the same read-only task:
- `workspace-memory-lint-reviewer-rerun-20260531`
The original reviewer Pod was treated as lost/unrecoverable from the parent after `AttachOrRestorePod` reported it was not visible.

View File

@ -32,8 +32,10 @@ context_window = 256000
[[model]]
id = "gpt-5.5"
provider = "codex-oauth"
context_window = 1050000
max_context_window = 272000
# OpenAI docs advertise GPT-5.5 with a 1.05M context window, but Codex OAuth /
# ChatGPT backend access is effectively limited around 272k tokens; this
# provider-specific entry records that effective route limit directly.
context_window = 272000
capability = { tool_calling = "parallel", structured_output = "json_schema", reasoning = "effort", vision = true, prompt_caching = { kind = "auto" } }
[[model]]

View File

@ -2,12 +2,12 @@
id: 20260527-000019-workspace-memory-lint-cli
slug: workspace-memory-lint-cli
title: ワークスペースのメモリーをLintするヘッドレスCLI
status: open
status: closed
kind: task
priority: P2
labels: [migrated, memory, cli]
created_at: 2026-05-27T00:00:19Z
updated_at: 2026-05-31T00:51:55Z
updated_at: 2026-05-31T02:15:17Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1 @@
Implemented `insomnia memory lint` as a headless command in the existing user-facing `insomnia` binary. The command lints workspace memory/knowledge records with the existing `memory::Linter` using `WriteMode::Update`, supports human and JSON output, handles warnings-as-errors, preserves `insomnia memory` as a positional Pod name, and returns before TUI/raw-terminal or Pod connection paths. External review approved and validation passed.

View File

@ -0,0 +1,105 @@
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:19Z -->
## Migrated
Migrated from TODO.md entry without a legacy ticket file. No legacy review file was present at migration time.
---
<!-- event: plan author: hare at: 2026-05-31T00:51:55Z -->
## Plan
Planning note:
- Keep this in the existing user-facing `insomnia` binary implemented by the `tui` crate. Do not add another installed command for this ticket.
- The command should be headless: parse args, lint files, print report, exit. It must not initialize terminal UI or connect to a Pod.
- `insomnia memory lint` is preferred, but `insomnia memory` alone should continue to be a valid Pod-name attach/create path if practical with the current parser.
- Use `memory::Linter` directly so CLI behavior tracks tool pre-write validation. Existing files should be linted with `WriteMode::Update`.
- Keep crate rename / single-binary architecture as future cleanup, not part of this ticket.
---
<!-- event: implementation_report author: hare at: 2026-05-31T02:14:28Z -->
## Implementation report
Implementation report from coder Pod `workspace-memory-lint-coder-20260531`:
- Branch: `workspace-memory-lint-cli`
- Commit: `7a717f2d259563df562913e0c3ceb388b094b697` (`cli: add workspace memory lint`)
- Added `insomnia memory lint [--workspace <PATH>] [--json] [--warnings-as-errors]` as a headless mode in the existing `tui` crate/user-facing `insomnia` binary.
- `insomnia memory` alone remains a positional Pod name.
- The lint command resolves workspace root, collects existing summary/decisions/requests/knowledge records through `memory::WorkspaceLayout`, and lints with existing `memory::Linter` using `WriteMode::Update`.
- The command prints deterministic human output by default and stable JSON with workspace/files/errors/warnings/counts when `--json` is requested.
- Exit codes follow the ticket: 0 clean, 1 lint failures or warnings-as-errors, 2 usage/I/O/output/runtime failures.
- The headless path returns before raw terminal setup or Pod connection/spawn logic.
Validation reported by coder:
- `cargo fmt --check` passed
- `cargo test -p tui memory_lint -- --nocapture` passed
- `cargo test -p tui` passed
- `cargo check -p tui` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
Unresolved issues: none.
---
<!-- event: review author: hare at: 2026-05-31T02:14:28Z status: approve -->
## Review: approve
External review by reviewer Pod `workspace-memory-lint-reviewer-rerun-20260531`: approve.
The original reviewer Pod `workspace-memory-lint-reviewer-20260531` became non-visible to the parent before output could be recovered; this review was rerun with a replacement read-only reviewer Pod.
Reviewer summary:
- The implementation adds `insomnia memory lint` as a headless mode in the existing user-facing `insomnia` binary.
- The memory lint path branches before raw terminal setup and Pod connection/spawn logic.
- Parser tests preserve `insomnia memory` as positional Pod name behavior.
- The collector targets summary, decisions, requests, and knowledge records while ignoring opaque memory subsystem directories and workflow files.
- Existing `memory::Linter` and `WriteMode::Update` are used, and the code only reads files / writes reports.
- Human and JSON outputs are deterministic enough for the ticket, and exit code mapping matches requirements.
Blockers: none.
Non-blocking follow-ups:
- Add broader fixture coverage for `_staging`, `_usage`, knowledge, and decisions if desired.
- Add process-level exit-code integration tests if a CLI test harness is introduced later.
Validation adequacy: coder-reported validation is sufficient for this ticket. Reviewer additionally checked `git diff --check develop...HEAD` read-only.
---
<!-- event: implementation_report author: hare at: 2026-05-31T02:15:16Z -->
## Implementation report
Main workspace validation after merge:
- `cargo fmt --check` passed
- `cargo test -p tui memory_lint -- --nocapture` passed (10 passed)
- `cargo test -p tui` passed (224 passed)
- `cargo check -p tui` passed with pre-existing dead-code warnings in `llm-worker` and `tui`
- `./tickets.sh doctor` passed
- `git diff --check` passed
---
<!-- event: close author: hare at: 2026-05-31T02:15:17Z status: closed -->
## Closed
Implemented `insomnia memory lint` as a headless command in the existing user-facing `insomnia` binary. The command lints workspace memory/knowledge records with the existing `memory::Linter` using `WriteMode::Update`, supports human and JSON output, handles warnings-as-errors, preserves `insomnia memory` as a positional Pod name, and returns before TUI/raw-terminal or Pod connection paths. External review approved and validation passed.
---

View File

@ -2,12 +2,12 @@
id: 20260531-003743-codex-gpt55-effective-context-window
slug: codex-gpt55-effective-context-window
title: Provider: make codex gpt-5.5 context window effective
status: open
status: closed
kind: task
priority: P2
labels: [provider, models, catalog]
created_at: 2026-05-31T00:37:43Z
updated_at: 2026-05-31T00:38:23Z
updated_at: 2026-05-31T01:58:24Z
assignee: null
legacy_ticket: null
---

View File

@ -0,0 +1 @@
Updated the builtin `codex-oauth/gpt-5.5` entry so `context_window = 272000` directly represents the effective Codex OAuth / ChatGPT backend route limit, with a catalog comment explaining that generic OpenAI docs advertise 1.05M. Removed catalog-level `max_context_window` from that entry while preserving inline/config `max_context_window` support. External review approved and validation passed.

View File

@ -0,0 +1,94 @@
<!-- event: create author: tickets.sh at: 2026-05-31T00:37:43Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T00:38:23Z -->
## Plan
Implementation plan:
1. Update only the provider-specific builtin catalog entry for `codex-oauth/gpt-5.5`: set `context_window = 272000`, remove `max_context_window`, and add a clear comment about generic OpenAI docs vs Codex OAuth effective limit.
2. Keep `resources/profiles/default.lua` unchanged.
3. Adjust tests so builtin catalog expectations use effective `context_window`, while `max_context_window` behavior remains covered by an inline/config-specific test if the field remains supported.
4. Validate with focused provider/manifest tests plus `cargo fmt --check`, `./tickets.sh doctor`, and `git diff --check`.
---
<!-- event: implementation_report author: hare at: 2026-05-31T01:57:37Z -->
## Implementation report
Implementation report from coder Pod `codex-gpt55-context-coder-20260531`:
- Branch: `codex-gpt55-effective-context-window`
- Commit: `ed3e97c22a8e21dce9223114526932a11b268589` (`provider: use codex gpt55 effective context`)
- Updated `resources/models/builtin.toml` so `codex-oauth/gpt-5.5` records the effective Codex OAuth route limit directly as `context_window = 272000` and no longer has `max_context_window`.
- Added a nearby comment explaining that OpenAI documents GPT-5.5 as 1.05M context, but Codex OAuth / ChatGPT backend access is effectively limited around 272k.
- Kept default profile unchanged and did not change OpenRouter `openai/gpt-5.5`.
- Preserved `max_context_window` support for inline/config overrides and updated tests accordingly.
Validation reported by coder:
- `cargo fmt --check` passed
- `cargo test -p provider catalog` passed
- `cargo test -p manifest model` passed
- `cargo check -p provider -p manifest` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
Unresolved issues: none.
---
<!-- event: review author: hare at: 2026-05-31T01:57:38Z status: approve -->
## Review: approve
External review by reviewer Pod `codex-gpt55-context-reviewer-20260531`: approve.
Reviewer summary:
- The change is correctly scoped to `codex-oauth/gpt-5.5` and relevant tests.
- `context_window = 272000` now represents the effective Codex OAuth route limit directly.
- The comment explains the distinction between OpenAI's advertised 1.05M context window and the observed/known Codex OAuth effective limit.
- `max_context_window` support remains available for inline/config cases.
- Default profile and OpenRouter `openai/gpt-5.5` were not changed.
Blockers: none.
---
<!-- event: implementation_report author: hare at: 2026-05-31T01:58:24Z -->
## Implementation report
Main workspace validation after merge:
- `cargo fmt --check` passed
- `cargo test -p provider catalog` passed
- `cargo test -p manifest model` passed
- `cargo check -p provider -p manifest` passed
- `./tickets.sh doctor` passed
- `git diff --check` passed
Warnings observed are pre-existing dead-code warnings in `llm-worker` / filtered manifest test helpers.
---
<!-- event: close author: hare at: 2026-05-31T01:58:24Z status: closed -->
## Closed
Updated the builtin `codex-oauth/gpt-5.5` entry so `context_window = 272000` directly represents the effective Codex OAuth / ChatGPT backend route limit, with a catalog comment explaining that generic OpenAI docs advertise 1.05M. Removed catalog-level `max_context_window` from that entry while preserving inline/config `max_context_window` support. External review approved and validation passed.
---

View File

@ -1,22 +0,0 @@
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:19Z -->
## Migrated
Migrated from TODO.md entry without a legacy ticket file. No legacy review file was present at migration time.
---
<!-- event: plan author: hare at: 2026-05-31T00:51:55Z -->
## Plan
Planning note:
- Keep this in the existing user-facing `insomnia` binary implemented by the `tui` crate. Do not add another installed command for this ticket.
- The command should be headless: parse args, lint files, print report, exit. It must not initialize terminal UI or connect to a Pod.
- `insomnia memory lint` is preferred, but `insomnia memory` alone should continue to be a valid Pod-name attach/create path if practical with the current parser.
- Use `memory::Linter` directly so CLI behavior tracks tool pre-write validation. Existing files should be linted with `WriteMode::Update`.
- Keep crate rename / single-binary architecture as future cleanup, not part of this ticket.
---

View File

@ -1,21 +0,0 @@
<!-- event: create author: tickets.sh at: 2026-05-31T00:37:43Z -->
## Created
Created by tickets.sh create.
---
<!-- event: plan author: hare at: 2026-05-31T00:38:23Z -->
## Plan
Implementation plan:
1. Update only the provider-specific builtin catalog entry for `codex-oauth/gpt-5.5`: set `context_window = 272000`, remove `max_context_window`, and add a clear comment about generic OpenAI docs vs Codex OAuth effective limit.
2. Keep `resources/profiles/default.lua` unchanged.
3. Adjust tests so builtin catalog expectations use effective `context_window`, while `max_context_window` behavior remains covered by an inline/config-specific test if the field remains supported.
4. Validate with focused provider/manifest tests plus `cargo fmt --check`, `./tickets.sh doctor`, and `git diff --check`.
---

View File

@ -0,0 +1,50 @@
---
id: 20260531-005557-single-binary-insomnia-cli
slug: single-binary-insomnia-cli
title: CLI: clarify single-binary insomnia architecture
status: open
kind: task
priority: P2
labels: [cli, architecture, nix]
created_at: 2026-05-31T00:55:57Z
updated_at: 2026-05-31T00:56:38Z
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 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.
## 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.
## Non-goals
- Reworking the Pod daemon binary protocol.
- Removing `insomnia-pod` unless a separate architecture decision covers Pod process lifecycle.
- 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.
## 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.

View File

@ -0,0 +1,22 @@
<!-- event: create author: tickets.sh at: 2026-05-31T00:55:57Z -->
## Created
Created by tickets.sh create.
---
<!-- event: decision author: hare at: 2026-05-31T00:56:38Z -->
## Decision
Decision note from discussion:
- Short-term direction: keep adding headless commands to the existing user-facing `insomnia` binary owned by the current `tui` crate.
- Product preference: a single standalone `insomnia` binary is easier to distribute and explain than many small binaries.
- Known tradeoff: headless commands inherit TUI dependencies such as ratatui/crossterm. This is acceptable until binary size/startup/runtime memory is measured as a real problem.
- Internal structure should still separate headless command dispatch from terminal/TUI initialization.
- 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.
---

View File

@ -0,0 +1,87 @@
---
id: 20260531-010005-plugin-extension-surface
slug: plugin-extension-surface
title: Plugin: define extension surface for hooks and tools
status: open
kind: feature
priority: P2
labels: [plugin, hooks, tools, wasm, mcp]
created_at: 2026-05-31T01:00:05Z
updated_at: 2026-05-31T01:01:09Z
assignee: null
legacy_ticket: null
---
## Background
insomnia currently has internal Hook / Tool concepts, plus a separate planned MCP integration ticket (`mcp-integration`). The next design step is to define the project-level Plugin surface: how user/project-provided extensions can contribute Tools and Hooks without weakening scope, permission, history, or prompt-context invariants.
The plugin surface should not be a grab bag of arbitrary code execution. Candidate extension mechanisms have different trust and protocol properties:
- MCP: protocol-bound external tool/resource/prompt provider surface.
- TOML/config-only hooks: declarative configuration for simple hook behavior without arbitrary code.
- WASM: planned first programmable plugin runtime for Hooks and Tools, with explicit capability imports and sandboxing.
- General scripting languages: considered, but not the initial direction because arbitrary script execution broadens the trust/runtime surface too quickly.
## Related work
- `work-items/open/20260529-161928-mcp-integration/` — MCP integration as one plugin backend / external capability bridge.
- Existing internal hooks/tools code: `crates/pod`, `crates/tools`, `crates/llm-worker`.
- Manifest permission policy and scope enforcement must remain authoritative for plugin-provided tools.
## Requirements
- Define a Plugin surface that can provide:
- Tools callable by the LLM through the normal ToolRegistry / permission / scope path;
- Hooks observing or influencing Pod/Worker lifecycle through the existing Hook boundary, not by directly mutating worker history/context.
- Separate plugin description/registration from plugin runtime implementation.
- A plugin manifest should declare provided tools/hooks, required capabilities, configuration schema or config values, and trust/runtime type.
- Runtime implementations can include MCP, declarative config hooks, and WASM in separate phases.
- Keep MCP as a related backend, not the whole plugin model.
- MCP servers remain untrusted external capability providers bridged through allowlists, bounded output, scope/permission policy, and explicit resource/prompt use.
- Define a declarative hook path for simple TOML/config-only behavior where code execution is unnecessary.
- Define a WASM plugin direction for programmable Hooks/Tools.
- WASM modules must receive explicit host imports/capabilities only.
- File/network/process access must not be ambient; all external effects go through host-provided capability APIs and existing policy checks.
- Tool outputs must be bounded and recorded through normal history/tool-result paths.
- Preserve LLM context/history invariants.
- Plugins must not inject cross-turn invisible context.
- If plugin output becomes model-visible, it must enter through durable history/tool/hook paths according to existing rules.
- Preserve scope and permission invariants.
- Plugin-provided tools must not bypass `ScopedFs`, manifest tool permission policy, child scope delegation, or web/network policy.
- Clarify trust model and lifecycle.
- Builtin vs project vs user plugins.
- Discovery/enablement through manifest/profile/config.
- Versioning / compatibility boundaries.
- Diagnostics when a plugin cannot load or asks for unavailable capabilities.
## Non-goals
- Implementing the full WASM runtime in the first design step.
- Implementing MCP itself beyond referencing the existing MCP integration ticket.
- Supporting arbitrary host scripting languages as a first-class plugin runtime.
- Allowing plugins to mutate session history, memory, prompt context, or scope outside approved APIs.
- Adding UI plugin systems or TUI rendering extensions.
## Suggested phases
1. **Design / architecture note**
- Define Plugin, PluginManifest, PluginRuntimeKind, Tool contribution, Hook contribution, capability request, and trust/source model.
- Map MCP, declarative hooks, and WASM onto that model.
2. **Internal registry boundary**
- Introduce code structure that can register plugin-provided tool/hook descriptors without changing runtime behavior yet.
3. **Declarative hooks MVP**
- Add a non-code configuration path for simple hook behavior if an immediate use case exists.
4. **WASM spike**
- Evaluate runtime (`wasmtime` or alternative), host imports, resource limits, serialization, and Nix/package impact.
5. **MCP bridge alignment**
- Ensure `mcp-integration` plugs into the same Tool/permission/output boundary rather than becoming a parallel extension path.
## Acceptance criteria
- The repository has a documented plugin architecture proposal covering Tools, Hooks, runtimes, capability model, trust model, and discovery/enablement.
- MCP is positioned as one plugin backend / bridge and linked to `mcp-integration`, not treated as the only extension mechanism.
- The proposal explicitly explains why arbitrary scripting languages are deferred and why WASM is the initial programmable runtime direction.
- The design preserves existing scope, permission, history, and prompt-context invariants.
- Follow-up implementation tickets can be cut independently for declarative hooks, WASM runtime, and MCP bridge integration.
- Any code changes in this ticket, if taken beyond design docs, are limited to safe internal boundaries and have focused tests.

View File

@ -0,0 +1,24 @@
<!-- event: create author: tickets.sh at: 2026-05-31T01:00:05Z -->
## Created
Created by tickets.sh create.
---
<!-- event: decision author: hare at: 2026-05-31T01:01:09Z -->
## Decision
Initial decision note from user discussion:
- The plugin surface should mainly expose Hooks and Tools.
- MCP is related, but should be treated as one protocol-bound backend/bridge rather than the entire plugin model.
- Planned plugin mechanisms:
- MCP for protocol-constrained external capability providers;
- TOML/config-only hooks for simple behavior that does not need arbitrary code;
- WASM for programmable Hooks/Tools with explicit host capabilities.
- General scripting languages were considered, but the initial direction is WASM because it offers a clearer sandbox/capability boundary.
---

View File

@ -0,0 +1,76 @@
---
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
kind: task
priority: P2
labels: [pod, tools, orchestration]
created_at: 2026-05-31T02:28:21Z
updated_at: 2026-05-31T02:29:17Z
assignee: null
legacy_ticket: null
---
## Background
The current LLM-callable Pod tool surface has overlapping concepts:
- `ListPods` is registry-oriented and lists Pods spawned by the current Pod.
- `ListVisiblePods` is state/visibility-oriented and lists visible live/stored Pods.
- `InspectPod` provides a named detail lookup.
- `AttachOrRestorePod` has an ambiguous name: the "attach" branch mostly detects an existing live socket and returns connection information; it does not clearly mean that the target becomes a comm-registered child for `SendToPod` / `ReadPodOutput`.
Recent multi-agent workflow incidents also showed that stopped/restorable child Pods and missing/unreachable children are not clearly represented. For LLM tool use, fewer, more precise operations are preferable.
User decision:
- Integrate `ListVisiblePods` semantics into `ListPods`.
- Remove `InspectPod` from the LLM tool surface.
- Rename `AttachOrRestorePod` to `RestorePod`.
## Requirements
- Replace the old `ListPods` result semantics with the state/visibility-aware listing currently represented by `ListVisiblePods`.
- The tool should list Pods visible to the current Pod, not host-wide Pods.
- Visible set should include self and spawned children according to current visibility rules.
- Preserve enough fields to distinguish live/reachable, stored/restorable, missing/corrupt/unrestorable, and comm/registry state where available.
- If preserving exact `PodList`/`PodListEntry` structures is easier, use them; do not keep two nearly identical listing tools.
- Remove `ListVisiblePods` as a separate LLM-callable tool.
- Update tool registration, tool descriptions, tests, prompts/docs if they mention it.
- If protocol enum compatibility must remain for non-LLM/TUI callers, keep internal compatibility only and do not expose it as an LLM tool; otherwise remove it cleanly.
- Remove `InspectPod` from the LLM-callable tool surface.
- Update tool registration, tool descriptions, schema, tests, prompts/docs.
- Prefer `ListPods` for discovery/detail and `RestorePod` for action.
- If protocol enum compatibility must remain, keep it internal/non-advertised only; otherwise remove it cleanly.
- Rename `AttachOrRestorePod` to `RestorePod` in the LLM-callable surface.
- The operation should restore a visible stopped/restorable Pod, or report that a visible Pod is already live.
- Avoid using “attach” terminology in tool name/description unless a real comm-registry materialization semantics is implemented.
- Update tool schema, descriptions, registration, tests, and prompts/docs.
- Decide whether to keep a backwards-compatible protocol alias internally; avoid exposing both names to the LLM.
- Preserve scope, visibility, and safety boundaries.
- Do not make host-wide Pod enumeration available.
- Do not allow restoring Pods outside the callers visibility set.
- Do not broaden delegated scope handling in this ticket.
- Keep behavior for `SpawnPod`, `SendToPod`, `ReadPodOutput`, and `StopPod` coherent with the new listing/restore tools.
- If full stopped/restorable child semantics require a larger registry change, document it as follow-up rather than half-implementing it.
## Non-goals
- Full redesign of stopped/restorable child lifecycle and delegated scope reclaim semantics.
- Changing Pod registry durable authority or parent restore pruning behavior.
- Host-wide Pod management tools.
- TUI dashboard redesign.
## Acceptance criteria
- The LLM-visible tool list exposes `ListPods` and `RestorePod`, and no longer exposes `ListVisiblePods`, `InspectPod`, or `AttachOrRestorePod`.
- `ListPods` returns visibility/state-aware data sufficient for the LLM to decide whether a Pod is live, stopped/restorable, or unavailable.
- `RestorePod` replaces the old `AttachOrRestorePod` name in tool descriptions and tests.
- Existing tests are updated and new/renamed tests cover:
- `ListPods` includes visible stored/live entries formerly covered by `ListVisiblePods`;
- `RestorePod` rejects invisible/missing Pods;
- `RestorePod` reports already-live visible Pods without spawning another process;
- removed tools are not registered in the LLM tool registry.
- Any intentional protocol compatibility aliases are documented in code comments and are not advertised to the LLM.
- `cargo fmt --check`, focused Pod/tools tests, `cargo check` for affected crates, `./tickets.sh doctor`, and `git diff --check` pass.

View File

@ -0,0 +1,21 @@
<!-- 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.
---