memoryが.insomnia配下ではなくworkspace root直下を想定していた問題の修正

This commit is contained in:
Keisuke Hirata 2026-04-28 11:53:08 +09:00
parent dfa6213c18
commit 8cf1d6c9cf
11 changed files with 87 additions and 70 deletions

View File

@ -322,7 +322,7 @@ mod tests {
#[test]
fn workflow_write_rejected() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/workflow/wf.md");
let path = dir.path().join(".insomnia/memory/workflow/wf.md");
let content = format!(
"---\nupdated_at: {now}\ndescription: x\nauto_invoke: false\nuser_invocable: true\n---\nbody",
now = iso_now()
@ -352,7 +352,7 @@ mod tests {
#[test]
fn decision_with_unknown_replaced_by_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: ghost\n---\nbody\n",
now = iso_now()
@ -369,7 +369,7 @@ mod tests {
#[test]
fn decision_replaced_by_self_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: foo\n---\nbody\n",
now = iso_now()
@ -387,7 +387,7 @@ mod tests {
fn decision_replaced_by_existing_ok() {
let (dir, linter) = workspace();
// Pre-create the target.
let target = dir.path().join("memory/decisions/bar.md");
let target = dir.path().join(".insomnia/memory/decisions/bar.md");
write(
&target,
&format!(
@ -395,7 +395,7 @@ mod tests {
now = iso_now()
),
);
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: replaced\nreplaced_by: bar\n---\nbody\n",
now = iso_now()
@ -407,7 +407,7 @@ mod tests {
#[test]
fn missing_required_field_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
// Missing `status`.
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\n---\nbody\n",
@ -423,7 +423,7 @@ mod tests {
#[test]
fn knowledge_long_description_with_model_invokation_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("knowledge/foo.md");
let path = dir.path().join(".insomnia/knowledge/foo.md");
let big_desc = "x".repeat(2000);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: true\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
@ -441,7 +441,7 @@ mod tests {
#[test]
fn knowledge_long_description_without_model_invokation_ok() {
let (dir, linter) = workspace();
let path = dir.path().join("knowledge/foo.md");
let path = dir.path().join(".insomnia/knowledge/foo.md");
let big_desc = "x".repeat(2000);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nkind: rule\ndescription: {big_desc}\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
@ -454,7 +454,7 @@ mod tests {
#[test]
fn summary_path_accepted() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
let content = format!(
"---\nupdated_at: {now}\n---\nsummary body\n",
now = iso_now()
@ -466,7 +466,7 @@ mod tests {
#[test]
fn create_when_existing_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
write(
&path,
&format!(
@ -491,7 +491,7 @@ mod tests {
fn workflow_lint_accepts_valid_record() {
let (dir, linter) = workspace();
// Place a Knowledge record that the workflow will reference.
let kn = dir.path().join("knowledge/foo.md");
let kn = dir.path().join(".insomnia/knowledge/foo.md");
write(
&kn,
&format!(
@ -548,14 +548,14 @@ mod tests {
// `db-pol` (1 deletion), `db-pools` (1 insertion).
for slug in ["db-pol", "db-pools"] {
write(
&dir.path().join(format!("memory/decisions/{slug}.md")),
&dir.path().join(format!(".insomnia/memory/decisions/{slug}.md")),
&format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
),
);
}
let path = dir.path().join("memory/decisions/db-pool.md");
let path = dir.path().join(".insomnia/memory/decisions/db-pool.md");
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nbody\n",
n = iso_now()
@ -577,14 +577,14 @@ mod tests {
let (dir, linter) = workspace();
for slug in ["alpha", "bravo"] {
write(
&dir.path().join(format!("memory/decisions/{slug}.md")),
&dir.path().join(format!(".insomnia/memory/decisions/{slug}.md")),
&format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
),
);
}
let path = dir.path().join("memory/decisions/charlie.md");
let path = dir.path().join(".insomnia/memory/decisions/charlie.md");
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n",
n = iso_now()
@ -603,7 +603,7 @@ mod tests {
#[test]
fn body_size_limit_errors() {
let (dir, linter) = workspace();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let big_body = "x".repeat(8001);
let content = format!(
"---\ncreated_at: {now}\nupdated_at: {now}\nsources: []\nstatus: open\n---\n{body}",

View File

@ -84,7 +84,7 @@ mod tests {
model_invokation: bool,
body: &str,
) {
let path = dir.join("knowledge").join(format!("{slug}.md"));
let path = dir.join(".insomnia/knowledge").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nkind: policy\ndescription: \"{description}\"\nmodel_invokation: {flag}\nuser_invocable: true\nlast_sources: []\n---\n{body}",
n = now(),
@ -95,7 +95,7 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout) {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("knowledge")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/knowledge")).unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
(dir, layout)
}
@ -141,7 +141,7 @@ mod tests {
write_knowledge(dir.path(), "good", "ok", true, "");
// Garbage in frontmatter — must be skipped, not panic.
std::fs::write(
dir.path().join("knowledge/bad.md"),
dir.path().join(".insomnia/knowledge/bad.md"),
"---\nthis is not yaml: : :\n---\nbody\n",
)
.unwrap();
@ -155,7 +155,7 @@ mod tests {
fn non_md_files_ignored() {
let (dir, layout) = setup();
write_knowledge(dir.path(), "good", "ok", true, "");
std::fs::write(dir.path().join("knowledge/note.txt"), "not markdown\n").unwrap();
std::fs::write(dir.path().join(".insomnia/knowledge/note.txt"), "not markdown\n").unwrap();
let got = collect_resident_knowledge(&layout);
assert_eq!(got.len(), 1);

View File

@ -41,9 +41,9 @@ mod tests {
let layout = WorkspaceLayout::new(PathBuf::from("/ws"));
let rules = deny_write_rules(&layout);
assert_eq!(rules.len(), 2);
assert_eq!(rules[0].target, PathBuf::from("/ws/memory"));
assert_eq!(rules[0].target, PathBuf::from("/ws/.insomnia/memory"));
assert_eq!(rules[0].permission, Permission::Write);
assert!(rules[0].recursive);
assert_eq!(rules[1].target, PathBuf::from("/ws/knowledge"));
assert_eq!(rules[1].target, PathBuf::from("/ws/.insomnia/knowledge"));
}
}

View File

@ -174,7 +174,7 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout, PathBuf) {
let dir = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let initial = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nbody body\n",

View File

@ -6,11 +6,11 @@
//! omitted, returns one entry per file (no excerpt) so the agent can
//! enumerate what records exist without knowing what's inside them.
//!
//! - `MemoryQuery` walks `memory/summary.md`, `memory/decisions/`,
//! `memory/requests/`. `memory/workflow/` and `memory/_staging/`
//! are excluded by construction.
//! - `KnowledgeQuery` walks `knowledge/*.md` and supports a `kind`
//! filter against the Knowledge frontmatter's `kind` field.
//! - `MemoryQuery` walks `.insomnia/memory/{summary.md,decisions/,
//! requests/}`. `.insomnia/memory/workflow/` and
//! `.insomnia/memory/_staging/` are excluded by construction.
//! - `KnowledgeQuery` walks `.insomnia/knowledge/*.md` and supports a
//! `kind` filter against the Knowledge frontmatter's `kind` field.
//!
//! No derived index — the file tree is the source of truth and is
//! re-scanned per call. 出現順: within a file by line order, across
@ -441,16 +441,16 @@ mod tests {
fn setup() -> (TempDir, WorkspaceLayout) {
let dir = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
std::fs::create_dir_all(dir.path().join("memory/decisions")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/requests")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/workflow")).unwrap();
std::fs::create_dir_all(dir.path().join("memory/_staging")).unwrap();
std::fs::create_dir_all(dir.path().join("knowledge")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/decisions")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/requests")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/workflow")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/memory/_staging")).unwrap();
std::fs::create_dir_all(dir.path().join(".insomnia/knowledge")).unwrap();
(dir, layout)
}
fn write_decision(dir: &Path, slug: &str, body: &str) {
let path = dir.join("memory/decisions").join(format!("{slug}.md"));
let path = dir.join(".insomnia/memory/decisions").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\n{body}",
n = now()
@ -459,7 +459,7 @@ mod tests {
}
fn write_knowledge(dir: &Path, slug: &str, kind: &str, description: &str, body: &str) {
let path = dir.join("knowledge").join(format!("{slug}.md"));
let path = dir.join(".insomnia/knowledge").join(format!("{slug}.md"));
let content = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nkind: {kind}\ndescription: \"{description}\"\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\n{body}",
n = now()
@ -514,7 +514,7 @@ mod tests {
let (dir, layout) = setup();
write_decision(dir.path(), "alpha", "body\n");
write_decision(dir.path(), "beta", "body\n");
let summary_path = dir.path().join("memory/summary.md");
let summary_path = dir.path().join(".insomnia/memory/summary.md");
std::fs::write(
&summary_path,
format!("---\nupdated_at: {n}\n---\nhello\n", n = now()),
@ -534,7 +534,7 @@ mod tests {
#[tokio::test]
async fn memory_query_finds_summary() {
let (dir, layout) = setup();
let summary_path = dir.path().join("memory/summary.md");
let summary_path = dir.path().join(".insomnia/memory/summary.md");
std::fs::write(
&summary_path,
format!("---\nupdated_at: {n}\n---\nthe needle is here\n", n = now()),
@ -552,9 +552,9 @@ mod tests {
#[tokio::test]
async fn memory_query_excludes_workflow_and_staging() {
let (dir, layout) = setup();
let wf = dir.path().join("memory/workflow/wf.md");
let wf = dir.path().join(".insomnia/memory/workflow/wf.md");
std::fs::write(&wf, "needle in workflow\n").unwrap();
let stg = dir.path().join("memory/_staging/abc.json");
let stg = dir.path().join(".insomnia/memory/_staging/abc.json");
std::fs::write(&stg, "needle in staging\n").unwrap();
let (_, tool) = memory_query_tool(layout, QueryConfig::default())();

View File

@ -144,7 +144,7 @@ mod tests {
#[tokio::test]
async fn read_decision_by_slug() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "alpha\nbeta\n").unwrap();
@ -159,7 +159,7 @@ mod tests {
#[tokio::test]
async fn read_summary_without_slug() {
let (dir, layout) = setup();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "summary body\n").unwrap();
@ -199,7 +199,7 @@ mod tests {
#[tokio::test]
async fn knowledge_path_resolution() {
let (dir, layout) = setup();
let path = dir.path().join("knowledge/policy.md");
let path = dir.path().join(".insomnia/knowledge/policy.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "k\n").unwrap();

View File

@ -149,7 +149,7 @@ mod tests {
#[tokio::test]
async fn write_creates_summary() {
let (dir, layout) = setup();
let path = dir.path().join("memory/summary.md");
let path = dir.path().join(".insomnia/memory/summary.md");
let content = format!("---\nupdated_at: {n}\n---\nbody\n", n = now());
let (meta, tool) = write_tool(layout)();
@ -187,7 +187,7 @@ mod tests {
#[tokio::test]
async fn write_update_existing() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let initial = format!(
"---\ncreated_at: {n}\nupdated_at: {n}\nsources: []\nstatus: open\n---\nold\n",
@ -220,7 +220,7 @@ mod tests {
#[tokio::test]
async fn write_does_not_persist_on_lint_failure() {
let (dir, layout) = setup();
let path = dir.path().join("memory/decisions/foo.md");
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
let bad = "no frontmatter at all";
let (_, tool) = write_tool(layout)();
let inp = serde_json::json!({

View File

@ -1,20 +1,28 @@
//! Workspace-level path layout for the memory subsystem.
//!
//! Resolves a workspace root into the concrete directories the linter
//! and tools operate on:
//! `WorkspaceLayout` carries the workspace root (typically the Pod's
//! pwd). All insomnia-managed content lives under the conventional
//! `<root>/.insomnia/` subdirectory — the same place that holds
//! `manifest.toml` and `prompts/`. The memory subsystem nests its
//! trees inside it:
//!
//! - `<root>/memory/summary.md`
//! - `<root>/memory/decisions/<slug>.md`
//! - `<root>/memory/requests/<slug>.md`
//! - `<root>/memory/workflow/<slug>.md`
//! - `<root>/memory/_staging/<id>.json`
//! - `<root>/knowledge/<slug>.md`
//! - `<root>/.insomnia/memory/summary.md`
//! - `<root>/.insomnia/memory/decisions/<slug>.md`
//! - `<root>/.insomnia/memory/requests/<slug>.md`
//! - `<root>/.insomnia/memory/workflow/<slug>.md`
//! - `<root>/.insomnia/memory/_staging/<id>.json`
//! - `<root>/.insomnia/knowledge/<slug>.md`
//!
//! Configuring `[memory]` with an empty body is therefore sufficient
//! for any workspace that already uses the `.insomnia/` convention; no
//! `workspace_root` override is needed.
use std::path::{Path, PathBuf};
use crate::error::LintError;
use crate::slug::Slug;
const INSOMNIA_DIR: &str = ".insomnia";
const MEMORY_DIR: &str = "memory";
const KNOWLEDGE_DIR: &str = "knowledge";
const SUMMARY_FILE: &str = "summary.md";
@ -81,12 +89,17 @@ impl WorkspaceLayout {
&self.root
}
/// `<root>/.insomnia/`. The base of every other memory path.
pub fn insomnia_dir(&self) -> PathBuf {
self.root.join(INSOMNIA_DIR)
}
pub fn memory_dir(&self) -> PathBuf {
self.root.join(MEMORY_DIR)
self.insomnia_dir().join(MEMORY_DIR)
}
pub fn knowledge_dir(&self) -> PathBuf {
self.root.join(KNOWLEDGE_DIR)
self.insomnia_dir().join(KNOWLEDGE_DIR)
}
pub fn summary_path(&self) -> PathBuf {
@ -126,13 +139,14 @@ impl WorkspaceLayout {
}
/// Classify a path under the memory tree. Returns `None` if the
/// path is not under `memory/` or `knowledge/` of this workspace,
/// or if it lives in `_staging/` (which is opaque to the linter).
/// path is not under `.insomnia/memory/` or `.insomnia/knowledge/`
/// of this workspace, or if it lives in `_staging/` (which is
/// opaque to the linter).
///
/// On a conventional path that's *almost* a record but malformed
/// (e.g. `decisions/Foo.md` with an invalid slug), returns
/// `Err(LintError::InvalidSlug | InvalidPath)` so the caller can
/// surface it as a write violation.
/// (e.g. `.insomnia/memory/decisions/Foo.md` with an invalid slug),
/// returns `Err(LintError::InvalidSlug | InvalidPath)` so the caller
/// can surface it as a write violation.
pub fn classify(&self, path: &Path) -> Result<Option<ClassifiedPath>, LintError> {
let memory = self.memory_dir();
let knowledge = self.knowledge_dir();
@ -221,7 +235,7 @@ mod tests {
#[test]
fn classifies_summary() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/summary.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/summary.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Summary);
@ -231,7 +245,7 @@ mod tests {
#[test]
fn classifies_decision_with_slug() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/decisions/foo-bar.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/foo-bar.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Decision);
@ -241,7 +255,7 @@ mod tests {
#[test]
fn classifies_knowledge() {
let cp = layout()
.classify(&PathBuf::from("/ws/knowledge/x.md"))
.classify(&PathBuf::from("/ws/.insomnia/knowledge/x.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Knowledge);
@ -250,7 +264,7 @@ mod tests {
#[test]
fn classifies_workflow() {
let cp = layout()
.classify(&PathBuf::from("/ws/memory/workflow/wf.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/workflow/wf.md"))
.unwrap()
.unwrap();
assert_eq!(cp.kind, RecordKind::Workflow);
@ -260,7 +274,7 @@ mod tests {
fn staging_returns_none() {
assert!(
layout()
.classify(&PathBuf::from("/ws/memory/_staging/abc.json"))
.classify(&PathBuf::from("/ws/.insomnia/memory/_staging/abc.json"))
.unwrap()
.is_none()
);
@ -285,7 +299,7 @@ mod tests {
#[test]
fn invalid_slug_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/decisions/Foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/Foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidSlug(_)));
}
@ -293,7 +307,7 @@ mod tests {
#[test]
fn nested_under_record_dir_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/decisions/sub/foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/decisions/sub/foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidPath(_)));
}
@ -301,7 +315,7 @@ mod tests {
#[test]
fn unknown_top_level_dir_rejected() {
let err = layout()
.classify(&PathBuf::from("/ws/memory/something/foo.md"))
.classify(&PathBuf::from("/ws/.insomnia/memory/something/foo.md"))
.unwrap_err();
assert!(matches!(err, LintError::InvalidPath(_)));
}

View File

@ -14,6 +14,8 @@ Workflow`/<slug>` で呼び出される制約付き作業フロー)は別 p
### 記録対象の 4 種
本ドキュメント以下のパスはすべて **`<workspace_root>/.insomnia/`** からの相対表記。`.insomnia/` は manifest / prompts と同じく workspace に紐付く insomnia コンテンツのルートで、memory もこの規約に従う。`workspace_root` 既定は Pod の pwd。
| 種別 | パス | 備考 |
| ---------------- | ---------------------------- | ------------------------------------------------------------------------------------------- |
| Always-on サマリ | `memory/summary.md` | 1-5k tokens 目安 |

View File

@ -2,3 +2,6 @@
Prefer the most specific tool for the job. When reading files you already know the path of, use the file-read tool directly instead of searching.
When searching, use grep/glob primitives rather than shell pipelines.
You can run multiple tools simultaneously by calling them within a single response.
It is recommended to run tools that handle asynchronous processing, such as queries and readings, in batches.

View File

@ -1,7 +1,5 @@
You are here as an agent of the "insomnia system".
Stay precise, edit code directly when asked, and avoid speculative refactoring. Explain what you changed in one short paragraph at the end of each turn.
{% include "common/workspace" %}
{% include "common/tool-usage" %}