hypomnema 1.10.0 → 1.10.2
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/CHANGELOG.md +31 -0
- package/README.ko.md +5 -5
- package/README.md +5 -5
- package/commands/crystallize.md +21 -15
- package/commands/upgrade.md +13 -2
- package/docs/ARCHITECTURE.md +127 -63
- package/docs/CONTRIBUTING.md +7 -11
- package/hooks/base-store.mjs +31 -17
- package/hooks/hypo-auto-minimal-crystallize.mjs +39 -6
- package/hooks/hypo-auto-stage.mjs +123 -63
- package/hooks/hypo-close-guard.mjs +3 -37
- package/hooks/hypo-compact-guard.mjs +9 -6
- package/hooks/hypo-file-watch.mjs +18 -10
- package/hooks/hypo-personal-check.mjs +137 -55
- package/hooks/hypo-session-start.mjs +17 -6
- package/hooks/hypo-shared.mjs +1507 -569
- package/hooks/hypo-stop.mjs +8 -0
- package/hooks/session-views.mjs +9 -3
- package/package.json +2 -1
- package/scripts/crystallize.mjs +1 -1
- package/scripts/doctor.mjs +160 -9
- package/scripts/init.mjs +60 -22
- package/scripts/lib/crystallize-close-apply.mjs +156 -22
- package/scripts/lib/crystallize-close-check.mjs +1 -1
- package/scripts/lib/extensions.mjs +2 -2
- package/scripts/lib/git-hooks-dir.mjs +228 -31
- package/scripts/lib/plugin-detect.mjs +57 -4
- package/scripts/lib/settings-write.mjs +254 -0
- package/scripts/lib/wikilink.mjs +10 -1
- package/scripts/uninstall.mjs +3 -2
- package/scripts/upgrade.mjs +94 -23
- package/templates/Home.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +60 -22
- package/templates/session-state.md +0 -12
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hypomnema — Architecture
|
|
2
2
|
|
|
3
|
-
> Internal reference for
|
|
3
|
+
> Internal reference for the current release. For the user-facing pitch, see [README.md](../README.md).
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -18,10 +18,12 @@ Hypomnema is a Claude Code extension with three runtime layers stacked on a plai
|
|
|
18
18
|
│ ↓ writes / reads │
|
|
19
19
|
│ Wiki vault (markdown + git) │
|
|
20
20
|
│ ↑ reads / stages / commits │
|
|
21
|
-
│ Lifecycle hooks
|
|
21
|
+
│ Lifecycle hooks (background automation) │
|
|
22
22
|
│ ├──► SessionStart, UserPromptSubmit, PreCompact, │
|
|
23
|
-
│ │ PostToolUse, Stop,
|
|
24
|
-
│
|
|
23
|
+
│ │ PreToolUse, PostToolUse, Stop, SessionEnd, │
|
|
24
|
+
│ │ CwdChanged, FileChanged │
|
|
25
|
+
│ └──► hypo-shared.mjs + other (copied beside the hooks, │
|
|
26
|
+
│ shared modules no imports outside the dir) │
|
|
25
27
|
└──────────────────────────────────────────────────────────────┘
|
|
26
28
|
```
|
|
27
29
|
|
|
@@ -34,36 +36,38 @@ The three layers are deliberately **decoupled by file artifacts**. Slash command
|
|
|
34
36
|
```
|
|
35
37
|
hypomnema/
|
|
36
38
|
├── commands/ ← slash command definitions (.md prompts)
|
|
37
|
-
├── hooks/ ← lifecycle hooks
|
|
38
|
-
│ ├── hypo-*.mjs ←
|
|
39
|
-
│ ├── hypo-shared.mjs ← shared utilities,
|
|
40
|
-
│
|
|
39
|
+
├── hooks/ ← lifecycle hooks (copied to ~/.claude/hooks/ on the npm channel)
|
|
40
|
+
│ ├── hypo-*.mjs ← hook entry points and modules (see Hook registry below)
|
|
41
|
+
│ ├── hypo-shared.mjs ← shared utilities, copied beside the hooks
|
|
42
|
+
│ ├── hooks.json ← source of truth for the event to hook map
|
|
43
|
+
│ └── shared.json ← source of truth for the shared module list
|
|
41
44
|
├── scripts/ ← Node.js implementations
|
|
42
45
|
│ ├── init.mjs / upgrade.mjs / uninstall.mjs / doctor.mjs
|
|
43
46
|
│ ├── ingest.mjs / query.mjs / lint.mjs / verify.mjs / graph.mjs / stats.mjs
|
|
44
|
-
│ ├── crystallize.mjs / resume.mjs / feedback.mjs
|
|
45
|
-
│ ├──
|
|
46
|
-
│
|
|
47
|
-
│
|
|
48
|
-
│ ├── hypo-root.mjs
|
|
49
|
-
│ └── hypo-ignore.mjs
|
|
47
|
+
│ ├── crystallize.mjs / resume.mjs / feedback.mjs / feedback-sync.mjs
|
|
48
|
+
│ ├── capture.mjs / rename.mjs / proposal.mjs / session-entries.mjs
|
|
49
|
+
│ ├── session-audit.mjs / weekly-report.mjs
|
|
50
|
+
│ └── lib/ ← helpers that scripts import (shipped set: `files` in package.json; one hook, hypo-pre-commit.mjs, also imports from here)
|
|
50
51
|
├── skills/ ← directory-form Agent Skills (skills/<name>/SKILL.md)
|
|
51
52
|
├── templates/ ← baseline files copied on init
|
|
52
|
-
│ ├── hypo-config.md, index.md,
|
|
53
|
+
│ ├── hypo-config.md, index.md, log.md, SCHEMA.md, hypo-guide.md
|
|
53
54
|
│ ├── Home.md, Overview.md, hypo-automation.md, hypo-help.md
|
|
54
|
-
│ ├──
|
|
55
|
+
│ ├── .hypoignore, .hyposcanignore, gitignore, gitattributes
|
|
56
|
+
│ ├── pages/_index.md, pages/observability/_index.md
|
|
55
57
|
│ └── projects/_template/
|
|
56
58
|
├── tests/
|
|
57
59
|
│ ├── harness.mjs ← test()/testAsync()/suite(), selection, reporting
|
|
58
60
|
│ ├── helpers.mjs ← fixtures shared by more than one area file
|
|
59
61
|
│ ├── runner.mjs ← entry: imports every *.test.mjs, owns no tests
|
|
60
62
|
│ ├── parallel.mjs ← runs the entry as N processes (`npm test`)
|
|
61
|
-
│ └── <area>.test.mjs ← one file per production area
|
|
63
|
+
│ └── <area>.test.mjs ← one file per production area
|
|
62
64
|
├── docs/ ← ARCHITECTURE.md, CONTRIBUTING.md
|
|
63
65
|
├── .claude-plugin/plugin.json← plugin manifest
|
|
64
66
|
└── package.json ← npm metadata, no runtime deps
|
|
65
67
|
```
|
|
66
68
|
|
|
69
|
+
On the npm channel, which `scripts/` files ship is decided by the `files` list in `package.json`, which names each one. Anything under `scripts/` that is not on that list is maintainer tooling (release checks, the git-hook installer, and similar) and is absent from the npm package. A product script left off `files` is missing from the npm package only. A hook helper left off `hooks/shared.json` breaks in every install that copies the hooks and runs the copy (the npm channel, a checkout deployed with init or upgrade, and `--codex` copies in `~/.codex/hooks`). It still works where hooks run in place: the plugin channel and a checkout run directly. The plugin channel ships the whole tracked tree instead (see the "What ships" row under Install channels). The entry points above are the ones a user runs.
|
|
70
|
+
|
|
67
71
|
The package has **zero npm runtime dependencies**. Everything is Node.js built-ins (`fs`, `path`, `os`, `child_process`, `crypto`).
|
|
68
72
|
|
|
69
73
|
---
|
|
@@ -77,7 +81,7 @@ Each user-facing operation is a pair: an LLM-facing prompt + a Node.js script.
|
|
|
77
81
|
| `commands/<name>.md` | Prompt definition — instructions Claude follows when the command is invoked |
|
|
78
82
|
| `scripts/<name>.mjs` | Script implementation — Node.js logic the prompt may call via Bash |
|
|
79
83
|
|
|
80
|
-
**Commands
|
|
84
|
+
**Commands** (one file each under `commands/`): `init`, `doctor`, `upgrade`, `uninstall`, `ingest`, `query`, `crystallize`, `resume`, `feedback`, `verify`, `lint`, `stats`, `graph`, `audit`, `capture`, `rename`.
|
|
81
85
|
|
|
82
86
|
Every one of those files is itself an Agent Skill; see below.
|
|
83
87
|
|
|
@@ -107,18 +111,22 @@ won every one of them. `scripts/smoke-plugin.mjs` now fails the build on a colli
|
|
|
107
111
|
|
|
108
112
|
## Lifecycle hooks
|
|
109
113
|
|
|
110
|
-
Hooks run automatically at Claude Code lifecycle events.
|
|
114
|
+
Hooks run automatically at Claude Code lifecycle events. On the plugin channel the harness runs them straight from the plugin directory. On the npm channel `init` / `upgrade` copy them to `~/.claude/hooks/` and register them in `~/.claude/settings.json` (see Install channels below).
|
|
111
115
|
|
|
112
116
|
### Hook registry (`hooks/hooks.json`)
|
|
113
117
|
|
|
114
|
-
`hooks.json` is the **single source of truth** for hook ↔ event mapping. `init`, `upgrade`, and `doctor` all read from it; the previous v1.0 design that maintained three separate `HOOK_MAP` constants was deleted in v1.1. A `git post-commit` hook re-applies
|
|
118
|
+
`hooks.json` is the **single source of truth** for hook ↔ event mapping. `init`, `upgrade`, and `doctor` all read from it; the previous v1.0 design that maintained three separate `HOOK_MAP` constants was deleted in v1.1. A `git post-commit` hook in a checkout re-applies the upgrade, but only when the last commit changed something under `hooks/`.
|
|
119
|
+
|
|
120
|
+
Counts are left out on purpose, because they go stale: `hooks/hooks.json` lists the events and registered handlers, and `hooks/shared.json` lists the modules copied beside them. At the time of writing that is nine events and twelve handlers.
|
|
115
121
|
|
|
116
122
|
| Event | Hooks (in order) |
|
|
117
123
|
|---|---|
|
|
124
|
+
| `PreToolUse` | `hypo-close-guard.mjs` |
|
|
118
125
|
| `SessionStart` | `hypo-session-start.mjs` |
|
|
126
|
+
| `SessionEnd` | `hypo-session-end.mjs` |
|
|
119
127
|
| `UserPromptSubmit` | `hypo-first-prompt.mjs` → `hypo-lookup.mjs` → `hypo-compact-guard.mjs` |
|
|
120
128
|
| `PreCompact` | `hypo-personal-check.mjs` |
|
|
121
|
-
| `PostToolUse` (
|
|
129
|
+
| `PostToolUse` (no matcher in `hooks.json`; each hook filters on `tool_name` itself) | `hypo-auto-stage.mjs` (Write/Edit/MultiEdit) → `hypo-web-fetch-ingest.mjs` (WebFetch/WebSearch) |
|
|
122
130
|
| `Stop` | `hypo-stop.mjs` (runs `hypo-hot-rebuild.mjs` → `hypo-session-record.mjs` → `hypo-auto-commit.mjs` → `hypo-auto-minimal-crystallize.mjs` as ordered stages, not separate registrations) |
|
|
123
131
|
| `CwdChanged` | `hypo-cwd-change.mjs` |
|
|
124
132
|
| `FileChanged` | `hypo-file-watch.mjs` |
|
|
@@ -127,12 +135,15 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
127
135
|
|
|
128
136
|
| Hook | Responsibility |
|
|
129
137
|
|---|---|
|
|
130
|
-
| `hypo-session-start` | Run `git pull --ff-only` (silent fail on missing remote), rebuild the generated session views from `projects/*/sessions/` (not on a vault with a remote that has not moved yet, which gets a notice instead), inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md` |
|
|
138
|
+
| `hypo-session-start` | Run `git pull --ff-only` (silent fail on missing remote), rebuild the generated session views from `projects/*/sessions/` (not on a vault with a remote that has not moved yet, which gets a notice instead), inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md`; leave a record stamp (`record-stamp.json`, in `.cache/sessions/<id>/`) for a session that starts on 1.10.2 or later, which is what lets the close gate trust that session's record of its writes |
|
|
139
|
+
| `hypo-close-guard` | `PreToolUse`: denies a direct Write/Edit/MultiEdit to a generated session view (root `hot.md`, a project's `hot.md` or `session-state.md`) or to a session entry under `projects/<p>/sessions/`. Writes made through Bash are out of its scope, and the regular close apply runs through Bash |
|
|
140
|
+
| `hypo-session-end` | `SessionEnd`: on `reason=clear` only, writes `.cache/clear-marker.json` so the next `SessionStart` (`source=clear`) can issue a recovery nudge. It also records the close-gate resolution in `.cache/close-gate/<session-id>.json` (the close gate is the check that a substantial session has been closed; `/clear` ends the session, so it counts as resolving the gate). That record is best effort: it is written only when the session's transcript resolves, and a failure never blocks `/clear`. Other exit reasons write nothing |
|
|
141
|
+
| `hypo-web-fetch-ingest` | `PostToolUse`: after a WebFetch or WebSearch, injects a nudge to run `/hypo:ingest` (URL reduced to origin and path). It is a signal only and never ingests anything itself |
|
|
131
142
|
| `hypo-first-prompt` | Reads the marker left by `hypo-session-start` or `hypo-cwd-change` and forces a one-line resume on the first user prompt (10-min TTL). It does not re-read or re-inject `hot.md` |
|
|
132
143
|
| `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` |
|
|
133
144
|
| `hypo-compact-guard` | Detect `/compact` or `/clear` typed in chat and, if session close is incomplete, tell Claude so. It never blocks `/compact` |
|
|
134
145
|
| `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
|
|
135
|
-
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`, and every path while a merge is open) |
|
|
146
|
+
| `hypo-auto-stage` | After Write/Edit/MultiEdit on a wiki path, run `git add` (skips paths matching `.hypoignore`, and every path while a merge is open), and record the path in the session's touched-paths (the close gate's record of what this session wrote; a record that fails to land leaves a failure flag under `touched-paths-failed.d/`) |
|
|
136
147
|
| `hypo-hot-rebuild` | At session stop, regenerate the generated session views (root `hot.md`, project `hot.md` and `session-state.md`; the same generator `hypo-session-start` also calls); emit growth metrics + cache for next SessionStart. A scan failure (not just an empty `projects/`) skips the write and leaves a health notice for the next `SessionStart` to surface |
|
|
137
148
|
| `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd, device}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
|
|
138
149
|
| `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, finish a merge Hypomnema left open, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote; the push is skipped after a pull that did not land, and when the tree tracks generated views under a migrated upstream) |
|
|
@@ -141,12 +152,12 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
141
152
|
|
|
142
153
|
### Deployment constraint
|
|
143
154
|
|
|
144
|
-
|
|
155
|
+
On the npm channel hooks run from `~/.claude/hooks/`, an isolated directory. They **cannot import from anything outside their own directory**; sibling modules in that directory are fine. Therefore:
|
|
145
156
|
|
|
146
|
-
- Shared hook logic lives in the modules `hooks/shared.json` declares, not only in `hypo-shared.mjs`. That file (a plain JSON array of `.mjs` basenames) is the source of truth for the list
|
|
147
|
-
- Hook utilities use **only Node.js built-ins**
|
|
157
|
+
- Shared hook logic lives in the modules `hooks/shared.json` declares, not only in `hypo-shared.mjs`. That file (a plain JSON array of `.mjs` basenames) is the source of truth for the list. Each listed file is copied next to the hooks at deploy time, not inlined into them. It used to be a `shared` field inside `hooks.json` itself; it moved to this sibling file because the harness started warning on that unknown top-level key once it began validating `hooks.json` against its own hook-registration schema.
|
|
158
|
+
- Hook utilities use **only Node.js built-ins** and sibling modules: no imports outside the hooks directory, no npm dependencies.
|
|
148
159
|
|
|
149
|
-
Scripts in `scripts/` are not
|
|
160
|
+
Scripts in `scripts/` are not copied anywhere. They run from the package install path, so they can import from `scripts/lib/`, and they may import from `hooks/`. The reverse is not allowed for any file that gets copied to `~/.claude/hooks` (anything in `hooks.json` or `shared.json`): a copied hook that imported from `scripts/` would resolve in a checkout and on the plugin channel, where hooks run in place, and fail with `ERR_MODULE_NOT_FOUND` wherever the hooks are copied (the npm channel, a checkout deployed with init or upgrade, and `--codex` copies in `~/.codex/hooks`). The one exception is `hooks/hypo-pre-commit.mjs`. It is a git hook worker that runs from the install root and is never copied, so it imports `../scripts/lib/hypo-ignore.mjs`.
|
|
150
161
|
|
|
151
162
|
---
|
|
152
163
|
|
|
@@ -156,26 +167,36 @@ Scripts in `scripts/` are not deployed — they run from the package install pat
|
|
|
156
167
|
|---|---|
|
|
157
168
|
| `HYPO_DIR` | Resolved wiki root path |
|
|
158
169
|
| `PKG_ROOT` | Package install path, read from `~/.claude/hypo-pkg.json` |
|
|
159
|
-
| `resolveHypoRoot()` | `HYPO_DIR` env → `hypo-config.md`
|
|
170
|
+
| `resolveHypoRoot()` | `HYPO_DIR` env → `hypo-config.md` in a fixed list of candidate directories (see Configuration resolution) → `~/hypomnema` fallback |
|
|
160
171
|
| `loadHypoIgnore()` | Parse `.hypoignore` into a pattern list |
|
|
161
172
|
| `isIgnored(path)` | Test a path against `.hypoignore` patterns |
|
|
162
173
|
| `hypoIsClean()` | Check git status + unpushed commits |
|
|
163
174
|
| `hotMdIsClean()` | Validate `hot.md` structure |
|
|
164
175
|
| `isCompactCommand(prompt)` | Detect `/compact` invocations |
|
|
165
176
|
| `buildOutput(hookEventName, context, extra)` | Build hook output that nests `additionalContext` under `hookSpecificOutput`, keeping control fields as top-level siblings. The event name is required: Claude Code drops a payload whose `hookEventName` does not match the firing event |
|
|
166
|
-
| `SESSION_STATE_NEXT_HEADINGS` | Allowed headings for "next tasks"
|
|
177
|
+
| `SESSION_STATE_NEXT_HEADINGS` | Allowed headings for "next tasks": `## 다음 이어받기`, `## 다음 작업`, `## Next Up`, `## Next`. Lint reuses this constant (DRY) |
|
|
178
|
+
|
|
179
|
+
`hypo-shared.mjs` is one of the shared modules. The full list is `hooks/shared.json`; besides the session-close stores and the update-check modules it includes `session-entries.mjs`, which holds the session entry format and the renderers, and `session-views.mjs`, which loads the entries and writes the generated views.
|
|
167
180
|
|
|
168
181
|
---
|
|
169
182
|
|
|
170
183
|
## Scripts lib (`scripts/lib/`)
|
|
171
184
|
|
|
172
|
-
Helpers used by command scripts only.
|
|
185
|
+
Helpers used by command scripts. They are not copied beside the hooks. The only hook that imports from here is `hypo-pre-commit.mjs`, a git hook worker that runs from the install root (see Deployment constraint). On the npm channel the shipped set is the `scripts/lib/` entries in `package.json`'s `files`. The main ones:
|
|
173
186
|
|
|
174
187
|
| File | Purpose |
|
|
175
188
|
|---|---|
|
|
176
189
|
| `frontmatter.mjs` | Parse and serialize YAML frontmatter |
|
|
177
190
|
| `hypo-root.mjs` | Wiki root resolution (mirrors hook logic, allows imports) |
|
|
178
191
|
| `hypo-ignore.mjs` | `.hypoignore` parsing (mirrors hook logic, allows imports) |
|
|
192
|
+
| `project-create.mjs` | Creates a project: `index.md`, `prd.md`, `decisions/`, `session-log/` |
|
|
193
|
+
| `crystallize-close-*.mjs` | The session-close check, gate and apply behind `/hypo:crystallize` |
|
|
194
|
+
| `extensions.mjs` | Syncs the vault's `extensions/` into `~/.claude` |
|
|
195
|
+
| `plugin-detect.mjs` | Detects whether the plugin is also enabled, so the npm path does not register every hook twice |
|
|
196
|
+
| `wikilink.mjs`, `slug-resolver.mjs` | Wikilink parsing and slug resolution |
|
|
197
|
+
| `rename-marker.mjs` | The crash-recovery marker `rename.mjs --apply` writes and `doctor.mjs` reads |
|
|
198
|
+
|
|
199
|
+
The session entry format and the generated views are not here. `hooks/session-entries.mjs` and `hooks/session-views.mjs` own them, because hooks need them too and scripts may import from `hooks/` but not the other way round. `scripts/session-entries.mjs` is the command-line entry for them (`rollback` and `render`; the move to entries is `/hypo:upgrade --migrate-sessions`).
|
|
179
200
|
|
|
180
201
|
---
|
|
181
202
|
|
|
@@ -190,10 +211,20 @@ Helpers used by command scripts only. Not deployed to hooks.
|
|
|
190
211
|
├── SCHEMA.md ← type system reference
|
|
191
212
|
├── hypo-guide.md ← operations guide
|
|
192
213
|
├── .hypoignore ← glob patterns the hooks must ignore
|
|
214
|
+
├── .hyposcanignore ← glob patterns excluded from catalog scans only (not a privacy boundary)
|
|
215
|
+
├── .gitignore ← written by init (generated views, .cache/)
|
|
216
|
+
├── .gitattributes ← written by init
|
|
217
|
+
├── extensions/ ← your own agents/commands/hooks/skills, mirrored to ~/.claude
|
|
218
|
+
│ └── {agents,commands,hooks,skills}/
|
|
193
219
|
├── pages/ ← permanent knowledge pages
|
|
194
|
-
│
|
|
220
|
+
│ ├── feedback/ ← AI behavior corrections
|
|
221
|
+
│ └── observability/ ← _index.md (the autonomy score definition)
|
|
195
222
|
├── projects/ ← per-project artifacts
|
|
223
|
+
│ ├── _template/ ← scaffold: index.md, prd.md, decisions/, session-log/
|
|
196
224
|
│ └── <name>/
|
|
225
|
+
│ ├── index.md ← overview (working_dir: field)
|
|
226
|
+
│ ├── prd.md ← purpose / success criteria
|
|
227
|
+
│ ├── decisions/ ← ADRs (created with the project)
|
|
197
228
|
│ ├── sessions/ ← one entry per session close (source of truth, never deleted)
|
|
198
229
|
│ ├── notes.md ← notes every session shares (hand-written, optional)
|
|
199
230
|
│ ├── hot.md ← recent session summaries (generated, not committed)
|
|
@@ -204,10 +235,11 @@ Helpers used by command scripts only. Not deployed to hooks.
|
|
|
204
235
|
│ ├── daily/
|
|
205
236
|
│ ├── weekly/
|
|
206
237
|
│ └── monthly/
|
|
207
|
-
|
|
238
|
+
├── sources/ ← raw ingested sources (append-only, never edit)
|
|
239
|
+
└── .cache/ ← runtime state (gitignored): session index, markers, backups
|
|
208
240
|
```
|
|
209
241
|
|
|
210
|
-
`hypo-config.md` presence is the wiki root marker. `resolveHypoRoot()`
|
|
242
|
+
`hypo-config.md` presence is the wiki root marker. `resolveHypoRoot()` checks a fixed list of candidate directories under `$HOME` for it (it does not walk up from the cwd).
|
|
211
243
|
|
|
212
244
|
---
|
|
213
245
|
|
|
@@ -216,36 +248,57 @@ Helpers used by command scripts only. Not deployed to hooks.
|
|
|
216
248
|
The wiki path is resolved in this order:
|
|
217
249
|
|
|
218
250
|
1. **`HYPO_DIR` environment variable** — explicit override
|
|
219
|
-
2. **`hypo-config.md` scan
|
|
220
|
-
3. **`~/hypomnema
|
|
251
|
+
2. **`hypo-config.md` scan**: the first of these directories that holds a `hypo-config.md` wins: `~/hypomnema`, `~/wiki`, `~/notes`, `~/knowledge`, `~/Documents/hypomnema`, `~/Documents/wiki`, and, in scripts only, `~/Documents/notes`
|
|
252
|
+
3. **`~/hypomnema`**: default fallback
|
|
221
253
|
|
|
222
|
-
Hooks
|
|
254
|
+
Hooks carry this logic in `hypo-shared.mjs`. Scripts use `scripts/lib/hypo-root.mjs`, which is importable but not identical: its candidate list has the extra `~/Documents/notes` entry that the hook copy lacks.
|
|
223
255
|
|
|
224
256
|
`PKG_ROOT` is stored in `~/.claude/hypo-pkg.json` by `init`. `hypo-personal-check.mjs` uses it to locate the lint script (`PKG_ROOT ?? HYPO_DIR`); `upgrade --apply` detects drift and refreshes it.
|
|
225
257
|
|
|
226
258
|
---
|
|
227
259
|
|
|
260
|
+
## Install channels
|
|
261
|
+
|
|
262
|
+
Hypomnema reaches a machine through two channels, and they deploy hooks differently.
|
|
263
|
+
|
|
264
|
+
| | Plugin channel | npm channel |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| Who runs the hooks | The harness, straight from `${CLAUDE_PLUGIN_ROOT}/hooks` as `hooks/hooks.json` says | `~/.claude/hooks/`, registered in `~/.claude/settings.json` |
|
|
267
|
+
| What ships | The whole git-tracked tree of that commit. Observed 2026-10-15 in the plugin cache (`~/.claude/plugins/cache/hypomnema/hypo/1.10.0/`): it holds `scripts/bump-version.mjs`, `scripts/lib/check-tracker-ids.mjs` and `tests/` | The `package.json` `files` allowlist, which names each shipped `scripts/` file one by one. `npm run check:pack-surface` compares the tarball against a committed snapshot |
|
|
268
|
+
| Deploy step | None for the Claude hooks. `--codex` still copies hooks to `~/.codex/hooks` | `init` and `upgrade` copy the hooks and shared modules to `~/.claude/hooks`, then merge `settings.json` |
|
|
269
|
+
| How an update arrives | The installer caches each version in a directory named after `plugin.json`'s `version` string. If the version has not changed, the directory already exists and new commits never reach the installed copy | `npm install -g hypomnema` fetches the new package, then `upgrade --apply` copies again |
|
|
270
|
+
|
|
271
|
+
The consequence for releases: on the plugin channel a fix reaches nobody until the version string moves. `init` and `upgrade` detect an enabled plugin and skip the copy and the `settings.json` merge, because doing both would fire every hook twice. In the steps below, each step is marked `[both]`, `[npm only]`, or `[plugin: skipped]`.
|
|
272
|
+
|
|
228
273
|
## Init and upgrade flow
|
|
229
274
|
|
|
230
275
|
### `/hypo:init`
|
|
231
276
|
|
|
232
|
-
1. Wizard collects: wiki path, hook scope (`~/.claude` and/or `~/.codex`), git remote URL.
|
|
233
|
-
2. Creates the vault directory structure: `pages/`, `projects/`, `sources/`, `journal/{daily,weekly,monthly}
|
|
234
|
-
3. Copies `templates/` files:
|
|
235
|
-
4.
|
|
236
|
-
5. Writes `~/.claude/hypo-pkg.json` with `pkgRoot` for upgrade tracking (skipped when the
|
|
277
|
+
1. `[both]` Wizard collects: wiki path, hook scope (`~/.claude` and/or `~/.codex`), git remote URL.
|
|
278
|
+
2. `[both]` Creates the vault directory structure: `pages/`, `pages/observability/`, `projects/`, `sources/`, `journal/{daily,weekly,monthly}/`, and `extensions/{hooks,commands,skills,agents}/` (each with a `.gitkeep`).
|
|
279
|
+
3. `[both]` Copies `templates/` files: `index.md`, a generated root `hot.md`, `log.md`, `SCHEMA.md`, `hypo-guide.md`, the helpers `Home.md`, `Overview.md`, `hypo-help.md`, `hypo-automation.md`, `pages/_index.md`, `pages/observability/_index.md`, and `projects/_template/` (`index.md`, `prd.md`, `decisions/`, `session-log/`). It then writes `hypo-config.md`, `.hypoignore`, `.hyposcanignore`, `.gitignore` and `.gitattributes`.
|
|
280
|
+
4. `[npm only]` Copies the hooks and shared modules to `~/.claude/hooks/`, installs the commands under `~/.claude/commands/hypo/`, and merges entries into `~/.claude/settings.json` (idempotent; preserves non-hypo hooks). `[plugin: skipped]` when the plugin is enabled.
|
|
281
|
+
5. `[both]` Writes `~/.claude/hypo-pkg.json` with `pkgRoot` for upgrade tracking (skipped when the
|
|
237
282
|
plugin channel judgment fails; see `init.mjs`'s `resolveDurableRoot`).
|
|
238
|
-
6. `
|
|
283
|
+
6. `[both]` Syncs the vault's `extensions/` into `~/.claude` (with hooks enabled), and into `~/.codex` for `--codex`. The codex mirror covers hooks and commands only; skills and agents are skipped.
|
|
284
|
+
7. `[both]` Installs the `claude()` shell function into the shell rc file (unless shell setup is turned off).
|
|
285
|
+
8. `[both]` With `--codex`, copies the hooks to `~/.codex/hooks` and merges `~/.codex/settings.json`. The plugin does not provide this surface.
|
|
286
|
+
9. `[both]` `git init` for the vault.
|
|
287
|
+
10. `[both]` Installs the vault's pre-commit hook, which guards `.hypoignore`. It is skipped when the channel judgment fails.
|
|
288
|
+
11. `[both]` Installs a post-commit hook in the package checkout that re-syncs the hooks after a commit there that changed files under `hooks/`. It is skipped when the package is not a git checkout, which is the case for npm and plugin installs.
|
|
289
|
+
12. `[both]` First commit `chore: init hypomnema (<date>)`. Pushes to remote when one is provided.
|
|
239
290
|
|
|
240
291
|
`--dry-run` previews; `--no-hooks` and `--no-git-init` are also supported. Re-running `init` is idempotent — existing files are skipped, never overwritten.
|
|
241
292
|
|
|
242
293
|
### `/hypo:upgrade`
|
|
243
294
|
|
|
244
|
-
1. Reads `pkgRoot` from `~/.claude/hypo-pkg.json`.
|
|
245
|
-
2. Re-deploys hooks from the current package version.
|
|
246
|
-
3. Re-merges `settings.json` entries.
|
|
247
|
-
4. Detects drift (e.g., `pkgRoot` no longer exists) and refreshes the pointer.
|
|
248
|
-
5.
|
|
295
|
+
1. `[both]` Reads `pkgRoot` from `~/.claude/hypo-pkg.json`.
|
|
296
|
+
2. `[npm only]` Re-deploys hooks from the current package version.
|
|
297
|
+
3. `[npm only]` Re-merges `settings.json` entries.
|
|
298
|
+
4. `[both]` Detects drift (e.g., `pkgRoot` no longer exists) and refreshes the pointer.
|
|
299
|
+
5. `[both]` Re-copies hooks for `--codex` and syncs vault extensions. Only the Claude core surface is skipped on the plugin channel.
|
|
300
|
+
6. `[both]` The check reports `hypo-guide.md` drift (a vault guide whose stamp is older than the package template's, or that has no stamp) as a warning and a non-zero exit, and `--apply` never overwrites the vault's guide. A stamp ahead of the package, or one that cannot be compared, is a warning without the non-zero exit. To clear drift, run the `diff` command the check prints against the package template, merge the changed parts by hand while keeping your own edits, raise `version:` in the vault guide's frontmatter to the package value, and run the check again. The stamp records that you reviewed the template. It does not prove the contents match.
|
|
301
|
+
7. `--json` output emits `{ schema, hooks, settings, applied }`. `--dry-run` previews; `--apply` commits changes.
|
|
249
302
|
|
|
250
303
|
The SessionStart hook (`hooks/hypo-session-start.mjs`) is a third writer of this file, outside init/upgrade entirely. `hooks/hypo-shared.mjs`'s `pkgRootDriftStatus` detects drift by comparing the canonicalized `pkgRoot` alone, not `pkgVersion`, so a file whose `pkgRoot` already matches self-location reads as `match` even when its `pkgVersion` is stale. Only a `pkgRoot` mismatch triggers the rewrite (temp file, then rename), and `pkgVersion` is carried along in that same write rather than checked or fixed on its own. The rewrite is guarded by a lockfile, so two sessions self-locating to different install roots at once cannot race each other's write, and it writes nothing on a downgrade (self-location resolves to an older version than the file already records), an incoming version that fails to parse as semver, a lock it could not acquire, or an opted-out session.
|
|
251
304
|
|
|
@@ -304,8 +357,12 @@ SessionStart
|
|
|
304
357
|
│ ├─► hypo-lookup.mjs (BM25 inject)
|
|
305
358
|
│ └─► hypo-compact-guard.mjs (report incomplete checklist; never blocks /compact)
|
|
306
359
|
│
|
|
307
|
-
├─►
|
|
308
|
-
│ └─► hypo-
|
|
360
|
+
├─► PreToolUse
|
|
361
|
+
│ └─► hypo-close-guard.mjs (denies a direct write to a generated view or a session entry)
|
|
362
|
+
│
|
|
363
|
+
├─► PostToolUse (no matcher; each hook filters on tool_name)
|
|
364
|
+
│ ├─► hypo-auto-stage.mjs (Write/Edit/MultiEdit: git add)
|
|
365
|
+
│ └─► hypo-web-fetch-ingest.mjs (WebFetch/WebSearch: nudge toward /hypo:ingest, never ingests)
|
|
309
366
|
│
|
|
310
367
|
├─► PreCompact
|
|
311
368
|
│ └─► hypo-personal-check.mjs (lint + session-close gate)
|
|
@@ -313,12 +370,18 @@ SessionStart
|
|
|
313
370
|
├─► CwdChanged
|
|
314
371
|
│ └─► hypo-cwd-change.mjs (builds a hot.md notice on systemMessage; the reference documents the terminal notification, tracing the installed binary as of Claude Code 2.1.276, checked 2026-09-18 and reconfirmed on 2.1.283, shows it does not reach the model, and no live-session observation exists either way; see the hook table above)
|
|
315
372
|
│
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
373
|
+
├─► FileChanged
|
|
374
|
+
│ └─► hypo-file-watch.mjs (builds a notice for a changed wiki file; no trigger on a stock install, treated as not reaching the model; see the hook table above)
|
|
375
|
+
│
|
|
376
|
+
├─► Stop
|
|
377
|
+
│ └─► hypo-stop.mjs, running four stages in order:
|
|
378
|
+
│ ├─► hypo-hot-rebuild.mjs (regenerate the session views + growth cache; SessionStart also calls this generator)
|
|
379
|
+
│ ├─► hypo-session-record.mjs (append .cache/sessions/index.jsonl)
|
|
380
|
+
│ ├─► hypo-auto-commit.mjs (.hypoignore-filtered stage + commit + pull + push)
|
|
381
|
+
│ └─► hypo-auto-minimal-crystallize.mjs (blocks Stop if session close never ran)
|
|
382
|
+
│
|
|
383
|
+
└─► SessionEnd
|
|
384
|
+
└─► hypo-session-end.mjs (on /clear only: writes .cache/clear-marker.json for the next SessionStart, and the close-gate resolution record .cache/close-gate/<session-id>.json)
|
|
322
385
|
```
|
|
323
386
|
|
|
324
387
|
Root hot.md's frontmatter `updated:` is the max of every project row date, and a row's date is the
|
|
@@ -481,7 +544,7 @@ tests/
|
|
|
481
544
|
├── helpers.mjs ← fixtures used by more than one area file
|
|
482
545
|
├── runner.mjs ← the entry: imports every *.test.mjs, owns no tests itself
|
|
483
546
|
├── parallel.mjs ← runs the entry as N concurrent processes (`npm test`)
|
|
484
|
-
└── <area>.test.mjs ← one file per production area
|
|
547
|
+
└── <area>.test.mjs ← one file per production area
|
|
485
548
|
```
|
|
486
549
|
|
|
487
550
|
Tests live next to the thing they test: `extensions.test.mjs`, `lint.test.mjs`,
|
|
@@ -493,9 +556,10 @@ Everything uses Node built-ins only. Fixtures are built in code with scoped help
|
|
|
493
556
|
|
|
494
557
|
**Selection is by suite, never by test.** `suite(label)` opens a selection unit, and
|
|
495
558
|
`--shard=i/n` keeps a whole suite in one process, in order. So tests inside one suite may build
|
|
496
|
-
on each other, and suites may not build on each other. `node tests/parallel.mjs --shards
|
|
497
|
-
runs every suite alone in a fresh process and
|
|
498
|
-
you add or split
|
|
559
|
+
on each other, and suites may not build on each other. `node tests/parallel.mjs --shards=<n>`,
|
|
560
|
+
with `n` at or above the current `suite()` count, runs every suite alone in a fresh process and
|
|
561
|
+
is the proof of that invariant. A smaller `n` stops proving it. Run it whenever you add or split
|
|
562
|
+
a suite.
|
|
499
563
|
|
|
500
564
|
| Command | What it does |
|
|
501
565
|
|---|---|
|
|
@@ -504,11 +568,11 @@ you add or split a suite.
|
|
|
504
568
|
| `node tests/runner.mjs --grep=<regex>` | just the matching tests, in seconds |
|
|
505
569
|
| `node tests/runner.mjs --file=lint` | one area file |
|
|
506
570
|
| `node tests/parallel.mjs --by-file` | one process per area file |
|
|
507
|
-
| `node tests/parallel.mjs --shards
|
|
571
|
+
| `node tests/parallel.mjs --shards=<n>` | every suite alone, with `n` at or above the `suite()` count: the order-independence proof |
|
|
508
572
|
|
|
509
573
|
Round-robin is the default rather than one-process-per-file, and the reason is measured, not
|
|
510
574
|
assumed: on a 2026 laptop the whole suite runs in about 70s round-robined and about 100s
|
|
511
|
-
`--by-file`. The areas differ by an order of magnitude in size, and `--by-file` starts
|
|
575
|
+
`--by-file`. The areas differ by an order of magnitude in size, and `--by-file` starts every area
|
|
512
576
|
at once, which oversubscribes the cores. The big files (`extensions`, `proposal-base`,
|
|
513
577
|
`close-global`) then finish last under contention while the small ones exit in seconds and
|
|
514
578
|
leave the tail of the run half idle. Reach for `--by-file` to read one area's output, not to go
|
|
@@ -572,7 +636,7 @@ Requires `NPM_TOKEN` secret.
|
|
|
572
636
|
{
|
|
573
637
|
"type": "command",
|
|
574
638
|
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/hypo-<name>.mjs",
|
|
575
|
-
"timeout": <
|
|
639
|
+
"timeout": <seconds>
|
|
576
640
|
}
|
|
577
641
|
]
|
|
578
642
|
}
|
|
@@ -587,7 +651,7 @@ The shared-file list (helpers a registered hook imports, such as `hypo-shared.mj
|
|
|
587
651
|
["hypo-shared.mjs", "version-check.mjs", "..."]
|
|
588
652
|
```
|
|
589
653
|
|
|
590
|
-
`hooks/hooks.json` is still the source of truth for the event-to-hook map; `hooks/shared.json` is the source of truth for the shared-file list. A hook registered in the former, or a helper listed in the latter, is what init/upgrade
|
|
654
|
+
`hooks/hooks.json` is still the source of truth for the event-to-hook map; `hooks/shared.json` is the source of truth for the shared-file list. A hook registered in the former, or a helper listed in the latter, is what init/upgrade copy on the npm channel. A hook left off the former never fires on the plugin channel either, because the harness reads that file. A helper left off the latter still resolves on the plugin channel, where the whole tree is present, and fails with `ERR_MODULE_NOT_FOUND` only in installs that copy the hooks (npm, a checkout deployed with init or upgrade, `--codex` copies) (see the "What ships" row under Install channels).
|
|
591
655
|
|
|
592
656
|
`claude plugin validate .` is run as part of CI's plugin-snapshot checks.
|
|
593
657
|
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -60,13 +60,9 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full breakdown. Short version:
|
|
|
60
60
|
|
|
61
61
|
## Hook deployment constraint
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Code that hooks and scripts share lives in `hooks/` and is listed in `hooks/shared.json`. Hooks do not import from `scripts/`, with one exception. The reasoning and the exception are in the [Deployment constraint](ARCHITECTURE.md#deployment-constraint) section of ARCHITECTURE.md.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
- Hook utilities may use **only** Node.js built-ins (`fs`, `path`, `os`, `child_process`, `crypto`).
|
|
67
|
-
- No relative imports, no npm dependencies.
|
|
68
|
-
|
|
69
|
-
Scripts under `scripts/` are not deployed and may freely import from `scripts/lib/`. If you find yourself wanting to share logic between scripts and hooks, the canonical pattern is: implement it inline in `hypo-shared.mjs`, then have `scripts/lib/` mirror the function (or thin-wrap it via the deployed copy).
|
|
65
|
+
Hook code may use only Node.js built-ins and sibling modules in `hooks/`, with no npm dependencies.
|
|
70
66
|
|
|
71
67
|
---
|
|
72
68
|
|
|
@@ -86,14 +82,14 @@ Scripts under `scripts/` are not deployed and may freely import from `scripts/li
|
|
|
86
82
|
2. If it's new, register it in `hooks/hooks.json` under the correct event key.
|
|
87
83
|
3. Shared utilities go in a module listed in `hooks/shared.json`, usually `hypo-shared.mjs`.
|
|
88
84
|
4. Add a contract test in the hook's area file, e.g. `tests/session-hooks.test.mjs` (input → expected `additionalContext` shape).
|
|
89
|
-
5. After your change,
|
|
85
|
+
5. After your change, load the checkout with `claude --plugin-dir <path to your checkout>`, trigger the hook's event in that session, and confirm the hook path the session runs is under the checkout. `/hypo:upgrade` is not a check for this: it uses the installed package, and in plugin mode `--apply` skips the core hook deployment. If the installed plugin is enabled too, the two copies can compete, so disable it for the test.
|
|
90
86
|
|
|
91
87
|
### Modifying `hypo-shared.mjs`
|
|
92
88
|
|
|
93
|
-
|
|
89
|
+
On the npm channel this file is copied verbatim to `~/.claude/hooks/`. On the plugin channel the harness runs it from the plugin directory. Constraints:
|
|
94
90
|
|
|
95
91
|
- Only Node.js built-ins.
|
|
96
|
-
- No
|
|
92
|
+
- No imports outside `hooks/`.
|
|
97
93
|
- No npm dependencies.
|
|
98
94
|
- Keep exports minimal — the file ships in every wiki install.
|
|
99
95
|
|
|
@@ -231,8 +227,8 @@ For non-trivial changes, request a second review before merging — a peer revie
|
|
|
231
227
|
Before opening:
|
|
232
228
|
|
|
233
229
|
1. `npm test` and `npm run lint` both pass.
|
|
234
|
-
2. If you changed a hook, you ran the manual verification step
|
|
235
|
-
3. If you changed `hypo-shared.mjs`, the
|
|
230
|
+
2. If you changed a hook, you ran the manual verification step: a session started with `claude --plugin-dir <path to your checkout>` that triggers the event and runs the hook from the checkout.
|
|
231
|
+
3. If you changed `hypo-shared.mjs`, the copied hook also works after `/hypo:upgrade` on the npm channel. On the plugin channel the installed plugin directory is what runs for users, and it picks up the change only after a release moves the version string.
|
|
236
232
|
4. If you changed a template or `init` flow, a fresh `init` produces the expected vault structure.
|
|
237
233
|
5. README / ARCHITECTURE / docs are updated to match.
|
|
238
234
|
|
package/hooks/base-store.mjs
CHANGED
|
@@ -314,28 +314,42 @@ export function advanceBase(hypoDir, sessionId, relPath, hash) {
|
|
|
314
314
|
* rule (spec line 40): only the session's OWN writes advance, so a concurrent
|
|
315
315
|
* writer's change to the same target is still observed as drift.
|
|
316
316
|
*
|
|
317
|
+
* `timeoutMs` caps the lock wait for a caller with a time budget (the auto-stage hook); a lock that
|
|
318
|
+
* is not free in time returns false like any other failure here.
|
|
319
|
+
*
|
|
317
320
|
* @returns {boolean} true when the base file was updated
|
|
318
321
|
*/
|
|
319
|
-
export function advanceBaseForWrite(
|
|
322
|
+
export function advanceBaseForWrite(
|
|
323
|
+
hypoDir,
|
|
324
|
+
sessionId,
|
|
325
|
+
relPath,
|
|
326
|
+
absPath,
|
|
327
|
+
knownHash = null,
|
|
328
|
+
{ timeoutMs } = {},
|
|
329
|
+
) {
|
|
320
330
|
if (!sessionId) return false;
|
|
321
331
|
const path = basePath(hypoDir, sessionId);
|
|
322
332
|
try {
|
|
323
|
-
return withFileLock(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
return withFileLock(
|
|
334
|
+
path,
|
|
335
|
+
() => {
|
|
336
|
+
const parsed = readBaseFile(hypoDir, sessionId);
|
|
337
|
+
if (!parsed) return false;
|
|
338
|
+
// Only a tracked target advances. hasOwnProperty, not truthiness: an
|
|
339
|
+
// observed-absent entry is `null` but still a legitimate key to advance from.
|
|
340
|
+
if (!Object.prototype.hasOwnProperty.call(parsed.targets, relPath)) return false;
|
|
341
|
+
const hash = typeof knownHash === 'string' ? knownHash : hashFile(absPath);
|
|
342
|
+
if (typeof hash !== 'string') return false; // absent/unreadable → leave base as-is
|
|
343
|
+
parsed.targets[relPath] = hash;
|
|
344
|
+
try {
|
|
345
|
+
atomicWrite(path, JSON.stringify(parsed, null, 2));
|
|
346
|
+
return true;
|
|
347
|
+
} catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
timeoutMs === undefined ? {} : { timeoutMs },
|
|
352
|
+
);
|
|
339
353
|
} catch {
|
|
340
354
|
return false;
|
|
341
355
|
}
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
isCloseReconfirmDeclined,
|
|
85
85
|
CLOSE_RECONFIRM_MARK,
|
|
86
86
|
resolveGateProjectOverride,
|
|
87
|
+
GATE_DEADLINE_MS,
|
|
87
88
|
gitDirtyFiles,
|
|
88
89
|
isForeignAppendOnlyShard,
|
|
89
90
|
isForeignUncommittedEntry,
|
|
@@ -92,6 +93,19 @@ import { closeCheckpointState, isCloseComplete, receiptPath } from './close-rece
|
|
|
92
93
|
import { closeGateStatus } from './close-gate-store.mjs';
|
|
93
94
|
import { atomicWrite } from './atomic-write.mjs';
|
|
94
95
|
|
|
96
|
+
// This stage's own share of the Stop budget: the `timeoutMs` of this file in the STAGES
|
|
97
|
+
// table of hypo-stop.mjs, which passes it down as HYPO_STAGE_TIMEOUT_MS (hypo-stop runs on
|
|
98
|
+
// import, so it cannot be imported for it). 10000 is the fallback when the variable is
|
|
99
|
+
// missing or not a positive integer, such as a run of this file on its own.
|
|
100
|
+
// The gate deadline is capped at that budget minus a reserve for the work after the gate,
|
|
101
|
+
// measured from this process's start (performance.now() counts from there), so a slow
|
|
102
|
+
// start leaves the gate less time instead of letting the stage be killed mid-gate.
|
|
103
|
+
const STAGE_BUDGET_MS = (() => {
|
|
104
|
+
const n = Number(process.env.HYPO_STAGE_TIMEOUT_MS);
|
|
105
|
+
return Number.isInteger(n) && n > 0 ? n : 10000;
|
|
106
|
+
})();
|
|
107
|
+
const STAGE_RESERVE_MS = 1000;
|
|
108
|
+
|
|
95
109
|
// Carrying a systemMessage does not turn this into a block: the session is
|
|
96
110
|
// still free to end, the message just rides along on the same continue reply
|
|
97
111
|
// (hooks/hypo-stop.mjs merges every stage's systemMessage into one).
|
|
@@ -365,6 +379,9 @@ process.stdin.on('end', () => {
|
|
|
365
379
|
? { closeMarker: isCloseComplete(checkpoint) ? checkpoint.marker : null }
|
|
366
380
|
: {}),
|
|
367
381
|
...(attributionScope ? { attributionScope } : {}),
|
|
382
|
+
deadline: {
|
|
383
|
+
end: Math.min(performance.now() + GATE_DEADLINE_MS, STAGE_BUDGET_MS - STAGE_RESERVE_MS),
|
|
384
|
+
},
|
|
368
385
|
});
|
|
369
386
|
} catch {
|
|
370
387
|
return null;
|
|
@@ -394,9 +411,17 @@ process.stdin.on('end', () => {
|
|
|
394
411
|
return;
|
|
395
412
|
}
|
|
396
413
|
gate = computeGate();
|
|
397
|
-
|
|
414
|
+
// A close-cwd blocker with `incomplete` means the gate deadline cut the proof:
|
|
415
|
+
// the close did not fail, and a transient timeout must not become a block (see
|
|
416
|
+
// hypo-stop.mjs), so it is said and checked again on the next turn.
|
|
417
|
+
const cwdBlockers = (gate?.blockers || []).filter((b) => b.type === 'close-cwd');
|
|
418
|
+
const cwdBlocked = cwdBlockers.some((b) => !b.incomplete);
|
|
398
419
|
if (!cwdBlocked) {
|
|
399
|
-
|
|
420
|
+
const cut = cwdBlockers.length
|
|
421
|
+
? 'hypo: the close of this session was recorded, but the check of the session cwd project did not finish within the time limit. It will be checked again on the next turn (마감 안에 cwd 프로젝트 close 증명을 마치지 못함, 다음 턴에 다시 확인함).'
|
|
422
|
+
: null;
|
|
423
|
+
const n = notice();
|
|
424
|
+
emitContinue([n, cut].filter(Boolean).join('\n\n') || undefined);
|
|
400
425
|
return;
|
|
401
426
|
}
|
|
402
427
|
// finished close, but the session's cwd project close is incomplete:
|
|
@@ -420,15 +445,23 @@ process.stdin.on('end', () => {
|
|
|
420
445
|
// "work-incomplete" together are exactly the case where the transcript's
|
|
421
446
|
// NL close phrase can't tell "close now" from "close once X is done" —
|
|
422
447
|
// regex can't disambiguate this (see spec background). Narrowed to the
|
|
423
|
-
// work-incomplete signals only: an uncommitted
|
|
424
|
-
//
|
|
448
|
+
// work-incomplete signals only: an uncommitted file this session wrote (a
|
|
449
|
+
// `known-session-write` blocker that names a file, or a `git` blocker for a
|
|
450
|
+
// project folder the write record lost track of; real unsaved work) OR
|
|
451
|
+
// pending background work — a non-terminal background task
|
|
425
452
|
// (delegated subagent OR shell, e.g. a deferred push/CI wait) or a
|
|
426
453
|
// scheduled cron wake (hasPendingBackgroundWork). `hot` / `lint` / `close`
|
|
427
454
|
// / `design-history` / `feedback` blockers are real close blockers but not
|
|
428
455
|
// evidence of "later" intent, so they keep the existing wording (unchanged
|
|
429
|
-
// from before this feature).
|
|
456
|
+
// from before this feature). The touched-paths lock timeout is a
|
|
457
|
+
// `known-session-write` blocker with no `file`: it says the record could not
|
|
458
|
+
// be read right now, not that work is unfinished.
|
|
430
459
|
const workIncomplete =
|
|
431
|
-
(gate &&
|
|
460
|
+
(gate &&
|
|
461
|
+
gate.blockers &&
|
|
462
|
+
gate.blockers.some(
|
|
463
|
+
(b) => (b.type === 'git' || b.type === 'known-session-write') && b.file,
|
|
464
|
+
)) ||
|
|
432
465
|
hasPendingBackgroundWork(payload);
|
|
433
466
|
if (workIncomplete && isCloseReconfirmDeclined(transcriptPath)) {
|
|
434
467
|
// The user already answered "아직" to the ambiguous close signal that
|