mcpField
D
dream

dream

github.com/yonatangrossエージェントワークフロー30075 回インストール今日更新
リポジトリ

dream について

エージェントのメモリファイルを決定的な方法で保守するスキルです。検証可能なルールと慎重な削除ガードを使い、LLM の判断に頼らず古い参照、重複、矛盾、インデックスの不整合を確認します。

主な機能

  1. 01古いファイル・シンボル参照の検出
  2. 02メモリエントリーの重複検出
  3. 03矛盾するメモリの指摘
  4. 04メモリインデックスの再構築と検証
  5. 05変更プレビューと慎重な削除

活用シーン

  1. 01蓄積したセッションメモリの整理
  2. 02古くなったプロジェクト参照の発見
  3. 03重複または矛盾するメモの整理
  4. 04変更前のメモリ整理内容のプレビュー

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yonatangross/orchestkit --skill dream

インストール後、Claude Code で/dreamを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
housekeeping.md
index-budget.md
safe-deletes.md
scripts/
index-budget.mjs

詳細

Dream - メモリの統合

決定論的なメモリメンテナンスです。古くなったエントリを検出し、重複を統合し、矛盾を解消して、MEMORY.md のインデックスを再構築します。削除の判断はすべて、検証可能なチェック(ファイルが存在するか、関数が存在するか、内容が重複しているか)に基づき、LLM の判断には依存しません。

引数の解決

DRY_RUN = "--dry-run" in "$ARGUMENTS"  # Preview changes without writing

概要

メモリファイルはセッションをまたいで蓄積されます。時間が経つにつれて、次のような問題が発生します。

  • 古くなった参照 — もう存在しないファイル、関数、クラスを指すメモリ
  • 重複 — 同じトピックを扱い、内容が重複する複数のメモリ
  • 矛盾 — 古いメモリを新しいメモリが置き換えているのに、古いものが整理されていない状態
  • インデックスのずれ — MEMORY.md のインデックスが実際のメモリファイルと一致していない状態

このスキルは、決定論的なチェックのみを使って、これら4つの問題をすべて修正します。

完了条件。 完了とは、残存ファイルから MEMORY.md が再構築され、STEP 5.5 の検証に合格し、STEP 6 のレポートが出力され、STEP 7~9 がそれぞれ実行されるか、1行のスキップ理由が出力された状態です。

予算:検出したメモリファイルを1回読み込み(STEP 1)、MEMORY.md を1回書き換え(STEP 5)、インデックスが予算を超えた場合は STEP 5.5 の末尾部分を書き換えます。サブエージェントは使用しません(すべてのチェックが決定論的であるため)。完了条件または最初の上限に到達した時点で停止して報告します。

実行頻度(CC 2.1.142+): リアクティブ圧縮では、最初の要約処理のサイズが実際の超過量に合わせて調整されるようになったため、長いセッション中にターンの途中で停止することが大幅に減りました。「毎晩実行」という頻度は「メモリファイルが蓄積したら実行」へと緩和できます。圧縮効率の低下を防ぐために統合を行う必要はなくなりました。


STEP 1: メモリファイルの検出

# Find the memory directory (agent-specific or project-level)
# Agent memory lives in: .claude/agent-memory/<agent-id>/
# Project memory lives in: .claude/projects/<hash>/memory/
# Also check: .claude/memory/

memory_dirs = []
Glob(pattern=".claude/agent-memory/*/MEMORY.md")
Glob(pattern=".claude/projects/*/memory/MEMORY.md")
Glob(pattern=".claude/memory/MEMORY.md")

# For each discovered MEMORY.md, glob all *.md files in that directory
for dir in memory_dirs:
    Glob(pattern=f"{dir}/../*.md")  # All memory files alongside MEMORY.md

検出したメモリファイルをすべて読み込みます。フロントマター(name、description、type)と本文の内容を解析します。メモリ内に次のインベントリを作成します。

