Compare commits
3 Commits
043c2e862c
...
862c38d7f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 862c38d7f7 | |||
| 0ad3923932 | |||
| 588c25a570 |
5
TODO.md
5
TODO.md
|
|
@ -1,4 +1,6 @@
|
|||
- [ ] Agent Skills サポート → [tickets/agent-skills.md](tickets/agent-skills.md)
|
||||
- [ ] Workflow / Skills
|
||||
- [ ] Workflow 実装 → [tickets/workflow.md](tickets/workflow.md)
|
||||
- [ ] Agent Skills を Workflow として ingest → [tickets/agent-skills.md](tickets/agent-skills.md)
|
||||
- [ ] ツール設計
|
||||
- [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md)
|
||||
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||
|
|
@ -10,6 +12,7 @@
|
|||
- [ ] フルスクリーン化によるオーバーホール → [tickets/tui-fullscreen-overhaul.md](tickets/tui-fullscreen-overhaul.md)
|
||||
- [ ] Run 中の入力キューイング → [tickets/tui-input-queue.md](tickets/tui-input-queue.md)
|
||||
- [ ] ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
|
||||
- [ ] 入力欄の単語単位カーソル移動・削除 → [tickets/tui-input-word-motion.md](tickets/tui-input-word-motion.md)
|
||||
- [ ] サブミット入力
|
||||
- [ ] TUI 補完 + 型付き atom 化 → [tickets/submit-tui-completion.md](tickets/submit-tui-completion.md)
|
||||
- [ ] セッションログの Segment 保持 → [tickets/session-log-segments.md](tickets/session-log-segments.md)
|
||||
|
|
|
|||
|
|
@ -398,12 +398,21 @@ impl App {
|
|||
pub fn delete_char_after(&mut self) {
|
||||
self.input.delete_after();
|
||||
}
|
||||
pub fn delete_word_before(&mut self) {
|
||||
self.input.delete_word_before();
|
||||
}
|
||||
pub fn move_cursor_left(&mut self) {
|
||||
self.input.move_left();
|
||||
}
|
||||
pub fn move_cursor_right(&mut self) {
|
||||
self.input.move_right();
|
||||
}
|
||||
pub fn move_cursor_word_left(&mut self) {
|
||||
self.input.move_word_left();
|
||||
}
|
||||
pub fn move_cursor_word_right(&mut self) {
|
||||
self.input.move_word_right();
|
||||
}
|
||||
pub fn move_cursor_home(&mut self) {
|
||||
self.input.move_home();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,48 @@ pub enum Atom {
|
|||
Paste(PasteRef),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AtomClass {
|
||||
Word(WordKind),
|
||||
Sep,
|
||||
Paste,
|
||||
}
|
||||
|
||||
/// Sub-classification of word atoms. A run of equal `WordKind` is one word;
|
||||
/// a kind switch is a word boundary. Lets `Ctrl+Left/Right` step over
|
||||
/// runs of hiragana/katakana/han/ASCII independently when they sit adjacent.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum WordKind {
|
||||
Ascii,
|
||||
Hiragana,
|
||||
Katakana,
|
||||
Han,
|
||||
Other,
|
||||
}
|
||||
|
||||
fn atom_class(atom: &Atom) -> AtomClass {
|
||||
match atom {
|
||||
Atom::Paste(_) => AtomClass::Paste,
|
||||
Atom::Char(c) => char_class(*c),
|
||||
}
|
||||
}
|
||||
|
||||
fn char_class(c: char) -> AtomClass {
|
||||
if c.is_ascii_alphanumeric() || c == '_' {
|
||||
return AtomClass::Word(WordKind::Ascii);
|
||||
}
|
||||
let cp = c as u32;
|
||||
match cp {
|
||||
0x3040..=0x309F => AtomClass::Word(WordKind::Hiragana),
|
||||
0x30A0..=0x30FF | 0x31F0..=0x31FF => AtomClass::Word(WordKind::Katakana),
|
||||
0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x2FFFF => {
|
||||
AtomClass::Word(WordKind::Han)
|
||||
}
|
||||
_ if c.is_alphanumeric() => AtomClass::Word(WordKind::Other),
|
||||
_ => AtomClass::Sep,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputBuffer {
|
||||
atoms: Vec<Atom>,
|
||||
/// Insertion point in `0..=atoms.len()`.
|
||||
|
|
@ -106,6 +148,15 @@ impl InputBuffer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Delete one word backward — the same span [`move_word_left`] would
|
||||
/// jump over.
|
||||
pub fn delete_word_before(&mut self) {
|
||||
let end = self.cursor;
|
||||
self.move_word_left();
|
||||
let start = self.cursor;
|
||||
self.atoms.drain(start..end);
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
}
|
||||
|
|
@ -114,6 +165,38 @@ impl InputBuffer {
|
|||
self.cursor = (self.cursor + 1).min(self.atoms.len());
|
||||
}
|
||||
|
||||
/// Move backward by one word. Skips a run of separators, then a run of
|
||||
/// atoms sharing the same [`AtomClass`] — so `Word(Hiragana)` next to
|
||||
/// `Word(Han)` are separate blocks, and a `Paste` atom is its own block.
|
||||
pub fn move_word_left(&mut self) {
|
||||
while self.cursor > 0 && atom_class(&self.atoms[self.cursor - 1]) == AtomClass::Sep {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
if self.cursor == 0 {
|
||||
return;
|
||||
}
|
||||
let kind = atom_class(&self.atoms[self.cursor - 1]);
|
||||
while self.cursor > 0 && atom_class(&self.atoms[self.cursor - 1]) == kind {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move forward by one word. Mirror of [`move_word_left`].
|
||||
pub fn move_word_right(&mut self) {
|
||||
while self.cursor < self.atoms.len()
|
||||
&& atom_class(&self.atoms[self.cursor]) == AtomClass::Sep
|
||||
{
|
||||
self.cursor += 1;
|
||||
}
|
||||
if self.cursor == self.atoms.len() {
|
||||
return;
|
||||
}
|
||||
let kind = atom_class(&self.atoms[self.cursor]);
|
||||
while self.cursor < self.atoms.len() && atom_class(&self.atoms[self.cursor]) == kind {
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_home(&mut self) {
|
||||
while self.cursor > 0 {
|
||||
if matches!(self.atoms[self.cursor - 1], Atom::Char('\n')) {
|
||||
|
|
@ -489,3 +572,258 @@ mod submit_segments_tests {
|
|||
assert!(matches!(segs[0], Segment::Paste { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod word_motion_tests {
|
||||
use super::*;
|
||||
|
||||
fn buf_from(text: &str) -> InputBuffer {
|
||||
let mut buf = InputBuffer::new();
|
||||
for c in text.chars() {
|
||||
buf.insert_char(c);
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
fn cursor(buf: &InputBuffer) -> usize {
|
||||
buf.cursor
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_buffer_is_noop() {
|
||||
let mut buf = InputBuffer::new();
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_from_start_lands_after_first_word() {
|
||||
let mut buf = buf_from("foo bar baz");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 3); // after "foo"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 7); // after "foo bar"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 11); // after "foo bar baz"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 11); // end stays put
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_from_end_lands_at_last_word_start() {
|
||||
let mut buf = buf_from("foo bar baz");
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 8); // start of "baz"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 4); // start of "bar"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0); // start of "foo"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_runs_of_separators() {
|
||||
let mut buf = buf_from("a , b");
|
||||
buf.cursor = 1; // just after "a"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 7); // after "b"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 6); // start of "b"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0); // start of "a"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newline_is_a_separator() {
|
||||
let mut buf = buf_from("foo\nbar");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 3);
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 7);
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 4);
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paste_counts_as_one_word() {
|
||||
let mut buf = InputBuffer::new();
|
||||
for c in "foo ".chars() {
|
||||
buf.insert_char(c);
|
||||
}
|
||||
buf.insert_paste("anything".into());
|
||||
for c in " bar".chars() {
|
||||
buf.insert_char(c);
|
||||
}
|
||||
// atoms: f o o ' ' [P] ' ' b a r → 9 atoms, paste at index 4
|
||||
let end = 9;
|
||||
buf.cursor = end;
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 6); // start of "bar"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 4); // before paste
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0); // start of "foo"
|
||||
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 3); // after "foo"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 5); // after paste
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 9); // after "bar"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underscore_is_a_word_char() {
|
||||
let mut buf = buf_from("foo_bar baz");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 7); // "foo_bar" is one word
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hiragana_run_is_one_word() {
|
||||
// "こんにちは" — 5 hiragana atoms, no separators.
|
||||
let mut buf = buf_from("こんにちは");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 5);
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_switch_is_a_word_boundary() {
|
||||
// 漢字 | ひらがな | ASCII
|
||||
let mut buf = buf_from("日本語のtest");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 3); // after "日本語"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 4); // after "の"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 8); // after "test"
|
||||
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 4); // start of "test"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 3); // start of "の"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0); // start of "日本語"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn katakana_separates_from_ascii() {
|
||||
let mut buf = buf_from("カタカナsecret");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 4); // after "カタカナ"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 10); // after "secret"
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 4);
|
||||
buf.move_word_left();
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
/// Render atoms as a string for assertions; pastes become `<P>`.
|
||||
fn as_text(buf: &InputBuffer) -> String {
|
||||
let mut out = String::new();
|
||||
for a in &buf.atoms {
|
||||
match a {
|
||||
Atom::Char(c) => out.push(*c),
|
||||
Atom::Paste(_) => out.push_str("<P>"),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_removes_trailing_word_at_end() {
|
||||
let mut buf = buf_from("foo bar");
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "foo ");
|
||||
assert_eq!(cursor(&buf), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_removes_word_at_cursor() {
|
||||
let mut buf = buf_from("foo bar");
|
||||
buf.cursor = 3; // right after "foo"
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), " bar");
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_swallows_trailing_separators() {
|
||||
let mut buf = buf_from("foo ");
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "");
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_at_start_is_noop() {
|
||||
let mut buf = buf_from("foo");
|
||||
buf.cursor = 0;
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "foo");
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_respects_script_boundary() {
|
||||
// 「日本語の」末尾から1回削除すると、ひらがな部分「の」だけ消える
|
||||
let mut buf = buf_from("日本語の");
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "日本語");
|
||||
assert_eq!(cursor(&buf), 3);
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "");
|
||||
assert_eq!(cursor(&buf), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_word_treats_paste_as_one_unit() {
|
||||
let mut buf = InputBuffer::new();
|
||||
for c in "foo ".chars() {
|
||||
buf.insert_char(c);
|
||||
}
|
||||
buf.insert_paste("anything".into());
|
||||
for c in " bar".chars() {
|
||||
buf.insert_char(c);
|
||||
}
|
||||
// atoms: f o o ' ' [P] ' ' b a r (cursor at end = 9)
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "foo <P> ");
|
||||
assert_eq!(cursor(&buf), 6);
|
||||
// Next deletion: trailing space then the paste atom (kind=Paste)
|
||||
buf.delete_word_before();
|
||||
assert_eq!(as_text(&buf), "foo ");
|
||||
assert_eq!(cursor(&buf), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_punctuation_is_a_separator() {
|
||||
// 「、」 (U+3001) and 「。」 (U+3002) are not word chars.
|
||||
let mut buf = buf_from("読んだ、走った。");
|
||||
buf.cursor = 0;
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 1); // after "読" (han run of 1)
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 3); // after "んだ" (hiragana run)
|
||||
// "、" is sep — skipped, then han "走"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 5); // after "走"
|
||||
buf.move_word_right();
|
||||
assert_eq!(cursor(&buf), 7); // after "った"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,6 +407,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
|||
None
|
||||
}
|
||||
KeyCode::Enter => app.submit_input(),
|
||||
KeyCode::Backspace if ctrl => {
|
||||
app.delete_word_before();
|
||||
None
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.delete_char_before();
|
||||
None
|
||||
|
|
@ -415,10 +419,18 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
|
|||
app.delete_char_after();
|
||||
None
|
||||
}
|
||||
KeyCode::Left if ctrl => {
|
||||
app.move_cursor_word_left();
|
||||
None
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
None
|
||||
}
|
||||
KeyCode::Right if ctrl => {
|
||||
app.move_cursor_word_right();
|
||||
None
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# Agent Skills サポート
|
||||
# Agent Skills を Workflow として ingest
|
||||
|
||||
## 背景
|
||||
|
||||
[agentskills.io](https://agentskills.io/) で公開されているオープン標準 "Agent Skills" は、エージェントに手続き的知識・ドメイン能力をフォルダ単位で供給する形式。Anthropic 発で Claude Code / Cursor / OpenCode / Gemini CLI / Goose / OpenHands など主要な coding agent 群が採用しつつあり、ユーザーが既に書いた skill を insomnia に持ち込める/他ツールと共有できる価値が明確になっている。
|
||||
[agentskills.io](https://agentskills.io/) の "Agent Skills" は Anthropic 発のオープン標準で、Claude Code / Cursor / OpenCode / Gemini CLI / Goose / OpenHands など主要な coding agent 群が採用している。ユーザーが既に書いた skill を insomnia に持ち込めるようにすることで、他ツールと能力資産を共有できる。
|
||||
|
||||
`docs/ref/memory-systems.md` でも Hermes Agent の "Skill Library" が agentskills.io 互換として言及済みで、将来のセルフ生成メモリ経路 (procedural memory) の受け皿としてもこのフォーマットに合わせておく利がある。
|
||||
insomnia 側の一級概念は **Workflow** (`/<slug>`、`tickets/workflow.md`)。SKILL.md 形式は Workflow と意味的にほぼ同型(procedural な指示本体 + メタデータ + 付随リソース)であり、別経路として並列管理するより **Workflow にマップして ingest する**方が呼び出し UX (`/<slug>`) と内部経路を一本化できる。
|
||||
|
||||
`docs/plan/memory.md` が「`SKILL.md` 形式は採用しない」と書いているのは Knowledge (`#<slug>`) の表現形式としての話で、本チケットの「Workflow への ingest 経路」とは矛盾しない。
|
||||
|
||||
### Skill の骨格 (仕様要旨)
|
||||
|
||||
|
|
@ -20,79 +22,99 @@ SKILL.md frontmatter:
|
|||
|
||||
| フィールド | 必須 | 制約 |
|
||||
|---|---|---|
|
||||
| `name` | ○ | 1-64 chars, `[a-z0-9-]+`, 先頭末尾 `-` 不可, `--` 連続不可, **ディレクトリ名と一致** |
|
||||
| `name` | ○ | 1-64 chars, `[a-z0-9-]+`, **ディレクトリ名と一致** |
|
||||
| `description` | ○ | 1-1024 chars, 非空 |
|
||||
| `license` | | 自由文 |
|
||||
| `compatibility` | | 1-500 chars |
|
||||
| `metadata` | | 任意の key-value map |
|
||||
| `allowed-tools` | | experimental (空白区切りのツールリスト) |
|
||||
| `allowed-tools` | | experimental |
|
||||
|
||||
仕様の肝は **progressive disclosure**:
|
||||
## 前提チケット
|
||||
|
||||
1. メタデータ (`name` + `description`) — セッション開始時に**全 skill 分**をプロンプトに載せる (~100 tokens/skill)
|
||||
2. 指示本体 (SKILL.md body) — skill が必要になった時点で agent 側が読みに行く
|
||||
3. リソース (`scripts/` / `references/` / `assets/`) — さらに on-demand
|
||||
|
||||
## 既存機構との関係
|
||||
|
||||
insomnia のシステムプロンプトは現状、`instruction` (テンプレ本体) + scope summary + AGENTS.md (trailing section) の 3 要素構成。Skill はこの並びに**任意個のオプショナル能力資産**として追加する位置取り。instruction / AGENTS.md を置き換えない。
|
||||
|
||||
配置は既存の prefix addressing と同じ軸:
|
||||
|
||||
- `$XDG_CONFIG_HOME/insomnia/skills/<name>/SKILL.md`
|
||||
- `<project>/.insomnia/skills/<name>/SKILL.md`
|
||||
- `$insomnia/skills/` (ビルトイン) は不要になるまで作らない
|
||||
- `tickets/workflow.md` — Workflow loader / `/<slug>` resolve / `auto_invoke` 注入の本実装。本チケットはその ingest 経路を増やすだけで、Workflow 側の意味論には手を入れない
|
||||
|
||||
## 方針
|
||||
|
||||
### MVP スコープ
|
||||
### ロードソース
|
||||
|
||||
1. **Skill ディスカバリと検証**
|
||||
- 上記 2 経路のサブディレクトリを走査して `SKILL.md` を読む
|
||||
- frontmatter を仕様通り検証。name 不一致・必須欠落・制約違反は hard error (Pod 起動失敗)
|
||||
- 未知フィールドは `tracing::warn!` して無視 (pod-factory と同方針)
|
||||
- 重複 name は workspace 層優先で user 層を上書き + ユーザー向け notification 発行
|
||||
- **`$user/skills/<name>/SKILL.md`** — `$XDG_CONFIG_HOME/insomnia/skills/` 配下。**デフォルトで有効**
|
||||
- **`$workspace/skills/`** — **デフォルトで無効**。manifest の `[skills]` セクションで明示指定したパスのみ ingest する
|
||||
|
||||
2. **システムプロンプトへの注入 (progressive disclosure 準拠)**
|
||||
- `SystemPromptContext` に `skills: Vec<SkillSummary>` を追加 (`name`, `description`, 絶対パス)
|
||||
- ビルトイン `$insomnia/default.md` に skill 列挙ブロックを追加 (空なら一切出力しない)
|
||||
- ユーザーテンプレートからも `{{ skills }}` で参照可能
|
||||
- **本体は注入しない**。description の末尾に絶対パスを併記し、agent が Read ツールで取れるようにする
|
||||
```toml
|
||||
[skills]
|
||||
directories = [".claude/skills", ".cursor/skills"]
|
||||
```
|
||||
|
||||
3. **本体への agent アクセス**
|
||||
- skill ディレクトリを `Scope` に `permission = read` で自動 allow
|
||||
- `scripts/` の実行は将来 Bash ツール + Permission 層で成立するまで Read 経由での閲覧に留める
|
||||
各パスは workspace root からの相対 or 絶対。manifest の base directory に対して resolve する(既存 path 解決と同方針)。Claude Code / Cursor 等が既に書いている `.claude/skills/` `.cursor/skills/` をそのまま流用できることが目的。
|
||||
|
||||
- ビルトイン `$insomnia/skills/` は不要になるまで作らない(前ガイドラインのまま)
|
||||
|
||||
### SKILL → Workflow マッピング
|
||||
|
||||
| SKILL.md frontmatter | Workflow frontmatter | 備考 |
|
||||
|---|---|---|
|
||||
| `name` | (ファイル名 = slug として扱う) | `name` がディレクトリ名と一致することは仕様上の不変。slug としてはディレクトリ名を使用 |
|
||||
| `description` | `description` | そのまま |
|
||||
| — | `auto_invoke` | **`true` 固定**。agentskills の progressive disclosure(メタデータ常時注入)と整合 |
|
||||
| — | `user_invocable` | **`true` 固定** |
|
||||
| — | `requires` | **空配列**。SKILL 側に概念がない |
|
||||
| `license` / `compatibility` / `metadata` | — | 保持はするが Workflow 実行には影響しない |
|
||||
| `allowed-tools` | — | `permission-extension-point.md` が Permission 層を整備するまで `tracing::warn!` して無視 |
|
||||
|
||||
Workflow 本文には SKILL.md 本体(frontmatter 直下の Markdown)をそのまま使う。
|
||||
|
||||
### scripts / references / assets
|
||||
|
||||
skill ディレクトリ全体(`SKILL.md` 本体だけでなく `scripts/` `references/` `assets/`)を Pod の Scope に `permission = read` で自動 union する。`scripts/` の実行は Bash ツール + Permission 層(`permission-extension-point.md`)が整うまで Read 経由の閲覧に留める。
|
||||
|
||||
### 衝突解決
|
||||
|
||||
同一 slug が複数ソースから来た場合の優先順位:
|
||||
|
||||
1. `<workspace>/.insomnia/memory/workflow/<slug>.md`(内製 Workflow)
|
||||
2. workspace skills(manifest 指定パス)
|
||||
3. user skills(`$user/skills/`)
|
||||
|
||||
衝突時は上位を採用し、shadow した側について `Event::Notification` を発行する。「明示的に書かれた内製 Workflow が外部資産より強い」順に並べる。
|
||||
|
||||
### 検証
|
||||
|
||||
- frontmatter は SKILL 仕様通り検証。`name` ↔ ディレクトリ名一致、`description` 長さ、`name` の文字種制約を hard error
|
||||
- 検証エラーは個別 skill 単位で skip + `tracing::warn!`。一個の壊れた SKILL が Pod 起動を止めない(内製 Workflow とは違う扱い: 外部資産は緩く受ける)
|
||||
- 未知フィールドは無視
|
||||
|
||||
### 範囲外
|
||||
|
||||
- `allowed-tools` フィールドの実効化 — `permission-extension-point.md` が Permission 層を整備するまで受信して無視
|
||||
- skill 専用の実行機構 — `scripts/` は Bash / Read で自然に扱う
|
||||
- skill の自動生成 (Hermes 風 Skill Library) — memory 系チケットで別途。本チケットで作る ingest 経路を再利用する側
|
||||
- `allowed-tools` の実効化 → `permission-extension-point.md` 待ち
|
||||
- skill 専用の実行機構(`scripts/` の自動実行)— Bash ツールと Permission 層で自然に扱う
|
||||
- skill の自動生成(Hermes 風 Skill Library)— memory 系チケットで別途、本チケットの ingest 経路を再利用する側
|
||||
- ビルトイン skill (`$insomnia/skills/`) — 必要になるまで追加しない
|
||||
- skill 間依存の解決 — 独立単位、相互参照は本体の Markdown リンクで
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `$user/skills/` と `$workspace/skills/` の両方から skill をロードし、frontmatter 違反は Pod 起動エラーになる
|
||||
- ビルトインの `$insomnia/default.md` をレンダした結果、存在する skill の name + description + 絶対パスがシステムプロンプトに列挙される
|
||||
- skill が 0 件のとき、既存の出力 (AGENTS.md / scope summary) が全く変わらない
|
||||
- skill ディレクトリが scope の readable に含まれ、agent が Read ツールで `SKILL.md` 本体・`scripts/`・`references/` にアクセスできる
|
||||
- 単体テストで frontmatter 検証の正常 / 異常系、progressive disclosure レンダリング、user/workspace 重複解決が verify される
|
||||
- `$user/skills/` 配下の SKILL.md が Workflow として登録され、`/<name>` で呼び出せる
|
||||
- manifest で `[skills] directories = [...]` を指定した workspace では、そのパス配下の SKILL.md だけが追加で ingest される。指定しない workspace では workspace 側 skill は 0 件
|
||||
- 内製 Workflow と同 slug の skill は内製優先で shadow され、Notification が発行される
|
||||
- skill ディレクトリ(SKILL.md 本体・`scripts/`・`references/`・`assets/`)が scope readable に含まれ、agent が Read ツールでアクセスできる
|
||||
- frontmatter 違反の skill は warn でスキップされ、他の skill / Pod 起動は影響を受けない
|
||||
- 単体テストで frontmatter 検証、Workflow へのマッピング、衝突解決(内製 > workspace > user)、manifest 未指定時の workspace skip が verify される
|
||||
|
||||
## 実装順序
|
||||
|
||||
1. `manifest` または新設 `skill` クレートに `Skill` 構造体と `SkillDirectoryLoader` を置く。SKILL.md パースと検証のみでテスト完結
|
||||
2. `pod::SystemPromptContext` に `skills` を生やし、`ensure_system_prompt_materialized` で loader を呼ぶ
|
||||
3. `resources/prompts/default.md` に skills ブロックを追加、`Pod::from_manifest` で skill ディレクトリを scope readable に union
|
||||
4. 重複 name 解決と notification 発行を乗せる
|
||||
1. SKILL.md パーサと frontmatter 検証を実装。Workflow frontmatter への変換器を含めてテスト完結
|
||||
2. `$user/skills/` の loader を Workflow registry に接続
|
||||
3. manifest に `[skills] directories: Vec<PathBuf>` を追加し、workspace 側 ingest を実装
|
||||
4. 衝突解決と Notification 発行を乗せる
|
||||
5. skill ディレクトリの Scope union(read 自動 allow)
|
||||
|
||||
各ステップ終了時点でビルド通過・既存テスト合格を維持する。skill 0 件時の後方互換は Phase 2 で確認する。
|
||||
各ステップ終了時点でビルド通過・既存テスト合格を維持する。
|
||||
|
||||
## 参照
|
||||
|
||||
- 仕様本体: https://agentskills.io/specification
|
||||
- 採用状況: https://agentskills.io/home
|
||||
- Anthropic 公式サンプル: https://github.com/anthropics/skills
|
||||
- 検証 CLI: https://github.com/agentskills/agentskills/tree/main/skills-ref (将来 `pod skills validate` 相当を作るなら参考)
|
||||
- 類似機構: `crates/pod/src/prompt_loader.rs` (prefix addressing), `crates/pod/src/agents_md.rs` (cwd-relative ingest), `crates/pod/src/system_prompt.rs` (render pipeline)
|
||||
- 後続接続先: `docs/ref/memory-systems.md` の Skill Library (自動生成 skill の保存先)
|
||||
- 検証 CLI: https://github.com/agentskills/agentskills/tree/main/skills-ref
|
||||
- 前提: `tickets/workflow.md`
|
||||
- 関連: `tickets/permission-extension-point.md`(`allowed-tools` 実効化の受け皿)、`docs/plan/workflow.md`、`docs/plan/memory.md`、`docs/ref/memory-systems.md` §Skill Library
|
||||
|
|
|
|||
37
tickets/tui-input-word-motion.md
Normal file
37
tickets/tui-input-word-motion.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# TUI: 入力欄の単語単位カーソル移動・削除
|
||||
|
||||
## 背景
|
||||
|
||||
TUI の入力欄では現在、`Left/Right` で1文字単位の移動、`Home/End` で行端への移動ができるが、単語単位で飛ぶ手段がない。`Backspace` も1文字ずつしか消せない。長めの行を編集するときに左右キーや Backspace を押し続けることになりテンポが悪い。
|
||||
|
||||
シェルやエディタで広く使われている `Ctrl+Left` / `Ctrl+Right` での単語単位移動と、`Ctrl+Backspace` での単語単位削除を提供したい。
|
||||
|
||||
## 要件
|
||||
|
||||
- `Ctrl+Left` で1単語ぶん後ろ(左)にカーソルが飛ぶ。
|
||||
- `Ctrl+Right` で1単語ぶん前(右)にカーソルが飛ぶ。
|
||||
- 「単語」の境界は文字種ベースで判定する:
|
||||
- **ASCII**: 英数字とアンダースコア (`_`)
|
||||
- **ひらがな** (U+3040..U+309F)
|
||||
- **カタカナ** (U+30A0..U+30FF, U+31F0..U+31FF)
|
||||
- **漢字** (CJK Unified Ideographs: U+3400..U+4DBF, U+4E00..U+9FFF, U+F900..U+FAFF, U+20000..U+2FFFF)
|
||||
- **その他の単語文字**: 上記に該当せず `char::is_alphanumeric()` が true(アクセント付きラテン、キリル、ハングル等をひとまとめ)
|
||||
- 上記以外(空白・句読点・改行)は区切り。
|
||||
- 同じ種別の連続は1単語、種別が切り替わる位置で境界となる。形態素解析は使わない(送り仮名の途中で切れることは許容、VSCode/emacs と同等の挙動)。
|
||||
- ペースト atom (`Atom::Paste`) は不可分な1単語として扱う(カーソルが内部に入らない既存の不変条件を維持する)。
|
||||
- 既存の `Ctrl+Home/End` (履歴のスクロール)や `Ctrl+[` / `Ctrl+]` (ターンジャンプ)と衝突しないこと。
|
||||
- 既存の `Left/Right`(1文字移動)と `Backspace`(1文字削除)の挙動は変えない。
|
||||
- `Ctrl+Backspace` でカーソルから1単語ぶん手前を削除する。境界判定は `Ctrl+Left` と同じ(同じロジックを共有)。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `crates/tui` で `Ctrl+Left` / `Ctrl+Right` が単語単位移動として動作する。
|
||||
- `Ctrl+Backspace` で単語単位削除が動作する。
|
||||
- 単語境界の判定にユニットテストが付いている(空・連続スペース・`\n` をまたぐ・Paste をまたぐ・ひらがな/カタカナ/漢字/ASCII の混在)。
|
||||
- 既存テストが通る。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- `Ctrl+Delete` / `Alt+d` などによる単語単位の前方削除(別チケット候補)。
|
||||
- `Alt+Left/Right` など他の単語移動キーバインドの追加。
|
||||
- 形態素解析による日本語の単語分割(辞書サイズ・起動コストの観点で TUI には過剰)。送り仮名や複合語の途中で切れる挙動は許容する。
|
||||
76
tickets/workflow.md
Normal file
76
tickets/workflow.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Workflow 実装
|
||||
|
||||
## 背景
|
||||
|
||||
`docs/plan/workflow.md` で決まった「制約付きの強制的な作業フロー」を `/<slug>` で呼び出せるようにする。Knowledge (`#<slug>`) を依存として inject できる経路を持つことで、procedural な能力を再利用可能な単位に固定する。
|
||||
|
||||
memory 機構(`docs/plan/memory.md`)からは独立してスタートできる: Workflow は人間が書く / consolidation の offer 経由でしか作られず、自動書き込み禁止のため Phase 2 / GC の前提に依存しない。Knowledge resolver は `requires` の inject 経路として相互依存する。
|
||||
|
||||
agent-skills (agentskills.io 形式) は本チケットの ingest 経路を再利用して Workflow として読み込む側になる(`tickets/agent-skills.md` 参照)。
|
||||
|
||||
## 決定事項の参照
|
||||
|
||||
詳細は `docs/plan/workflow.md` 参照。要点のみ:
|
||||
|
||||
- 呼び出し: `/<slug>`、フラットな名前空間、kebab-case
|
||||
- 配置: `<workspace_root>/.insomnia/memory/workflow/<slug>.md`(ファイル名 = slug、frontmatter に `name` を持たない)
|
||||
- frontmatter: `description` / `auto_invoke` (default OFF) / `user_invocable` (default ON) / `requires: [knowledge-slug, ...]`
|
||||
- 実行: `requires` の Knowledge 本文を context に inject してから Workflow 本文を実行
|
||||
- 自動書き込み禁止(consolidation の write tool schema に `workflow` カテゴリを含めないことで構造的に担保。Linter で人間にも見える形で再保証)
|
||||
|
||||
## 方針
|
||||
|
||||
### MVP スコープ
|
||||
|
||||
1. **Workflow loader / 検証**
|
||||
- `<workspace_root>/.insomnia/memory/workflow/*.md` を走査
|
||||
- frontmatter を仕様通り検証。必須欠落・型不一致・slug とファイル名の不一致は hard error(Pod 起動失敗)
|
||||
- 未知フィールドは `tracing::warn!` して無視(既存 manifest と同方針)
|
||||
- 重複 slug は最初に見つかったものを採用 + warn(後述の skill ingest が乗ると衝突解決ルールが追加で必要になる)
|
||||
|
||||
2. **`/<slug>` 呼び出し経路**
|
||||
- `Segment::WorkflowInvoke { slug }` を Pod 側で resolve
|
||||
- 解決失敗(slug 未登録 / `user_invocable: false`)は `ToolError` 相当でユーザーに返し、Worker には届かない
|
||||
- `requires` の Knowledge 本文を Knowledge 検索ツールの slug 完全一致経路で取得し、Workflow 本文の前に context へ inject
|
||||
- Workflow 本文は Markdown のままサブミット内容として扱う(DSL 化はしない)
|
||||
|
||||
3. **`auto_invoke` 注入**
|
||||
- `auto_invoke: true` な Workflow の `description` を通常 Pod の system prompt に常駐注入する。Phase 2 prompt には入れない
|
||||
- 予算は Knowledge の常駐注入(`memory.md` §retrieval 経路)と合算管理。description 上限は agentskills 準拠の 1024 chars に揃える
|
||||
|
||||
4. **Linter ルール**
|
||||
- `memory/workflow/*.md` への write/edit は memory 専用 Tool 経由でのみ許可(汎用 Write/Edit は Scope deny)
|
||||
- consolidation の write tool schema からは `workflow` カテゴリを除外(自動書き込み禁止の構造的担保)
|
||||
- Workflow 自体の Linter は frontmatter 検証 + slug/ファイル名一致のみ。意味検証は将来検討
|
||||
|
||||
### 範囲外
|
||||
|
||||
- DSL 化や step 粒度の制約(Markdown 本文をそのまま実行)
|
||||
- 中断・再開・トランザクション管理
|
||||
- 品質検証フロー(mizchi empirical-prompt-tuning 相当、`docs/plan/workflow.md` §将来検討)
|
||||
- LLM による Workflow 自律生成(offer までで留める方針は本チケットでは扱わず、consolidation 側の責務)
|
||||
- Knowledge 検索ツール本体の実装(memory チケット側)。本チケットは slug 完全一致経路の利用者に留まる
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `<workspace_root>/.insomnia/memory/workflow/*.md` をロードし、frontmatter 違反は Pod 起動エラーになる
|
||||
- `/<slug>` を含む submit が `Segment::WorkflowInvoke` として送られ、Pod 側で `requires` Knowledge を inject した上で本文が実行される
|
||||
- `auto_invoke: true` の Workflow description が通常 Pod の system prompt に列挙される
|
||||
- `user_invocable: false` の Workflow は `/<slug>` 補完候補から除外され、明示呼び出しもエラーになる
|
||||
- 単体テストで frontmatter 検証の正常 / 異常系、`requires` 解決、フラグ別の挙動が verify される
|
||||
|
||||
## 実装順序
|
||||
|
||||
1. `manifest` または既存 memory クレートに `Workflow` 構造体と `WorkflowDirectoryLoader` を置く。frontmatter パースと検証のみでテスト完結
|
||||
2. Pod に Workflow registry を持たせ、`auto_invoke` description の system prompt 注入を組む
|
||||
3. `Segment::WorkflowInvoke` の resolver を Pod 側に実装。Knowledge 検索ツールの slug 完全一致経路で `requires` を inject
|
||||
4. 汎用 Write/Edit に対する `memory/workflow/` deny を Scope に追加、Linter 仕上げ
|
||||
|
||||
各ステップ終了時点でビルド通過・既存テスト合格を維持する。
|
||||
|
||||
## 参照
|
||||
|
||||
- 設計: `docs/plan/workflow.md`
|
||||
- Knowledge / `#<slug>` の retrieval: `docs/plan/memory.md` §retrieval 経路
|
||||
- Submit segment: `tickets/submit-tui-completion.md`(`Atom::WorkflowInvoke`)、`tickets/session-log-segments.md`
|
||||
- 後続: `tickets/agent-skills.md`(外部 SKILL を Workflow として ingest する経路)
|
||||
Loading…
Reference in New Issue
Block a user