hypomnema 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +8 -3
- package/README.md +8 -3
- package/commands/audit.md +46 -0
- package/docs/ARCHITECTURE.md +83 -9
- package/docs/CONTRIBUTING.md +1 -1
- package/hooks/hooks.json +9 -0
- package/hooks/hypo-auto-commit.mjs +15 -2
- package/hooks/hypo-auto-stage.mjs +5 -2
- package/hooks/hypo-hot-rebuild.mjs +17 -6
- package/hooks/hypo-session-record.mjs +54 -0
- package/hooks/hypo-session-start.mjs +28 -6
- package/hooks/hypo-shared.mjs +96 -0
- package/package.json +1 -1
- package/scripts/doctor.mjs +10 -0
- package/scripts/init.mjs +46 -3
- package/scripts/session-audit.mjs +267 -0
- package/scripts/uninstall.mjs +7 -0
- package/scripts/upgrade.mjs +72 -5
- package/scripts/weekly-report.mjs +177 -0
- package/skills/crystallize/SKILL.md +4 -0
- package/skills/graph/SKILL.md +4 -0
- package/skills/ingest/SKILL.md +4 -0
- package/skills/lint/SKILL.md +4 -0
- package/skills/query/SKILL.md +4 -0
- package/skills/verify/SKILL.md +4 -0
- package/templates/.hypoignore +3 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +22 -0
package/README.ko.md
CHANGED
|
@@ -10,17 +10,19 @@
|
|
|
10
10
|
[](https://www.npmjs.com/package/hypomnema)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
|
-
[](https://github.com/sk-lim19f/Hypomnema/actions/workflows/ci.yml)
|
|
14
14
|
[](https://github.com/sk-lim19f/Hypomnema/stargazers)
|
|
15
15
|
|
|
16
16
|
**Claude Code를 위한 LLM 네이티브 개인 위키. 복리로 성장하는 지식.**
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
_Claude가 노트하게 만드세요 — 그리고 그게 실제로 일어나는지 측정하세요._
|
|
19
19
|
|
|
20
20
|
[빠른 시작](#빠른-시작) • [다른 시스템과의 비교](#다른-시스템과의-비교) • [설계 결정](#설계-결정) • [기능](#기능) • [아키텍처](docs/ARCHITECTURE.md) • [기여](docs/CONTRIBUTING.md)
|
|
21
21
|
|
|
22
22
|
> Andrej Karpathy의 "LLM 네이티브 위키" 스케치에서 영감을 받아, 10개월간의 개인 운영 경험으로 다듬어진 도구입니다. 캡처 → 합성 → 검색 → 세션 재개로 이어지는 전체 라이프사이클을 Claude Code 명령어와 라이프사이클 훅으로 제공합니다.
|
|
23
23
|
|
|
24
|
+
> **현재 상태 vs. v2 비전.** v1.0.1(현재)은 트리거 모델을 정직하게 밝힙니다: 대부분의 위키 동작 — 인제스트, 쿼리, 세션 클로즈 — 은 **명시적 `/hypo:*` 커맨드**로 시작되며, 일부 훅이 자동 staging·세션 상태 주입·룩업 신호를 처리합니다. v2 thesis는 *완전 자율* — Claude가 요청 없이 위키를 읽고·쓰고·합성하는 상태입니다. **v1.1.0**은 그 램프의 첫 단계로, 자율을 주장하는 대신 **observability 점수**를 출력해 세션당 위키가 실제로 얼마나 쓰였는지(ingest 수, query 수, session-close 비율, citation 비율)를 측정·공개합니다. v2 구현 이전에 자율 vs. 수동 비율을 사용자가 직접 확인할 수 있게 하기 위함입니다.
|
|
25
|
+
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## 빠른 시작
|
|
@@ -105,6 +107,7 @@ Hypomnema ───► 합성 · 마크다운 · git · 훅 · 로컬
|
|
|
105
107
|
| **포맷** | 평문 마크다운 + frontmatter | 마크다운 | 독자 포맷 | 벡터 스토어 | 독자 포맷 | HTML |
|
|
106
108
|
| **백엔드** | 로컬 파일 + git | 로컬 파일 | SaaS | 서비스 / DB | SaaS | 서비스 |
|
|
107
109
|
| **행동 튜닝** | `/hypo:feedback` → 영구 규칙 | 없음 | 없음 | 없음 | 일부 | 없음 |
|
|
110
|
+
| **자율 동작(auto-behavior)** | 현재는 명시적 `/hypo:*` 트리거 기반; **v1.1에서 observability 점수 도입**, v2 목표 = 완전 자율 | 없음 | 없음 | 없음 | 블랙박스 | 없음 |
|
|
108
111
|
| **셋업 비용** | 명령 1개 | 설치 1번 | 가입 | 파이프라인 구축 | 가입 | 레포 연결 |
|
|
109
112
|
| **락인** | 0 (마크다운 + git) | 낮음 | 높음 | 중간 | 높음 | 중간 |
|
|
110
113
|
|
|
@@ -295,6 +298,8 @@ Claude가 잘못하거나 정확히 맞았을 때마다 `/hypo:feedback`을 실
|
|
|
295
298
|
|
|
296
299
|
`.hypoignore`는 훅이 무시할 경로를 정의합니다 (기본: `*.pdf`, `*.zip`, `*.pem`, `*.env` 등). 직접 편집하면 됩니다 — privacy mode 플래그는 없습니다. 단일 파일, 단일 진실 소스.
|
|
297
300
|
|
|
301
|
+
> **git sync 범위.** Hypomnema는 `~/hypomnema/` 위키 자체만 git sync합니다. Claude Code 설정(`~/.claude/`)은 의도적으로 Hypomnema가 **관리하지 않습니다** — agent·skill·settings의 기기 간 동기화는 [chezmoi](https://www.chezmoi.io/) 같은 별도 dotfiles 매니저 사용을 권장합니다.
|
|
302
|
+
|
|
298
303
|
### `/hypo:*` 커맨드는 어디서 오는가?
|
|
299
304
|
|
|
300
305
|
| 설치 경로 | 슬래시 커맨드 위치 |
|
|
@@ -315,7 +320,7 @@ Claude가 잘못하거나 정확히 맞았을 때마다 `/hypo:feedback`을 실
|
|
|
315
320
|
|
|
316
321
|
## 상태
|
|
317
322
|
|
|
318
|
-
- **테스트:**
|
|
323
|
+
- **테스트:** `npm test` 참조 — lane이 ship될 때마다 카운트가 변하므로 러너가 단일 진실 공급원
|
|
319
324
|
- **CI:** 7개 독립 job (test matrix, lint, init/upgrade snapshots, replay, hypo-absent, uninstall-smoke)
|
|
320
325
|
- **릴리스:** `v*` 태그 push 시 `npm publish --provenance` 자동 실행
|
|
321
326
|
|
package/README.md
CHANGED
|
@@ -10,17 +10,19 @@ English | [한국어](README.ko.md)
|
|
|
10
10
|
[](https://www.npmjs.com/package/hypomnema)
|
|
11
11
|
[](https://nodejs.org)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
|
-
[](https://github.com/sk-lim19f/Hypomnema/actions/workflows/ci.yml)
|
|
14
14
|
[](https://github.com/sk-lim19f/Hypomnema/stargazers)
|
|
15
15
|
|
|
16
16
|
**LLM-native personal wiki for Claude Code. Knowledge that compounds.**
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
_Make Claude take notes — and measure whether it actually does._
|
|
19
19
|
|
|
20
20
|
[Quick Start](#quick-start) • [How It Compares](#how-it-compares) • [Design Decisions](#design-decisions) • [Features](#features) • [Architecture](docs/ARCHITECTURE.md) • [Contributing](docs/CONTRIBUTING.md)
|
|
21
21
|
|
|
22
22
|
> Inspired by Andrej Karpathy's "LLM-native wiki" sketch and shaped by ten months of personal use, Hypomnema ships the full lifecycle — capture, synthesis, retrieval, session-resume — as Claude Code commands and lifecycle hooks.
|
|
23
23
|
|
|
24
|
+
> **Current state vs. v2 vision.** v1.0.1 (today) is honest about its trigger model: most wiki behavior — ingest, query, session-close — fires on **explicit `/hypo:*` commands**, with a handful of hooks providing auto-staging, session-state injection, and lookup signals. The v2 thesis is *fully autonomous* — Claude reading, writing, and synthesizing the wiki without being asked. **v1.1.0** is the first step on that ramp: instead of claiming auto-behavior, it ships an **observability score** that measures how often the wiki is actually used per session (ingest count, query count, session-close rate, citation rate) so you can see the auto-vs-manual ratio with your own eyes before the v2 work lands.
|
|
25
|
+
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## Quick Start
|
|
@@ -105,6 +107,7 @@ Hypomnema ───► synthesis · markdown · git · hooks · local
|
|
|
105
107
|
| **Format** | Plain markdown + frontmatter | Markdown | Proprietary | Vector store | Proprietary | HTML |
|
|
106
108
|
| **Backend** | Local file + git | Local file | SaaS | Service / DB | SaaS | Service |
|
|
107
109
|
| **Behavior tuning** | `/hypo:feedback` → permanent rules | None | None | None | Sometimes | None |
|
|
110
|
+
| **Auto-behavior** | Explicit `/hypo:*` triggers today; **v1.1 ships an observability score**, v2 target = fully autonomous | None | None | None | Black box | None |
|
|
108
111
|
| **Setup cost** | One command | One install | Sign-up | Pipeline build | Sign-up | Repo connect |
|
|
109
112
|
| **Lock-in** | Zero (markdown + git) | Low | High | Medium | High | Medium |
|
|
110
113
|
|
|
@@ -295,6 +298,8 @@ Place a `hypo-config.md` at the wiki root to make it portable across machines wi
|
|
|
295
298
|
|
|
296
299
|
`.hypoignore` controls which paths the hooks ignore (default: `*.pdf`, `*.zip`, `*.pem`, `*.env`, …). Edit it directly — there is no privacy mode flag; one file, one source of truth.
|
|
297
300
|
|
|
301
|
+
> **Scope of git sync.** Hypomnema git-syncs only the `~/hypomnema/` wiki itself. Your Claude Code configuration (`~/.claude/`) is intentionally **not** managed by Hypomnema — for cross-machine sync of agents, skills, and settings, the recommended pattern is a separate dotfiles manager such as [chezmoi](https://www.chezmoi.io/).
|
|
302
|
+
|
|
298
303
|
### Where do `/hypo:*` commands live?
|
|
299
304
|
|
|
300
305
|
| Install path | Slash commands served from |
|
|
@@ -315,7 +320,7 @@ No external services. No API keys. No vector databases.
|
|
|
315
320
|
|
|
316
321
|
## Status
|
|
317
322
|
|
|
318
|
-
- **Tests:**
|
|
323
|
+
- **Tests:** see `npm test` — exact totals shift as lanes ship, so the runner is the source of truth
|
|
319
324
|
- **CI:** 7 independent jobs (test matrix, lint, init/upgrade snapshots, replay, hypo-absent, uninstall-smoke)
|
|
320
325
|
- **Release:** `npm publish --provenance` on `v*` tag push
|
|
321
326
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the observability audit on recent sessions and (optionally) write the weekly report
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running `/hypo:audit`. Inspect recent Claude Code sessions to see how much of the wiki's value motion is actually happening (search, ingest, citation, feedback), then either show the result inline or write a weekly observability page.
|
|
6
|
+
|
|
7
|
+
## What this shows
|
|
8
|
+
|
|
9
|
+
- Per-session metrics — search count, ingest count, URL mentions, feedback count
|
|
10
|
+
- Classification — `normal` / `search-0` / `search-many` / `ingest-missed` / `staleness-skip`
|
|
11
|
+
- Optional weekly aggregation with an autonomy score (heuristic v0)
|
|
12
|
+
|
|
13
|
+
Definition: [[pages/observability/_index]].
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 1 — Locate package root
|
|
18
|
+
|
|
19
|
+
Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
|
|
20
|
+
|
|
21
|
+
If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Step 2 — Decide the mode
|
|
26
|
+
|
|
27
|
+
- **Per-session view (default)** — show recent sessions with metrics + classification:
|
|
28
|
+
```bash
|
|
29
|
+
node <package-root>/scripts/session-audit.mjs [--hypo-dir="<path>"] [--limit=20]
|
|
30
|
+
```
|
|
31
|
+
- **Weekly report (when the user asks for "weekly", "score", or names a week)** — write the report to `pages/observability/<YYYY-WW>.md`:
|
|
32
|
+
```bash
|
|
33
|
+
node <package-root>/scripts/weekly-report.mjs [--hypo-dir="<path>"] [--week=YYYY-WW] --write
|
|
34
|
+
```
|
|
35
|
+
- **JSON for tooling** — append `--json` to either script.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Step 3 — Report results
|
|
40
|
+
|
|
41
|
+
Show the output verbatim. Then add a brief interpretation:
|
|
42
|
+
|
|
43
|
+
- If `staleness-skip` dominates: "Most sessions audited are older than 30 days — score reflects backlog, not current behavior."
|
|
44
|
+
- If `search-0` count is high: "Sessions ran without consulting the wiki — consider whether `/hypo:query` was the right reflex."
|
|
45
|
+
- If `ingest-missed` is non-zero: "Sessions discussed URLs but no `/hypo:ingest` ran — those captures got lost."
|
|
46
|
+
- Otherwise: "Audit window looks healthy; weekly report committed to the wiki for trend tracking."
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -53,7 +53,7 @@ hypomnema/
|
|
|
53
53
|
│ ├── Home.md, Overview.md, hypo-automation.md, hypo-help.md
|
|
54
54
|
│ ├── pages/_index.md
|
|
55
55
|
│ └── projects/_template/
|
|
56
|
-
├── tests/runner.mjs ← no-dependency test runner
|
|
56
|
+
├── tests/runner.mjs ← no-dependency test runner
|
|
57
57
|
├── docs/ ← ARCHITECTURE.md, CONTRIBUTING.md
|
|
58
58
|
├── .claude-plugin/plugin.json← plugin manifest
|
|
59
59
|
└── package.json ← npm metadata, no runtime deps
|
|
@@ -100,7 +100,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
100
100
|
| `UserPromptSubmit` | `hypo-first-prompt.mjs` → `hypo-lookup.mjs` → `hypo-compact-guard.mjs` |
|
|
101
101
|
| `PreCompact` | `hypo-personal-check.mjs` |
|
|
102
102
|
| `PostToolUse` (Write/Edit) | `hypo-auto-stage.mjs` |
|
|
103
|
-
| `Stop` | `hypo-hot-rebuild.mjs` → `hypo-auto-commit.mjs` |
|
|
103
|
+
| `Stop` | `hypo-hot-rebuild.mjs` → `hypo-session-record.mjs` → `hypo-auto-commit.mjs` |
|
|
104
104
|
| `CwdChanged` | `hypo-cwd-change.mjs` |
|
|
105
105
|
| `FileChanged` | `hypo-file-watch.mjs` |
|
|
106
106
|
|
|
@@ -113,9 +113,10 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
113
113
|
| `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each, with verify-by-date warnings). **MISS** → emit closest-slug signal that prompts Claude to research + `/hypo:ingest` |
|
|
114
114
|
| `hypo-compact-guard` | Detect `/compact` invocations → enforce session-close checklist before allowing compact |
|
|
115
115
|
| `hypo-personal-check` | PreCompact validation: lint blockers, uncommitted changes, missing session-log entries → block compact |
|
|
116
|
-
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (
|
|
117
|
-
| `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity |
|
|
118
|
-
| `hypo-
|
|
116
|
+
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
|
|
117
|
+
| `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
|
|
118
|
+
| `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
|
|
119
|
+
| `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
|
|
119
120
|
| `hypo-cwd-change` | When working directory changes, re-resolve the active project and inject its `hot.md` |
|
|
120
121
|
| `hypo-file-watch` | Notify on external wiki edits so the in-session view stays consistent |
|
|
121
122
|
|
|
@@ -289,8 +290,9 @@ SessionStart
|
|
|
289
290
|
│ └─► hypo-cwd-change.mjs (re-inject project hot.md)
|
|
290
291
|
│
|
|
291
292
|
└─► Stop
|
|
292
|
-
├─► hypo-hot-rebuild.mjs (regenerate root hot.md)
|
|
293
|
-
|
|
293
|
+
├─► hypo-hot-rebuild.mjs (regenerate root hot.md + growth cache)
|
|
294
|
+
├─► hypo-session-record.mjs (append .cache/sessions/index.jsonl)
|
|
295
|
+
└─► hypo-auto-commit.mjs (.hypoignore-filtered stage + commit + pull + push)
|
|
294
296
|
```
|
|
295
297
|
|
|
296
298
|
---
|
|
@@ -307,6 +309,78 @@ Promotion is intentionally manual to keep `<learned_behaviors>` curated. The pip
|
|
|
307
309
|
|
|
308
310
|
---
|
|
309
311
|
|
|
312
|
+
## Observability (v1.1)
|
|
313
|
+
|
|
314
|
+
v1.1 ships an **observability wedge** — the wiki measures whether it's actually being used per session, rather than claiming autonomy it can't yet deliver.
|
|
315
|
+
|
|
316
|
+
### Data flow
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
Stop hook (hypo-session-record.mjs)
|
|
320
|
+
│ appends one JSONL entry per session
|
|
321
|
+
▼
|
|
322
|
+
<hypo-root>/.cache/sessions/index.jsonl ← primary source
|
|
323
|
+
│
|
|
324
|
+
▼
|
|
325
|
+
scripts/session-audit.mjs ← per-session metrics + classification
|
|
326
|
+
│
|
|
327
|
+
▼
|
|
328
|
+
scripts/weekly-report.mjs ← aggregated weekly autonomy score
|
|
329
|
+
│
|
|
330
|
+
▼
|
|
331
|
+
pages/observability/<YYYY-WW>.md ← committed report (heuristic v0)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Transcript dual-source (ADR 0019)
|
|
335
|
+
|
|
336
|
+
`session-audit.mjs` reads transcripts from two locations, in priority order:
|
|
337
|
+
|
|
338
|
+
1. **Primary:** `<hypo-root>/.cache/sessions/index.jsonl` — written by the Stop hook `hypo-session-record.mjs`. Each line: `{ session_id, transcript_path, recorded_at, cwd }`.
|
|
339
|
+
2. **Fallback:** `~/.claude/projects/<encoded>/*.jsonl` — scanned when the index is missing or empty (legacy / freshly-installed wikis).
|
|
340
|
+
|
|
341
|
+
### Classification
|
|
342
|
+
|
|
343
|
+
| Class | Rule |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `staleness-skip` | `recorded_at` older than `--max-age-days` (default 30) |
|
|
346
|
+
| `ingest-missed` | `urls >= 2` and `ingest_count == 0` |
|
|
347
|
+
| `search-many` | `search_count >= 5` (heavy retrieval; suggests missing synthesis) |
|
|
348
|
+
| `search-0` | `search_count == 0` |
|
|
349
|
+
| `normal` | otherwise |
|
|
350
|
+
|
|
351
|
+
Counted tool names: `Grep`, `WebSearch`, `WebFetch`. Counted slash commands: `/hypo:query`, `/hypo:ingest`, `/hypo:feedback`. A single transcript record contributes to exactly one of (tool-use search OR text-based command search) — `computeMetrics` short-circuits after a tool-use match to prevent double counting.
|
|
352
|
+
|
|
353
|
+
### Autonomy score (heuristic v0)
|
|
354
|
+
|
|
355
|
+
`weekly-report.mjs` aggregates the week's results into a 0–100 score. The score is **clamped to `[0, 100]`** and skips `staleness-skip` sessions. Formula sketch (see `pages/observability/_index.md` for the formal definition):
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
numerator = Σ min(search,3) + ingest*3 + feedback*2
|
|
359
|
+
denominator = Σ 1 + (urls > 0 ? min(urls,5)*2 : 0)
|
|
360
|
+
score = clamp(round(num/den * 100), 0, 100)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The score is a **proxy, not ground truth**. The four-week baseline plan (capture v0 numbers, then revisit with LLM-judge classification before v2) is recorded in the same `_index.md`.
|
|
364
|
+
|
|
365
|
+
### Privacy
|
|
366
|
+
|
|
367
|
+
The observability pipeline reads but never republishes raw transcripts. Weekly reports only emit `session_id` plus aggregate counts — no transcript content, no URLs, no tool inputs. Transcripts themselves live under `~/.claude/projects/` or `.cache/sessions/` which `.hypoignore` already excludes from any sync.
|
|
368
|
+
|
|
369
|
+
### Growth metrics (Lane B)
|
|
370
|
+
|
|
371
|
+
A separate, lightweight counter — distinct from the audit pipeline — runs at every Stop / SessionStart pair:
|
|
372
|
+
|
|
373
|
+
- **Stop** (`hypo-hot-rebuild.mjs`) computes `{ addedPages, updatedPages, newWikilinks }` by reading `git status --porcelain` plus a conditional `git diff HEAD --unified=0`, writes the result to `<hypo-root>/.cache/last-session-growth.json`, and echoes one line to stderr.
|
|
374
|
+
- **SessionStart** (`hypo-session-start.mjs`) reads the cache and surfaces the same line in both stderr (cyan) and the LLM's `additionalContext` so user and model see the same "직전 세션" prefix.
|
|
375
|
+
|
|
376
|
+
If `git status` shows no `.md` changes, the diff step is skipped — Stop hook fast path.
|
|
377
|
+
|
|
378
|
+
### Citation convention
|
|
379
|
+
|
|
380
|
+
The six writer-side skills (`crystallize`, `query`, `ingest`, `verify`, `graph`, `lint`) carry an identical footer instructing Claude to cite wiki pages inline as `[[page-slug]]`. The audit script counts these citations as a "wiki was actually consulted" signal in future iterations.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
310
384
|
## Privacy & exclusions
|
|
311
385
|
|
|
312
386
|
`.hypoignore` is the **only** privacy mechanism. The v1.0 `personal / shared / public` mode matrix was deleted in v1.1 — every privacy decision turned out to be a per-path question, and a single ignore file handles per-path natively.
|
|
@@ -329,9 +403,9 @@ Default patterns: `*.pdf`, `*.zip`, `*.pem`, `*.env`, `*.key`, `*.crt`, `*creden
|
|
|
329
403
|
| `upgrade.mjs` regression | ~10 (includes migration fixture) |
|
|
330
404
|
| `lint.mjs` (fix / json / session-state) | ~10 |
|
|
331
405
|
| Misc (`expandHome`, `resolveHypoRoot`, …) | ~remainder |
|
|
332
|
-
| **Total** |
|
|
406
|
+
| **Total** | Run `npm test` for the live count |
|
|
333
407
|
|
|
334
|
-
Run with `npm test`. The runner uses only Node.js built-ins; tests create scoped temp dirs and clean up after themselves.
|
|
408
|
+
Run with `npm test`. The runner uses only Node.js built-ins; tests create scoped temp dirs and clean up after themselves. The count above is a layout sketch — exact totals shift as lanes ship, so `npm test` is the source of truth.
|
|
335
409
|
|
|
336
410
|
---
|
|
337
411
|
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -22,7 +22,7 @@ No build step. The package is plain ESM with **zero npm runtime dependencies**.
|
|
|
22
22
|
git clone https://github.com/sk-lim19f/Hypomnema.git
|
|
23
23
|
cd Hypomnema
|
|
24
24
|
npm install # installs dev tooling only — runtime deps are zero
|
|
25
|
-
npm test #
|
|
25
|
+
npm test # all tests should pass — exact count shifts as lanes ship
|
|
26
26
|
npm run lint # frontmatter + wikilink validation
|
|
27
27
|
```
|
|
28
28
|
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'child_process';
|
|
9
|
-
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
9
|
+
import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
|
|
10
|
+
import { join } from 'path';
|
|
10
11
|
|
|
11
12
|
function git(...args) {
|
|
12
13
|
return spawnSync('git', ['-C', HYPO_DIR, ...args], { encoding: 'utf-8', timeout: 30000 });
|
|
@@ -17,7 +18,19 @@ function hasRemote() {
|
|
|
17
18
|
return (r.stdout || '').trim().length > 0;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
git
|
|
21
|
+
// `.hypoignore` is the project privacy boundary. `git add -A` ignores it, so
|
|
22
|
+
// enumerate changed paths, drop ignored ones, then stage explicitly.
|
|
23
|
+
const ignorePatterns = loadHypoIgnore(HYPO_DIR);
|
|
24
|
+
const porcelain = git('status', '--porcelain', '-uall');
|
|
25
|
+
const paths = [];
|
|
26
|
+
for (const line of (porcelain.stdout || '').split('\n')) {
|
|
27
|
+
if (!line) continue;
|
|
28
|
+
const file = line.slice(3).replace(/^"|"$/g, '').split(' -> ').pop().trim();
|
|
29
|
+
if (!file) continue;
|
|
30
|
+
if (ignorePatterns.length > 0 && isIgnored(join(HYPO_DIR, file), HYPO_DIR, ignorePatterns)) continue;
|
|
31
|
+
paths.push(file);
|
|
32
|
+
}
|
|
33
|
+
if (paths.length > 0) git('add', '--', ...paths);
|
|
21
34
|
const staged = git('diff', '--cached', '--name-only').stdout?.trim() || '';
|
|
22
35
|
if (staged) {
|
|
23
36
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'child_process';
|
|
9
|
-
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
9
|
+
import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
|
|
10
10
|
|
|
11
11
|
let input = {};
|
|
12
12
|
try {
|
|
@@ -24,7 +24,10 @@ try {
|
|
|
24
24
|
const filePath = input.tool_input?.file_path ?? '';
|
|
25
25
|
|
|
26
26
|
if (filePath.startsWith(HYPO_DIR + '/') || filePath === HYPO_DIR) {
|
|
27
|
-
|
|
27
|
+
const patterns = loadHypoIgnore(HYPO_DIR);
|
|
28
|
+
if (patterns.length === 0 || !isIgnored(filePath, HYPO_DIR, patterns)) {
|
|
29
|
+
spawnSync('git', ['-C', HYPO_DIR, 'add', filePath], { stdio: 'ignore' });
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* This script manages: frontmatter, H2 structure, date fields.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
|
-
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
15
|
+
import { HYPO_DIR, computeSessionGrowth, formatGrowthMetrics } from './hypo-shared.mjs';
|
|
16
16
|
|
|
17
|
-
const HOT_PATH
|
|
17
|
+
const HOT_PATH = join(HYPO_DIR, 'hot.md');
|
|
18
|
+
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
18
19
|
|
|
19
20
|
function parseFrontmatter(content) {
|
|
20
21
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -88,8 +89,18 @@ ${tableRows}
|
|
|
88
89
|
if (canonical !== current) writeFileSync(HOT_PATH, canonical);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
function emitGrowth() {
|
|
93
|
+
if (!existsSync(HYPO_DIR)) return;
|
|
94
|
+
const stats = computeSessionGrowth(HYPO_DIR);
|
|
95
|
+
const line = formatGrowthMetrics('stop', stats);
|
|
96
|
+
if (line) process.stderr.write(`${line}\n`);
|
|
97
|
+
try {
|
|
98
|
+
mkdirSync(join(HYPO_DIR, '.cache'), { recursive: true });
|
|
99
|
+
writeFileSync(GROWTH_CACHE, JSON.stringify({ ...stats, ts: Date.now() }));
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try { rebuild(); } catch {}
|
|
104
|
+
try { emitGrowth(); } catch {}
|
|
94
105
|
|
|
95
106
|
try { console.log(JSON.stringify({ continue: true, suppressOutput: true })); } catch {}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hypo-session-record.mjs — Stop hook
|
|
4
|
+
*
|
|
5
|
+
* Appends an entry to ~/hypomnema/.cache/sessions/index.jsonl for each
|
|
6
|
+
* completed session. The index.jsonl is the **primary** source for
|
|
7
|
+
* scripts/session-audit.mjs (which falls back to ~/.claude/projects/<encoded>/
|
|
8
|
+
* if the index is empty or missing — see ADR 0019).
|
|
9
|
+
*
|
|
10
|
+
* Silent: never blocks, never emits user-visible output.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
14
|
+
import { dirname, join } from 'path';
|
|
15
|
+
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
16
|
+
|
|
17
|
+
const INDEX_PATH = join(HYPO_DIR, '.cache', 'sessions', 'index.jsonl');
|
|
18
|
+
|
|
19
|
+
function emitContinue() {
|
|
20
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let raw = '';
|
|
24
|
+
process.stdin.setEncoding('utf-8');
|
|
25
|
+
process.stdin.on('data', chunk => raw += chunk);
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
let payload = {};
|
|
29
|
+
try { payload = JSON.parse(raw) || {}; } catch {}
|
|
30
|
+
|
|
31
|
+
const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
|
|
32
|
+
const sessionId = payload.session_id || payload.sessionId || null;
|
|
33
|
+
if (!transcriptPath || !sessionId) {
|
|
34
|
+
// Older Claude Code (no transcript_path) — fallback path in
|
|
35
|
+
// scripts/session-audit.mjs handles this case.
|
|
36
|
+
emitContinue();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!existsSync(HYPO_DIR)) { emitContinue(); return; }
|
|
41
|
+
mkdirSync(dirname(INDEX_PATH), { recursive: true });
|
|
42
|
+
|
|
43
|
+
const entry = {
|
|
44
|
+
session_id: sessionId,
|
|
45
|
+
transcript_path: transcriptPath,
|
|
46
|
+
recorded_at: new Date().toISOString(),
|
|
47
|
+
cwd: payload.cwd || process.cwd(),
|
|
48
|
+
};
|
|
49
|
+
appendFileSync(INDEX_PATH, JSON.stringify(entry) + '\n');
|
|
50
|
+
} catch {
|
|
51
|
+
// Audit is best-effort observability — never let it block session close.
|
|
52
|
+
}
|
|
53
|
+
emitContinue();
|
|
54
|
+
});
|
|
@@ -11,9 +11,20 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from '
|
|
|
11
11
|
import { homedir, tmpdir } from 'os';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { spawnSync } from 'child_process';
|
|
14
|
-
import { HYPO_DIR, buildOutput, SESSION_STATE_NEXT_HEADINGS } from './hypo-shared.mjs';
|
|
14
|
+
import { HYPO_DIR, buildOutput, SESSION_STATE_NEXT_HEADINGS, formatGrowthMetrics } from './hypo-shared.mjs';
|
|
15
15
|
|
|
16
|
-
const PROJECTS_DIR
|
|
16
|
+
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
17
|
+
const GROWTH_CACHE = join(HYPO_DIR, '.cache', 'last-session-growth.json');
|
|
18
|
+
|
|
19
|
+
function readLastGrowthLine() {
|
|
20
|
+
if (!existsSync(GROWTH_CACHE)) return '';
|
|
21
|
+
try {
|
|
22
|
+
const stats = JSON.parse(readFileSync(GROWTH_CACHE, 'utf-8'));
|
|
23
|
+
return formatGrowthMetrics('start', stats);
|
|
24
|
+
} catch {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
17
28
|
|
|
18
29
|
function gitPull(dir) {
|
|
19
30
|
if (!existsSync(join(dir, '.git'))) return;
|
|
@@ -97,6 +108,13 @@ process.stdin.on('end', () => {
|
|
|
97
108
|
try { data = JSON.parse(raw); } catch {}
|
|
98
109
|
|
|
99
110
|
gitPull(HYPO_DIR);
|
|
111
|
+
const growthLine = readLastGrowthLine();
|
|
112
|
+
// Intentional dual emit: stderr (cyan) is the human-visible nudge in the
|
|
113
|
+
// terminal; growthPrefix injects the same plain-text line into the LLM's
|
|
114
|
+
// additionalContext so model and user start the session looking at the
|
|
115
|
+
// same state. ANSI escapes are kept out of additionalContext on purpose.
|
|
116
|
+
const growthPrefix = growthLine ? `${growthLine}\n\n` : '';
|
|
117
|
+
if (growthLine) process.stderr.write(`\n\x1b[36m${growthLine}\x1b[0m\n`);
|
|
100
118
|
const cwd = data.cwd || data.directory || process.cwd();
|
|
101
119
|
const sessionId = data.session_id || 'default';
|
|
102
120
|
const MARKER_FILE = join(tmpdir(), `hypo-session-marker-${sessionId}.json`);
|
|
@@ -113,26 +131,30 @@ process.stdin.on('end', () => {
|
|
|
113
131
|
if (hotContent) parts.push(`[HOT]\n${hotContent}`);
|
|
114
132
|
if (stateContent) parts.push(`[SESSION STATE — 다음 작업]\n${stateContent}`);
|
|
115
133
|
console.log(JSON.stringify(
|
|
116
|
-
buildOutput(
|
|
134
|
+
buildOutput(`${growthPrefix}[WIKI HOT CACHE: project=${hit.proj}]\n\n${parts.join('\n\n')}`, { continue: true, suppressOutput: true })
|
|
117
135
|
));
|
|
118
136
|
} else {
|
|
119
137
|
process.stderr.write(`\n\x1b[36m[Hypomnema]\x1b[0m project: \x1b[1m${hit.proj}\x1b[0m (no snapshot yet)\n\n`);
|
|
120
138
|
writeFileSync(MARKER_FILE, JSON.stringify({ proj: hit.proj, hotPath: null, ts: Date.now() }));
|
|
121
139
|
console.log(JSON.stringify(
|
|
122
|
-
buildOutput(
|
|
140
|
+
buildOutput(`${growthPrefix}[WIKI HOT CACHE: project=${hit.proj}, no snapshot yet]`, { continue: true, suppressOutput: true })
|
|
123
141
|
));
|
|
124
142
|
}
|
|
125
143
|
return;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
if (!existsSync(GLOBAL_HOT)) {
|
|
129
|
-
|
|
147
|
+
if (growthLine) {
|
|
148
|
+
console.log(JSON.stringify(buildOutput(growthLine, { continue: true, suppressOutput: true })));
|
|
149
|
+
} else {
|
|
150
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
151
|
+
}
|
|
130
152
|
return;
|
|
131
153
|
}
|
|
132
154
|
|
|
133
155
|
const globalContent = readFileSync(GLOBAL_HOT, 'utf-8').slice(0, HOT_CHARS);
|
|
134
156
|
console.log(JSON.stringify(
|
|
135
|
-
buildOutput(
|
|
157
|
+
buildOutput(`${growthPrefix}[WIKI HOT CACHE: global — no project matched cwd=${cwd}]\n\n${globalContent}`, { continue: true, suppressOutput: true })
|
|
136
158
|
));
|
|
137
159
|
|
|
138
160
|
} catch {
|
package/hooks/hypo-shared.mjs
CHANGED
|
@@ -160,6 +160,102 @@ export function buildOutput(context, extra = {}) {
|
|
|
160
160
|
return { ...extra, additionalContext: context };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// ── growth metrics (F2 + E4) ───────────────────────────────────────────────
|
|
164
|
+
// Single formatter used by Stop (hot-rebuild) and SessionStart hooks so the
|
|
165
|
+
// "[hypo] +N pages, ~M updated, K wikilinks" line stays consistent at both
|
|
166
|
+
// ends of a session. See ADR-0018 / Lane B.
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format a growth-metrics one-liner. Returns '' when all counts are 0 so
|
|
170
|
+
* callers can no-op silently.
|
|
171
|
+
*
|
|
172
|
+
* @param {'stop'|'start'} mode
|
|
173
|
+
* @param {{addedPages?:number, updatedPages?:number, newWikilinks?:number}} stats
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
export function formatGrowthMetrics(mode, stats) {
|
|
177
|
+
const a = Number(stats?.addedPages) || 0;
|
|
178
|
+
const u = Number(stats?.updatedPages) || 0;
|
|
179
|
+
const w = Number(stats?.newWikilinks) || 0;
|
|
180
|
+
if (a === 0 && u === 0 && w === 0) return '';
|
|
181
|
+
const body = `+${a} pages, ~${u} updated, ${w} wikilinks`;
|
|
182
|
+
if (mode === 'stop') return `[hypo] ${body}`;
|
|
183
|
+
if (mode === 'start') return `[hypo] 직전 세션: ${body}. 이어서 볼까요?`;
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Compute session growth by inspecting the wiki repo's working tree against
|
|
189
|
+
* HEAD. Counts every modified/added/untracked markdown file under `pages/`
|
|
190
|
+
* or `projects/` and totals net-new `[[wikilink]]` occurrences in the diff.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} hypoDir
|
|
193
|
+
* @returns {{addedPages:number, updatedPages:number, newWikilinks:number}}
|
|
194
|
+
*/
|
|
195
|
+
export function computeSessionGrowth(hypoDir) {
|
|
196
|
+
const empty = { addedPages: 0, updatedPages: 0, newWikilinks: 0 };
|
|
197
|
+
if (!existsSync(join(hypoDir, '.git'))) return empty;
|
|
198
|
+
try {
|
|
199
|
+
// Single `git status --porcelain` enumerates tracked + untracked. On a
|
|
200
|
+
// clean tree (no .md changes at all) we return early and skip the much
|
|
201
|
+
// more expensive `git diff HEAD --unified=0` — Stop hook P95 win.
|
|
202
|
+
// `-uall` expands untracked directories so a brand-new `pages/new.md`
|
|
203
|
+
// isn't hidden behind a single `?? pages/` line.
|
|
204
|
+
const porcelain = spawnSync('git', ['-C', hypoDir, 'status', '--porcelain', '-uall'], { encoding: 'utf-8', timeout: 5000 });
|
|
205
|
+
if (porcelain.status !== 0) return empty;
|
|
206
|
+
let addedPages = 0, updatedPages = 0;
|
|
207
|
+
let hasTrackedMdChange = false;
|
|
208
|
+
const untrackedMd = [];
|
|
209
|
+
// Growth metrics describe wiki page activity, so restrict to the two
|
|
210
|
+
// page-bearing trees. Top-level files like README.md or root hot.md are
|
|
211
|
+
// intentionally excluded — they're scaffolding, not page growth.
|
|
212
|
+
const inPagesScope = (file) =>
|
|
213
|
+
file.endsWith('.md') && (file.startsWith('pages/') || file.startsWith('projects/'));
|
|
214
|
+
for (const line of (porcelain.stdout || '').split('\n')) {
|
|
215
|
+
if (!line) continue;
|
|
216
|
+
const xy = line.slice(0, 2);
|
|
217
|
+
const file = line.slice(3).replace(/^"|"$/g, '').split(' -> ').pop().trim();
|
|
218
|
+
if (!inPagesScope(file)) continue;
|
|
219
|
+
if (xy === '??') { untrackedMd.push(file); addedPages++; continue; }
|
|
220
|
+
hasTrackedMdChange = true;
|
|
221
|
+
if (xy.includes('A')) addedPages++;
|
|
222
|
+
else if (xy.includes('M') || xy.includes('R')) updatedPages++;
|
|
223
|
+
}
|
|
224
|
+
if (!hasTrackedMdChange && untrackedMd.length === 0) return empty;
|
|
225
|
+
|
|
226
|
+
let plus = 0, minus = 0;
|
|
227
|
+
if (hasTrackedMdChange) {
|
|
228
|
+
// pathspec keeps non-Markdown / out-of-scope diffs from polluting the
|
|
229
|
+
// wikilink count. Without it, a `[[…]]` string in a script.js diff was
|
|
230
|
+
// being credited as a new wikilink.
|
|
231
|
+
const diff = spawnSync(
|
|
232
|
+
'git',
|
|
233
|
+
['-C', hypoDir, 'diff', 'HEAD', '--unified=0', '--', 'pages/', 'projects/'],
|
|
234
|
+
{ encoding: 'utf-8', timeout: 10000 }
|
|
235
|
+
);
|
|
236
|
+
if (diff.status === 0) {
|
|
237
|
+
for (const line of (diff.stdout || '').split('\n')) {
|
|
238
|
+
if (line.startsWith('+++') || line.startsWith('---')) continue;
|
|
239
|
+
const matches = line.match(/\[\[[^\]\n]+\]\]/g);
|
|
240
|
+
if (!matches) continue;
|
|
241
|
+
if (line.startsWith('+')) plus += matches.length;
|
|
242
|
+
else if (line.startsWith('-')) minus += matches.length;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const f of untrackedMd) {
|
|
247
|
+
try {
|
|
248
|
+
const body = readFileSync(join(hypoDir, f), 'utf-8');
|
|
249
|
+
const matches = body.match(/\[\[[^\]\n]+\]\]/g);
|
|
250
|
+
if (matches) plus += matches.length;
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
return { addedPages, updatedPages, newWikilinks: Math.max(0, plus - minus) };
|
|
254
|
+
} catch {
|
|
255
|
+
return empty;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
163
259
|
// ── .hypoignore support ────────────────────────────────────────────────────
|
|
164
260
|
// Inlined here so deployed hooks (~/.claude/hooks/) don't need scripts/lib/.
|
|
165
261
|
|