inventory = [{
    "path": "/abs/path/to/file.md",
    "name": frontmatter.name,
    "type": frontmatter.type,  # user, feedback, project, reference
    "description": frontmatter.description,
    "body": body_text,
    "file_refs": [],      # extracted file paths
    "symbol_refs": [],    # extracted function/class names
    "topics": [],         # key phrases for duplicate detection
}]

STEP 2: 古さの検出

各メモリファイルから参照を抽出し、現在も存在するかを確認します。

2a: ファイルパスの参照

ファイル参照と思われるパスを抽出します(パターン:/ とファイル拡張子を含むパス、バッククォートで囲まれたパス)。

# Regex-like extraction from body text:
# - Paths containing / with common extensions: .py, .ts, .tsx, .js, .json, .md, .yaml, .yml, .sh
# - Backtick-wrapped paths: `src/something/file.ts`
# - Quoted paths in frontmatter descriptions

検証する前に、各参照のスコープを分類します。 Glob が検索できるのは現在のリポジトリ内だけなので、それ以外の場所にあるパスは一致するはずがなく、存在しないものとして誤って判定されてしまいます。~/.claude のフック、homebrew cask、cmux 設定、別のリポジトリに関するメモリは、このリポジトリに含まれていないというだけで古いものにはなりません。

def scope(ref):
    # Anything rooted outside the working repo is UNVERIFIABLE, not missing.
    if ref.startswith(("~", "/", "$")):          return "UNVERIFIABLE"
    if ref.startswith(("http://", "https://")):  return "UNVERIFIABLE"
    if re.match(r'^[A-Za-z0-9_.-]+/', ref) and not (REPO / ref.split("/")[0]).exists():
        return "UNVERIFIABLE"   # first segment is not a real top-level dir here
    return "REPO_RELATIVE"

verifiable = [r for r in file_refs if scope(r) == "REPO_RELATIVE"]
external   = [r for r in file_refs if scope(r) == "UNVERIFIABLE"]

missing = []
for ref in verifiable:
    Glob(pattern=ref)
    # If no match → missing.append(ref)

古さの比率は verifiable のみを対象に計算します。 external の参照はレポート用に記録し、削除の判断には一切含めません。検証可能な参照が0件のメモリは、外部パスをいくつ挙げていても EVERGREEN です。

2b: シンボルの参照

関数名/クラス名を抽出します(パターン:function_name()、ClassName、def function_name)。

for symbol in symbol_refs:
    Grep(pattern=symbol, path=".", output_mode="files_with_matches", head_limit=1)
    # If no match → mark as STALE_SYMBOL_REF
2c: 古さの分類
検出結果分類対応
VERIFIABLE の参照が0件(参照なし、またはすべて UNVERIFIABLE)EVERGREEN保持
検証可能な参照がすべて有効で、すべてのシンボルが見つかったFRESH保持
検証可能な参照の一部が見つからないPARTIALLY_STALEレビュー対象としてフラグ付け
検証可能な参照がすべて見つからず、すべてのシンボルも見つからないFULLY_STALE削除候補

自動削除されるのは FULLY_STALE に分類されたメモリだけです。PARTIALLY_STALE のメモリは報告しますが、保持します。判断するのはユーザーです。

2d: 削除ガード — 分類後、削除前にチェック

FULLY_STALE は削除に必要ですが、それだけでは十分ではありません。以下のガードに該当すると、PARTIALLY_STALE に格下げされ(保持してフラグ付け)、削除されません。メモリファイルは git に含まれないため、誤って削除すると気づかれず、復元もできません。そのため、常に保持する方向を優先します。

GUARD_DAYS = 14

