SLOPSHOPPER

observe-mod

Logs every function-hook event (on '*') to JSONL for inspection.

newtimer
A shopper browsing a rack in a slop shop
README

claude-rules-personal

kawaz の Claude Code 用ルール / スキルの central リポジトリ。 claude-rules-* overlay 群を束ね、just setup で各 CLAUDE_CONFIG_DIR に配備する。

リポジトリ群の構成

claude-rules-* は 1 つの central + 複数の overlay で構成される。

リポ役割専用環境 (CLAUDE_CONFIG_DIR)
kawaz/claude-rules-personal (これ)central。全 overlay を束ね、配備 recipe (justfile + scripts/) を持つ。環境一覧 repos_mapping.local.json は gitignore 対象で各マシンに置く (雛形は repos_mapping.example.json)~/.claude-personal
業務用 overlay (private)業務面のルール差分専用環境
kawaz/claude-rules-zunsystemzunsystem 識別子 overlay (private)(専用環境なし)
業務識別子 overlay (private)特定業務面の識別子差分(専用環境なし)
  • 配備 recipe は この personal リポにのみ置く (2 重管理しない)。repos_mapping.local.json は追跡しない
  • どの overlay のルール/スキルを変更しても、反映は personal で just setup を実行する
  • 整理方法・設計判断などの詳細ドキュメントは personal の docs/ に集約する

レイアウト

各リポ共通:

  • for-all/rules/ — 全環境向けルール (全 ~/.claude*/rules/ に注入)
  • for-all/plugins.json — 全環境に入れる Claude Code plugin の宣言 (just plugins-setup が install)
  • for-me/rules/ — その面の専用環境にのみ注入
  • for-me/plugins.json — その面の専用環境にのみ install する plugin の宣言。 plugin の skill / agent description は全セッションの context に載るので、 面固有の plugin はこちらに置く (for-all に置くと他の面にも語彙が漏れる)
  • for-others/rules/ — 他環境から参照される情報 (固有名詞リスト等のサニタイズ規定)

skill と agent は リポ自体を Claude Code plugin として配布する (.claude-plugin/plugin.json + リポ直下の skills/<slug>/ agents/ hooks/)。 各リポの for-all/plugins.json に自リポの plugin を宣言し、just plugins-setup が install することで配備される。Skill tool からは <plugin名>:<slug> (例: rules-personal:eli5) で呼ぶ。skill はユーザが /名前 で起動する実行系だけを置き、 読むだけの手順書は reference/ に置く (判定は for-all/rules/rule-writing-guidelines.md)。 AI に自動起動させたくないユーザ専用の skill は frontmatter に disable-model-invocation: true を付け (AI の一覧から消える)、 本文は reference の該当ファイルを読ませる 1〜2 行に留める。

読むだけの参照知識はこのリポの直下に平置きする (索引と本文の 2 段構成、本文は必要時にだけ Read):

  • reference/ — 公開してよい体系知識。索引は reference/_index.md
  • memory/ — rule にするほど一般化していない横断メモ。索引は memory/_index.md

個人情報系 (本人の表記・アカウント名・連絡先) は public 候補のこのリポではなく private リポ kawaz/privacy-personal に同じ構造で置き、その索引だけを reference/_index.md の「private 層」節に写す (業務面でしか効かないものは各 overlay の for-me/)。 書き先の判定は for-all/rules/knowledge-guide.md。

for-me の "me" は「個人 vs 他者」ではなく、kawaz が持つ複数の面 (個人開発 / emrd 業務 / ...) のうちの その overlay の面を指す。

personal リポ固有:

  • scripts/rules.sh / scripts/plugins.sh — 配備スクリプト (subject ごと。justfile の recipe から呼ぶ)
  • repos_mapping.local.json — 全 overlay リポと各 home (CLAUDE_CONFIG_DIR) の定義
  • docs/ — 設計判断・課題 (issue/)、運用手順 (runbooks/) 等

セットアップ

配備は just の recipe で行う。subject (rules / plugins) ごとに <subject>-{setup,check,update} があり、setup / check / update がそれらを束ねる。home 引数を省略すると repos_mapping.local.json に宣言された全面が対象:

