feat: Workflowの読み取り位置変更の実装

This commit is contained in:
Keisuke Hirata 2026-05-08 00:15:50 +09:00
parent 40cde699a8
commit b6b4168503
18 changed files with 330 additions and 63 deletions

View File

@ -1,2 +1,7 @@
_staging
memory
# Generated / session-derived memory state
/memory/_staging/
/memory/summary.md
/memory/decisions/
/memory/requests/
# Project-authored workflows and knowledge are intentionally tracked.

View File

@ -0,0 +1,149 @@
---
description: TODO.md と tickets/ から半自動で実装候補を選び、worktree・実装 Pod・reviewer・人間確認を使い分けて完了候補まで進める
model_invokation: false
user_invocable: true
requires: []
---
# Auto Maintain Workflow
半自動 maintainer として、`TODO.md` と `tickets/` を俯瞰し、実装できる作業を選び、必要に応じて worktree / 実装 Pod / reviewer Pod を orchestration する。最終判断、git commit / merge / push、ticket 完了削除は人間に戻す。
この Workflow は常駐 scheduler ではない。ユーザーが `/auto-maintain` を明示的に呼んだ時だけ動く。
## 基本方針
- main workspace は制御面として扱う。
- `.insomnia/`
- `TODO.md`
- `tickets/`
- `docs/report/`
- maintainer inbox / trial log
- 実装差分は原則 child git worktree に隔離する。
- child worktree には `.insomnia` を置かない。必要なら `/worktree-workflow` の手順に従い sparse checkout で `.insomnia` を除外する。
- 実装 Pod と reviewer Pod は原則分ける。ただし scope 衝突や作業粒度により、親 Pod が review してよい。
- review artifact や完了候補の記録は main workspace 側に置き、実装 Pod には書かせない。
- git commit / merge / push / ticket 完了削除は行わず、人間へ完了候補として報告する。
## 事前調査
最初に以下を読む。
1. `TODO.md`
2. 対象候補の `tickets/*.md`
3. 関連 docs / report
4. 既存 review file があれば `tickets/*.review.md`
TODO と tickets の不整合を見つけた場合は、勝手に ticket を作成・削除せず、人間へ報告する。
## 着手候補の選び方
優先する作業:
- ticket の要件と完了条件が具体的
- 影響範囲が限定的
- build / test で確認しやすい
- scope / permission / history 永続化 / prompt context 加工原則に触れない
- narrow write scope を切りやすい
初回試走や小さな運用確認では、TUI 表示改善など局所的な ticket を優先する。
避ける、または人間確認してから進める作業:
- 複数の設計方針が自然に導け、将来構造に影響する
- permission / scope / Pod lifecycle / history persistence / prompt context 加工原則に触れる
- manifest cascade や protocol wire を広く変える
- test 不能または再現不能
- ticket 自体の大幅な要件変更が必要
## エスカレーション基準
以下では作業を止めて人間へ確認する。
- ticket の要件から複数の設計方針が自然に導ける
- scope / permission / history 永続化 / prompt context 加工原則など安全モデルに触れる
- 新 ticket 追加、既存 ticket の大幅変更、ticket 完了削除が必要
- git commit / merge / push / branch cleanup などの git 書き込み操作が必要
- `git worktree add` など、作業ツリー作成の許可が明示されていない
- テスト不能、再現不能、作業範囲外の不具合に遭遇した
- child worktree に `.insomnia` が出てしまった
## Worktree 作成
実装差分を隔離する必要がある場合、`/worktree-workflow` の手順を使う。要点は以下。
```bash
git worktree add .worktree/<task-name> -b <task-name>
git -C .worktree/<task-name> sparse-checkout init --no-cone
git -C .worktree/<task-name> sparse-checkout set --no-cone \
'/*' \
'!/.insomnia/' \
'!/.insomnia/**'
```
`git worktree add` は git 書き込み操作なので、ユーザーが明示的に許可していない場合は実行前に確認する。
## 実装 Pod の spawn
実装 Pod を使う場合は、対象 ticket、child worktree path、write scope、禁止事項を明示する。
推奨 scope:
- read: main workspace 全体
- write: child worktree 内の必要最小ディレクトリ
実装 Pod への依頼には以下を含める。
- 対象 ticket と完了条件
- 作業対象は child worktree であり、Bash は `cd <child-worktree> && ...` で実行すること
- `.insomnia` / `TODO.md` / `tickets/` / review artifact / inbox は書かないこと
- git commit / merge / push はしないこと
- build / test / format の結果を報告すること
- 未解決事項と人間確認が必要な点を報告すること
子 Pod の出力は `ReadPodOutput` で確認し、追加依頼が必要なら `SendToPod` を使う。不要になった Pod は `StopPod` する。
## Review
実装 Pod 完了後、原則として実装 Pod を停止して scope を回収してから review する。
reviewer Pod を使う場合は read-only を基本にする。reviewer には以下を依頼する。
- ticket の背景・要件・完了条件を読む
- diff が要件を満たすか確認する
- 既存設計を歪めていないか確認する
- 不必要な実装や過剰な抽象がないか確認する
- build / test 結果が妥当か確認する
review artifact を書く場合は、親 Pod が main workspace 側に書く。child worktree 内の `tickets/` に書く運用は scope 衝突を起こしやすいため避ける。
## 修正依頼
review 指摘があれば、指摘内容をまとめて実装 Pod に再依頼する。既存 Pod を止めている場合は、同じ child worktree と narrow write scope で再 spawn する。
修正後は build / test を再確認し、必要なら再 review する。
## 完了候補報告
最後は作業を完了削除せず、次の形式で人間へ報告する。
```text
完了候補:
- ticket: <path>
- worktree: <path>
- branch: <name>
- 実装概要: ...
- 変更ファイル: ...
- build/test: ...
- review: approve / changes requested / skipped
- 未解決事項: ...
- 人間に戻す判断:
- commit するか
- merge するか
- ticket / review file を更新または削除するか
- TODO.md から削除するか
```
## 試走記録
この Workflow 自体の試走では、対象 ticket、scope 方針、実装 Pod / reviewer の使い分け、停止した理由、不足点を記録する。不足が見つかっても、この Workflow 内で永続ジョブキュー、scope handoff、Pod sleep、ticket lifecycle 再設計を実装しない。必要なら別 ticket として人間に提案する。

