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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +74 -26
- package/README.md +57 -9
- package/commands/audit.md +2 -2
- package/commands/crystallize.md +113 -23
- package/commands/feedback.md +40 -26
- package/commands/ingest.md +31 -9
- package/commands/upgrade.md +2 -2
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONTRIBUTING.md +1 -1
- package/hooks/hooks.json +30 -1
- package/hooks/hypo-auto-commit.mjs +10 -4
- package/hooks/hypo-auto-minimal-crystallize.mjs +145 -0
- package/hooks/hypo-auto-stage.mjs +4 -3
- package/hooks/hypo-compact-guard.mjs +33 -24
- package/hooks/hypo-cwd-change.mjs +111 -24
- package/hooks/hypo-file-watch.mjs +23 -10
- package/hooks/hypo-first-prompt.mjs +69 -23
- package/hooks/hypo-hot-rebuild.mjs +22 -10
- package/hooks/hypo-lookup.mjs +171 -65
- package/hooks/hypo-personal-check.mjs +209 -112
- package/hooks/hypo-pre-commit.mjs +46 -0
- package/hooks/hypo-session-end.mjs +58 -0
- package/hooks/hypo-session-record.mjs +11 -5
- package/hooks/hypo-session-start.mjs +302 -52
- package/hooks/hypo-shared.mjs +817 -37
- package/hooks/hypo-web-fetch-ingest.mjs +121 -0
- package/hooks/version-check-fetch.mjs +74 -0
- package/hooks/version-check.mjs +184 -0
- package/package.json +17 -3
- package/scripts/crystallize.mjs +623 -18
- package/scripts/doctor.mjs +730 -47
- package/scripts/feedback-sync.mjs +974 -0
- package/scripts/feedback.mjs +253 -44
- package/scripts/graph.mjs +35 -22
- package/scripts/ingest.mjs +89 -16
- package/scripts/init.mjs +398 -113
- package/scripts/lib/design-history-stale.mjs +83 -0
- package/scripts/lib/extensions.mjs +749 -0
- package/scripts/lib/frontmatter.mjs +5 -1
- package/scripts/lib/hypo-ignore.mjs +12 -10
- package/scripts/lib/pkg-json.mjs +23 -5
- package/scripts/lib/project-create.mjs +225 -0
- package/scripts/lib/schema-vocab.mjs +96 -0
- package/scripts/lint.mjs +238 -31
- package/scripts/query.mjs +26 -10
- package/scripts/resume.mjs +16 -6
- package/scripts/session-audit.mjs +37 -27
- package/scripts/smoke-pack.mjs +224 -0
- package/scripts/stats.mjs +24 -10
- package/scripts/uninstall.mjs +363 -49
- package/scripts/upgrade.mjs +706 -202
- package/scripts/verify.mjs +24 -14
- package/scripts/weekly-report.mjs +59 -25
- package/skills/crystallize/SKILL.md +20 -7
- package/skills/ingest/SKILL.md +25 -5
- package/templates/.hypoignore +16 -2
- package/templates/Home.md +2 -0
- package/templates/SCHEMA.md +61 -6
- package/templates/extensions/agents/.gitkeep +0 -0
- package/templates/extensions/commands/.gitkeep +0 -0
- package/templates/extensions/hooks/.gitkeep +0 -0
- package/templates/extensions/skills/.gitkeep +0 -0
- package/templates/gitignore +5 -0
- package/templates/hot.md +2 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +42 -2
- package/templates/hypo-help.md +1 -1
- package/templates/pages/observability/_index.md +77 -0
- package/templates/projects/_template/index.md +2 -2
- package/templates/projects/_template/prd.md +1 -1
package/commands/feedback.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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="<
|
|
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
|
-
|
|
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
|
|
77
|
+
## Step 4 — Confirm
|
|
64
78
|
|
|
65
|
-
After writing:
|
|
66
|
-
-
|
|
67
|
-
- If
|
|
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`.
|
package/commands/ingest.md
CHANGED
|
@@ -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
|
-
|
|
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 —
|
|
28
|
+
## Step 2 — Privacy guard (`.hypoignore`)
|
|
29
29
|
|
|
30
|
-
Locate the Hypomnema package root
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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
|
|
102
|
+
## Step 7 — Report
|
|
81
103
|
|
|
82
104
|
Show:
|
|
83
105
|
- ✓ Saved source: `sources/<slug>.<ext>`
|
package/commands/upgrade.md
CHANGED
|
@@ -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**:
|
|
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**:
|
|
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
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -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))
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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 => {
|
|
26
|
+
process.stdin.on('data', (chunk) => {
|
|
27
|
+
input += chunk;
|
|
28
|
+
});
|
|
25
29
|
process.stdin.on('end', () => {
|
|
26
30
|
try {
|
|
27
|
-
const data
|
|
31
|
+
const data = JSON.parse(input);
|
|
28
32
|
const prompt = (data.prompt || '').trim();
|
|
29
33
|
|
|
30
|
-
if (!
|
|
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
|
|
37
|
-
const hotStatus
|
|
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
|
|
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
|
|
56
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
51
57
|
const checklist = readChecklist(today);
|
|
52
|
-
const body
|
|
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(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
});
|