just setup                      # 全面: rules の symlink + plugin の install
just check                      # 全面: 配備状態の検査 (変更しない)
just update                     # 全面: plugin の update
just rules-setup ~/.claude-emrd # 1 面だけ
  • rules-setup: for-*/rules/ を $HOME_DIR/rules/ 配下にディレクトリ symlink し、dangling link を掃除 (rules-check は期待する link が揃って実体を指しているかを検査)
  • plugins-setup: for-all/plugins.json (全 repo) と自面の for-me/plugins.json に宣言された plugin を marketplace add + install。bare 面 (~/.claude-bare) は repos_mapping.local.json の pluginOnlyHomes に列挙した ccmsg だけを入れる
  • plugins-check: rules 面は宣言の不足だけ、bare 面は ccmsg 以外が入っていないことも検査。宣言外に手で入れた plugin と enable / disable 状態は触らない

ドキュメント

  • docs/issue/ — TODO・課題
  • docs/runbooks/ — 運用・セットアップ手順

サニタイズ

業務固有名詞は public 候補の personal には置かず、各 overlay リポ (private) で管理。 共通サニタイズの仕組みは for-all/rules/sanitize-work-identifiers.md を参照。

Source 1 files
hooks/register.ts 89 lines
1// Logs every event this plugin can see to JSONL.
2//   <dir>/events-<sessionId>-<part>.jsonl   one line per event: seq, time, event, caller, keys, origin
3//   <dir>/samples-<sessionId>.jsonl         the first `e` of each event name (clipped)
4// <dir> is $MODS_LAB_DIR, or <plugin root>/log when unset.
5
6const CLIP = 1500
7const PART_BYTES = 3 * 1024 * 1024 // $.fs.read / write reject over 4 MiB
8
9let dir: string | undefined
10let sid = 'unknown'
11let part = 0
12let seq = 0
13let buf: unknown[] = []
14let samples: unknown[] = []
15const seen = new Set<string>()
16let flushing = false
17
18function clip(v: unknown): unknown {
19  try {
20    const s = JSON.stringify(v)
21    if (s === undefined) return String(v)
22    return s.length > CLIP ? s.slice(0, CLIP) + '...<clipped>' : JSON.parse(s)
23  } catch (err) {
24    return `<unserializable: ${String(err)}>`
25  }
26}
27
28async function append($: any, path: string, lines: unknown[]): Promise<number> {
29  let prev = ''
30  try { prev = await $.fs.read(path) } catch { prev = '' }
31  const next = prev + lines.map((l) => JSON.stringify(l)).join('\n') + '\n'
32  await $.fs.write(path, next)
33  return next.length
34}
35
36async function flush($: any): Promise<void> {
37  if (flushing || dir === undefined) return
38  if (buf.length === 0 && samples.length === 0) return
39  flushing = true
40  try {
41    const lines = buf; buf = []
42    const firsts = samples; samples = []
43    if (lines.length > 0) {
44      const size = await append($, `${dir}/events-${sid}-${part}.jsonl`, lines)
45      if (size > PART_BYTES) part += 1
46    }
47    if (firsts.length > 0) await append($, `${dir}/samples-${sid}.jsonl`, firsts)
48  } finally {
49    flushing = false
50  }
51}
52
53export function register(on: any): void {
54  // Registered first, so it is this plugin's outermost hook. It must not touch `$`:
55  // at engine.create `$` is still empty.
56  on('*', ($: any, e: any, next: any) => {
57    const ev = String(next.event)
58    seq += 1
59    const entry: Record<string, unknown> = {
60      n: seq,
61      t: Date.now(),
62      ev,
63      by: next.origin,
64      keys: e !== null && typeof e === 'object' ? Object.keys(e) : typeof e,
65    }
66    if (e && typeof e === 'object' && 'origin' in e) entry.origin = clip(e.origin)
67    if (ev === 'ui.render') entry.component = e?.component
68    if (ev.startsWith('tool.')) entry.tool = e?.tool
69    buf.push(entry)
70    if (!seen.has(ev)) {
71      seen.add(ev)
72      samples.push({ n: seq, ev, by: next.origin, e: clip(e) })
73    }
74    return next(e)
75  })
76
77  on('session.start', async ($: any, e: any, next: any) => {
78    dir = (await $.env.get('MODS_LAB_DIR')) ?? `${$.plugin.root}/log`
79    sid = await $.session.id()
80    $.clock.every(1000, () => { void flush($) })
81    return next(e)
82  })
83
84  on('session.end', async ($: any, e: any, next: any) => {
85    await flush($)
86    return next(e)
87  })
88}
89