hypomnema 1.4.2 → 1.5.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 +35 -18
- package/README.md +25 -8
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +22 -14
- package/commands/doctor.md +1 -1
- package/commands/feedback.md +2 -2
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +2 -2
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +4 -2
- package/commands/upgrade.md +1 -1
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +4 -4
- package/docs/CONTRIBUTING.md +8 -7
- package/hooks/hypo-auto-commit.mjs +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
- package/hooks/hypo-compact-guard.mjs +2 -2
- package/hooks/hypo-cwd-change.mjs +27 -37
- package/hooks/hypo-lookup.mjs +37 -2
- package/hooks/hypo-personal-check.mjs +37 -22
- package/hooks/hypo-session-end.mjs +1 -1
- package/hooks/hypo-session-record.mjs +2 -2
- package/hooks/hypo-session-start.mjs +62 -42
- package/hooks/hypo-shared.mjs +430 -85
- package/hooks/version-check.mjs +1 -1
- package/package.json +5 -1
- package/scripts/check-tracker-ids.mjs +69 -31
- package/scripts/crystallize.mjs +151 -53
- package/scripts/doctor.mjs +7 -7
- package/scripts/feedback-sync.mjs +5 -5
- package/scripts/feedback.mjs +9 -10
- package/scripts/init.mjs +7 -7
- package/scripts/lib/check-tracker-ids.mjs +90 -32
- package/scripts/lib/design-history-stale.mjs +1 -1
- package/scripts/lib/extensions.mjs +7 -7
- package/scripts/lib/failure-type.mjs +1 -1
- package/scripts/lib/feedback-scope.mjs +1 -1
- package/scripts/lib/page-usage.mjs +141 -0
- package/scripts/lib/project-create.mjs +2 -2
- package/scripts/lib/template-schema-version.mjs +1 -1
- package/scripts/lib/wd-match.mjs +181 -0
- package/scripts/lib/wikilink.mjs +1 -1
- package/scripts/lint.mjs +5 -5
- package/scripts/rename.mjs +1 -1
- package/scripts/resume.mjs +20 -22
- package/scripts/session-audit.mjs +1 -1
- package/scripts/stats.mjs +3 -3
- package/scripts/uninstall.mjs +3 -3
- package/scripts/upgrade.mjs +85 -18
- package/skills/crystallize/SKILL.md +17 -11
- package/skills/graph/SKILL.md +3 -3
- package/skills/ingest/SKILL.md +5 -5
- package/skills/lint/SKILL.md +3 -3
- package/skills/query/SKILL.md +3 -3
- package/skills/verify/SKILL.md +3 -3
- package/templates/SCHEMA.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +21 -15
- package/templates/projects/_template/hot.md +1 -1
- package/scripts/fix-status-verify.mjs +0 -256
- package/scripts/lib/adr-corpus.mjs +0 -79
- package/scripts/lib/fix-manifest.mjs +0 -109
- package/scripts/lib/fix-status-verify.mjs +0 -439
package/commands/stats.md
CHANGED
|
@@ -15,7 +15,7 @@ You are running `/hypo:stats`. Display a summary of wiki health and activity.
|
|
|
15
15
|
|
|
16
16
|
## Step 1 — Run script
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` or reinstall instead of guessing the cache layout.
|
|
19
19
|
|
|
20
20
|
If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
|
|
21
21
|
|
package/commands/uninstall.md
CHANGED
|
@@ -20,13 +20,15 @@ Say:
|
|
|
20
20
|
|
|
21
21
|
Default: yes (dry-run first)
|
|
22
22
|
|
|
23
|
+
Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` or reinstall instead of guessing the cache layout.
|
|
24
|
+
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Step 2 — Dry run
|
|
26
28
|
|
|
27
29
|
Run:
|
|
28
30
|
```
|
|
29
|
-
node scripts/uninstall.mjs
|
|
31
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/uninstall.mjs
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
Show the output to the user. Ask:
|
|
@@ -39,7 +41,7 @@ If no → abort and confirm nothing was changed.
|
|
|
39
41
|
## Step 3 — Apply (if confirmed)
|
|
40
42
|
|
|
41
43
|
```
|
|
42
|
-
node scripts/uninstall.mjs --apply
|
|
44
|
+
node ${CLAUDE_PLUGIN_ROOT}/scripts/uninstall.mjs --apply
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
If the user also wants Codex hooks removed, append `--codex`.
|
package/commands/upgrade.md
CHANGED
|
@@ -14,7 +14,7 @@ You are running `/hypo:upgrade`. Check if the installed Hypomnema wiki is out of
|
|
|
14
14
|
|
|
15
15
|
## Step 1 — Run script
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` or reinstall instead of guessing the cache layout.
|
|
18
18
|
|
|
19
19
|
If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
|
|
20
20
|
|
package/commands/verify.md
CHANGED
|
@@ -13,7 +13,7 @@ You are running `/hypo:verify`. Audit wiki pages for overdue or missing `verify_
|
|
|
13
13
|
|
|
14
14
|
## Step 1 — Run
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Bundled scripts here run via `${CLAUDE_PLUGIN_ROOT}/scripts/`. To resolve that package root: if `${CLAUDE_PLUGIN_ROOT}` is already an absolute path, use it; otherwise read `pkgRoot` from `~/.claude/hypo-pkg.json` (only when non-empty and the target script exists under it); otherwise use the `hypo@hypomnema` (or legacy `hypomnema@hypomnema`) installPath in `~/.claude/plugins/installed_plugins.json`; if none resolve, stop and tell the user to run `hypomnema upgrade --apply` or reinstall instead of guessing the cache layout.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -110,7 +110,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
110
110
|
|---|---|
|
|
111
111
|
| `hypo-session-start` | Inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md`. Run `git pull --ff-only` (silent fail on missing remote) |
|
|
112
112
|
| `hypo-first-prompt` | Marker-based one-shot `hot.md` injection on first user prompt (10-min TTL) — for sessions that bypass `SessionStart` |
|
|
113
|
-
| `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each
|
|
113
|
+
| `hypo-lookup` | BM25 search over the wiki on every prompt. **HIT** → inject top-3 page snippets (≤2000 chars each; a page whose `verify_by_date` is overdue gets a `[STALE verify_by_date=…]` marker prepended). **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
116
|
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
|
|
@@ -335,7 +335,7 @@ journal/weekly/<YYYY-Www>.md ← committed report (heuristic v0, s
|
|
|
335
335
|
|
|
336
336
|
`session-audit.mjs` reads transcripts from two locations, in priority order:
|
|
337
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, device }`. `device`
|
|
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, device }`. `device` is `os.hostname()` for multi-machine audit; the index lives under `.cache/` (gitignored in a normal vault) so it is a local-only per-session record.
|
|
339
339
|
2. **Fallback:** `~/.claude/projects/<encoded>/*.jsonl` — scanned when the index is missing or empty (legacy / freshly-installed wikis).
|
|
340
340
|
|
|
341
341
|
### Classification
|
|
@@ -366,7 +366,7 @@ The score is a **proxy, not ground truth**. The four-week baseline plan (capture
|
|
|
366
366
|
|
|
367
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
368
|
|
|
369
|
-
**
|
|
369
|
+
**Audit fields (`device`, `session_id`).** `device` (`os.hostname()`) is recorded in two places with different sync boundaries, by design. In `.cache/sessions/index.jsonl` it is per-session-accurate but **local-only** (`.cache/` is gitignored in a normal vault). In the seeded session-log daily-shard frontmatter it is an **intentional synced field** — a creator-only stamp (the session/machine that first created the day's shard), carried across machines so the wiki itself records which machine opened each day. `session_id` (the Claude session UUID) is seeded into that same synced frontmatter, but only on the Stop-chain close path that passes `--session-id`; on manual/skill closes it is omitted. This deliberately puts a hostname (and, on the automated path, the session UUID) into git-tracked, synced content; it is acceptable for a single-user private vault where multi-machine identification is the goal. Operators who do not want a hostname synced can override `os.hostname()` at the OS level or treat the field as a pseudonymous label.
|
|
370
370
|
|
|
371
371
|
### Growth metrics (Lane B)
|
|
372
372
|
|
|
@@ -379,7 +379,7 @@ If `git status` shows no `.md` changes, the diff step is skipped — Stop hook f
|
|
|
379
379
|
|
|
380
380
|
### Citation convention
|
|
381
381
|
|
|
382
|
-
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]]
|
|
382
|
+
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]]`, which keeps them connected in the graph. The observability audit does not scan for these inline citations: it scores sessions on tool and command usage recorded by `hypo-session-record` (search / ingest / feedback counts). Counting citations as a signal is a possible future iteration, not current behavior.
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -124,7 +124,8 @@ npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8
|
|
|
124
124
|
npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
|
|
125
125
|
# a wiki spec against `// @fix #N: <test-name>` anchors in
|
|
126
126
|
# tests/runner.mjs. Maintainer dogfood; needs a local wiki at
|
|
127
|
-
# $HYPO_DIR or ~/hypomnema
|
|
127
|
+
# $HYPO_DIR or ~/hypomnema (source checkout only; not shipped in
|
|
128
|
+
# the npm package). Does NOT grep ADR core decision lines.
|
|
128
129
|
```
|
|
129
130
|
|
|
130
131
|
> **`fix:verify` needs an explicit `--spec`.** The default path
|
|
@@ -242,7 +243,7 @@ After both blocks, language-neutral:
|
|
|
242
243
|
|
|
243
244
|
- **PR title**: Conventional Commits plus a scope, e.g. `feat(feedback): add failure_type enum`. The type drives the CHANGELOG section (see the classification table below).
|
|
244
245
|
- **Merge commit**: the squash-merge subject carries the PR number (`#123`). That is where `#N` comes from, not the PR title. The two conventions stay separate.
|
|
245
|
-
- Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`) may appear in your local notes and in
|
|
246
|
+
- Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`) may appear in your local notes and in `tests/` / `qa-runs/` (where they aid test-to-issue traceability and never reach an installed user), but not in shipped code or workflow comments, and never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only identifier that ships in those is the PR number `#N`. `check-tracker-ids` gates every surface (`--all`/`--staged` for files, `--commit-msg` for messages, `--tag` for the tag body); the migration keeps the CHANGELOG body clean.
|
|
246
247
|
|
|
247
248
|
### The `## Changelog` block
|
|
248
249
|
|
|
@@ -272,9 +273,9 @@ At and after the v1.2.0 cutoff, each non-empty gated section (`New Features`, `B
|
|
|
272
273
|
```
|
|
273
274
|
### New Features
|
|
274
275
|
#### English
|
|
275
|
-
- Feedback pages accept an optional `failure_type`. (#141)
|
|
276
|
+
- Feedback pages accept an optional `failure_type`. ([#141](https://github.com/sk-lim19f/Hypomnema/pull/141))
|
|
276
277
|
#### 한국어
|
|
277
|
-
- 피드백 페이지가 선택적 `failure_type`를 받습니다. (#141)
|
|
278
|
+
- 피드백 페이지가 선택적 `failure_type`를 받습니다. ([#141](https://github.com/sk-lim19f/Hypomnema/pull/141))
|
|
278
279
|
```
|
|
279
280
|
|
|
280
281
|
Versions below the v1.2.0 cutoff (1.0.0, 1.0.1, 1.1.0) are English-only: the English body sits directly under the section header with no `####` sub-blocks. Korean is not back-filled for those releases.
|
|
@@ -283,12 +284,12 @@ The `### Changelog` index is language-neutral, one merged PR per line:
|
|
|
283
284
|
|
|
284
285
|
```
|
|
285
286
|
### Changelog
|
|
286
|
-
- #141 feedback failure_type classification
|
|
287
|
-
- #140 invalid-YAML lint guard
|
|
287
|
+
- [#141](https://github.com/sk-lim19f/Hypomnema/pull/141) feedback failure_type classification
|
|
288
|
+
- [#140](https://github.com/sk-lim19f/Hypomnema/pull/140) invalid-YAML lint guard
|
|
288
289
|
Contributors: @handle
|
|
289
290
|
```
|
|
290
291
|
|
|
291
|
-
Each line is `- #N <short title>`, no em dash. The `Contributors:` line lists that version's PR authors, de-duplicated; the release collector fills the handles from the GitHub API.
|
|
292
|
+
Each line is `- [#N](<pr-url>) <short title>`, no em dash: the collector links each `#N` to its GitHub PR (a repo markdown file does not autolink a bare `#N`), deriving the URL from the `repository` field in `package.json`. The `Contributors:` line lists that version's PR authors, de-duplicated; the release collector fills the handles from the GitHub API.
|
|
292
293
|
|
|
293
294
|
A `### Known Issues` or `### Notes` block, when a version has one, is a trailing note (a caveat, not a change). It is not one of the gated sections, so it carries no `####` sub-blocks even at or after the cutoff; bilingual text there is recommended, not enforced.
|
|
294
295
|
|
|
@@ -14,7 +14,7 @@ function hasRemote() {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Stage + commit via the shared helper (same .hypoignore filter the apply path
|
|
17
|
-
// uses
|
|
17
|
+
// uses). A real commit failure short-circuits before sync, exactly as
|
|
18
18
|
// the inline logic did; "nothing to commit" is success and falls through to sync.
|
|
19
19
|
const result = commitWikiChanges(HYPO_DIR);
|
|
20
20
|
if (!result.committed) {
|
|
@@ -26,7 +26,7 @@ if (hasRemote()) {
|
|
|
26
26
|
// pull/push failures must not stop the session, but they can no longer be
|
|
27
27
|
// swallowed silently — syncRemote records each to .cache/sync-state.json and,
|
|
28
28
|
// on a merge conflict, aborts the merge so the tree is never left half-merged
|
|
29
|
-
// (
|
|
29
|
+
// (part of the v1.4 sync hardening). session-start + doctor surface the result next session.
|
|
30
30
|
syncRemote(HYPO_DIR);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-auto-minimal-crystallize.mjs — Stop hook (
|
|
3
|
+
* hypo-auto-minimal-crystallize.mjs — Stop hook (Layer 3)
|
|
4
4
|
*
|
|
5
5
|
* Last hook in the Stop chain: a final-line defense that blocks `Stop` when
|
|
6
6
|
* the current session did substantial work (mutation, or a high-volume
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* session-close. Forces Claude to run minimal session-close before the
|
|
9
9
|
* conversation context evaporates.
|
|
10
10
|
*
|
|
11
|
-
* Decision flow (see
|
|
11
|
+
* Decision flow (see amendment 2026-05-19 Q1+Q2 + 2nd amendment Q-close-gate):
|
|
12
12
|
*
|
|
13
13
|
* 1. stop_hook_active === true → continue (loop guard; PoC 2026-05-14)
|
|
14
14
|
* 2. wiki absent → continue (fail-open)
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Close-intent gate (added after PR-C dogfooding revealed every-turn block —
|
|
25
25
|
* codex 2-worker debate 2026-05-19, both REQUEST_CHANGES). Stop fires after
|
|
26
26
|
* EVERY assistant turn, not at session end; blocking on "mutation + no marker"
|
|
27
|
-
* alone nags the user on every turn of a long mutating session.
|
|
27
|
+
* alone nags the user on every turn of a long mutating session. The hook's
|
|
28
28
|
* real intent is "block when the session is ENDING and close is incomplete".
|
|
29
29
|
* We approximate the end signal by reusing isClosePattern() over recent
|
|
30
30
|
* user-message text (the same low-false-positive signal PreCompact uses):
|
|
@@ -82,11 +82,11 @@ function emitBlock(sessionId, transcriptPath, gate = null) {
|
|
|
82
82
|
// The log-only escape hatch for a non-project (wiki/tooling-only)
|
|
83
83
|
// session. Offered ONLY as an explicit alternative when a close blocker is
|
|
84
84
|
// present — never as the default recovery, so a real project session is not
|
|
85
|
-
// taught to bypass the
|
|
85
|
+
// taught to bypass the close invariant (codex design Finding 3).
|
|
86
86
|
const logOnlyCmd = cliBase
|
|
87
87
|
? `${cliBase} --mark-session-closed --log-only --session-id=${sessionId}${transcriptHint}`
|
|
88
88
|
: `/hypo:crystallize --log-only (session_id=${sessionId}${transcriptHint})`;
|
|
89
|
-
//
|
|
89
|
+
// Refine the message with the read-only /compact gate result.
|
|
90
90
|
// - gate green → the close is compact-ready and ONLY the marker is missing
|
|
91
91
|
// (the hand-edit close case: files Written + committed directly, bypassing
|
|
92
92
|
// the marker writer). Say so precisely + give the one command, instead of
|
|
@@ -112,7 +112,7 @@ function emitBlock(sessionId, transcriptPath, gate = null) {
|
|
|
112
112
|
JSON.stringify({
|
|
113
113
|
decision: 'block',
|
|
114
114
|
reason,
|
|
115
|
-
stopReason: 'session-close incomplete (
|
|
115
|
+
stopReason: 'session-close incomplete (Layer 3)',
|
|
116
116
|
}),
|
|
117
117
|
);
|
|
118
118
|
}
|
|
@@ -186,7 +186,7 @@ process.stdin.on('end', () => {
|
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// Read-only /compact gate (same precompactGateStatus the real
|
|
190
190
|
// PreCompact hook uses) sharpens the block message — distinguishes "close
|
|
191
191
|
// is compact-ready, only the marker is missing" from "there are real
|
|
192
192
|
// blockers". The hook NEVER writes the marker here (file-header invariant);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* hypo-compact-guard.mjs — UserPromptSubmit hook
|
|
4
4
|
*
|
|
5
|
-
* Scope: detects "/compact" or "/clear" typed in chat only (
|
|
5
|
+
* Scope: detects "/compact" or "/clear" typed in chat only (Layer 2).
|
|
6
6
|
* The CLI built-in /compact does NOT fire UserPromptSubmit — use personal-wiki-check.mjs
|
|
7
7
|
* (PreCompact hook) as the hard gate for that path. /clear has no PreCompact event, so
|
|
8
8
|
* this hook is the only chat-side gate that can prompt session-close before context wipe.
|
|
@@ -42,7 +42,7 @@ process.stdin.on('end', () => {
|
|
|
42
42
|
const gitStatus = hypoIsClean();
|
|
43
43
|
const hotStatus = hotMdIsClean();
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Block on uncommitted (real unsaved work); unpushed commits (ahead)
|
|
46
46
|
// are a soft, auto-synced state and must not block /compact or /clear — mirrors
|
|
47
47
|
// the precompactGateStatus demote so the chat-side gate stays consistent.
|
|
48
48
|
if (hasSession && !gitStatus.uncommitted && hotStatus.clean) {
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* project hot.md. Skips if still within the same project subtree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync, writeFileSync, existsSync,
|
|
10
|
-
import { homedir } from 'os';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'fs';
|
|
11
10
|
import { join } from 'path';
|
|
12
11
|
import {
|
|
13
12
|
HYPO_DIR,
|
|
@@ -19,6 +18,9 @@ import {
|
|
|
19
18
|
buildProjectSuggestionLine,
|
|
20
19
|
recordSuggestionCooldown,
|
|
21
20
|
sanitizeProjForPrompt,
|
|
21
|
+
pickProjectByCwd,
|
|
22
|
+
collectProjectWorkingDirs,
|
|
23
|
+
buildVaultOrientation,
|
|
22
24
|
} from './hypo-shared.mjs';
|
|
23
25
|
|
|
24
26
|
const PROJECTS_DIR = join(HYPO_DIR, 'projects');
|
|
@@ -33,42 +35,26 @@ function readIfNotIgnored(path, patterns) {
|
|
|
33
35
|
return readFileSync(path, 'utf-8').slice(0, MAX_CHARS);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
function parseFrontmatterField(content, key) {
|
|
37
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
38
|
-
if (!match) return null;
|
|
39
|
-
const line = match[1].split('\n').find((l) => l.startsWith(`${key}:`));
|
|
40
|
-
if (!line) return null;
|
|
41
|
-
return line
|
|
42
|
-
.slice(key.length + 1)
|
|
43
|
-
.trim()
|
|
44
|
-
.replace(/^['"]|['"]$/g, '');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
38
|
function findProjectHot(cwd) {
|
|
48
39
|
if (!existsSync(PROJECTS_DIR)) return null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const content = readFileSync(indexPath, 'utf-8');
|
|
55
|
-
const workingDir = parseFrontmatterField(content, 'working_dir');
|
|
56
|
-
if (!workingDir) continue;
|
|
57
|
-
const resolved = workingDir.startsWith('~/')
|
|
58
|
-
? join(homedir(), workingDir.slice(2))
|
|
59
|
-
: workingDir;
|
|
60
|
-
if (cwd === resolved || cwd.startsWith(resolved + '/')) {
|
|
61
|
-
const hotPath = join(projDir, 'hot.md');
|
|
62
|
-
const statePath = join(projDir, 'session-state.md');
|
|
63
|
-
return {
|
|
64
|
-
proj,
|
|
65
|
-
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
66
|
-
statePath: existsSync(statePath) ? statePath : null,
|
|
67
|
-
resolved,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
40
|
+
let realpathCwd = null;
|
|
41
|
+
try {
|
|
42
|
+
realpathCwd = realpathSync(cwd);
|
|
43
|
+
} catch {
|
|
44
|
+
realpathCwd = null;
|
|
70
45
|
}
|
|
71
|
-
|
|
46
|
+
// Two-tier match (absolute prefix, then cross-machine unique basename) so a
|
|
47
|
+
// vault synced from another machine still resolves the cwd to its project.
|
|
48
|
+
const proj = pickProjectByCwd(collectProjectWorkingDirs(HYPO_DIR), cwd, { realpathCwd });
|
|
49
|
+
if (!proj) return null;
|
|
50
|
+
const projDir = join(PROJECTS_DIR, proj);
|
|
51
|
+
const hotPath = join(projDir, 'hot.md');
|
|
52
|
+
const statePath = join(projDir, 'session-state.md');
|
|
53
|
+
return {
|
|
54
|
+
proj,
|
|
55
|
+
hotPath: existsSync(hotPath) ? hotPath : null,
|
|
56
|
+
statePath: existsSync(statePath) ? statePath : null,
|
|
57
|
+
};
|
|
72
58
|
}
|
|
73
59
|
|
|
74
60
|
let raw = '';
|
|
@@ -123,10 +109,14 @@ process.stdin.on('end', () => {
|
|
|
123
109
|
);
|
|
124
110
|
}
|
|
125
111
|
}
|
|
112
|
+
// Same vault orientation as session-start: when the new cwd is a project
|
|
113
|
+
// working_dir distinct from the vault, surface where wiki files live.
|
|
114
|
+
const vaultOrientation = buildVaultOrientation(newCwd);
|
|
115
|
+
const orientPrefix = vaultOrientation ? `${vaultOrientation}\n\n` : '';
|
|
126
116
|
console.log(
|
|
127
117
|
JSON.stringify(
|
|
128
118
|
buildOutput(
|
|
129
|
-
|
|
119
|
+
`${orientPrefix}[WIKI: cwd changed → project=${sanitizeProjForPrompt(newHit.proj)}]\n\n${content}`,
|
|
130
120
|
{
|
|
131
121
|
continue: true,
|
|
132
122
|
suppressOutput: true,
|
|
@@ -137,7 +127,7 @@ process.stdin.on('end', () => {
|
|
|
137
127
|
return;
|
|
138
128
|
}
|
|
139
129
|
|
|
140
|
-
// MISS: cwd matches no project.
|
|
130
|
+
// MISS: cwd matches no project. Offer to create one
|
|
141
131
|
// when the trigger conditions hold. Same nudge-only model as session-start.
|
|
142
132
|
let suggestPrefix = '';
|
|
143
133
|
if (shouldSuggestProjectCreation(newCwd, HYPO_DIR)) {
|
package/hooks/hypo-lookup.mjs
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
13
13
|
import { join, basename } from 'path';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
HYPO_DIR,
|
|
16
|
+
buildOutput,
|
|
17
|
+
loadHypoIgnore,
|
|
18
|
+
isIgnored,
|
|
19
|
+
staleMarkerFor,
|
|
20
|
+
pageUsageLoggingAllowed,
|
|
21
|
+
recordLookupUsage,
|
|
22
|
+
} from './hypo-shared.mjs';
|
|
15
23
|
|
|
16
24
|
const INDEX_PATH = join(HYPO_DIR, 'index.md');
|
|
17
25
|
const MAX_HITS = 3;
|
|
@@ -178,6 +186,8 @@ process.stdin.on('end', () => {
|
|
|
178
186
|
try {
|
|
179
187
|
const data = JSON.parse(input);
|
|
180
188
|
const prompt = (data.prompt || '').trim();
|
|
189
|
+
// Runtime sends a session UUID; older payloads may omit it (parse defensively).
|
|
190
|
+
const sessionId = data.session_id || null;
|
|
181
191
|
|
|
182
192
|
if (!prompt || !existsSync(INDEX_PATH)) {
|
|
183
193
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
@@ -235,14 +245,32 @@ process.stdin.on('end', () => {
|
|
|
235
245
|
),
|
|
236
246
|
};
|
|
237
247
|
|
|
248
|
+
// UTC to match doctor.mjs' overdue set (D1/D2). STALE is advisory, so a
|
|
249
|
+
// one-day UTC/local skew never misleads.
|
|
250
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
251
|
+
|
|
238
252
|
const injected = [];
|
|
253
|
+
const injectedSlugs = [];
|
|
239
254
|
for (const { slug } of matched.slice(0, MAX_HITS)) {
|
|
240
255
|
const path =
|
|
241
256
|
pageMap[slug] ??
|
|
242
257
|
pageMap[slug.replace(/^(pages|projects)\//, '')] ??
|
|
243
258
|
pageMap[basename(slug)];
|
|
244
259
|
if (path && existsSync(path)) {
|
|
245
|
-
|
|
260
|
+
const raw = readFileSync(path, 'utf-8');
|
|
261
|
+
// Compute the marker on raw (pre-slice) so a long body can't truncate
|
|
262
|
+
// frontmatter out of reach. fail-open: a marker failure drops the marker,
|
|
263
|
+
// never the page.
|
|
264
|
+
let marker = '';
|
|
265
|
+
try {
|
|
266
|
+
marker = staleMarkerFor(raw, TODAY);
|
|
267
|
+
} catch {
|
|
268
|
+
marker = '';
|
|
269
|
+
}
|
|
270
|
+
injected.push(
|
|
271
|
+
`=== [[${slug}]] ===\n${marker ? marker + '\n' : ''}${raw.slice(0, MAX_CHARS)}`,
|
|
272
|
+
);
|
|
273
|
+
injectedSlugs.push(slug);
|
|
246
274
|
}
|
|
247
275
|
}
|
|
248
276
|
|
|
@@ -262,6 +290,13 @@ process.stdin.on('end', () => {
|
|
|
262
290
|
return;
|
|
263
291
|
}
|
|
264
292
|
|
|
293
|
+
// Page-usage logging (B): observability only, gated fail-closed on commit
|
|
294
|
+
// coverage, and fail-open so it never disturbs the injection above. Only the
|
|
295
|
+
// real HIT branch reaches here (miss / index-hit-but-missing returned early).
|
|
296
|
+
if (pageUsageLoggingAllowed(HYPO_DIR, sessionId)) {
|
|
297
|
+
recordLookupUsage(HYPO_DIR, { sessionId, slugs: injectedSlugs });
|
|
298
|
+
}
|
|
299
|
+
|
|
265
300
|
const overflow =
|
|
266
301
|
matched.length > MAX_HITS
|
|
267
302
|
? `\n(+${matched.length - MAX_HITS} more matches — search wiki index for more)`
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
* - hot.md has forbidden structure
|
|
10
10
|
* - lint blockers exist
|
|
11
11
|
*
|
|
12
|
-
* Bypass options (checked in order, per
|
|
12
|
+
* Bypass options (checked in order, per spec §7.5):
|
|
13
13
|
* 1. HYPO_SKIP_GATE=1 env var
|
|
14
14
|
* 2. HYPO_SKIP_GATE=1 in a recent *user-role* transcript message
|
|
15
15
|
* (assistant/tool output is excluded to prevent self-triggering from block reason text)
|
|
16
16
|
*
|
|
17
17
|
* NOTE: capacity bypass (wiki-context-critical.json ≥90%) was REMOVED
|
|
18
|
-
* (
|
|
18
|
+
* (amendment 2026-05-13). Spec §7.5: even at full context, minimal
|
|
19
19
|
* session-close is mandatory — auto-bypass on capacity caused silent state loss.
|
|
20
20
|
*/
|
|
21
21
|
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
isGateSkipped,
|
|
32
32
|
isClosePattern,
|
|
33
33
|
extractUserMessages,
|
|
34
|
+
isUnderProjectDirs,
|
|
34
35
|
} from './hypo-shared.mjs';
|
|
35
36
|
|
|
36
37
|
const WARNING_FILE = join(homedir(), '.claude', 'state', 'wiki-context-warning.json');
|
|
@@ -52,7 +53,7 @@ process.stdin.on('end', () => {
|
|
|
52
53
|
/* fail-open */
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// ── Capacity bypass (≥90%) REMOVED
|
|
56
|
+
// ── Capacity bypass (≥90%) REMOVED: amendment 2026-05-13.
|
|
56
57
|
// Even at full context, minimal session-close is mandatory (spec §7.5).
|
|
57
58
|
// Bypass paths are now only: HYPO_SKIP_GATE env / HYPO_SKIP_GATE in transcript.
|
|
58
59
|
|
|
@@ -93,12 +94,12 @@ process.stdin.on('end', () => {
|
|
|
93
94
|
// ── Heavy checks ──
|
|
94
95
|
const today = new Date().toISOString().slice(0, 10);
|
|
95
96
|
|
|
96
|
-
// The full PreCompact gate decision, single-sourced
|
|
97
|
+
// The full PreCompact gate decision, single-sourced. The SAME
|
|
97
98
|
// function backs `crystallize --check-session-close`, so a green self-check
|
|
98
99
|
// there means this hook will not block. precompactGateStatus runs git-clean +
|
|
99
|
-
// hot.md structure + session-close files (
|
|
100
|
+
// hot.md structure + session-close files (global invariant) + scoped
|
|
100
101
|
// lint + W8 design-history + feedback projection. The transcript widens the
|
|
101
|
-
// lint scope to this session's edited files
|
|
102
|
+
// lint scope to this session's edited files; without one the scope
|
|
102
103
|
// is the mandatory close files. Read-only: pure feedback drift comes back as
|
|
103
104
|
// gate.driftTargets, a self-heal effect requirement we run as --write below.
|
|
104
105
|
let gate;
|
|
@@ -116,7 +117,7 @@ process.stdin.on('end', () => {
|
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
// Self-heal pure feedback projection drift
|
|
120
|
+
// Self-heal pure feedback projection drift: the one mutation the
|
|
120
121
|
// read-only gate leaves to the caller. Fails CLOSED — if the --write errors we
|
|
121
122
|
// turn the (otherwise non-blocking) drift into a blocker, since real drift is
|
|
122
123
|
// confirmed and silently passing it would defeat the gate. --write only applies
|
|
@@ -152,20 +153,34 @@ process.stdin.on('end', () => {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
// Non-blocking heads-up about pre-existing lint / out-of-scope design-history
|
|
155
|
-
// debt in untouched files
|
|
156
|
-
//
|
|
157
|
-
//
|
|
156
|
+
// debt in untouched files. Surfaced so it is visible but never blocks compact.
|
|
157
|
+
// Scoped to the close-target (today-active) projects: debt under one of their
|
|
158
|
+
// dirs stays listed by filename; debt elsewhere (other projects, shared pages,
|
|
159
|
+
// root files) folds into a count so the same untouched-file debt does not
|
|
160
|
+
// re-list its filenames on every compact. Non-file diagnostics (a fail-open
|
|
161
|
+
// "lint skipped" notice carries no path) are preserved verbatim, never folded.
|
|
158
162
|
const debtNotices = gate.notices.filter((n) => n.type === 'lint' || n.type === 'design-history');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const activeSlugs = (gate.close?.projects || []).map((p) => p.project).filter(Boolean);
|
|
164
|
+
const nonFileNotices = debtNotices.filter((n) => !n.file);
|
|
165
|
+
const fileNotices = debtNotices.filter((n) => n.file);
|
|
166
|
+
const inScopeNotices = fileNotices.filter((n) => isUnderProjectDirs(n.file, activeSlugs));
|
|
167
|
+
const otherDebtCount = fileNotices.length - inScopeNotices.length;
|
|
168
|
+
const listed = [
|
|
169
|
+
...nonFileNotices.map((n) => n.reason),
|
|
170
|
+
...new Set(inScopeNotices.map((n) => n.reason.replace(/ \([^)]*\)$/, ''))),
|
|
171
|
+
];
|
|
172
|
+
let noticeText = '';
|
|
173
|
+
if (listed.length > 0) {
|
|
174
|
+
noticeText = `[WIKI CHECK] ${listed.length} pre-existing lint issue(s) in files this session did not touch (not blocking): ${listed
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
.join(', ')}${listed.length > 5 ? ', …' : ''} — clean up when convenient.`;
|
|
177
|
+
}
|
|
178
|
+
if (otherDebtCount > 0) {
|
|
179
|
+
const fold = `+${otherDebtCount} pre-existing lint issue(s) elsewhere in the vault (other projects / shared pages, not blocking) — run \`/hypo:lint\` for the full list.`;
|
|
180
|
+
noticeText = noticeText ? `${noticeText}\n${fold}` : `[WIKI CHECK] ${fold}`;
|
|
181
|
+
}
|
|
167
182
|
// Surface the self-heal so a re-synced projection is not a silent mutation of
|
|
168
|
-
// the user's MEMORY.md / CLAUDE.md (
|
|
183
|
+
// the user's MEMORY.md / CLAUDE.md (transparency).
|
|
169
184
|
if (feedbackHealed) noticeText = noticeText ? `${noticeText}\n${feedbackHealed}` : feedbackHealed;
|
|
170
185
|
|
|
171
186
|
if (gate.ok) {
|
|
@@ -200,7 +215,7 @@ process.stdin.on('end', () => {
|
|
|
200
215
|
// ── Block ──
|
|
201
216
|
// gate.blockers already carry per-type reasons in the canonical order
|
|
202
217
|
// (git, hot, close, lint, design-history, feedback) — same strings as before
|
|
203
|
-
//
|
|
218
|
+
// Now sourced from the shared gate instead of inline checks.
|
|
204
219
|
const reasons = [
|
|
205
220
|
...gate.blockers.map((b) => b.reason),
|
|
206
221
|
gate.skipped.lint ? 'lint skipped (run `hypomnema init` to enable lint gate)' : '',
|
|
@@ -224,10 +239,10 @@ process.stdin.on('end', () => {
|
|
|
224
239
|
` [ ] 9. hot.md — update projects/<name>/hot.md (no exceptions)`,
|
|
225
240
|
` [ ] 10. root hot.md — update ~/hypomnema/hot.md active project table`,
|
|
226
241
|
` [ ] 11. updated: field — verify today's date on all touched .md files`,
|
|
227
|
-
` [ ] 12. lint — run
|
|
242
|
+
` [ ] 12. lint — run /hypo:lint; fix errors in files YOU touched`,
|
|
228
243
|
` (other projects' / shared-page debt is reported as non-blocking notice)`,
|
|
229
244
|
` [ ] 13. git commit & push`,
|
|
230
|
-
` [ ] 14. verify — run
|
|
245
|
+
` [ ] 14. verify — run /hypo:crystallize (--check-session-close mode); only declare`,
|
|
231
246
|
` the session closed once it prints "Compact-ready" (= this gate passes).`,
|
|
232
247
|
].join('\n');
|
|
233
248
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* hypo-session-end.mjs — SessionEnd hook (
|
|
3
|
+
* hypo-session-end.mjs — SessionEnd hook (Layer 2)
|
|
4
4
|
*
|
|
5
5
|
* `/clear` cannot be blocked: it never fires UserPromptSubmit (Stage 0 PoC,
|
|
6
6
|
* 2026-05-14). The only intervention point is the SessionEnd(reason='clear')
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Appends an entry to ~/hypomnema/.cache/sessions/index.jsonl for each
|
|
6
6
|
* completed session. The index.jsonl is the **primary** source for
|
|
7
7
|
* scripts/session-audit.mjs (which falls back to ~/.claude/projects/<encoded>/
|
|
8
|
-
* if the index is empty or missing
|
|
8
|
+
* if the index is empty or missing).
|
|
9
9
|
*
|
|
10
10
|
* Silent: never blocks, never emits user-visible output.
|
|
11
11
|
*/
|
|
@@ -51,7 +51,7 @@ process.stdin.on('end', () => {
|
|
|
51
51
|
transcript_path: transcriptPath,
|
|
52
52
|
recorded_at: new Date().toISOString(),
|
|
53
53
|
cwd: payload.cwd || process.cwd(),
|
|
54
|
-
//
|
|
54
|
+
// machine identity for multi-machine audit. index.jsonl lives
|
|
55
55
|
// under .cache/ (gitignored in a normal vault), so this is a LOCAL-only
|
|
56
56
|
// per-session record — accurate for every session, no sync/privacy cost.
|
|
57
57
|
device: hostname() || 'unknown',
|