View File

@ -0,0 +1,87 @@
---
description: Git worktree を使って実装用作業ツリーを作り、main workspace の管理ファイルと子 worktree のコード差分を分離して開発を進める
model_invokation: false
user_invocable: true
requires: []
---
# Worktree Workflow
Git worktree を使って、実装差分を main workspace から分離して進める。main workspace は ticket / review / inbox / `.insomnia` を持つ制御面、子 worktree はコード変更専用の作業面として扱う。
この Workflow は `.claude/skills/worktree-workflow/SKILL.md` の運用を insomnia 向けに移植したもの。insomnia では Pod の write scope が排他的に委譲されるため、子 worktree に `.insomnia` を置かず、親 Pod が main workspace 側の管理ファイルを書ける状態を保つ。
## 原則
- 1 セッション / 1 ticket / 1 task につき 1 worktree を作る。
- worktree は `./.worktree/<task-name>` に作る。
- branch 名は原則 `<task-name>` と同じにする。
- 子 worktree はコード差分専用にし、`TODO.md` / `tickets/` / `docs/report/` / inbox などの管理ファイルは main workspace 側で扱う。
- 子 worktree には `.insomnia` を出さない。worktree 作成後に sparse checkout で `.insomnia` を除外する。
- git commit / merge / push / branch deletion / worktree remove は、人間が明示した場合以外は行わない。
## 事前確認
作業前に以下を確認する。
1. 対象 ticket / task 名が決まっているか。
2. branch / worktree 名に使える kebab-case の `<task-name>` があるか。
3. git 書き込み操作を行ってよい明示許可があるか。
4. main workspace の未保存差分や既存 worktree と衝突しないか。
許可が曖昧な場合、`git worktree add` の前に人間へ確認する。
## 作成手順
`<task-name>` を確定したら、main workspace で以下を実行する。
```bash
git worktree add .worktree/<task-name> -b <task-name>
git -C .worktree/<task-name> sparse-checkout init --no-cone
git -C .worktree/<task-name> sparse-checkout set --no-cone \
'/*' \
'!/.insomnia/' \
'!/.insomnia/**'
```
既に branch がある場合は `-b` で新規作成せず、既存 branch を使うか人間へ確認する。`git worktree add` が失敗した場合は、worktree / branch / lock の状態を確認してから人間へ報告する。
## 子 Pod へ渡す scope
子 Pod を使う場合、子 Pod の cwd は現状 main workspace のままになる。子 Pod には作業対象が child worktree であることを明示し、Bash 実行時は必ず `cd .worktree/<task-name> && ...` させる。
推奨 scope:
- read: main workspace 全体
- write: child worktree の必要最小ディレクトリだけ
例:
```text
read: <repo>
write: <repo>/.worktree/<task-name>/crates/tui
```
子 Pod には `tickets/` や inbox を書かせない。review artifact や完了候補の記録は親 Pod が main workspace 側に書く。
## 実装中のルール
- child worktree 内で build / test / format を実行する。
- main workspace の `.insomnia` や memory は child worktree にコピーしない。
- `.insomnia` が child worktree に現れた場合は作業を止め、sparse checkout 設定を確認する。
- ticket 要件外の設計変更、scope / permission / history 永続化 / prompt context 加工原則に触れる変更は実装前に人間へ確認する。
- 依存関係追加や大きな設計変更が必要になった場合も人間へ確認する。
## 完了時
実装が終わったら、merge は行わず、以下を報告する。
- worktree path
- branch 名
- 変更ファイル
- 実装概要
- 実行した build / test / format
- 未解決事項
- review に回せるかどうか
マージウィンドウとして人間が明示的にこの Workflow を呼んだ場合だけ、merge / worktree remove / branch cleanup の候補手順を提示する。実行は人間の明示許可を待つ。

