feat: Workflowの読み取り位置変更の実装
This commit is contained in:
@@ -70,7 +70,7 @@ pub enum LintError {
|
||||
BodyTooLong { actual: usize, limit: usize },
|
||||
|
||||
#[error(
|
||||
"write to `memory/workflow/` is forbidden via the memory tool — Workflows are human-edited"
|
||||
"write to a Workflow path is forbidden via the memory tool — Workflows are human-edited"
|
||||
)]
|
||||
WorkflowWriteForbidden,
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Walks `<workspace>/memory/{decisions,requests}/`, `memory/workflow/`,
|
||||
//! and `<workspace>/knowledge/` to collect the slug set the linter
|
||||
//! needs for reference-integrity and same-slug-duplication checks.
|
||||
//! Walks `<workspace>/memory/{decisions,requests}/`,
|
||||
//! `<workspace>/workflow/`, and `<workspace>/knowledge/` to collect
|
||||
//! the slug set the linter needs for reference-integrity and
|
||||
//! same-slug-duplication checks.
|
||||
//!
|
||||
//! No caching: each lint call walks fresh. Tree size is expected to
|
||||
//! stay small (hundreds of files, not thousands).
|
||||
|
||||
@@ -335,7 +335,7 @@ mod tests {
|
||||
#[test]
|
||||
fn workflow_write_rejected() {
|
||||
let (dir, linter) = workspace();
|
||||
let path = dir.path().join(".insomnia/memory/workflow/wf.md");
|
||||
let path = dir.path().join(".insomnia/workflow/wf.md");
|
||||
let content =
|
||||
"---\ndescription: x\nmodel_invokation: false\nuser_invocable: true\n---\nbody"
|
||||
.to_string();
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! NOTE: Workflows are written by humans, not by the memory tool. The
|
||||
//! linter only validates frontmatter when invoked directly (e.g. by a
|
||||
//! future CLI / pre-commit hook). The memory write/edit tool rejects
|
||||
//! `memory/workflow/` paths outright via [`LintError::WorkflowWriteForbidden`].
|
||||
//! `.insomnia/workflow/` paths outright via
|
||||
//! [`LintError::WorkflowWriteForbidden`].
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -13,13 +13,17 @@ use manifest::{Permission, ScopeRule};
|
||||
|
||||
use crate::workspace::WorkspaceLayout;
|
||||
|
||||
/// Build deny rules that strip Write permission from `<workspace>/memory/`
|
||||
/// and `<workspace>/knowledge/`. Recursive — every descendant is capped
|
||||
/// at Read for the generic tools, including `memory/workflow/`.
|
||||
/// Build deny rules that strip Write permission from `<workspace>/memory/`,
|
||||
/// `<workspace>/knowledge/`, and `<workspace>/workflow/`. Recursive —
|
||||
/// every descendant is capped at Read for the generic tools.
|
||||
///
|
||||
/// Workflow files are human-edited on the host side; the generic CRUD
|
||||
/// tools must not touch them.
|
||||
pub fn deny_write_rules(layout: &WorkspaceLayout) -> Vec<ScopeRule> {
|
||||
vec![
|
||||
deny_write(layout.memory_dir().as_path()),
|
||||
deny_write(layout.knowledge_dir().as_path()),
|
||||
deny_write(layout.workflow_dir().as_path()),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -37,13 +41,14 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn deny_targets_memory_and_knowledge() {
|
||||
fn deny_targets_memory_knowledge_and_workflow() {
|
||||
let layout = WorkspaceLayout::new(PathBuf::from("/ws"));
|
||||
let rules = deny_write_rules(&layout);
|
||||
assert_eq!(rules.len(), 2);
|
||||
assert_eq!(rules.len(), 3);
|
||||
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/.insomnia/knowledge"));
|
||||
assert_eq!(rules[2].target, PathBuf::from("/ws/.insomnia/workflow"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! Parsing is intentionally lenient at the directory-scan level — one
|
||||
//! malformed SKILL.md emits `tracing::warn!` and is skipped, leaving sibling
|
||||
//! skills loadable. Internal Workflows (`memory/workflow/<slug>.md`) keep
|
||||
//! skills loadable. Internal Workflows (`.insomnia/workflow/<slug>.md`) keep
|
||||
//! their hard-error semantics.
|
||||
|
||||
use std::io;
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//! enumerate what records exist without knowing what's inside them.
|
||||
//!
|
||||
//! - `MemoryQuery` walks `.insomnia/memory/{summary.md,decisions/,
|
||||
//! requests/}`. `.insomnia/memory/workflow/` and
|
||||
//! `.insomnia/memory/_staging/` are excluded by construction.
|
||||
//! requests/}`. `.insomnia/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.
|
||||
//!
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Workflow loader and registry.
|
||||
//!
|
||||
//! Workflows live under `<workspace>/.insomnia/memory/workflow/<slug>.md`.
|
||||
//! They are human-authored Markdown documents with YAML frontmatter. The loader
|
||||
//! is intentionally strict about malformed records because Pod startup should
|
||||
//! Workflows live under `<workspace>/.insomnia/workflow/<slug>.md`. They are
|
||||
//! human-authored Markdown documents with YAML frontmatter. The loader is
|
||||
//! intentionally strict about malformed records because Pod startup should
|
||||
//! fail rather than silently ignoring a broken procedural instruction.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
@@ -26,8 +26,8 @@ pub const WORKFLOW_DESCRIPTION_HARD_CAP: usize = 1024;
|
||||
/// win over external skills.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkflowSource {
|
||||
/// `<workspace>/.insomnia/memory/workflow/<slug>.md`. Authored
|
||||
/// in-tree by the project.
|
||||
/// `<workspace>/.insomnia/workflow/<slug>.md`. Authored in-tree by
|
||||
/// the project.
|
||||
WorkspaceWorkflow,
|
||||
/// SKILL.md ingested from a `[skills] directories` entry in the
|
||||
/// manifest. `dir` is the skills root that contained
|
||||
@@ -309,15 +309,13 @@ mod tests {
|
||||
|
||||
fn setup() -> (TempDir, WorkspaceLayout) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".insomnia/memory/workflow")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".insomnia/workflow")).unwrap();
|
||||
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||
(dir, layout)
|
||||
}
|
||||
|
||||
fn write_workflow(root: &Path, slug: &str, frontmatter: &str, body: &str) {
|
||||
let path = root
|
||||
.join(".insomnia/memory/workflow")
|
||||
.join(format!("{slug}.md"));
|
||||
let path = root.join(".insomnia/workflow").join(format!("{slug}.md"));
|
||||
std::fs::write(path, format!("---\n{frontmatter}\n---\n{body}")).unwrap();
|
||||
}
|
||||
|
||||
@@ -373,6 +371,24 @@ mod tests {
|
||||
assert!(matches!(err, WorkflowLoadError::Frontmatter { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_under_memory_is_ignored() {
|
||||
// The legacy `.insomnia/memory/workflow/` location is no longer
|
||||
// a Workflow source. Files placed there must be ignored (the
|
||||
// loader is rooted at `.insomnia/workflow/` only).
|
||||
let dir = TempDir::new().unwrap();
|
||||
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||
let legacy = dir.path().join(".insomnia/memory/workflow");
|
||||
std::fs::create_dir_all(&legacy).unwrap();
|
||||
std::fs::write(
|
||||
legacy.join("ghost.md"),
|
||||
"---\ndescription: ghost\n---\nbody\n",
|
||||
)
|
||||
.unwrap();
|
||||
let got = load_workflows(&layout).unwrap();
|
||||
assert!(got.is_empty());
|
||||
}
|
||||
|
||||
fn skill_record(slug: &str, path: &Path) -> WorkflowRecord {
|
||||
WorkflowRecord {
|
||||
slug: Slug::parse(slug).unwrap(),
|
||||
@@ -471,7 +487,7 @@ mod tests {
|
||||
let s = ShadowedSkill {
|
||||
slug: Slug::parse("x").unwrap(),
|
||||
kept_source: WorkflowSource::WorkspaceWorkflow,
|
||||
kept_path: std::path::PathBuf::from("/ws/.insomnia/memory/workflow/x.md"),
|
||||
kept_path: std::path::PathBuf::from("/ws/.insomnia/workflow/x.md"),
|
||||
shadowed_source: WorkflowSource::Skill {
|
||||
dir: std::path::PathBuf::from("/skills"),
|
||||
},
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
//! `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:
|
||||
//! `manifest.toml` and `prompts/`. The trees inside it:
|
||||
//!
|
||||
//! - `<root>/.insomnia/workflow/<slug>.md`
|
||||
//! - `<root>/.insomnia/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`
|
||||
//!
|
||||
//! `memory/` is reserved for session-derived / generated state;
|
||||
//! Workflows are human-managed and live one level up under
|
||||
//! `.insomnia/workflow/`.
|
||||
//!
|
||||
//! Configuring `[memory]` with an empty body is therefore sufficient
|
||||
//! for any workspace that already uses the `.insomnia/` convention; no
|
||||
@@ -25,10 +28,10 @@ use crate::slug::Slug;
|
||||
const INSOMNIA_DIR: &str = ".insomnia";
|
||||
const MEMORY_DIR: &str = "memory";
|
||||
const KNOWLEDGE_DIR: &str = "knowledge";
|
||||
const WORKFLOW_DIR: &str = "workflow";
|
||||
const SUMMARY_FILE: &str = "summary.md";
|
||||
const DECISIONS_DIR: &str = "decisions";
|
||||
const REQUESTS_DIR: &str = "requests";
|
||||
const WORKFLOW_DIR: &str = "workflow";
|
||||
const STAGING_DIR: &str = "_staging";
|
||||
|
||||
/// What kind of record a path under the memory tree represents.
|
||||
@@ -114,8 +117,9 @@ impl WorkspaceLayout {
|
||||
self.memory_dir().join(REQUESTS_DIR)
|
||||
}
|
||||
|
||||
/// Workflow directory: `<root>/.insomnia/workflow/`.
|
||||
pub fn workflow_dir(&self) -> PathBuf {
|
||||
self.memory_dir().join(WORKFLOW_DIR)
|
||||
self.insomnia_dir().join(WORKFLOW_DIR)
|
||||
}
|
||||
|
||||
pub fn staging_dir(&self) -> PathBuf {
|
||||
@@ -139,9 +143,9 @@ impl WorkspaceLayout {
|
||||
}
|
||||
|
||||
/// Classify a path under the memory tree. Returns `None` if the
|
||||
/// path is not under `.insomnia/memory/` or `.insomnia/knowledge/`
|
||||
/// of this workspace, or if it lives in `_staging/` (which is
|
||||
/// opaque to the linter).
|
||||
/// path is not under `.insomnia/memory/`, `.insomnia/knowledge/`,
|
||||
/// or `.insomnia/workflow/` 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. `.insomnia/memory/decisions/Foo.md` with an invalid slug),
|
||||
@@ -150,10 +154,14 @@ impl WorkspaceLayout {
|
||||
pub fn classify(&self, path: &Path) -> Result<Option<ClassifiedPath>, LintError> {
|
||||
let memory = self.memory_dir();
|
||||
let knowledge = self.knowledge_dir();
|
||||
let workflow = self.workflow_dir();
|
||||
|
||||
if let Ok(rel) = path.strip_prefix(&knowledge) {
|
||||
return Ok(Some(classify_kinded_md(rel, RecordKind::Knowledge, path)?));
|
||||
}
|
||||
if let Ok(rel) = path.strip_prefix(&workflow) {
|
||||
return Ok(Some(classify_kinded_md(rel, RecordKind::Workflow, path)?));
|
||||
}
|
||||
let rel = match path.strip_prefix(&memory) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return Ok(None),
|
||||
@@ -183,8 +191,6 @@ impl WorkspaceLayout {
|
||||
RecordKind::Decision
|
||||
} else if first == REQUESTS_DIR {
|
||||
RecordKind::Request
|
||||
} else if first == WORKFLOW_DIR {
|
||||
RecordKind::Workflow
|
||||
} else {
|
||||
return Err(LintError::InvalidPath(path.to_path_buf()));
|
||||
};
|
||||
@@ -264,10 +270,19 @@ mod tests {
|
||||
#[test]
|
||||
fn classifies_workflow() {
|
||||
let cp = layout()
|
||||
.classify(&PathBuf::from("/ws/.insomnia/memory/workflow/wf.md"))
|
||||
.classify(&PathBuf::from("/ws/.insomnia/workflow/wf.md"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(cp.kind, RecordKind::Workflow);
|
||||
assert_eq!(cp.slug.unwrap().as_str(), "wf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workflow_under_memory_is_invalid_path() {
|
||||
let err = layout()
|
||||
.classify(&PathBuf::from("/ws/.insomnia/memory/workflow/wf.md"))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, LintError::InvalidPath(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user