for m in list(fully_stale_files):
    reason = None
    # 1. Preferences do not decay because a path moved.
    if m["type"] == "user":
        reason = "type:user is never auto-pruned"
    # 2. A feedback/reference memory carries a LESSON; the file paths in it are
    #    illustrations, not a manifest. Its worth does not expire when an
    #    illustrative path moves, and ref-extraction is lossy anyway (it catches
    #    `file.ts` but misses `file.ts:186` and `functionName()`). Only project
    #    memories — which track live work against concrete files — are eligible
    #    to go fully stale on ref death.
    elif m["type"] in ("feedback", "reference"):
        reason = f"type:{m['type']} value is the lesson, not its file refs"
    # 3. Recently written memories describe the present, whatever their refs say.
    elif (now - m["mtime"]) < GUARD_DAYS * 86400:
        reason = f"modified within {GUARD_DAYS}d"
    # 4. A memory that exists to prevent a regression must outlive the code it cites.
    elif re.search(r'\b(do not|don\'t|never|avoid)\b', m["body"], re.I):
        reason = "carries a do-not/never directive"
    if reason:
        m["classification"] = "PARTIALLY_STALE"
        m["kept_reason"] = f"prune-guard: {reason}"
        fully_stale_files.remove(m)
        partially_stale_files.append(m)

ガード3は見落としやすいものです。reference_cmux_scroll_blank_research には 「解決済み。cmux で tui:fullscreen を再提案しないこと」 と記されており、参照されていたパスはすべて移動していました。これを削除すると、防止するために書かれた回帰がそのまま再発します。禁止事項に価値があるメモリは、元のコードがなくなったときこそ最も役立ちます。

