hypomnema 1.2.1 → 1.3.1
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 +4 -2
- package/README.md +4 -2
- package/commands/crystallize.md +23 -6
- package/commands/feedback.md +1 -1
- package/commands/upgrade.md +2 -0
- package/docs/CONTRIBUTING.md +96 -11
- package/hooks/hypo-auto-commit.mjs +3 -3
- package/hooks/hypo-auto-minimal-crystallize.mjs +8 -3
- package/hooks/hypo-cwd-change.mjs +2 -2
- package/hooks/hypo-first-prompt.mjs +1 -1
- package/hooks/hypo-personal-check.mjs +57 -7
- package/hooks/hypo-session-start.mjs +73 -19
- package/hooks/hypo-shared.mjs +206 -16
- package/hooks/version-check.mjs +204 -6
- package/package.json +5 -2
- package/scripts/bump-version.mjs +9 -3
- package/scripts/check-bilingual.mjs +115 -0
- package/scripts/crystallize.mjs +130 -16
- package/scripts/doctor.mjs +45 -9
- package/scripts/feedback-sync.mjs +44 -15
- package/scripts/feedback.mjs +5 -5
- package/scripts/fix-status-verify.mjs +256 -0
- package/scripts/init.mjs +45 -4
- package/scripts/install-git-hooks.mjs +258 -0
- package/scripts/lib/adr-corpus.mjs +79 -0
- package/scripts/lib/check-bilingual.mjs +141 -0
- package/scripts/lib/extensions.mjs +3 -3
- package/scripts/lib/feedback-scope.mjs +21 -0
- package/scripts/lib/fix-manifest.mjs +109 -0
- package/scripts/lib/fix-status-verify.mjs +438 -0
- package/scripts/lib/plugin-detect.mjs +51 -0
- package/scripts/lib/pre-commit-format.mjs +251 -0
- package/scripts/lib/project-create.mjs +2 -2
- package/scripts/lint.mjs +48 -8
- package/scripts/pre-commit-format.mjs +198 -0
- package/scripts/resume.mjs +61 -3
- package/scripts/smoke-pack.mjs +39 -2
- package/scripts/upgrade.mjs +308 -58
- package/skills/crystallize/SKILL.md +13 -2
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +4 -0
package/README.ko.md
CHANGED
|
@@ -23,13 +23,15 @@ _Claude에게 기록을 맡기세요 — 그리고 그 기록이 실제로 쌓
|
|
|
23
23
|
|
|
24
24
|
> **아래에서 자주 쓰이는 용어 간단 정리.** *프런트매터(frontmatter)* = 마크다운 파일 맨 위의 YAML 블록. *위키링크(wikilink)* = `[[페이지-슬러그]]` 형태의 교차 참조. *ADR* = "Architecture Decision Record" — 어떤 설계 결정을 *왜* 했는지 짧게 적은 마크다운 페이지. *projection*(투영) = 한 방향 자동 파생(`pages/feedback/*.md` → `MEMORY.md` / `<learned_behaviors>`). *훅(hook)* = Claude Code가 라이프사이클 이벤트에서 자동으로 실행하는 스크립트. *hot.md* / *session-state.md* = "방금 무엇을 했는지"와 "다음에 무엇을 할지"를 담는 프로젝트별 캐시 파일 — 멈춘 프로젝트를 한 번에 이어 받을 수 있게 합니다. 전체 용어 풀이는 [용어 사전](#용어-사전) 참조.
|
|
25
25
|
|
|
26
|
-
> **현재 자동화 범위와 다음 목표.** v1.
|
|
26
|
+
> **현재 자동화 범위와 다음 목표.** v1.3.0(현재)은 트리거 모델을 솔직하게 정리합니다. 위키 작업(자료 정리·검색·세션 마무리)은 여전히 사용자가 `/hypo:*` 명령어를 직접 입력해 시작합니다. 다만 **v1.1.0**부터 위키가 한 세션에서 얼마나 활용됐는지를 측정하는 *관측성 지표(observability score)* 가 들어갔고, **v1.2.0**은 그 위에 사용자가 시키지 않아도 자동으로 동작하는 영역 4개를 추가했습니다:
|
|
27
27
|
> - **`feedback` 페이지를 단일 원천(source of truth)으로**(ADR 0031) — `pages/feedback/`에 한 번만 적으면, 위키가 `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록을 자동으로 갱신합니다.
|
|
28
28
|
> - **확장 파일 동봉 동기화**(ADR 0024) — 위키 안의 `~/hypomnema/extensions/{agents,commands,hooks,skills}/`에 둔 파일을 자동으로 `~/.claude/`에 반영합니다. `--codex` 옵션을 추가하면 `hooks`·`commands`는 `~/.codex/`에도 반영되지만, `agents`와 `skills`는 Claude 전용이라 의도적으로 건너뜁니다.
|
|
29
29
|
> - **프로젝트 자동 생성**(ADR 0023) — 작업 디렉터리를 git 저장소(`package.json`·`Cargo.toml` 등의 프로젝트 표식이 있는 곳)로 옮겼을 때 대응하는 위키 프로젝트가 없으면, 새로 만들지 물어봅니다.
|
|
30
30
|
> - **세션 종료 자동 정리와 `/clear` 복구**(ADR 0022) — 의미 있는 세션이 끝날 때 "마무리 메모를 짧게 남길까요?"가 자동으로 뜨고, 마무리하지 않은 채 `/clear`를 입력해도 다음 세션 시작 시 이어서 정리할 수 있습니다.
|
|
31
31
|
>
|
|
32
32
|
> 스키마(`SCHEMA.md`)는 2.0으로 올라갑니다. `feedback` 페이지 타입에 9개의 필수 항목이 추가되며, `hypomnema upgrade --apply`를 실행하면 위키 루트에 `MIGRATION-v2.0.md`가 생성되어 단계별 보강 체크리스트를 제공합니다. 사용자가 직접 편집한 `SCHEMA.md`는 upgrade가 **덮어쓰지 않습니다** — 안내만 표시하고, 실제 반영은 사용자가 수동으로 결정합니다(이 정책을 코드에서는 *Option C*로 부릅니다).
|
|
33
|
+
>
|
|
34
|
+
> **v1.3.0**은 자율성을 넓히기보다 이 레이어를 다듬습니다. 세션 마무리 흐름에 *권고형* 성찰 4가지(자동 실행 없이 제안만 — ADR 0029)가 들어가고, `hypomnema lint --strict`가 선택된 경고를 에러로 승격해 릴리스 게이트로 쓸 수 있으며, 설치가 **stale-sibling 감지**로 단단해집니다 — `$PATH`에 남은 더 오래된 `hypomnema`가 더는 활성 훅을 조용히 다운그레이드하지 못합니다(ADR 0038).
|
|
33
35
|
|
|
34
36
|
---
|
|
35
37
|
|
|
@@ -247,7 +249,7 @@ v1.0에서는 `personal / shared / public` 3-mode를 만들었습니다. 현실
|
|
|
247
249
|
|
|
248
250
|
이와 별도로 `SessionStart` 훅은 npm 레지스트리와 Claude Code 플러그인 마켓플레이스를 백그라운드에서 확인합니다(세션 시작을 막지 않습니다). 새 버전이 게시되어 있으면 다음 세션 시작 시 "Update available!" 안내가 한 줄 표시됩니다. `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`을 지정하거나 `CI=true` 환경에서 실행하면 점검을 건너뜁니다.
|
|
249
251
|
|
|
250
|
-
위 네 가지 레인 외의 v1.
|
|
252
|
+
위 네 가지 레인 외의 v1.3 세부 수정 — 세션 마무리 lint를 건드린 파일로 스코프해 무관한 debt로 `/compact`가 막히지 않게 한 변경(ADR 0037), `feedback` scope 검증기가 cwd 유래 project id를 수용하게 한 수정(OQ-34), `--strict`가 에러로 승격하는 안정적 lint 경고 ID `W1`/`W2`/`W4`(`--fix`로 자동복구되는 `W3`는 경고로 유지) — 은 [`CHANGELOG.md`](CHANGELOG.md)를 참고하세요.
|
|
251
253
|
|
|
252
254
|
### 셋업 & 유지보수
|
|
253
255
|
|
package/README.md
CHANGED
|
@@ -23,13 +23,15 @@ _Make Claude take notes — and measure whether it actually does._
|
|
|
23
23
|
|
|
24
24
|
> **Quick decoder for terms used below.** *frontmatter* = the YAML block at the top of a markdown file; *wikilink* = a `[[page-slug]]` cross-reference; *ADR* = "Architecture Decision Record", a short markdown page that records *why* a design choice was made; *projection* = a one-way derive (`pages/feedback/*.md` → `MEMORY.md` / `<learned_behaviors>`); *hook* = a script that Claude Code runs automatically on lifecycle events; *hot.md* / *session-state.md* = the per-project cache files that hold "what just happened" and "what's next" so a paused project resumes in one read. Full glossary lives under [Term decoder](#term-decoder).
|
|
25
25
|
|
|
26
|
-
> **Current state vs. v2 vision.** v1.
|
|
26
|
+
> **Current state vs. v2 vision.** v1.3.0 (today) is honest about its trigger model: most wiki behavior — ingest, query, session-close — still fires on **explicit `/hypo:*` commands**, but the auto-behavior surface is growing. The v2 thesis is *fully autonomous* — Claude reading, writing, and synthesizing the wiki without being asked. **v1.1.0** shipped the **observability score** that measures how often the wiki is actually used per session (ingest / query / session-close / citation rates). **v1.2.0** adds four load-bearing autonomous lanes on top:
|
|
27
27
|
> - **feedback-as-SoT with one-way projections** — `pages/feedback/` becomes the single source-of-truth (SoT) for behavior corrections; the wiki one-way derives `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md`, so you edit one place and the projections refresh on their own (ADR 0031 — full design rationale lives in `projects/hypomnema/decisions/0031-*.md` inside your wiki).
|
|
28
28
|
> - **extensions companion sync** — anything you drop under `~/hypomnema/extensions/{agents,commands,hooks,skills}/` is mirrored into `~/.claude/` automatically; the optional `--codex` flag additionally mirrors `hooks` and `commands` into `~/.codex/` (agents/skills are Claude-only and skipped on the Codex target by design) (ADR 0024).
|
|
29
29
|
> - **auto-project creation on cwd match** — when you `cd` into a git repo with a project marker (`package.json`, `Cargo.toml`, etc.) and no matching wiki project exists, Hypomnema offers to scaffold one for you (ADR 0023).
|
|
30
30
|
> - **Stop-chain auto-minimal-crystallize + `/clear` recovery** — non-trivial sessions get an automatic "save a minimal session-close note?" prompt; `/clear` after a forgotten close is detected and recovered cleanly (ADR 0022).
|
|
31
31
|
>
|
|
32
32
|
> The schema (`SCHEMA.md`) bumps to 2.0 — the `feedback` page type now requires 9 mandatory frontmatter fields. `hypomnema upgrade --apply` writes `MIGRATION-v2.0.md` into the wiki root with a step-by-step backfill checklist. Your own `SCHEMA.md` is **never overwritten** by upgrade — we call this policy *Option C*: the upgrade only tells you what changed, and you apply the diff yourself.
|
|
33
|
+
>
|
|
34
|
+
> **v1.3.0** refines this layer rather than expanding autonomy: the session-close flow gains four *advisory* reflections (they suggest, never auto-act — ADR 0029), `hypomnema lint --strict` promotes selected warnings to errors for release gates, and install hardens with **stale-sibling detection** — an older `hypomnema` on `$PATH` can no longer silently downgrade your active hooks (ADR 0038).
|
|
33
35
|
|
|
34
36
|
---
|
|
35
37
|
|
|
@@ -247,7 +249,7 @@ All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan →
|
|
|
247
249
|
|
|
248
250
|
Additionally, the `SessionStart` hook performs a non-blocking background check against npm and the Claude Code plugin marketplace and prints an "Update available!" banner the next time a newer Hypomnema version has been published. Opt out with `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`, or by running under `CI=true`.
|
|
249
251
|
|
|
250
|
-
For fix-level v1.
|
|
252
|
+
For fix-level v1.3 detail beyond the lanes above — session-close lint scoped to touched files so `/compact` is no longer blocked by unrelated debt (ADR 0037), the `feedback` scope validator accepting cwd-derived project ids (OQ-34), and the stable lint warning IDs `W1`/`W2`/`W4` that `--strict` promotes to errors (while `W3`, auto-repaired by `--fix`, stays a warning) — see [`CHANGELOG.md`](CHANGELOG.md).
|
|
251
253
|
|
|
252
254
|
### Setup & maintenance
|
|
253
255
|
|
package/commands/crystallize.md
CHANGED
|
@@ -11,7 +11,20 @@ You are running `/hypo:crystallize`. This command serves two purposes:
|
|
|
11
11
|
|
|
12
12
|
## Step 1 — Detect context
|
|
13
13
|
|
|
14
|
-
If the user invoked `/hypo:crystallize` to close a session (phrases like "세션 종료", "오늘 작업 마무리", "session close", or "wrap up"), run Steps 2–4 (session-close mechanical apply + recovery) **before** the synthesis scan. Otherwise skip to Step 5.
|
|
14
|
+
If the user invoked `/hypo:crystallize` to close a session (phrases like "세션 종료", "오늘 작업 마무리", "session close", or "wrap up"), run Step 1a (advisory reflections) then Steps 2–4 (session-close mechanical apply + recovery) **before** the synthesis scan. Otherwise skip to Step 5.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Step 1a — Session-close advisory reflections
|
|
19
|
+
|
|
20
|
+
Before composing the payload (Step 2), run these four reflections and surface each to the user. Every one is **advisory** (ADR 0029 identity guard) — the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
|
|
21
|
+
|
|
22
|
+
1. **Trivial-session check (#44)** — Was this session trivial (a single bug fix, a single-file edit, or Q&A with no durable artifact)? If so, recommend skipping session-close: *"이 세션은 trivial해 보입니다 — session-close를 건너뛸까요?"* and proceed only if the user wants a close. A trivial skip is a recommendation, **not** a bypass: it must not mark the session closed, must not run `--mark-session-closed`, and must not claim `/compact` can pass. Any real close still requires all 5 mandatory files.
|
|
23
|
+
2. **ADR-candidate check (#41)** — Did this session make an architectural or design decision (a new pattern, a tradeoff chosen, a convention established)? If yes, ask whether it warrants an ADR and, if so, capture that intent in the `sessionLog` entry you compose in Step 2. If nothing rose to ADR level, record `ADR 없음 — <one-line reason>` in that same `sessionLog` entry. **Never auto-write an ADR file** — recording the decision (or its absence) in the session-log payload is the only action here.
|
|
24
|
+
3. **design-history staleness check (#42)** — If `projects/<name>/design-history.md` exists and this session changed design decisions it does not yet reflect, recommend updating it (the W8 lint warning flags this mechanically; an active-project W8 can also block at PreCompact). If the file does not exist, skip silently — do **not** create it just for this check. Never auto-update it.
|
|
25
|
+
4. **Ingest check (#43)** — Did this session consume trustworthy external knowledge (a fetched URL, official docs, or code you verified directly)? If so, recommend running `/hypo:ingest` to capture it under `sources/`. Proceed only on the user's confirmation.
|
|
26
|
+
|
|
27
|
+
These are judgment calls; when uncertain, surface the question rather than skip it. None of the four blocks the close or writes on its own.
|
|
15
28
|
|
|
16
29
|
---
|
|
17
30
|
|
|
@@ -89,10 +102,14 @@ synthesis (no session-close intent) — the marker is then simply not written.
|
|
|
89
102
|
| `--apply-session-close --payload=<path> --session-id=<id>` | Same as above, **plus** writes the per-session closed marker on success (clean git required). The Stop-chain Layer 3 path. |
|
|
90
103
|
| `--apply-session-close --force` | Skips the probe early-exit. `--payload` still required for any actual apply work. |
|
|
91
104
|
|
|
92
|
-
**Two lint gates run automatically (fix #40):**
|
|
105
|
+
**Two lint gates run automatically (fix #40), scoped to the files this close writes:**
|
|
106
|
+
|
|
107
|
+
Both gates judge only the **payload files** (the 5 mandatory close files + `open-questions.md`). Lint debt in other projects or shared `pages/` this close did not author is reported as a non-blocking `notices[]` entry, never gated — so an unrelated broken page elsewhere cannot block your close.
|
|
108
|
+
|
|
109
|
+
1. **Preflight** — `lint.mjs --json` runs **before** any payload bytes are written. Errors in overwrite targets (sessionState / projectHot / rootHot / openQuestions) are filtered (about to be replaced). Errors in an **append target** (session-log / log.md) still block (appending can't repair existing corruption) → exit 1 with `stage='preflight-lint'`. Errors outside the payload files → `notices[]`, apply proceeds.
|
|
110
|
+
2. **Post-apply** — lint re-runs after the writes. Blocks only on **errors** in payload files (a payload-introduced malformed body / bad frontmatter); pre-existing errors elsewhere → `notices[]`. A lint crash (unparseable output) always blocks. Broken wikilinks are lint **warnings** (W4 — forward references to planned pages are normal) and are not gated here. Surfaces as `stage='post-apply-lint'` (or `'post-apply-verification+lint'` if freshness also fails).
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
2. **Post-apply** — lint re-runs after the writes. Catches payloads that introduce a broken wikilink or malformed body. Surfaces as `stage='post-apply-lint'` (or `'post-apply-verification+lint'` if the freshness gate also fails).
|
|
112
|
+
> **Manual close (direct Write tool calls)** clears the Stop-chain block via `--mark-session-closed --session-id=<id>`. Pass `--transcript-path=<path>` (the Stop hook surfaces it in its block message) so the marker is refused when a file **this session edited** still has lint errors — keeping the marker coherent with the PreCompact gate. Without `--transcript-path` it falls back to freshness + clean-git only (lint left to PreCompact).
|
|
96
113
|
|
|
97
114
|
---
|
|
98
115
|
|
|
@@ -102,9 +119,9 @@ The result JSON includes a `stage` field when `ok: false`. Branch on it:
|
|
|
102
119
|
|
|
103
120
|
| `stage` | What broke | How to recover |
|
|
104
121
|
|---|---|---|
|
|
105
|
-
| `preflight-lint` | A
|
|
122
|
+
| `preflight-lint` | A payload file (append target — session-log / log.md) has a pre-existing blocking lint error. | Fix the lint error in that file, then re-run. No payload bytes were written. (Debt outside the payload files is a non-blocking notice, not this stage.) |
|
|
106
123
|
| `post-apply-verification` | A mandatory file's `updated:` frontmatter is stale (≠ today) after apply. | Edit the payload's stale `content` (or supply correct `date`), then re-run. Writes are idempotent — re-applying a corrected payload is safe. |
|
|
107
|
-
| `post-apply-lint` | The payload
|
|
124
|
+
| `post-apply-lint` | The payload introduced an error-level lint blocker in a payload file (malformed body / bad frontmatter), or lint crashed. | Fix the offending content in the payload, then re-run. (Broken wikilinks are W4 warnings — not gated.) |
|
|
108
125
|
| `post-apply-verification+lint` | Both above. | Fix both; re-run. |
|
|
109
126
|
|
|
110
127
|
Once `ok: true`, report:
|
package/commands/feedback.md
CHANGED
|
@@ -68,7 +68,7 @@ node <package-root>/scripts/feedback.mjs \
|
|
|
68
68
|
|
|
69
69
|
When `--targets` includes `claude-learned`, `--global-summary` and `--promote-to-global` are required (and `--scope=global --tier=L1`).
|
|
70
70
|
|
|
71
|
-
> **`scope: project:<project-id>`
|
|
71
|
+
> **`scope: project:<project-id>` 주의.** `<project-id>`는 `feedback-sync`가 resolve한 project-id와 정확히 일치해야 한다 (default: cwd의 `/`,`.` → `-` 치환; `--project-id=<id>` 로 override). 일치하지 않으면 그 페이지는 해당 project의 MEMORY로 projection되지 **않는다** (silent skip — lint error 아님). v1.3.0부터 scope regex(`^(global|project:[A-Za-z0-9_-]+)$`)가 cwd-derived id 형식(`-Users-...`)을 그대로 허용하므로 lint 통과를 위해 `--project-id=<slug>`를 override할 필요는 없다. 단 cwd에 공백 등 `[A-Za-z0-9_-]` 밖 문자가 있으면 그 id는 여전히 거부되니 그때만 `--project-id=<id>`로 override한다.
|
|
72
72
|
|
|
73
73
|
On a real (non-dry-run) write, the script automatically runs `feedback-sync --write` to refresh MEMORY.md / CLAUDE.md. If that post-step reports drift it prints a one-line warning — the page is still saved; reconcile with `hypomnema feedback-sync --check`.
|
|
74
74
|
|
package/commands/upgrade.md
CHANGED
|
@@ -28,6 +28,8 @@ node <package-root>/scripts/upgrade.mjs [--hypo-dir="<path>"]
|
|
|
28
28
|
|
|
29
29
|
Show the output verbatim.
|
|
30
30
|
|
|
31
|
+
> **Plugin installs**: if the output begins with `ℹ Plugin install detected`, the core hooks, slash commands, and `settings.json` wiring are managed by the Claude Code plugin loader — **not** by `/hypo:upgrade`. Do **not** run `--apply` expecting it to update them (it intentionally skips those to avoid double-registering every hook). To upgrade the plugin itself: `/plugin marketplace update hypomnema` then `/reload-plugins`. `--apply` in plugin mode applies vault-side migrations (SCHEMA, `.hypoignore`), refreshes package metadata, and still syncs any vault extensions — but does **not** install the core hooks/commands/settings (the plugin provides those).
|
|
32
|
+
|
|
31
33
|
> **Note**: A major SCHEMA bump is only **detected** in this step. The informational `MIGRATION-vX.Y.md` file is written later by `--apply` (Step 4) and only on a major bump. `SCHEMA.md` is never auto-overwritten.
|
|
32
34
|
|
|
33
35
|
---
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -119,10 +119,37 @@ If you need to share new logic, prefer extending an existing helper over adding
|
|
|
119
119
|
## Testing
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
npm test
|
|
123
|
-
npm run lint
|
|
122
|
+
npm test # tests/runner.mjs — unit + smoke + contract tests
|
|
123
|
+
npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale vs session-log)
|
|
124
|
+
npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
|
|
125
|
+
# a wiki spec against `// @fix #N: <test-name>` anchors in
|
|
126
|
+
# tests/runner.mjs. Maintainer dogfood; needs a local wiki at
|
|
127
|
+
# $HYPO_DIR or ~/hypomnema. Does NOT grep ADR core decision lines.
|
|
124
128
|
```
|
|
125
129
|
|
|
130
|
+
> **`fix:verify` needs an explicit `--spec`.** The default path
|
|
131
|
+
> (`projects/hypomnema/spec-v1.2.md`) is now a `type: reference` redirect stub —
|
|
132
|
+
> the real spec moved to `archive/`. Running the bare command fails with
|
|
133
|
+
> `STUB_SPEC` by design (a stub carries zero status claims, so greening it would
|
|
134
|
+
> be vacuous). Point it at the real spec:
|
|
135
|
+
>
|
|
136
|
+
> ```bash
|
|
137
|
+
> npm run fix:verify -- --spec ~/hypomnema/projects/hypomnema/archive/spec-v1.2.md
|
|
138
|
+
> ```
|
|
139
|
+
|
|
140
|
+
### `// @fix #N:` anchor convention
|
|
141
|
+
|
|
142
|
+
When a test verifies behavior tied to a numbered fix in the wiki spec, add an anchor immediately above the `suite(...)` or `test(...)` call:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// @fix #25: replay-compact-guard-detects-slash-clear: /clear with incomplete wiki → WIKI_AUTOCLOSE
|
|
146
|
+
test('replay-compact-guard-detects-slash-clear: /clear with incomplete wiki → WIKI_AUTOCLOSE', () => { ... });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The anchor's body must be the EXACT test name (whole rest of the line; no comma splitting). Multiple anchor lines per fix # accumulate. For fixes whose verification is behavioral / prompt-driven (no automated test by design), use the sentinel `// @fix #N: NO_AUTO_TEST`.
|
|
150
|
+
|
|
151
|
+
`npm run fix:verify` reads these anchors plus the spec status claims and reports any drift (`NO_ANCHOR`, `MISSING_TEST`, `FAILING_TEST`, `ORPHAN_ANCHOR`, `STUB_SPEC`). Plain `// fix #N: …` comments without the `@` prefix are treated as prose and ignored by the verifier.
|
|
152
|
+
|
|
126
153
|
The test runner uses only Node.js built-ins. Tests create scoped temp directories and clean up after themselves; you can run the suite without any environment setup.
|
|
127
154
|
|
|
128
155
|
When adding a feature, add a corresponding test. For hook-event behavior that's hard to unit-test, document the manual verification step in the PR description (see the manual verification section below).
|
|
@@ -139,6 +166,34 @@ Some hook behavior is only observable inside a Claude Code session. Document the
|
|
|
139
166
|
|
|
140
167
|
---
|
|
141
168
|
|
|
169
|
+
## Pre-commit auto-format hook
|
|
170
|
+
|
|
171
|
+
`npm install` in this checkout installs a git `pre-commit` hook that runs `prettier --write` on staged files only. The hook is **non-blocking**: formatter failures print a notice but the commit still proceeds. The only block is when `git add` itself fails during restage (true index corruption).
|
|
172
|
+
|
|
173
|
+
**Requirements**: Git ≥ 2.13 (uses `--absolute-git-dir`; `--git-common-dir` is 2.5+).
|
|
174
|
+
|
|
175
|
+
**Path-locked to your checkout.** The shim embeds the absolute paths of your `HYPOMNEMA_ROOT` and `.git/` directory at install time. If you `mv` the checkout, re-run `npm install` to regenerate the shim — until then it safely no-ops.
|
|
176
|
+
|
|
177
|
+
**Main worktree only.** `git worktree add` checkouts silently skip — the shared `.git/hooks/pre-commit` can only point at one embedded root at a time. Commit from the main worktree to get auto-format, or accept the no-op in linked worktrees.
|
|
178
|
+
|
|
179
|
+
**CI is skipped.** `npm ci` runs `prepare`, but the installer detects `CI=true` and exits 0 without touching `.git/hooks/`. CI runs never mutate hooks.
|
|
180
|
+
|
|
181
|
+
**Symlink-safe.** If `.git/hooks/` is a symlink, or an existing `pre-commit` is a symlink, the installer refuses to write through it.
|
|
182
|
+
|
|
183
|
+
**Shared `core.hooksPath` safe.** The shim verifies both `--show-toplevel` and `--absolute-git-dir` against the embedded values before executing. Foreign repos that share your global `core.hooksPath` will silently no-op.
|
|
184
|
+
|
|
185
|
+
**Env-override defense.** The Node side strips every `GIT_*` env from `git rev-parse --local-env-vars` (plus `GIT_NAMESPACE`, `GIT_CEILING_DIRECTORIES`, `GIT_CONFIG_*`) before its own git spawns. Inherited `GIT_INDEX_FILE` is preserved **only** when invoked from the installed shell shim (signalled via a sentinel env var). Direct `node scripts/pre-commit-format.mjs` invocation drops `GIT_INDEX_FILE` and falls back to the default `.git/index`, closing the class of attacks that try to drive the formatter against a crafted alternate index.
|
|
186
|
+
|
|
187
|
+
**Existing non-marker pre-commit?** If you have your own `pre-commit` hook (no `# hypomnema-pre-commit-marker v1` on line 2), the installer logs a notice and never overwrites it.
|
|
188
|
+
|
|
189
|
+
**Skip a single commit**: `git commit --no-verify`. AI agents must not use this without explicit user authorization.
|
|
190
|
+
|
|
191
|
+
**Verbose install logs**: `HYPOMNEMA_HOOK_VERBOSE=1 npm install` prints skip/install reasons to stderr.
|
|
192
|
+
|
|
193
|
+
**Distinction from `hooks/hypo-pre-commit.mjs`**: that file is the git pre-commit worker template that `scripts/init.mjs` installs into `<wiki>/.git/hooks/pre-commit` when a user runs `hypomnema init` in their own wiki repo. It lives in the *user's wiki* repo, not in this package repo. The two hooks never interact.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
142
197
|
## Branch and commit conventions
|
|
143
198
|
|
|
144
199
|
- One logical change per branch.
|
|
@@ -177,27 +232,57 @@ Hypomnema uses semver. Releases are automated via `release.yml` on `v*` tag push
|
|
|
177
232
|
|
|
178
233
|
### Cutting a release
|
|
179
234
|
|
|
235
|
+
Every Hypomnema release must carry a Korean summary alongside the English body
|
|
236
|
+
in **both** the CHANGELOG section AND the git tag annotation. The release
|
|
237
|
+
workflow enforces this with `scripts/check-bilingual.mjs`; lightweight tags or
|
|
238
|
+
a missing `### 한글 요약` section will block `npm publish`.
|
|
239
|
+
|
|
180
240
|
```bash
|
|
181
|
-
# 1. Bump the version
|
|
182
|
-
|
|
241
|
+
# 1. Bump the version across package.json, plugin.json, marketplace.json,
|
|
242
|
+
# and templates/hypo-config.md. Takes a concrete semver (not patch/minor/major).
|
|
243
|
+
node scripts/bump-version.mjs <new-semver> # e.g. 1.2.2 or 1.3.0-rc.1
|
|
244
|
+
|
|
245
|
+
# 2. Edit CHANGELOG.md — the new section MUST include a "### 한글 요약"
|
|
246
|
+
# sub-section with at least 10 Hangul characters of real summary text.
|
|
247
|
+
$EDITOR CHANGELOG.md
|
|
183
248
|
|
|
184
|
-
#
|
|
185
|
-
|
|
249
|
+
# 3. Verify locally before tagging (same check that runs in CI)
|
|
250
|
+
node scripts/check-bilingual.mjs --changelog
|
|
186
251
|
|
|
187
|
-
#
|
|
252
|
+
# 4. Commit
|
|
188
253
|
git add package.json CHANGELOG.md
|
|
189
254
|
git commit -m "chore: release v<version>"
|
|
190
255
|
|
|
191
|
-
#
|
|
192
|
-
|
|
256
|
+
# 5. Tag with an ANNOTATED tag — never a lightweight tag.
|
|
257
|
+
# Annotation body shape: English summary, then "---" on its own line,
|
|
258
|
+
# then a Korean summary block.
|
|
259
|
+
git tag -a v<version> -m "$(cat <<'EOF'
|
|
260
|
+
Hypomnema v<version> — <one-line English summary>
|
|
261
|
+
|
|
262
|
+
<a few lines of English body — what shipped, links, etc.>
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
Hypomnema v<version> — <한 줄 한글 요약>
|
|
267
|
+
|
|
268
|
+
<몇 줄의 한글 요약 본문.>
|
|
269
|
+
EOF
|
|
270
|
+
)"
|
|
271
|
+
|
|
272
|
+
# 6. Verify the tag annotation locally (same check that runs in CI)
|
|
273
|
+
node scripts/check-bilingual.mjs --tag v<version>
|
|
274
|
+
|
|
275
|
+
# 7. Push
|
|
193
276
|
git push origin main --tags
|
|
194
277
|
```
|
|
195
278
|
|
|
196
279
|
The `release.yml` workflow then:
|
|
197
280
|
|
|
198
281
|
1. Verifies the tag matches `package.json` version.
|
|
199
|
-
2.
|
|
200
|
-
|
|
282
|
+
2. Validates the CHANGELOG section AND the tag annotation are bilingual
|
|
283
|
+
(`scripts/check-bilingual.mjs`).
|
|
284
|
+
3. Runs `npm test` and `npm run lint`.
|
|
285
|
+
4. Publishes to npm with `npm publish --access public --provenance`.
|
|
201
286
|
|
|
202
287
|
`NPM_TOKEN` must be set as a repository secret.
|
|
203
288
|
|
|
@@ -43,9 +43,9 @@ if (staged) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
if (hasRemote()) {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
46
|
+
// pull/push failures must not stop the session, but they can no longer be
|
|
47
|
+
// swallowed silently — record each to .cache/sync-state.json so session-start
|
|
48
|
+
// and doctor can surface them next session.
|
|
49
49
|
const pull = git('pull', '--no-rebase', '-q');
|
|
50
50
|
if (pull.status !== 0) appendSyncFailure(HYPO_DIR, 'pull', pull.stderr || pull.stdout);
|
|
51
51
|
const push = git('push');
|
|
@@ -54,12 +54,17 @@ function emitContinue() {
|
|
|
54
54
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function emitBlock(sessionId) {
|
|
57
|
+
function emitBlock(sessionId, transcriptPath) {
|
|
58
58
|
// One-line, skill-first. /hypo:crystallize is the documented session-close
|
|
59
59
|
// alias; passing --session-id there writes the per-session marker that clears
|
|
60
60
|
// this block. CLI fallback + bypass live in commands/crystallize.md, not here
|
|
61
61
|
// — keep the Stop reason terse so the actionable instruction stands out.
|
|
62
|
-
|
|
62
|
+
// Surface the transcript path so the close can pass --transcript-path=<path>,
|
|
63
|
+
// which scopes the marker's lint gate to this session's own files (Bug A
|
|
64
|
+
// coherence: a marker written without lint would only let Stop pass for
|
|
65
|
+
// /compact to immediately re-block on the same errors).
|
|
66
|
+
const transcriptHint = transcriptPath ? ` --transcript-path=${transcriptPath}` : '';
|
|
67
|
+
const reason = `[WIKI_AUTOCLOSE] session-close 미완료 — /hypo:crystallize 실행으로 마무리 (session_id=${sessionId}${transcriptHint}).`;
|
|
63
68
|
console.log(
|
|
64
69
|
JSON.stringify({
|
|
65
70
|
decision: 'block',
|
|
@@ -136,7 +141,7 @@ process.stdin.on('end', () => {
|
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
emitBlock(sessionId);
|
|
144
|
+
emitBlock(sessionId, transcriptPath);
|
|
140
145
|
} catch (err) {
|
|
141
146
|
// Fail-open on any unexpected error.
|
|
142
147
|
process.stderr.write(`[hypo-auto-minimal-crystallize] error: ${err?.message ?? String(err)}\n`);
|
|
@@ -99,11 +99,11 @@ process.stdin.on('end', () => {
|
|
|
99
99
|
if (newHit) {
|
|
100
100
|
const fromFile = readIfNotIgnored(newHit.hotPath, ignorePatterns);
|
|
101
101
|
const content = fromFile ?? '(no hot.md yet — will be created at session close)';
|
|
102
|
-
//
|
|
102
|
+
// arm the first-prompt marker so the NEXT user prompt re-triggers
|
|
103
103
|
// hypo-first-prompt, which forces a "Resuming <project>" summary line.
|
|
104
104
|
// Only arm when real hot content was actually injected — if hot.md is
|
|
105
105
|
// missing or .hypoignore'd (fromFile null), there is nothing for the LLM
|
|
106
|
-
// to summarize, so forcing "Resuming" would be empty noise
|
|
106
|
+
// to summarize, so forcing "Resuming" would be empty noise.
|
|
107
107
|
if (fromFile) {
|
|
108
108
|
try {
|
|
109
109
|
writeFileSync(
|
|
@@ -49,7 +49,7 @@ process.stdin.on('end', () => {
|
|
|
49
49
|
|
|
50
50
|
const hasSnapshot = marker.hasSnapshot ?? (marker.hotPath && existsSync(marker.hotPath));
|
|
51
51
|
const snapshotNote = hasSnapshot ? '' : ' (no snapshot yet — first session)';
|
|
52
|
-
//
|
|
52
|
+
// a cwd-change re-trigger says "Resuming"; a fresh session start
|
|
53
53
|
// (default source) says "Previously working on".
|
|
54
54
|
const verb = marker.source === 'cwd-change' ? 'Resuming' : 'Previously working on';
|
|
55
55
|
// marker.proj originates from a wiki directory name read by findProjectFiles;
|
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
isGateSkipped,
|
|
34
34
|
isClosePattern,
|
|
35
35
|
extractUserMessages,
|
|
36
|
+
extractTouchedWikiFiles,
|
|
37
|
+
closeFileTargets,
|
|
38
|
+
partitionLintScope,
|
|
36
39
|
} from './hypo-shared.mjs';
|
|
37
40
|
|
|
38
41
|
const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
|
|
@@ -92,7 +95,7 @@ process.stdin.on('end', () => {
|
|
|
92
95
|
|
|
93
96
|
const gitStatus = hypoIsClean();
|
|
94
97
|
const hotStatus = hotMdIsClean();
|
|
95
|
-
//
|
|
98
|
+
// strict session-close (steps 1~6 of the 11-step crystallize
|
|
96
99
|
// checklist). closeFiles gates the 5 mandatory files (steps 1-4 + log.md);
|
|
97
100
|
// open-questions.md (step 5) is conditional ("변경 시") and intentionally
|
|
98
101
|
// ungated — see hypo-shared.mjs sessionCloseFileStatus and spec §5.2.7.
|
|
@@ -107,6 +110,7 @@ process.stdin.on('end', () => {
|
|
|
107
110
|
const lintPath = PKG_ROOT ? join(PKG_ROOT, 'scripts', 'lint.mjs') : null;
|
|
108
111
|
let lintBlockers = [];
|
|
109
112
|
let lintW8 = [];
|
|
113
|
+
let lintNotices = []; // pre-existing debt in files this session did not touch
|
|
110
114
|
let lintSkipped = false;
|
|
111
115
|
if (!lintPath || !existsSync(lintPath)) {
|
|
112
116
|
lintSkipped = true;
|
|
@@ -118,8 +122,35 @@ process.stdin.on('end', () => {
|
|
|
118
122
|
timeout: 30000,
|
|
119
123
|
});
|
|
120
124
|
const parsed = JSON.parse(r.stdout || '{}');
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const allErrors = parsed.errors || [];
|
|
126
|
+
const allW8 = (parsed.warns || []).filter((w) => w.id === 'W8');
|
|
127
|
+
// Bug B: judge this session on the files IT touched, not the whole vault.
|
|
128
|
+
// A readable transcript lets us scope (edited files ∪ mandatory close
|
|
129
|
+
// files); a missing/unreadable transcript falls back to the conservative
|
|
130
|
+
// global gate (never weaker than before).
|
|
131
|
+
const haveTranscript = !!(transcriptPath && existsSync(transcriptPath));
|
|
132
|
+
if (haveTranscript) {
|
|
133
|
+
const scope = new Set([
|
|
134
|
+
...extractTouchedWikiFiles(transcriptPath, HYPO_DIR),
|
|
135
|
+
...closeFileTargets(HYPO_DIR),
|
|
136
|
+
]);
|
|
137
|
+
const part = partitionLintScope(allErrors, scope);
|
|
138
|
+
lintBlockers = part.blocking;
|
|
139
|
+
lintNotices = part.notice;
|
|
140
|
+
// W8 (design-history stale) is the CURRENT project's close
|
|
141
|
+
// responsibility, not cross-project debt — block on the active
|
|
142
|
+
// project's, surface others' as notices.
|
|
143
|
+
if (closeFiles.project) {
|
|
144
|
+
const mine = `projects/${closeFiles.project}/design-history.md`;
|
|
145
|
+
lintW8 = allW8.filter((w) => w.file === mine);
|
|
146
|
+
lintNotices.push(...allW8.filter((w) => w.file !== mine));
|
|
147
|
+
} else {
|
|
148
|
+
lintW8 = allW8;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
lintBlockers = allErrors;
|
|
152
|
+
lintW8 = allW8;
|
|
153
|
+
}
|
|
123
154
|
} catch (err) {
|
|
124
155
|
/* fail-open */
|
|
125
156
|
process.stderr.write(`[hypo-personal-check] error: ${err?.message ?? String(err)}\n`);
|
|
@@ -128,6 +159,16 @@ process.stdin.on('end', () => {
|
|
|
128
159
|
|
|
129
160
|
const lintOk = lintBlockers.length === 0;
|
|
130
161
|
const designHistoryOk = lintW8.length === 0;
|
|
162
|
+
// Non-blocking heads-up about pre-existing lint debt in untouched files (other
|
|
163
|
+
// projects / shared pages). Surfaced so it is visible but never blocks compact.
|
|
164
|
+
const noticeText =
|
|
165
|
+
lintNotices.length > 0
|
|
166
|
+
? `[WIKI CHECK] ${lintNotices.length} pre-existing lint issue(s) in files this session did not touch (not blocking): ${[
|
|
167
|
+
...new Set(lintNotices.map((b) => b.file)),
|
|
168
|
+
]
|
|
169
|
+
.slice(0, 5)
|
|
170
|
+
.join(', ')}${lintNotices.length > 5 ? ', …' : ''} — clean up when convenient.`
|
|
171
|
+
: '';
|
|
131
172
|
|
|
132
173
|
// ── fix #37 Phase C: feedback projection drift (ADR 0031) ──
|
|
133
174
|
// Single blocking gate invariant (spec §7.5): integrate into THIS hook, never
|
|
@@ -169,8 +210,8 @@ process.stdin.on('end', () => {
|
|
|
169
210
|
// ONLY when some target has a genuine, actionable issue (drift,
|
|
170
211
|
// conflict, over-cap, or a malformed managed region). buildError is
|
|
171
212
|
// never actionable here, so any mix that lacks a real issue fails open
|
|
172
|
-
// — including memory:clean + claude:buildError
|
|
173
|
-
// `every(buildError)` predicate wrongly blocked that case
|
|
213
|
+
// — including memory:clean + claude:buildError, where the prior
|
|
214
|
+
// `every(buildError)` predicate wrongly blocked that case. Mirrors
|
|
174
215
|
// doctor's buildError→warn (non-fatal) handling.
|
|
175
216
|
let report = null;
|
|
176
217
|
try {
|
|
@@ -210,7 +251,13 @@ process.stdin.on('end', () => {
|
|
|
210
251
|
closeFiles.ok &&
|
|
211
252
|
feedbackOk
|
|
212
253
|
) {
|
|
213
|
-
console.log(
|
|
254
|
+
console.log(
|
|
255
|
+
JSON.stringify(
|
|
256
|
+
noticeText
|
|
257
|
+
? { continue: true, systemMessage: noticeText }
|
|
258
|
+
: { continue: true, suppressOutput: true },
|
|
259
|
+
),
|
|
260
|
+
);
|
|
214
261
|
return;
|
|
215
262
|
}
|
|
216
263
|
|
|
@@ -267,7 +314,9 @@ process.stdin.on('end', () => {
|
|
|
267
314
|
` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
|
|
268
315
|
` [ ] 10. root hot.md — update ~/hypomnema/hot.md active project table`,
|
|
269
316
|
` [ ] 11. updated: field — verify today's date on all touched .md files`,
|
|
270
|
-
` [ ] 12.
|
|
317
|
+
` [ ] 12. lint — run scripts/lint.mjs; fix errors in files YOU touched`,
|
|
318
|
+
` (other projects' / shared-page debt is reported as non-blocking notice)`,
|
|
319
|
+
` [ ] 13. git commit & push`,
|
|
271
320
|
].join('\n');
|
|
272
321
|
|
|
273
322
|
const closeIntentNote = hasCloseIntent
|
|
@@ -282,6 +331,7 @@ process.stdin.on('end', () => {
|
|
|
282
331
|
`Run the checklist below in order, then retry /compact:`,
|
|
283
332
|
``,
|
|
284
333
|
checklistText,
|
|
334
|
+
...(noticeText ? ['', noticeText] : []),
|
|
285
335
|
``,
|
|
286
336
|
`Trivial session? Bypass with HYPO_SKIP_GATE=1`,
|
|
287
337
|
].join('\n'),
|