hypomnema 1.8.2 → 1.9.0
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 +141 -51
- package/README.ko.md +18 -17
- package/README.md +16 -15
- package/commands/crystallize.md +89 -24
- package/commands/doctor.md +3 -1
- package/docs/ARCHITECTURE.md +45 -14
- package/docs/CONTRIBUTING.md +26 -8
- package/hooks/atomic-write.mjs +44 -0
- package/hooks/base-store.mjs +409 -90
- package/hooks/close-gate-store.mjs +131 -16
- package/hooks/close-journal.mjs +250 -0
- package/hooks/close-receipt.mjs +559 -0
- package/hooks/hooks.json +3 -38
- package/hooks/hypo-auto-commit.mjs +80 -10
- package/hooks/hypo-auto-minimal-crystallize.mjs +129 -25
- package/hooks/hypo-auto-stage.mjs +13 -1
- package/hooks/hypo-close-guard.mjs +2 -1
- package/hooks/hypo-cwd-change.mjs +45 -20
- package/hooks/hypo-file-watch.mjs +44 -6
- package/hooks/hypo-first-prompt.mjs +15 -9
- package/hooks/hypo-hot-rebuild.mjs +169 -112
- package/hooks/hypo-personal-check.mjs +9 -1
- package/hooks/hypo-session-record.mjs +1 -1
- package/hooks/hypo-session-start.mjs +566 -20
- package/hooks/hypo-shared.mjs +2250 -206
- package/hooks/hypo-stop.mjs +207 -0
- package/hooks/proposal-store.mjs +201 -35
- package/hooks/shared.json +15 -0
- package/hooks/version-check.mjs +9 -7
- package/package.json +3 -1
- package/scripts/crystallize.mjs +3 -4
- package/scripts/doctor.mjs +549 -123
- package/scripts/init.mjs +86 -109
- package/scripts/lib/core-hooks.mjs +48 -22
- package/scripts/lib/crystallize-close-apply.mjs +2397 -188
- package/scripts/lib/crystallize-close-check.mjs +43 -23
- package/scripts/lib/design-history-stale.mjs +36 -9
- package/scripts/lib/frontmatter.mjs +39 -0
- package/scripts/lib/git-hooks-dir.mjs +427 -52
- package/scripts/lib/hook-inventory.mjs +180 -0
- package/scripts/lib/page-usage.mjs +1 -1
- package/scripts/lib/pkg-provenance.mjs +18 -1
- package/scripts/lib/plugin-detect.mjs +43 -9
- package/scripts/lib/project-create.mjs +40 -44
- package/scripts/lib/schema-vocab.mjs +22 -11
- package/scripts/lib/template-schema-version.mjs +48 -0
- package/scripts/lib/time.mjs +46 -0
- package/scripts/lint.mjs +66 -18
- package/scripts/proposal.mjs +423 -18
- package/scripts/resume.mjs +3 -2
- package/scripts/session-audit.mjs +2 -1
- package/scripts/uninstall.mjs +144 -66
- package/scripts/upgrade.mjs +523 -234
- package/scripts/verify.mjs +2 -1
- package/scripts/weekly-report.mjs +2 -1
- package/templates/SCHEMA.md +27 -7
- package/templates/gitignore +11 -0
- package/templates/hypo-automation.md +10 -13
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +53 -25
- package/templates/hot.md +0 -24
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -119,7 +119,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
119
119
|
| `UserPromptSubmit` | `hypo-first-prompt.mjs` → `hypo-lookup.mjs` → `hypo-compact-guard.mjs` |
|
|
120
120
|
| `PreCompact` | `hypo-personal-check.mjs` |
|
|
121
121
|
| `PostToolUse` (Write/Edit) | `hypo-auto-stage.mjs` |
|
|
122
|
-
| `Stop` | `hypo-hot-rebuild.mjs` → `hypo-session-record.mjs` → `hypo-auto-commit.mjs` |
|
|
122
|
+
| `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
123
|
| `CwdChanged` | `hypo-cwd-change.mjs` |
|
|
124
124
|
| `FileChanged` | `hypo-file-watch.mjs` |
|
|
125
125
|
|
|
@@ -127,24 +127,23 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
127
127
|
|
|
128
128
|
| Hook | Responsibility |
|
|
129
129
|
|---|---|
|
|
130
|
-
| `hypo-session-start` |
|
|
130
|
+
| `hypo-session-start` | Run `git pull --ff-only` (silent fail on missing remote), rebuild the root `hot.md` projection, inject `index.md`, root `hot.md`, project `hot.md`/`session-state.md` |
|
|
131
131
|
| `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
132
|
| `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
133
|
| `hypo-compact-guard` | Detect `/compact` or `/clear` typed in chat and, if session close is incomplete, tell Claude so. It never blocks `/compact` |
|
|
134
134
|
| `hypo-personal-check` | PreCompact detection: lint blockers, uncommitted changes, missing session-log entries surface as a `systemMessage`; `/compact` is never blocked here |
|
|
135
135
|
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
|
|
136
|
-
| `hypo-hot-rebuild` | At session stop, regenerate root `hot.md`
|
|
136
|
+
| `hypo-hot-rebuild` | At session stop, regenerate the root `hot.md` projection (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
137
|
| `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
138
|
| `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
|
|
139
|
-
| `hypo-cwd-change` | When working directory changes, re-resolve the active project and build
|
|
140
|
-
| `hypo-file-watch` | Build `
|
|
139
|
+
| `hypo-cwd-change` | When working directory changes, re-resolve the active project and build a notice carrying its `hot.md`. The hook reference documents `watchPaths` and `systemMessage` as `CwdChanged`'s output fields and says only that "it shows the `systemMessage` as a brief terminal notification" and "the message doesn't reach the SDK message stream"; it does not say whether `systemMessage` reaches the model on this event either way. As of Claude Code 2.1.276 (checked 2026-09-18) and reconfirmed on 2.1.283, tracing the installed binary shows the only consumer of that `systemMessage` is the terminal notification queue, a low-priority toast that a same-keyed message replaces and that disappears after five seconds, with no branch that forwards it to the model; outside the interactive REPL the injecting function is null. No live-session observation of this path exists either way. Until the documentation says otherwise, this event is treated as not reaching the model |
|
|
140
|
+
| `hypo-file-watch` | Build a `systemMessage` notice for a changed wiki file, over the same field as `hypo-cwd-change` whose reach to the model the reference leaves undocumented; as of Claude Code 2.1.276 (checked 2026-09-18) and reconfirmed on 2.1.283 it is likewise treated as not reaching the model, for the same shared-consumer reason (see the `hypo-cwd-change` row). On a stock install the event still has no registered trigger: its matcher key is omitted (not blank), which the reference says matches every watched file while adding nothing to the watch list, and nothing in this package returns `watchPaths` to seed that list. The watch list is session-global, not scoped to this plugin, so another hook or plugin that does seed it would make this matcher-less group fire on every watched file; the hook still returns immediately for any path that does not start with `HYPO_DIR` (a string-prefix check, not a symlink-resolving one). That check only screens the path string: a symlink living inside the wiki still starts with `HYPO_DIR` and passes it, and the hook's `readFileSync` follows the link, so such a symlink can still surface content from wherever it points. The event also fires regardless of who changed the file, so it is not an external-edit signal |
|
|
141
141
|
|
|
142
142
|
### Deployment constraint
|
|
143
143
|
|
|
144
144
|
Hooks run in an isolated environment at `~/.claude/hooks/`. They **cannot import from relative paths** outside their own directory. Therefore:
|
|
145
145
|
|
|
146
|
-
-
|
|
147
|
-
- `hypo-shared.mjs` is declared via the `shared` field in `hooks.json` and copied alongside hooks at deploy time.
|
|
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 and is copied alongside hooks at deploy time. 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.
|
|
148
147
|
- Hook utilities use **only Node.js built-ins** — no relative imports, no npm dependencies.
|
|
149
148
|
|
|
150
149
|
Scripts in `scripts/` are not deployed — they run from the package install path — so they can import from `scripts/lib/`.
|
|
@@ -246,6 +245,8 @@ Hooks inline this logic in `hypo-shared.mjs`. Scripts use `scripts/lib/hypo-root
|
|
|
246
245
|
4. Detects drift (e.g., `pkgRoot` no longer exists) and refreshes the pointer.
|
|
247
246
|
5. `--json` output emits `{ schema, hooks, settings, applied }`. `--dry-run` previews; `--apply` commits changes.
|
|
248
247
|
|
|
248
|
+
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.
|
|
249
|
+
|
|
249
250
|
### `/hypo:uninstall`
|
|
250
251
|
|
|
251
252
|
Removes hypo-prefixed hooks from `~/.claude/hooks/` and matching entries from `~/.claude/settings.json`. **Non-hypo hooks are preserved**. `--apply` also reaches past `~/.claude/`: it strips the marked `claude()` block from the shell rc file(s) init wrote to, and removes the marked pre-commit hook from the wiki's own git repo. Both removals are marker-gated the same way the hooks-dir cleanup is, so a hand-edited block or hook is left in place rather than guessed at.
|
|
@@ -292,6 +293,7 @@ SessionStart
|
|
|
292
293
|
│
|
|
293
294
|
├─► hypo-session-start.mjs
|
|
294
295
|
│ ├─► git pull --ff-only (if remote)
|
|
296
|
+
│ ├─► rebuild root hot.md (projection of projects/*/hot.md)
|
|
295
297
|
│ ├─► inject index.md / hot.md / project session-state.md
|
|
296
298
|
│ └─► (Claude resumes work)
|
|
297
299
|
│
|
|
@@ -307,14 +309,36 @@ SessionStart
|
|
|
307
309
|
│ └─► hypo-personal-check.mjs (lint + session-close gate)
|
|
308
310
|
│
|
|
309
311
|
├─► CwdChanged
|
|
310
|
-
│ └─► hypo-cwd-change.mjs (builds hot.md
|
|
312
|
+
│ └─► 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)
|
|
311
313
|
│
|
|
312
314
|
└─► Stop
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
315
|
+
└─► hypo-stop.mjs, running four stages in order:
|
|
316
|
+
├─► hypo-hot-rebuild.mjs (regenerate root hot.md projection + growth cache; SessionStart also calls this generator)
|
|
317
|
+
├─► hypo-session-record.mjs (append .cache/sessions/index.jsonl)
|
|
318
|
+
├─► hypo-auto-commit.mjs (.hypoignore-filtered stage + commit + pull + push)
|
|
319
|
+
└─► hypo-auto-minimal-crystallize.mjs (blocks Stop if session close never ran)
|
|
316
320
|
```
|
|
317
321
|
|
|
322
|
+
Root hot.md's frontmatter `updated:` is the max of every `projects/*/hot.md` row date, not the
|
|
323
|
+
date the projection itself was last rewritten. A vault whose newest project row is a week old
|
|
324
|
+
keeps that same week-old date across every SessionStart and Stop until a project's own `hot.md`
|
|
325
|
+
moves forward, even though the root file's bytes get recomputed and checked far more often.
|
|
326
|
+
|
|
327
|
+
Before overwriting content it does not recognize as its own, the projection backs it up next to
|
|
328
|
+
`hot.md` as `hot.md.pre-projection-backup.md` (or a numbered variant, `-2.md`, `-3.md`, ...). These
|
|
329
|
+
backups are never cleaned up automatically. Delete the ones you no longer need by hand.
|
|
330
|
+
|
|
331
|
+
The write itself goes through `writeRootHotProjection` in `hooks/hypo-shared.mjs`, which holds
|
|
332
|
+
the vault commit lock (`vaultCommitLockTarget`, the same lock `hypo-auto-commit.mjs` and
|
|
333
|
+
`crystallize.mjs`'s apply take around stage and commit) across the whole projection write: the
|
|
334
|
+
first read of `hot.md`, the ownership check, the backup, and the replacing rename. `hypo-session-start.mjs`'s
|
|
335
|
+
own `git pull` takes and releases the same lock before calling into the projection, so the two
|
|
336
|
+
never overlap; they still serialize against each other and against a concurrent `auto-commit` or
|
|
337
|
+
`crystallize` apply. The lock is not reentrant: nothing inside the projection's write path may
|
|
338
|
+
take it a second time, and a caller that already holds it has to call the unlocked form instead.
|
|
339
|
+
When the lock cannot be acquired within its timeout, the write is skipped rather than blocked on
|
|
340
|
+
indefinitely, and the caller is told the pointer table was left as-is.
|
|
341
|
+
|
|
318
342
|
---
|
|
319
343
|
|
|
320
344
|
## Feedback → Behavior pipeline
|
|
@@ -501,7 +525,7 @@ Requires `NPM_TOKEN` secret.
|
|
|
501
525
|
|
|
502
526
|
## Plugin manifest
|
|
503
527
|
|
|
504
|
-
`.claude-plugin/plugin.json` declares the plugin to Claude Code. `hooks/hooks.json` follows the standard plugin hooks schema:
|
|
528
|
+
`.claude-plugin/plugin.json` declares the plugin to Claude Code. `hooks/hooks.json` follows the standard plugin hooks schema, and carries only what that schema knows about: an event-to-hook map, nothing else.
|
|
505
529
|
|
|
506
530
|
```json
|
|
507
531
|
{
|
|
@@ -517,11 +541,18 @@ Requires `NPM_TOKEN` secret.
|
|
|
517
541
|
]
|
|
518
542
|
}
|
|
519
543
|
]
|
|
520
|
-
}
|
|
521
|
-
"shared": ["hypo-shared.mjs"]
|
|
544
|
+
}
|
|
522
545
|
}
|
|
523
546
|
```
|
|
524
547
|
|
|
548
|
+
The shared-file list (helpers a registered hook imports, such as `hypo-shared.mjs`) is Hypomnema's own convention, not something the harness's schema recognizes, so it lives in a sibling file instead: `hooks/shared.json`, a plain JSON array of `.mjs` basenames.
|
|
549
|
+
|
|
550
|
+
```json
|
|
551
|
+
["hypo-shared.mjs", "version-check.mjs", "..."]
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
`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 deploy. Anything left off either file never reaches an installed copy.
|
|
555
|
+
|
|
525
556
|
`claude plugin validate .` is run as part of CI's plugin-snapshot checks.
|
|
526
557
|
|
|
527
558
|
---
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -62,8 +62,7 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full breakdown. Short version:
|
|
|
62
62
|
|
|
63
63
|
**Hooks run in `~/.claude/hooks/` in isolation. They cannot import from relative paths.**
|
|
64
64
|
|
|
65
|
-
-
|
|
66
|
-
- `hypo-shared.mjs` is declared via the `shared` field in `hooks/hooks.json` and copied alongside each hook at deploy time.
|
|
65
|
+
- Shared logic must live in one of 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 and is copied alongside each hook at deploy time. It used to be a `shared` field inside `hooks.json` itself; that moved to this sibling file once the harness started warning on that unknown top-level key.
|
|
67
66
|
- Hook utilities may use **only** Node.js built-ins (`fs`, `path`, `os`, `child_process`, `crypto`).
|
|
68
67
|
- No relative imports, no npm dependencies.
|
|
69
68
|
|
|
@@ -85,7 +84,7 @@ Scripts under `scripts/` are not deployed and may freely import from `scripts/li
|
|
|
85
84
|
|
|
86
85
|
1. Edit the hook file in `hooks/`.
|
|
87
86
|
2. If it's new, register it in `hooks/hooks.json` under the correct event key.
|
|
88
|
-
3. Shared utilities go in `hooks/hypo-shared.mjs`.
|
|
87
|
+
3. Shared utilities go in a module listed in `hooks/shared.json`, usually `hypo-shared.mjs`.
|
|
89
88
|
4. Add a contract test in the hook's area file, e.g. `tests/session-hooks.test.mjs` (input → expected `additionalContext` shape).
|
|
90
89
|
5. After your change, run `/hypo:upgrade` in a real Claude Code session and verify the hook fires.
|
|
91
90
|
|
|
@@ -130,7 +129,11 @@ npm test # tests/*.test.mjs, sharded across processes — unit + smoke
|
|
|
130
129
|
npm run lint # scripts/lint.mjs — frontmatter + wikilink validation + W8 (design-history stale
|
|
131
130
|
# vs session-log) + W14 (design-history missing but session-log implies one)
|
|
132
131
|
# + W15 (synthesis older than the newest page in its sources_consulted)
|
|
133
|
-
# + W16 (sources_consulted
|
|
132
|
+
# + W16 (sources_consulted content problems: a source name W15 could not
|
|
133
|
+
# resolve, a source or the synthesis's own updated that fails a strict
|
|
134
|
+
# YYYY-MM-DD parse, or sources_consulted written as a YAML block list
|
|
135
|
+
# instead of the required flow list; a single page can carry more than one
|
|
136
|
+
# W16 at once)
|
|
134
137
|
npm run fix:verify # Phase 1 of learned_behavior #6 — verifies fix #N status claims in
|
|
135
138
|
# a wiki spec against `// @fix #N: <test-name>` anchors, read as a
|
|
136
139
|
# union across every tests/*.mjs. Maintainer dogfood; needs a wiki at
|
|
@@ -256,7 +259,7 @@ After both blocks, language-neutral:
|
|
|
256
259
|
|
|
257
260
|
- **PR title**: Conventional Commits plus a scope, e.g. `feat(feedback): add failure_type enum`. The type drives the CHANGELOG section (see the classification table below).
|
|
258
261
|
- **Merge commit**: the squash-merge subject carries the PR number (`#123`). That is where `#N` comes from, not the PR title. The two conventions stay separate.
|
|
259
|
-
- Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`, `fix #N`) may appear in your local notes and in `tests/` (where they aid test-to-issue traceability and never reach an installed user), but not in shipped code or workflow comments, and never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only
|
|
262
|
+
- Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`, `fix #N`, `ADR NNNN`, `decisions/NNNN`) may appear in your local notes and in `tests/` (where they aid test-to-issue traceability and never reach an installed user), but not in shipped code or workflow comments, and never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only identifier that ships in those is the PR number `#N`. `ADR NNNN` / `decisions/NNNN` used to be exempt on the changelog surfaces, on the theory that a release line legitimately cites the decision behind it; that exemption is gone, because this repo ships no `decisions/` directory for anyone outside the maintainer's private wiki to open — an external reader hits the same dead end on `decisions/NNNN` as on `ISSUE-N`, so the exemption was blocking nothing but its own contradiction. `check-tracker-ids` gates the file, message, and tag surfaces (`--all`/`--staged` for files, `--commit-msg` for messages, `--tag` for the tag body), and `check-pr-surface` gates the PR title and body; the migration keeps the CHANGELOG body clean.
|
|
260
263
|
|
|
261
264
|
### The `## Changelog` block
|
|
262
265
|
|
|
@@ -372,6 +375,14 @@ node scripts/bump-version.mjs <new-semver> # e.g. 1.2.2 or 1.3.0-rc.1
|
|
|
372
375
|
# version-consistency gate stay green (the lock carries the version twice).
|
|
373
376
|
npm install --package-lock-only
|
|
374
377
|
|
|
378
|
+
# 1c. If this release also bumps templates/SCHEMA.md's `version:` frontmatter,
|
|
379
|
+
# add a matching line to SCHEMA_VERSION_DELTAS in
|
|
380
|
+
# scripts/lib/template-schema-version.mjs, naming what that version added.
|
|
381
|
+
# Skipping this is silent: upgrade.mjs's notice just falls back to "review
|
|
382
|
+
# manually" for that version, with no test failing at bump time except
|
|
383
|
+
# the one in tests/upgrade.test.mjs that pins the SHIPPED version has an
|
|
384
|
+
# entry.
|
|
385
|
+
|
|
375
386
|
# 2. Draft the new CHANGELOG.md section from the merged PRs' `## Changelog`
|
|
376
387
|
# blocks (see CHANGELOG conventions above). The collector prints a draft for
|
|
377
388
|
# the range since the last tag; paste it, then finalize the wording by hand,
|
|
@@ -406,8 +417,10 @@ git commit -m "chore: release v<version>"
|
|
|
406
417
|
# then a Korean summary block. The GitHub Release republishes this body
|
|
407
418
|
# verbatim, so keep it on the same public surface as the CHANGELOG: PR
|
|
408
419
|
# numbers (#N) only, no `FEAT-`/`IMPR-`/`ISSUE-`/`PRAC-`/`fix #N` tracker
|
|
409
|
-
# ids (check-tracker-ids --tag gates it)
|
|
410
|
-
#
|
|
420
|
+
# ids (check-tracker-ids --tag gates it), and no `ADR NNNN` / `decisions/NNNN`
|
|
421
|
+
# either: this repo ships no `decisions/` directory, so a reader outside the
|
|
422
|
+
# maintainer's private wiki cannot open either one. Say what changed in
|
|
423
|
+
# plain prose, or cite the PR number.
|
|
411
424
|
git tag -a v<version> -m "$(cat <<'EOF'
|
|
412
425
|
Hypomnema v<version>: <one-line English summary>
|
|
413
426
|
|
|
@@ -421,7 +434,12 @@ Hypomnema v<version>: <한 줄 한글 요약>
|
|
|
421
434
|
EOF
|
|
422
435
|
)"
|
|
423
436
|
|
|
424
|
-
# 6. Rehearse the release locally (same checks CI runs against the tag)
|
|
437
|
+
# 6. Rehearse the release locally (same checks CI runs against the tag).
|
|
438
|
+
# This is the ONLY gate that runs before the tag object is public. The
|
|
439
|
+
# release workflow re-runs these, but it starts when the push in step 7
|
|
440
|
+
# lands, so by then the annotated tag body is already on GitHub: CI can
|
|
441
|
+
# stop the npm publish and the Release, not the tag. Skip these three and
|
|
442
|
+
# nothing catches a leaked id until a reader finds it.
|
|
425
443
|
node scripts/check-bilingual.mjs --tag v<version>
|
|
426
444
|
node scripts/check-versions.mjs --tag v<version>
|
|
427
445
|
node scripts/check-tracker-ids.mjs --tag v<version> # no tracker ids leak into the tag body
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The one temp+rename writer. Six byte-identical copies of this function used to
|
|
2
|
+
// live in hooks/ and scripts/lib/, and a fix landed in two of them (#296) while
|
|
3
|
+
// the other four kept the bug. That is the whole reason this file exists: a
|
|
4
|
+
// defense that lives in six places is a defense you can only half-apply.
|
|
5
|
+
//
|
|
6
|
+
// Hooks are copied standalone into ~/.claude/hooks/, so this has to live in
|
|
7
|
+
// hooks/ and be listed in hooks/shared.json. scripts/ may import it; the reverse
|
|
8
|
+
// direction is what breaks for every installed copy.
|
|
9
|
+
//
|
|
10
|
+
// Node built-ins only, per the hooks rule.
|
|
11
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Commit `content` to `path` via a temp file and a rename, so a partial or
|
|
16
|
+
* failed write lands on a throwaway name and the target is never torn.
|
|
17
|
+
*
|
|
18
|
+
* The temp name carries this pid and a fresh random rather than a shared
|
|
19
|
+
* `<path>.tmp` slot, so concurrent writers never fight over it. That is also
|
|
20
|
+
* why a leaked temp is permanent: nothing ever picks the same name again, so
|
|
21
|
+
* the file sits in the vault forever, close after close. Both failure points
|
|
22
|
+
* leak one: the rename (the target stays untouched, which is the point) and
|
|
23
|
+
* the write itself (ENOSPC or EDQUOT partway through). Clean up on either, and
|
|
24
|
+
* never let the cleanup hide the error that got us here.
|
|
25
|
+
*
|
|
26
|
+
* Rename atomicity swaps the directory entry. It is NOT power-loss durable.
|
|
27
|
+
* there is no fsync, same as everything else in the vault.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} path absolute path to write
|
|
30
|
+
* @param {string|Buffer} content bytes to commit
|
|
31
|
+
*/
|
|
32
|
+
export function atomicWrite(path, content) {
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
34
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(tmp, content);
|
|
37
|
+
renameSync(tmp, path);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
try {
|
|
40
|
+
rmSync(tmp, { force: true });
|
|
41
|
+
} catch {}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|