STEP 2.5:参照ゲート(#2351)— まだ使われているメモリは削除しない

VERIFY ループを締めくくります。削除する前に、「実際に参照されていたか」という問いをくぐり抜ける必要があります。外部参照がすべて消えた(FULLY_STALE)メモリでも、エージェントが引き続き検索しているなら、依然として重要な役割を担っています。つまり、参照は古くても、知識は現役です。そのため、削除する前に .claude/logs/memory-consult.jsonl(memory-validator が mcp__memory__search_nodes/open_nodes/read_graph のたびに書き込みます)を読み、最近参照された FULLY_STALE メモリを**一部古い(PARTIALLY_STALE)**状態に降格します(保持してフラグを付け、自動削除しません)。

import json, time
from pathlib import Path

def recently_consulted_terms(days=14):
    log = Path(".claude/logs/memory-consult.jsonl")
    if not log.exists():
        return set()
    cutoff = time.time() - days * 86400
    terms = set()
    for line in log.read_text().splitlines():
        try:
            e = json.loads(line)
        except ValueError:
            continue  # best-effort: skip malformed lines
        # open_nodes carries exact entity names; search carries a query string
        terms.update(n.lower() for n in e.get("names", []))
        if e.get("query"):
            terms.update(w.lower() for w in e["query"].split() if len(w) > 2)
    return terms

consulted = recently_consulted_terms()
for m in list(fully_stale_files):
    slug = Path(m["path"]).stem.lower()
    name = (m.get("name") or "").lower()
    if name in consulted or any(c in slug or slug in c for c in consulted):
        m["classification"] = "PARTIALLY_STALE"
        m["kept_reason"] = "consult-gate: looked up in the last 14 days (#2351)"
        fully_stale_files.remove(m)
        partially_stale_files.append(m)

これは意図的に保守的な設計です。曖昧な用語の照合では、参照された可能性があるメモリを削除するより、保持する方向に判断します。Step 6 のレポートには、参照ゲートによって保持した各メモリが記録されます(ループに欠けていた「それは役に立ったか?」という監査です)。ログがない場合(参照の計測がまだ行われていない場合)、ゲートは何もせず、従来どおり削除処理を進めます。


STEP 3:重複を検出する

同じディレクトリ内のメモリをペアごとに比較します。次の条件を満たす場合、2つのメモリは重複しています。

  1. 同じ種類(両方とも feedback、両方とも project など)
  2. トピックが重複している — 重要な単語(ストップワードを除く)の60%以上が、両方の本文に含まれている
  3. 同じ対象 — name または description フィールドが同じ概念を指している
stopwords = {"the", "a", "an", "is", "are", "was", "were", "be", "been",
             "have", "has", "had", "do", "does", "did", "will", "would",
             "could", "should", "may", "might", "can", "shall", "to", "of",
             "in", "for", "on", "with", "at", "by", "from", "as", "into",
             "through", "during", "before", "after", "this", "that", "it",
             "not", "no", "but", "or", "and", "if", "then", "than", "so"}

def significant_words(text):
    words = set(text.lower().split()) - stopwords
    return {w for w in words if len(w) > 2}

def overlap_ratio(words_a, words_b):
    if not words_a or not words_b:
        return 0.0
    intersection = words_a & words_b
    smaller = min(len(words_a), len(words_b))
    return len(intersection) / smaller if smaller > 0 else 0.0

# For each pair with same type:
#   if overlap_ratio >= 0.6 → DUPLICATE pair
#   Keep the NEWER file (by filesystem mtime), prune the older

STEP 4:矛盾を解決する

同じ種類の2つのメモリが、同じ対象について相反する主張をしている場合、矛盾が発生します。検出方法:

  1. 同じ種類+同じトピック(重複度が0.4以上0.6未満 — 関連はあるが重複ではない)
  2. 否定の兆候 — 一方の本文に、もう一方の主張を否定する表現が含まれている:
    • 「X をする」対「X をしない」/「X をしない」/「決して X をしない」
    • 「X を使う」対「X を避ける」/「X の使用をやめる」
    • 「X を優先する」対「Y を優先する」(同じ意思決定領域の場合)
negation_pairs = [
    ("do ", "do not "), ("do ", "don't "),
    ("use ", "avoid "), ("use ", "stop using "),
    ("prefer ", "don't prefer "), ("always ", "never "),
]

# For each pair flagged as contradictory:
#   Keep the NEWER file (more recent decision supersedes)
#   Prune the older file

STEP 5:変更を実行する(またはドライラン)

ドライランモード(--dry-run)

--dry-run フラグがある場合、すべての書き込みをスキップします。[DRY RUN] 接頭辞を付けた完全なレポート(Step 6)を出力し、変更される内容を一覧表示します。

[DRY RUN] Would delete: .claude/agent-memory/foo/stale_old_path.md (FULLY_STALE)
[DRY RUN] Would delete: .claude/agent-memory/foo/duplicate_auth.md (DUPLICATE of auth_patterns.md)
[DRY RUN] Would delete: .claude/agent-memory/foo/old_preference.md (CONTRADICTED by new_preference.md)
[DRY RUN] Would rebuild: .claude/agent-memory/foo/MEMORY.md (3 entries removed, 12 remaining)
ライブモード
# Move, never rm: memory files are not in git (see references/safe-deletes.md)
trash = f"{memory_dir}/.trash/{date.today().isoformat()}"
Bash(command=f"mkdir -p '{trash}'")

# 1. FULLY_STALE files, 2. DUPLICATE (older), 3. CONTRADICTED (older)
to_remove = [s["path"] for s in fully_stale_files]
to_remove += [d["older"]["path"] for d in duplicate_pairs]
to_remove += [c["older"]["path"] for c in contradiction_pairs]
for path in to_remove:
    Bash(command=f"mv '{path}' '{trash}/'")

# 4. Rebuild MEMORY.md index from surviving files (exclude .trash/ from the walk)
MEMORY.md を再構築する

生き残った .md ファイル(MEMORY.md 自体を除く)をすべて読み込みます。インデックスを生成します。

# <Directory Name> Memory

- [Name](filename.md) -- one-line description from frontmatter

再構築したインデックスのルール:

  • メモリファイルごとに1行とし、ファイル名のアルファベット順に並べる
  • 上限となるのは行数ではなくバイト数です。 MEMORY.md は毎回のセッションで読み込まれ、読み込み上限(約24 KB)を超えると、その時点でインデックス全体が気付かれないまま機能しなくなります。行数は目安にすぎず、これを見落とします。平均147文字の151件のインデックスは22.5 KBでほぼ機能しなくなりますが、同じ151件でも平均112文字なら16.2 KBで問題ありません。
  • 合計17 KB以下を目標にします。固定値をハードコードせず、1行あたりの予算を算出します。 budget_chars = (17 * 1024 - non_entry_overhead) / entry_count
  • 再構築したインデックスが目標を超える場合は、エントリを削除する前に、フックを算出した予算に合わせて短縮します。 単語の途中で切らず、先頭の節(対象を区別する詳細が含まれます)を残します。
  • 1対1の不変条件は2ファイルを対象とします(#3741)。 各メモリファイルは、MEMORY.md と MEMORY-ARCHIVE.md 全体を通じて、正確に1回だけインデックスに登録されます。つまり、indexed(MEMORY.md) + indexed(MEMORY-ARCHIVE.md) == files_on_disk であり、2つの集合は互いに素で、すべてのリンク先が大文字・小文字を含めて正確な名前で存在し、MEMORY.md には MEMORY-ARCHIVE.md へのリンクを含む末尾行(> N memory files ...)が正確に1行含まれます。files_on_disk からは MEMORY*.md、.MEMORY.md.prev、.trash/、_backup/ を除外します。単一ファイルのチェック(indexed == files_on_disk)では、アーカイブ済みの各エントリが欠落として検出されます。アーカイブが存在する場合は、これは誤りです。
  • 約90文字まで短縮してもなお上限を超える場合に限り、ユーザーに警告します。インデックスに収めるためにメモリを自動削除してはいけません。インデックスはポインターテーブルであり、サイズを小さくするのは保持の問題ではなく、書式設定の問題です。
# Write the rebuilt MEMORY.md. Copy to .MEMORY.md.prev FIRST: this one write
# replaces every memory's pointer, so a bad index degrades sessions silently.
Write(path="<memory_dir>/MEMORY.md", content=rebuilt_index)
STEP 5.5: インデックスの予算(報告してから、ルールに従って降格)

整合性(ファイルとの1対1対応)と予算(セッションで扱えるバイト数)は別の問題です。 再構築で解決できるのは前者だけです。予算の確認はスクリプトで行うため、誰が実行しても 同じレポートになります。デフォルトでは読み取り専用です。ユーザーが移動を承認した後にのみ --apply を実行します(3件を超える場合はまとめて確認します)。

Bash(command=f"node ${{CLAUDE_PLUGIN_ROOT}}/skills/dream/scripts/index-budget.mjs '{memory_dir}' --json")
# bytes vs ceiling (ORK_CONTEXT_FILE_BUDGET_BYTES, else 17,408 B), per-section sizes, long entries,
# the two-file invariant, and moves proposed BY RULE (oldest first). Add --apply to rotate
# .MEMORY.md.prev, move the lines to MEMORY-ARCHIVE.md, rewrite the trailer, re-verify (exit 1 on failure).

ルール、移動禁止セット、候補が尽きた場合のフォールバックについては、Read("references/index-budget.md") を参照してください。


STEP 6: レポート

統合後、サマリーテーブルを出力します。

## Dream Consolidation Report

| Metric | Count |
|--------|-------|
| Memory directories scanned | N |
| Total memory files scanned | N |
| Stale entries pruned | N |
| Duplicates merged | N |
| Contradictions resolved | N |
| Partially stale (kept, flagged) | N |
| Evergreen (no external refs) | N |
| Surviving memories | N |
| MEMORY.md indexes rebuilt | N |
| Promotion candidates (2+ repos, STEP 9) | N |

### Changes Made

| File | Action | Reason |
|------|--------|--------|
| `path/to/file.md` | DELETED | Fully stale: all referenced files removed |
| `path/to/old.md` | DELETED | Duplicate of `path/to/new.md` |
| `path/to/outdated.md` | DELETED | Contradicted by `path/to/current.md` |

### Flagged for Review (PARTIALLY_STALE)

| File | Missing References |
|------|-------------------|
| `path/to/file.md` | `src/old/path.ts` no longer exists |

--dry-run の場合は、レポート全体の先頭に次を付けます。

[DRY RUN] No files were modified. Run without --dry-run to apply changes.

エラー処理

条件対応
メモリーディレクトリが見つからない「No memory directories found」と報告して終了します
ディレクトリにメモリーファイルがない「Directory empty, nothing to consolidate」と報告します
すべてのメモリーがFRESH「All N memories are current, nothing to prune」と報告します
再構築と降格の後もMEMORY.mdがバイト数の上限を超えているユーザーに警告し、残りの候補を列挙します。自動で切り詰めることはありません
ファイルの削除に失敗エラーを報告し、残りのファイルの処理を続けます
メモリーファイルにフロントマターがないEVERGREENとして扱います(メタデータがないため、参照を検証できません)

STEPs 7-8: 整理

STEP 6の後、STEP 7(孤立したプラグインの削除提案)とSTEP 8(古いプロジェクト状態のヒント。プレビューのみで、削除はしません)については、Read("references/housekeeping.md") を参照してください。


STEP 9: リポジトリ横断の昇格候補 (#3295)

2つ以上のプロジェクトで見られるメモリーパターンは、リポジトリ内だけでは収まらなくなった機能です。 統合中にこれらを決定論的に検出し、昇格を提案します。dreamはコンテンツを自ら移動しないため、 このステップはdreamがモデルから呼び出された場合も安全です。

# For each memory file touched in this run, derive a topic key: the filename slug minus
# scope words (dates, project names). Then look for the same key in OTHER projects'
# memory indexes (index lines are "- [Title](file.md) -- hook"):
grep -l -i "<topic-key>" ~/.claude/projects/*/memory/MEMORY.md \
  | grep -v "<current-project-dir>"
  • 異なるプロジェクトが2つ以上一致 -> そのメモリーは昇格候補です。
  • 決定論的な方法のみを使用します。正規化したスラッグ/タイトルのトークンで照合し、意味的な判断は行いません。
  • 誤検出によるコストは小さい(ユーザーが断ればよい)一方、見落としは問題です。このステップは、同じインフラの教訓がリポジトリごとに何度も再発見され、その状況を誰も把握していない事態を防ぐためにあります。

対話型の実行: 候補ごとにAskUserQuestionを行います(3件を超える場合はまとめて確認します)。

  • 「共有プラグインに昇格」 — 組織固有のパターンは組織のプライベートプラグインに、汎用的なパターンは公開プラグインに追加します。dreamはきっかけを作るだけで、振り分けはユーザーが行います。
  • 「ローカルに保持」 — 正当にリポジトリ固有の重複です。
  • 「この候補の提案を停止」 — メモリーのフロントマターのメタデータにpromotion: declinedを追加し、以後の実行でスキップします。

非対話型の実行/ドライラン: 一致したプロジェクトのパスとともに、Dream Consolidation Reportの Promotion candidates:の下に候補を列挙します。プロンプトの表示も変更も行いません。


使用しない場合

  • 新しい決定を保存する場合 — rememberを使用します
  • 過去の決定を検索する場合 — memory searchを使用します
  • セッション開始時にコンテキストを読み込む場合 — memory loadを使用します
  • セッションが5回未満の場合 — メモリーファイルに古さが十分蓄積していない可能性があります

関連スキル

  • ork:remember — 決定とパターンを保存します(書き込み側)
  • ork:memory — 検索、読み込み、同期、可視化を行います(読み取り側)

コメント