View File

@ -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,

View File

@ -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).

View File

@ -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();

View File

@ -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};

View File

@ -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"));
}
}

View File

@ -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;

View File

@ -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.
//!

View File

@ -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"),
},

View File

@ -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]

View File

@ -148,8 +148,8 @@ pub struct Pod<C: LlmClient, St: Store> {
/// [`Self::from_manifest`], or defaults to the builtin pack when a
/// Pod is constructed through lower-level paths that have no loader.
prompts: Arc<PromptCatalog>,
/// Registry loaded from `<workspace>/.insomnia/memory/workflow/*.md`
/// when memory is enabled. Missing memory config keeps this empty.
/// Registry loaded from `<workspace>/.insomnia/workflow/*.md` when
/// memory is enabled. Missing memory config keeps this empty.
workflow_registry: memory::WorkflowRegistry,
/// Memory workspace layout used by the workflow resolver to load required
/// Knowledge records by exact slug.

View File

@ -154,7 +154,7 @@ pub struct SystemPromptContext<'a> {
/// section entirely (memory disabled, or a Phase 2 worker that opts
/// out); `Some(&[])` also yields no section.
pub resident_knowledge: Option<&'a [ResidentKnowledgeEntry]>,
/// Resident workflow descriptions from `<workspace>/memory/workflow/*`
/// Resident workflow descriptions from `<workspace>/.insomnia/workflow/*`
/// whose frontmatter has `model_invokation: true`. `None` disables the
/// section; Phase 2 workers opt out together with resident Knowledge.
pub resident_workflows: Option<&'a [ResidentWorkflowEntry]>,

View File

@ -147,7 +147,7 @@ mod tests {
"---\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\nkind: policy\ndescription: p\nmodel_invokation: false\nuser_invocable: true\nlast_sources: []\n---\npolicy body\n",
);
write(
&dir.path().join(".insomnia/memory/workflow/run-it.md"),
&dir.path().join(".insomnia/workflow/run-it.md"),
"---\ndescription: run\nrequires: [policy]\n---\nworkflow body\n",
);
let registry = memory::load_workflows(&layout).unwrap();
@ -171,7 +171,7 @@ mod tests {
fn user_invocable_false_errors() {
let (dir, layout, _registry) = setup();
write(
&dir.path().join(".insomnia/memory/workflow/hidden.md"),
&dir.path().join(".insomnia/workflow/hidden.md"),
"---\ndescription: hidden\nuser_invocable: false\n---\nbody\n",
);
let registry = memory::load_workflows(&layout).unwrap();
@ -184,7 +184,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
write(
&dir.path().join(".insomnia/memory/workflow/bad.md"),
&dir.path().join(".insomnia/workflow/bad.md"),
"---\ndescription: bad\nrequires: [ghost]\n---\nbody\n",
);
let registry = memory::load_workflows(&layout).unwrap();

View File

@ -98,7 +98,7 @@ Linter ルールは 2 系統:
- Decisions / Requests: `created_at`, `updated_at`, `sources`
- Knowledge: `kind`, `description`, `model_invokation`, `user_invocable`, `last_sources`, `created_at`, `updated_at`
- Summary: `updated_at`optional: `last_rewritten_from_range`
- `memory/workflow/` への書き込み禁止sub-Worker context のみ、人間編集は除外)
- Workflow パス(`.insomnia/workflow/`への書き込み禁止sub-Worker context のみ、人間編集は除外)
- 同 slug での新規作成禁止(既存があれば update に切り替えるサイン)
- `#<slug>` 参照が実在ファイルを指す
- `replaced_by: <slug>` が実在 record を指す

View File

@ -24,7 +24,8 @@ Workflow は制約付きの強制的な作業フロー。`/<slug>` で明示的
### 格納先とファイル形式
- `memory/workflow/<slug>.md`(ファイル名 = slug がそのまま識別子、`name` field は持たない)
- `.insomnia/workflow/<slug>.md`(ファイル名 = slug がそのまま識別子、`name` field は持たない)
- `.insomnia/memory/` は session-derived state 専用、Workflow は配置しない
- frontmatter + Markdown 本文
- frontmatter フィールド: `description`, `auto_invoke`, `user_invocable`, `requires`

View File

@ -26,25 +26,14 @@ Workflow の標準配置を以下に変更する。
<workspace>/.insomnia/memory/workflow/<slug>.md
```
新規作成・ドキュメント・補完・resolver の説明は新配置を使う。
### 互換読み取り
移行期間のため、loader は当面旧配置も読む。
- 新配置 `.insomnia/workflow/<slug>.md` を優先する
- 旧配置 `.insomnia/memory/workflow/<slug>.md` も fallback として読む
- 同じ slug が両方にある場合は新配置を採用する
- 旧配置を読んだ場合、可能なら warning / notification / log のいずれかで migration hint を出す
旧配置の完全廃止は本チケットでは行わない。廃止が必要になったら別 ticket とする。
新規作成・ドキュメント・補完・resolver の説明は新配置を使う。旧配置の互換読み取りは行わない(このリポジトリ内で完結する移行であり、外部利用者の互換配慮は不要との判断)。旧配置にファイルが残っていても loader は読まないし、linter / scope deny も新配置のみを対象にする。
### linter / registry / completion
- Workflow linter は新配置を検査できる
- `requires` の Knowledge 参照検査は従来通り維持する
- `WorkflowRegistry` source path として新旧どちらも保持でき
- `/<slug>` completion / invocation は新配置・旧配置の双方から読める
- `WorkflowRegistry` は新配置の source path を保持する
- `/<slug>` completion / invocation は新配置から読める
- resident workflow (`model_invokation: true`) の広告も新配置を対象にする
### memory state との分離
@ -104,15 +93,13 @@ workspace に既存 Workflow がある場合、新配置へ移す。
- Workflow の自動生成ポリシー変更
- Workflow frontmatter schema の大幅変更
- Workflow DSL 化
- 旧配置の即時廃止
- memory tool で Workflow を書けるようにする変更
- 内部 Worker Workflow 化そのもの(`tickets/internal-worker-workflow.md` の対象)
## 完了条件
- Workflow の canonical path が `.insomnia/workflow/<slug>.md` として実装・文書化されている
- 既存の `.insomnia/memory/workflow/<slug>.md` も互換読み取りできる
- 新旧両方に同じ slug がある場合、新配置が優先される
- loader / linter / scope deny / completion / invocation が新配置のみを対象にしている
- Workflow linter / loader / completion / invocation のテストが新配置をカバーしている
- `.insomnia/.gitignore` が generated memory state のみを ignore し、`.insomnia/workflow/*.md` を Git 管理できる
- この workspace の既存 Workflow が `.insomnia/workflow/` に移されている