hypomnema 1.1.0 → 1.2.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.
Files changed (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +74 -26
  4. package/README.md +57 -9
  5. package/commands/audit.md +2 -2
  6. package/commands/crystallize.md +113 -23
  7. package/commands/feedback.md +40 -26
  8. package/commands/ingest.md +31 -9
  9. package/commands/upgrade.md +2 -2
  10. package/docs/ARCHITECTURE.md +1 -1
  11. package/docs/CONTRIBUTING.md +1 -1
  12. package/hooks/hooks.json +30 -1
  13. package/hooks/hypo-auto-commit.mjs +10 -4
  14. package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
  15. package/hooks/hypo-auto-stage.mjs +4 -3
  16. package/hooks/hypo-compact-guard.mjs +33 -24
  17. package/hooks/hypo-cwd-change.mjs +111 -24
  18. package/hooks/hypo-file-watch.mjs +23 -10
  19. package/hooks/hypo-first-prompt.mjs +69 -23
  20. package/hooks/hypo-hot-rebuild.mjs +22 -10
  21. package/hooks/hypo-lookup.mjs +171 -65
  22. package/hooks/hypo-personal-check.mjs +209 -112
  23. package/hooks/hypo-pre-commit.mjs +46 -0
  24. package/hooks/hypo-session-end.mjs +58 -0
  25. package/hooks/hypo-session-record.mjs +11 -5
  26. package/hooks/hypo-session-start.mjs +302 -52
  27. package/hooks/hypo-shared.mjs +817 -37
  28. package/hooks/hypo-web-fetch-ingest.mjs +121 -0
  29. package/hooks/version-check-fetch.mjs +74 -0
  30. package/hooks/version-check.mjs +184 -0
  31. package/package.json +17 -3
  32. package/scripts/crystallize.mjs +623 -18
  33. package/scripts/doctor.mjs +730 -47
  34. package/scripts/feedback-sync.mjs +974 -0
  35. package/scripts/feedback.mjs +253 -44
  36. package/scripts/graph.mjs +35 -22
  37. package/scripts/ingest.mjs +89 -16
  38. package/scripts/init.mjs +398 -113
  39. package/scripts/lib/design-history-stale.mjs +83 -0
  40. package/scripts/lib/extensions.mjs +749 -0
  41. package/scripts/lib/frontmatter.mjs +5 -1
  42. package/scripts/lib/hypo-ignore.mjs +12 -10
  43. package/scripts/lib/pkg-json.mjs +23 -5
  44. package/scripts/lib/project-create.mjs +225 -0
  45. package/scripts/lib/schema-vocab.mjs +96 -0
  46. package/scripts/lint.mjs +238 -31
  47. package/scripts/query.mjs +26 -10
  48. package/scripts/resume.mjs +16 -6
  49. package/scripts/session-audit.mjs +37 -27
  50. package/scripts/smoke-pack.mjs +224 -0
  51. package/scripts/stats.mjs +24 -10
  52. package/scripts/uninstall.mjs +363 -49
  53. package/scripts/upgrade.mjs +706 -202
  54. package/scripts/verify.mjs +24 -14
  55. package/scripts/weekly-report.mjs +59 -25
  56. package/skills/crystallize/SKILL.md +20 -7
  57. package/skills/ingest/SKILL.md +25 -5
  58. package/templates/.hypoignore +16 -2
  59. package/templates/Home.md +2 -0
  60. package/templates/SCHEMA.md +61 -6
  61. package/templates/extensions/agents/.gitkeep +0 -0
  62. package/templates/extensions/commands/.gitkeep +0 -0
  63. package/templates/extensions/hooks/.gitkeep +0 -0
  64. package/templates/extensions/skills/.gitkeep +0 -0
  65. package/templates/gitignore +5 -0
  66. package/templates/hot.md +2 -0
  67. package/templates/hypo-config.md +1 -1
  68. package/templates/hypo-guide.md +42 -2
  69. package/templates/hypo-help.md +1 -1
  70. package/templates/pages/observability/_index.md +77 -0
  71. package/templates/projects/_template/index.md +2 -2
  72. package/templates/projects/_template/prd.md +1 -1
@@ -2,23 +2,34 @@
2
2
  description: Record an AI behavior correction or preference into the wiki
3
3
  ---
4
4
 
5
- You are running `/hypo:feedback`. Capture a behavior correction or preference into `pages/feedback/` for future sessions.
5
+ You are running `/hypo:feedback`. Capture a behavior correction or preference into `pages/feedback/` — the **single source of truth** for learned behaviors (ADR 0031 / fix #37).
6
6
 
7
7
  ## What this does
8
8
 
9
- - Creates or updates `pages/feedback/<topic>.md` with a dated entry
9
+ - Creates or updates `pages/feedback/<topic>.md` with a dated entry and full classification frontmatter
10
10
  - Appends a reference to `log.md`
11
- - Ensures the feedback is findable in future sessions
11
+ - **Automatically refreshes the projection** into `MEMORY.md` and the user's CLAUDE.md `<learned_behaviors>` via `feedback-sync --write`
12
+
13
+ > ⚠️ Do **not** hand-edit MEMORY.md or CLAUDE.md `<learned_behaviors>` for feedback. Those are one-way projections derived from the wiki page. Edit the wiki page; the projection follows.
12
14
 
13
15
  ---
14
16
 
15
17
  ## Step 1 — Gather feedback details
16
18
 
17
- If the user did not provide them, ask:
19
+ If the user did not provide them, ask. The classification fields are required so the page can project correctly:
20
+
21
+ 1. **Topic** (slug): "What topic does this feedback apply to? (e.g. `response-length`, `commit-style`)"
22
+ 2. **Rule** (entry): "State the rule or correction in one or two sentences."
23
+ 3. **Reason**: "What incident or reasoning prompted this?"
24
+ 4. **Scope**: "Does this apply globally (all projects) or to this project only?" → `global` | `project:<project-id>` (project-id must exact-match the resolved id; see Step 3 note)
25
+ 5. **Tier**: "Is this a hard rule (L1) or a softer preference (L2)?" → `L1` | `L2`
26
+ 6. **Targets**: "Where should this project?" → `project-memory` (MEMORY.md) and/or `claude-learned` (global CLAUDE.md). Default `project-memory`.
27
+ 7. **Priority** (1–5, higher sorts first; default 3).
28
+ 8. **Sensitivity**: `public` (default) or `sanitized` (redacted secrets/paths). `private` is not allowed — the wiki is git-pushed.
18
29
 
19
- 1. **Topic** (slug): "What topic does this feedback apply to? (e.g. `response-length`, `commit-style`, `code-comments`)"
20
- 2. **Rule**: "State the rule or correction in one or two sentences."
21
- 3. **Why** (optional): "What was the reason or incident that prompted this?"
30
+ If **claude-learned** is among the targets, the page must be `scope: global` + `tier: L1`, and you must also collect:
31
+ - **Global summary**: a one-line summary for the CLAUDE.md learned-behaviors entry.
32
+ - Confirm **promote to global** (the page is only projected to CLAUDE.md when promoted).
22
33
 
23
34
  ---
24
35
 
@@ -30,38 +41,41 @@ To check for an existing topic, locate the Hypomnema package root and run:
30
41
  node <package-root>/scripts/feedback.mjs --list [--hypo-dir="<path>"]
31
42
  ```
32
43
 
33
- If a matching topic exists, confirm with the user whether to append to it or create a new one.
44
+ If a matching topic exists, appending adds a dated entry and bumps `updated:` (classification frontmatter is preserved).
34
45
 
35
46
  ---
36
47
 
37
- ## Step 3 — Write the feedback entry
38
-
39
- Compose the entry text. Format:
40
-
41
- ```
42
- **Rule**: <one-line rule>
43
-
44
- **Why**: <reason or incident>
45
-
46
- **How to apply**: <when this kicks in>
47
- ```
48
+ ## Step 3 — Write the feedback page
48
49
 
49
- Then run:
50
+ Run with `--dry-run` first to preview the generated page, then without it to write. Pass every collected field:
50
51
 
51
52
  ```bash
52
53
  node <package-root>/scripts/feedback.mjs \
53
54
  --topic="<slug>" \
54
- --entry="<formatted entry text>" \
55
+ --entry="<one-line rule>" \
56
+ --scope="global|project:<project-id>" \
57
+ --tier="L1|L2" \
58
+ --targets="project-memory[,claude-learned]" \
59
+ --priority=<1-5> \
60
+ --sensitivity="public|sanitized" \
61
+ --memory-summary="<one-line MEMORY.md summary>" \
62
+ --reason="<why this rule exists>" \
63
+ [--global-summary="<one-line CLAUDE.md summary>" --promote-to-global] \
64
+ [--source="session:<date>"] \
55
65
  [--hypo-dir="<path>"] \
56
66
  [--dry-run]
57
67
  ```
58
68
 
59
- Run with `--dry-run` first to preview, then without it to write.
69
+ When `--targets` includes `claude-learned`, `--global-summary` and `--promote-to-global` are required (and `--scope=global --tier=L1`).
70
+
71
+ > **`scope: project:<project-id>` 주의 (v1.2.0).** `<project-id>`는 `feedback-sync`가 resolve한 project-id와 정확히 일치해야 한다 (default: cwd의 `/`,`.` → `-` 치환; `--project-id=<id>` 로 override). 일치하지 않으면 그 페이지는 해당 project의 MEMORY로 projection되지 **않는다** (silent skip — lint error 아님). 다만 현재 lint scope regex(`^project:[a-z0-9][a-z0-9-]*$`)는 cwd-derived id 형식(`-Users-...`)을 거부하므로, **`project:*` scope를 사용하려면 slug-safe id로 `--project-id=<slug>`를 override해서 wiki 디렉터리도 그 id에 맞추는 운영 패턴이 필요하다**. resolved-id ↔ slug 정합화는 v1.3.0 트랙에서 다룸.
72
+
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`.
60
74
 
61
75
  ---
62
76
 
63
- ## Step 4 — Confirm and cross-reference
77
+ ## Step 4 — Confirm
64
78
 
65
- After writing:
66
- - Tell the user: "Saved to `pages/feedback/<topic>.md`."
67
- - If this feedback should also update the project's `session-state.md` or the user's CLAUDE.md `<learned_behaviors>`, ask: "Should I also add this to your CLAUDE.md learned behaviors?"
79
+ After writing, tell the user:
80
+ - "Saved to `pages/feedback/<topic>.md` and refreshed the MEMORY/CLAUDE projection."
81
+ - If the projection post-step warned (over-cap, conflict, unresolved project-id), surface that and suggest `hypomnema feedback-sync --check`.
@@ -21,13 +21,33 @@ Ask the user:
21
21
  2. **Slug**: "What slug should this source have? (e.g. `openai-swarm-paper`, `team-retro-2026-04`)"
22
22
  - Default: derive from title or filename
23
23
 
24
- If a URL is provided, fetch the content. If a file path is provided, read it.
24
+ Do **not** fetch the URL or read the file yet — the privacy guard in Step 2 must run first.
25
25
 
26
26
  ---
27
27
 
28
- ## Step 2 — Check for orphaned sources
28
+ ## Step 2 — Privacy guard (`.hypoignore`)
29
29
 
30
- Locate the Hypomnema package root. Run the ingest helper to surface existing orphaned sources:
30
+ Refuse to ingest secrets (`.env`, SSH keys, credentials) before they ever reach `sources/`. Locate the Hypomnema package root and run the guard for **both** the input path and the destination path:
31
+
32
+ 1. **If the source is a file path**, check it (use an absolute path):
33
+
34
+ ```bash
35
+ node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"] --check="<absolute-input-path>"
36
+ ```
37
+
38
+ 2. **Always** check the destination `sources/<slug>.<ext>`:
39
+
40
+ ```bash
41
+ node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"] --check="sources/<slug>.<ext>"
42
+ ```
43
+
44
+ If either command exits non-zero, **stop**: surface the `Refused: ...` message to the user and do not fetch, read, or save the source. The slug check matters because a user could rename a `.env` to an innocuous slug — the destination must still be blocked.
45
+
46
+ ---
47
+
48
+ ## Step 3 — Check for orphaned sources
49
+
50
+ Run the ingest helper to surface existing orphaned sources:
31
51
 
32
52
  ```bash
33
53
  node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"]
@@ -35,9 +55,11 @@ node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"]
35
55
 
36
56
  If there are orphaned sources already in `sources/`, ask: "There are N unprocessed sources — do you want to ingest one of those instead?"
37
57
 
58
+ Once the guard has passed: if a URL is provided, fetch the content; if a file path is provided, read it.
59
+
38
60
  ---
39
61
 
40
- ## Step 3 — Save raw source
62
+ ## Step 4 — Save raw source
41
63
 
42
64
  Save the raw content to `sources/<slug>.<ext>` (use `.md` for text, `.txt` for plain text, `.pdf` or original extension for documents).
43
65
 
@@ -45,13 +67,13 @@ Do **not** modify or summarize in the sources file — save it as-is.
45
67
 
46
68
  ---
47
69
 
48
- ## Step 4 — Synthesize
70
+ ## Step 5 — Synthesize
49
71
 
50
72
  Read and synthesize the source:
51
73
 
52
74
  1. **Check index.md** — does a page on this topic already exist?
53
75
  - If yes: update the existing page (merge new information, mark `updated:` today)
54
- - If no: create a new page in `pages/` with `type: source-summary` and `source: <slug>`
76
+ - If no: create a new page in `pages/` with `type: source-summary` and `sources: [<slug>]`
55
77
 
56
78
  2. **Frontmatter** for new pages:
57
79
  ```yaml
@@ -60,7 +82,7 @@ Read and synthesize the source:
60
82
  type: source-summary
61
83
  updated: YYYY-MM-DD
62
84
  tags: [<relevant tags>]
63
- source: <slug>
85
+ sources: [<slug>]
64
86
  confidence: high | medium | low
65
87
  evidence_strength: direct
66
88
  ---
@@ -70,14 +92,14 @@ Read and synthesize the source:
70
92
 
71
93
  ---
72
94
 
73
- ## Step 5 — Update index.md and log.md
95
+ ## Step 6 — Update index.md and log.md
74
96
 
75
97
  - Append a line to `index.md`: `- [[pages/<slug>]] — <one-line description>`
76
98
  - Append to `log.md`: `- YYYY-MM-DD ingest: [[pages/<slug>]] from sources/<slug>.<ext>`
77
99
 
78
100
  ---
79
101
 
80
- ## Step 6 — Report
102
+ ## Step 7 — Report
81
103
 
82
104
  Show:
83
105
  - ✓ Saved source: `sources/<slug>.<ext>`
@@ -28,7 +28,7 @@ node <package-root>/scripts/upgrade.mjs [--hypo-dir="<path>"]
28
28
 
29
29
  Show the output verbatim.
30
30
 
31
- > **Note**: If a major SCHEMA bump is detected, this step generates a `MIGRATION-vX.Y.md` file in the Hypomnema root. This is a new informational file no existing files are overwritten.
31
+ > **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
32
 
33
33
  ---
34
34
 
@@ -38,7 +38,7 @@ Show the output verbatim.
38
38
  - `⚠` — minor update available (stale hook or missing settings entry)
39
39
  - `✗` — major version bump or missing hook files (action required)
40
40
 
41
- For a **major SCHEMA bump**: point the user to the generated `MIGRATION-vX.Y.md` file in their Hypomnema root and ask them to review it before applying.
41
+ For a **major SCHEMA bump**: warn the user that `--apply` will additionally write a `MIGRATION-vX.Y.md` informational file in their Hypomnema root and that they must manually merge the SCHEMA diff after applying.
42
42
 
43
43
  ---
44
44
 
@@ -328,7 +328,7 @@ scripts/session-audit.mjs ← per-session metrics + classificat
328
328
  scripts/weekly-report.mjs ← aggregated weekly autonomy score
329
329
 
330
330
 
331
- pages/observability/<YYYY-WW>.md ← committed report (heuristic v0)
331
+ journal/weekly/<YYYY-Www>.md ← committed report (heuristic v0, spec §6.4 SoT)
332
332
  ```
333
333
 
334
334
  ### Transcript dual-source (ADR 0019)
@@ -120,7 +120,7 @@ If you need to share new logic, prefer extending an existing helper over adding
120
120
 
121
121
  ```bash
122
122
  npm test # tests/runner.mjs — unit + smoke + contract tests
123
- npm run lint # scripts/lint.mjs — frontmatter + wikilink validation
123
+ npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale vs session-log)
124
124
  ```
125
125
 
126
126
  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.
package/hooks/hooks.json CHANGED
@@ -11,6 +11,17 @@
11
11
  ]
12
12
  }
13
13
  ],
14
+ "SessionEnd": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hypo-session-end.mjs",
20
+ "timeout": 10
21
+ }
22
+ ]
23
+ }
24
+ ],
14
25
  "UserPromptSubmit": [
15
26
  {
16
27
  "hooks": [
@@ -60,6 +71,15 @@
60
71
  "timeout": 10
61
72
  }
62
73
  ]
74
+ },
75
+ {
76
+ "hooks": [
77
+ {
78
+ "type": "command",
79
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hypo-web-fetch-ingest.mjs",
80
+ "timeout": 10
81
+ }
82
+ ]
63
83
  }
64
84
  ],
65
85
  "Stop": [
@@ -89,6 +109,15 @@
89
109
  "timeout": 60
90
110
  }
91
111
  ]
112
+ },
113
+ {
114
+ "hooks": [
115
+ {
116
+ "type": "command",
117
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hypo-auto-minimal-crystallize.mjs",
118
+ "timeout": 10
119
+ }
120
+ ]
92
121
  }
93
122
  ],
94
123
  "CwdChanged": [
@@ -114,5 +143,5 @@
114
143
  }
115
144
  ]
116
145
  },
117
- "shared": ["hypo-shared.mjs"]
146
+ "shared": ["hypo-shared.mjs", "version-check.mjs", "version-check-fetch.mjs"]
118
147
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { spawnSync } from 'child_process';
9
- import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
9
+ import { HYPO_DIR, loadHypoIgnore, isIgnored, appendSyncFailure } from './hypo-shared.mjs';
10
10
  import { join } from 'path';
11
11
 
12
12
  function git(...args) {
@@ -27,7 +27,8 @@ for (const line of (porcelain.stdout || '').split('\n')) {
27
27
  if (!line) continue;
28
28
  const file = line.slice(3).replace(/^"|"$/g, '').split(' -> ').pop().trim();
29
29
  if (!file) continue;
30
- if (ignorePatterns.length > 0 && isIgnored(join(HYPO_DIR, file), HYPO_DIR, ignorePatterns)) continue;
30
+ if (ignorePatterns.length > 0 && isIgnored(join(HYPO_DIR, file), HYPO_DIR, ignorePatterns))
31
+ continue;
31
32
  paths.push(file);
32
33
  }
33
34
  if (paths.length > 0) git('add', '--', ...paths);
@@ -42,8 +43,13 @@ if (staged) {
42
43
  }
43
44
 
44
45
  if (hasRemote()) {
45
- git('pull', '--no-rebase', '-q');
46
- git('push');
46
+ // fix #9: pull/push failures must not stop the session, but they can no
47
+ // longer be swallowed silently — record each to .cache/sync-state.json so
48
+ // session-start (#10) and doctor (#11) can surface them next session.
49
+ const pull = git('pull', '--no-rebase', '-q');
50
+ if (pull.status !== 0) appendSyncFailure(HYPO_DIR, 'pull', pull.stderr || pull.stdout);
51
+ const push = git('push');
52
+ if (push.status !== 0) appendSyncFailure(HYPO_DIR, 'push', push.stderr || push.stdout);
47
53
  }
48
54
 
49
55
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hypo-auto-minimal-crystallize.mjs — Stop hook (fix #27 PR-C, ADR 0022 Layer 3)
4
+ *
5
+ * Last hook in the Stop chain: a final-line defense that blocks `Stop` when
6
+ * the current session performed mutation work but never produced a verified
7
+ * session-close. Forces Claude to run minimal session-close before the
8
+ * conversation context evaporates.
9
+ *
10
+ * Decision flow (see ADR 0022 amendment 2026-05-19 Q1+Q2 + 2nd amendment Q-close-gate):
11
+ *
12
+ * 1. stop_hook_active === true → continue (loop guard; PoC 2026-05-14)
13
+ * 2. wiki absent → continue (fail-open)
14
+ * 3. transcript has zero Edit/Write/MultiEdit/NotebookEdit tool_use
15
+ * → continue (substantial-session gate)
16
+ * 4. no recent user close-intent → continue (close-intent gate, see below)
17
+ * 5. readSessionClosedMarker(session_id) valid
18
+ * → continue (close already verified)
19
+ * 6. otherwise → decision:block
20
+ *
21
+ * Close-intent gate (added after PR-C dogfooding revealed every-turn block —
22
+ * codex 2-worker debate 2026-05-19, both REQUEST_CHANGES). Stop fires after
23
+ * EVERY assistant turn, not at session end; blocking on "mutation + no marker"
24
+ * alone nags the user on every turn of a long mutating session. ADR 0022's
25
+ * real intent is "block when the session is ENDING and close is incomplete".
26
+ * We approximate the end signal by reusing isClosePattern() over recent
27
+ * user-message text (the same low-false-positive signal PreCompact uses):
28
+ * only block when the user actually signalled wrap-up ("이만 마치자",
29
+ * "오늘 여기까지", "wrap up", "session close"). last_assistant_message is NOT
30
+ * used — "커밋했습니다"/"작업 완료" type phrases produce false positives.
31
+ *
32
+ * The hook NEVER writes the marker — even in the loop-guard branch. Writer
33
+ * authority lives in `scripts/crystallize.mjs` (`--apply-session-close
34
+ * --session-id=X` or standalone `--mark-session-closed --session-id=X`),
35
+ * which gates the write on sessionCloseFileStatus.ok. Doing the write here
36
+ * would let a Claude that ignored the block (did other work, hit Stop again)
37
+ * silently get a marker without performing the close — exactly the failure
38
+ * mode the per-session marker was introduced to prevent.
39
+ */
40
+
41
+ import { existsSync } from 'fs';
42
+ import { join } from 'path';
43
+ import {
44
+ HYPO_DIR,
45
+ PKG_ROOT,
46
+ hasMutatingTranscriptActivity,
47
+ readSessionClosedMarker,
48
+ extractUserMessages,
49
+ isClosePattern,
50
+ isGateSkipped,
51
+ } from './hypo-shared.mjs';
52
+
53
+ function emitContinue() {
54
+ console.log(JSON.stringify({ continue: true, suppressOutput: true }));
55
+ }
56
+
57
+ function emitBlock(sessionId) {
58
+ // One-line, skill-first. /hypo:crystallize is the documented session-close
59
+ // alias; passing --session-id there writes the per-session marker that clears
60
+ // this block. CLI fallback + bypass live in commands/crystallize.md, not here
61
+ // — keep the Stop reason terse so the actionable instruction stands out.
62
+ const reason = `[WIKI_AUTOCLOSE] session-close 미완료 — /hypo:crystallize 실행으로 마무리 (session_id=${sessionId}).`;
63
+ console.log(
64
+ JSON.stringify({
65
+ decision: 'block',
66
+ reason,
67
+ stopReason: 'session-close incomplete (fix #27 PR-C / ADR 0022 Layer 3)',
68
+ }),
69
+ );
70
+ }
71
+
72
+ let raw = '';
73
+ process.stdin.setEncoding('utf-8');
74
+ process.stdin.on('data', (chunk) => (raw += chunk));
75
+ process.stdin.on('end', () => {
76
+ try {
77
+ let payload = {};
78
+ try {
79
+ payload = JSON.parse(raw) || {};
80
+ } catch (err) {
81
+ // Any malformed payload is fail-open — we never want a parse error to
82
+ // strand Claude in a blocked Stop with no recovery context.
83
+ process.stderr.write(
84
+ `[hypo-auto-minimal-crystallize] error: ${err?.message ?? String(err)}\n`,
85
+ );
86
+ emitContinue();
87
+ return;
88
+ }
89
+
90
+ // 1. loop guard. NEVER write marker here (see file header).
91
+ if (payload.stop_hook_active === true) {
92
+ emitContinue();
93
+ return;
94
+ }
95
+
96
+ if (isGateSkipped()) {
97
+ emitContinue();
98
+ return;
99
+ }
100
+
101
+ // 2. wiki absent → can't enforce anything meaningful.
102
+ if (!existsSync(HYPO_DIR)) {
103
+ emitContinue();
104
+ return;
105
+ }
106
+
107
+ const sessionId = payload.session_id || payload.sessionId || null;
108
+ const transcriptPath = payload.transcript_path || payload.transcriptPath || null;
109
+
110
+ // 3. substantial-session gate. Read-only / Q&A sessions skip the block.
111
+ if (!hasMutatingTranscriptActivity(transcriptPath)) {
112
+ emitContinue();
113
+ return;
114
+ }
115
+
116
+ // 4. close-intent gate. Stop fires every turn; only nudge when the user
117
+ // actually signalled session wrap-up. Without this, a long mutating
118
+ // session is blocked on every turn (PR-C dogfooding regression).
119
+ const userText = transcriptPath ? extractUserMessages(transcriptPath) : '';
120
+ if (!isClosePattern(userText)) {
121
+ emitContinue();
122
+ return;
123
+ }
124
+
125
+ // 5. close already verified for this session_id.
126
+ if (sessionId && readSessionClosedMarker(HYPO_DIR, sessionId)) {
127
+ emitContinue();
128
+ return;
129
+ }
130
+
131
+ // 6. block — but only when we have a session_id to address the recovery
132
+ // instruction to. Without one, the marker contract can't be honored, so
133
+ // failing-open is safer than blocking forever.
134
+ if (!sessionId) {
135
+ emitContinue();
136
+ return;
137
+ }
138
+
139
+ emitBlock(sessionId);
140
+ } catch (err) {
141
+ // Fail-open on any unexpected error.
142
+ process.stderr.write(`[hypo-auto-minimal-crystallize] error: ${err?.message ?? String(err)}\n`);
143
+ emitContinue();
144
+ }
145
+ });
@@ -10,13 +10,14 @@ import { HYPO_DIR, loadHypoIgnore, isIgnored } from './hypo-shared.mjs';
10
10
 
11
11
  let input = {};
12
12
  try {
13
- const raw = await new Promise(r => {
13
+ const raw = await new Promise((r) => {
14
14
  let d = '';
15
- process.stdin.on('data', c => d += c);
15
+ process.stdin.on('data', (c) => (d += c));
16
16
  process.stdin.on('end', () => r(d));
17
17
  });
18
18
  input = JSON.parse(raw);
19
- } catch {
19
+ } catch (err) {
20
+ process.stderr.write(`[hypo-auto-stage] error: ${err?.message ?? String(err)}\n`);
20
21
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
21
22
  process.exit(0);
22
23
  }
@@ -2,12 +2,13 @@
2
2
  /**
3
3
  * hypo-compact-guard.mjs — UserPromptSubmit hook
4
4
  *
5
- * Scope: detects "/compact" typed in chat only.
5
+ * Scope: detects "/compact" or "/clear" typed in chat only (ADR 0022 Layer 2, fix #25).
6
6
  * The CLI built-in /compact does NOT fire UserPromptSubmit — use personal-wiki-check.mjs
7
- * (PreCompact hook) as the hard gate for that path.
7
+ * (PreCompact hook) as the hard gate for that path. /clear has no PreCompact event, so
8
+ * this hook is the only chat-side gate that can prompt session-close before context wipe.
8
9
  *
9
10
  * Behavior: if session close is incomplete → instruct Claude to run session close
10
- * immediately before /compact.
11
+ * immediately before /compact or /clear.
11
12
  */
12
13
 
13
14
  import {
@@ -15,26 +16,31 @@ import {
15
16
  hypoIsClean,
16
17
  hotMdIsClean,
17
18
  readChecklist,
18
- isCompactCommand,
19
+ isClearCommand,
20
+ isCompactOrClearCommand,
19
21
  isGateSkipped,
20
22
  } from './hypo-shared.mjs';
21
23
 
22
24
  let input = '';
23
25
  process.stdin.setEncoding('utf-8');
24
- process.stdin.on('data', chunk => { input += chunk; });
26
+ process.stdin.on('data', (chunk) => {
27
+ input += chunk;
28
+ });
25
29
  process.stdin.on('end', () => {
26
30
  try {
27
- const data = JSON.parse(input);
31
+ const data = JSON.parse(input);
28
32
  const prompt = (data.prompt || '').trim();
29
33
 
30
- if (!isCompactCommand(prompt) || isGateSkipped()) {
34
+ if (!isCompactOrClearCommand(prompt) || isGateSkipped()) {
31
35
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
32
36
  return;
33
37
  }
34
38
 
39
+ const detected = isClearCommand(prompt) ? '/clear' : '/compact';
40
+
35
41
  const hasSession = lastSubstantialOpIsSession();
36
- const gitStatus = hypoIsClean();
37
- const hotStatus = hotMdIsClean();
42
+ const gitStatus = hypoIsClean();
43
+ const hotStatus = hotMdIsClean();
38
44
 
39
45
  if (hasSession && gitStatus.clean && hotStatus.clean) {
40
46
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
@@ -42,30 +48,33 @@ process.stdin.on('end', () => {
42
48
  }
43
49
 
44
50
  const reasons = [
45
- !hasSession ? 'session log entry missing' : '',
51
+ !hasSession ? 'session log entry missing' : '',
46
52
  !gitStatus.clean ? gitStatus.reason : '',
47
53
  !hotStatus.clean ? hotStatus.reason : '',
48
54
  ].filter(Boolean);
49
55
 
50
- const today = new Date().toISOString().slice(0, 10);
56
+ const today = new Date().toISOString().slice(0, 10);
51
57
  const checklist = readChecklist(today);
52
- const body = checklist
58
+ const body = checklist
53
59
  ? `Checklist:\n${checklist}`
54
60
  : 'See hypo-guide.md for the session-close checklist.';
55
61
 
56
- console.log(JSON.stringify({
57
- continue: true,
58
- additionalContext: [
59
- `[WIKI_AUTOCLOSE] /compact detected — session close incomplete (${reasons.join(', ')}).`,
60
- `Do NOT wait for user input. Run wiki session close NOW, then retry /compact.`,
61
- ``,
62
- body,
63
- ``,
64
- `To bypass: set HYPO_SKIP_GATE=1`,
65
- ].join('\n'),
66
- }));
67
- } catch {
62
+ console.log(
63
+ JSON.stringify({
64
+ continue: true,
65
+ additionalContext: [
66
+ `[WIKI_AUTOCLOSE] ${detected} detected session close incomplete (${reasons.join(', ')}).`,
67
+ `Do NOT wait for user input. Run wiki session close NOW, then retry ${detected}.`,
68
+ ``,
69
+ body,
70
+ ``,
71
+ `To bypass: set HYPO_SKIP_GATE=1`,
72
+ ].join('\n'),
73
+ }),
74
+ );
75
+ } catch (err) {
68
76
  // Fail-open: any parse/runtime error must not block the user's prompt.
77
+ process.stderr.write(`[hypo-compact-guard] error: ${err?.message ?? String(err)}\n`);
69
78
  console.log(JSON.stringify({ continue: true, suppressOutput: true }));
70
79
  }
71
80
  });