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/scripts/uninstall.mjs
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
statSync,
|
|
52
52
|
realpathSync,
|
|
53
53
|
} from 'fs';
|
|
54
|
-
import { join } from 'path';
|
|
54
|
+
import { join, resolve, sep } from 'path';
|
|
55
55
|
import { homedir } from 'os';
|
|
56
56
|
import { fileURLToPath } from 'url';
|
|
57
57
|
import {
|
|
@@ -74,7 +74,12 @@ import {
|
|
|
74
74
|
hasSymlinkAncestor,
|
|
75
75
|
buildHookCommand,
|
|
76
76
|
} from './lib/extensions.mjs';
|
|
77
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
removeProvenanceSidecar,
|
|
79
|
+
readProvenanceSidecar,
|
|
80
|
+
provenancePath,
|
|
81
|
+
} from './lib/pkg-provenance.mjs';
|
|
82
|
+
import { loadHookInventory } from './lib/hook-inventory.mjs';
|
|
78
83
|
import {
|
|
79
84
|
hooksDirForInstall,
|
|
80
85
|
unsafeHookTargetReason,
|
|
@@ -94,12 +99,6 @@ const HOME = homedir();
|
|
|
94
99
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
95
100
|
const PKG_ROOT = join(SCRIPT_DIR, '..');
|
|
96
101
|
|
|
97
|
-
// Shown after every fatal package-integrity error. These conditions mean the
|
|
98
|
-
// shipped hooks/hooks.json is missing or malformed — never a user mistake —
|
|
99
|
-
// so the only useful next step is a re-install of the package.
|
|
100
|
-
const PKG_INTEGRITY_HINT =
|
|
101
|
-
'→ This indicates a corrupt or incomplete install. Re-install with `npm install -g hypomnema` (or re-install the Claude Code plugin).';
|
|
102
|
-
|
|
103
102
|
function removeCommands(apply, force) {
|
|
104
103
|
const targetDir = join(HOME, '.claude', 'commands', 'hypo');
|
|
105
104
|
const pkgPath = join(HOME, '.claude', 'hypo-pkg.json');
|
|
@@ -725,53 +724,74 @@ function parseArgs(argv) {
|
|
|
725
724
|
return args;
|
|
726
725
|
}
|
|
727
726
|
|
|
728
|
-
// ── hook
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
727
|
+
// ── hook file set (single source of truth, with a provenance fallback) ──────
|
|
728
|
+
//
|
|
729
|
+
// The normal path reads PKG_ROOT's own hooks.json/shared.json through
|
|
730
|
+
// loadHookInventory (scripts/lib/hook-inventory.mjs) — the same parser every
|
|
731
|
+
// install/uninstall consumer shares, so a command shape one of them would
|
|
732
|
+
// accept (or a trailing-argument command this file's own old `/hooks/(...)$`
|
|
733
|
+
// anchor silently ignored) cannot read differently here.
|
|
734
|
+
//
|
|
735
|
+
// But uninstall is the one command whose whole point is letting someone leave
|
|
736
|
+
// even when the package itself is broken. The other four consumers fail closed
|
|
737
|
+
// on a malformed hooks.json; refusing to run here instead would trade "a few
|
|
738
|
+
// files linger" for "you cannot remove this at all". So when the package list
|
|
739
|
+
// cannot be read, this falls back to the provenance sidecar THIS hooksDir's own
|
|
740
|
+
// last install/upgrade wrote: writeProvenanceSidecar (lib/pkg-provenance.mjs)
|
|
741
|
+
// records `managedFiles`, the exact basename set that run copied, so a hooks.json
|
|
742
|
+
// broken in the CURRENTLY-INSTALLED package does not strand the files a PAST,
|
|
743
|
+
// working package put there.
|
|
744
|
+
//
|
|
745
|
+
// Only when neither the package list nor a provenance record is available does
|
|
746
|
+
// this give up on identifying anything here as ours — `source: 'none'` below
|
|
747
|
+
// never guesses at deletion; the caller names what remains and reports failure
|
|
748
|
+
// via its exit code rather than silently succeeding at nothing.
|
|
749
|
+
function resolveHookFileSet(pkgRoot, hooksDir) {
|
|
750
|
+
const inv = loadHookInventory(pkgRoot);
|
|
751
|
+
if (inv.ok) {
|
|
752
|
+
// Reverse of the install order, so removal mirrors it: entry hooks go
|
|
753
|
+
// first, the shared modules they import go last. An uninstall interrupted
|
|
754
|
+
// halfway then leaves entry hooks that are already gone rather than entry
|
|
755
|
+
// hooks still registered against modules that are, which is the shape that
|
|
756
|
+
// breaks the next session instead of merely being incomplete.
|
|
757
|
+
// A Set keeps insertion order, and removeHookFiles iterates it in that order.
|
|
758
|
+
//
|
|
759
|
+
// LEGACY_INSTALLED is not part of the install set and never should have
|
|
760
|
+
// been: init used to copy every `.mjs` in hooks/, which put
|
|
761
|
+
// hypo-pre-commit.mjs into installs that never invoke it (the vault's
|
|
762
|
+
// pre-commit wrapper runs the package-root copy). init no longer does that,
|
|
763
|
+
// so the only copies left are the ones already on disk, and this is the one
|
|
764
|
+
// command that can take them away.
|
|
765
|
+
const LEGACY_INSTALLED = ['hypo-pre-commit.mjs'];
|
|
766
|
+
const files = new Set([...[...inv.ordered].reverse(), ...LEGACY_INSTALLED]);
|
|
767
|
+
return { files, source: 'package', warning: null };
|
|
769
768
|
}
|
|
770
|
-
|
|
771
|
-
if (Array.isArray(
|
|
772
|
-
|
|
769
|
+
const sidecar = readProvenanceSidecar(hooksDir);
|
|
770
|
+
if (sidecar && Array.isArray(sidecar.managedFiles) && sidecar.managedFiles.length > 0) {
|
|
771
|
+
return {
|
|
772
|
+
files: new Set(sidecar.managedFiles),
|
|
773
|
+
source: 'provenance',
|
|
774
|
+
warning:
|
|
775
|
+
`hooks/hooks.json could not be read (${inv.error}). Recovered the managed file list for ` +
|
|
776
|
+
`${hooksDir} from ${provenancePath(hooksDir)} (recorded at the last install/upgrade) instead.`,
|
|
777
|
+
};
|
|
773
778
|
}
|
|
774
|
-
|
|
779
|
+
const present = existsSync(hooksDir)
|
|
780
|
+
? readdirSync(hooksDir).filter((f) => f.endsWith('.mjs'))
|
|
781
|
+
: [];
|
|
782
|
+
return {
|
|
783
|
+
files: new Set(),
|
|
784
|
+
source: 'none',
|
|
785
|
+
warning:
|
|
786
|
+
`hooks/hooks.json could not be read (${inv.error}) and no provenance record exists at ` +
|
|
787
|
+
`${provenancePath(hooksDir)} to recover a managed file list from (an install made before ` +
|
|
788
|
+
`this sidecar field existed never wrote one). Hook file removal for ${hooksDir} is skipped ` +
|
|
789
|
+
'entirely.' +
|
|
790
|
+
(present.length
|
|
791
|
+
? ` The following .mjs files are still there and were not evaluated — remove them by ` +
|
|
792
|
+
`hand after checking what they are: ${present.join(', ')}`
|
|
793
|
+
: ' No .mjs files were found there either.'),
|
|
794
|
+
};
|
|
775
795
|
}
|
|
776
796
|
|
|
777
797
|
// ── hook file removal ────────────────────────────────────────────────────────
|
|
@@ -779,8 +799,22 @@ function loadHookFiles() {
|
|
|
779
799
|
function removeHookFiles(hooksDir, hookFiles, apply) {
|
|
780
800
|
const removed = [],
|
|
781
801
|
missing = [];
|
|
802
|
+
const skipped = [];
|
|
803
|
+
// Every name here comes out of a file on disk: hooks.json's event map and
|
|
804
|
+
// hooks/shared.json. A name is supposed to be a bare basename, but nothing
|
|
805
|
+
// upstream forces that, and join() resolves `../x.mjs` straight out of the
|
|
806
|
+
// hooks directory. This function DELETES what it is handed, so it confirms
|
|
807
|
+
// the containment itself rather than trusting the list it was given. An
|
|
808
|
+
// out-of-tree name is skipped and named, never removed: a corrupt list is a
|
|
809
|
+
// reason to leave files alone, not to delete somewhere else.
|
|
810
|
+
const root = resolve(hooksDir);
|
|
811
|
+
const inside = (p) => p === root || p.startsWith(root + sep);
|
|
782
812
|
for (const file of hookFiles) {
|
|
783
813
|
const p = join(hooksDir, file);
|
|
814
|
+
if (!inside(resolve(p))) {
|
|
815
|
+
skipped.push(file);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
784
818
|
if (existsSync(p)) {
|
|
785
819
|
if (apply) rmSync(p);
|
|
786
820
|
removed.push(p);
|
|
@@ -788,6 +822,12 @@ function removeHookFiles(hooksDir, hookFiles, apply) {
|
|
|
788
822
|
missing.push(p);
|
|
789
823
|
}
|
|
790
824
|
}
|
|
825
|
+
if (skipped.length > 0) {
|
|
826
|
+
console.warn(
|
|
827
|
+
`Warning: ${skipped.length} hook name(s) resolve outside ${hooksDir} and were left alone: ` +
|
|
828
|
+
`${skipped.join(', ')}. Remove them by hand after checking what they are.`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
791
831
|
// .hypo-provenance.json (scripts/lib/pkg-provenance.mjs) is written next to
|
|
792
832
|
// this exact hooksDir by installHooks/applyHookFiles — same lifecycle as the
|
|
793
833
|
// hook files themselves, so it is removed in the same pass rather than left
|
|
@@ -804,7 +844,13 @@ function removeHookFiles(hooksDir, hookFiles, apply) {
|
|
|
804
844
|
|
|
805
845
|
// ── settings.json cleanup ────────────────────────────────────────────────────
|
|
806
846
|
|
|
807
|
-
|
|
847
|
+
// `hookFiles` is a flat Set of basenames (not a per-event map): a command
|
|
848
|
+
// string already names the exact hook file (`node <hooksDir>/<file>`), so
|
|
849
|
+
// scoping "ours" by which event a settings.json group happens to sit under adds
|
|
850
|
+
// no protection a flat membership check does not already give — and a flat set
|
|
851
|
+
// is what both the normal (package) and provenance-fallback paths in
|
|
852
|
+
// resolveHookFileSet actually have on hand.
|
|
853
|
+
function stripSettingsJson(settingsPath, hooksDir, hookFiles, apply) {
|
|
808
854
|
if (!existsSync(settingsPath)) return { stripped: [], kept: 0 };
|
|
809
855
|
|
|
810
856
|
let settings;
|
|
@@ -816,18 +862,18 @@ function stripSettingsJson(settingsPath, hooksDir, hookMap, apply) {
|
|
|
816
862
|
|
|
817
863
|
if (!settings.hooks || typeof settings.hooks !== 'object') return { stripped: [], kept: 0 };
|
|
818
864
|
|
|
865
|
+
const expectedCmds = new Set(
|
|
866
|
+
[...hookFiles].map((file) => `node ${hooksDir.replace(HOME, '$HOME')}/${file}`),
|
|
867
|
+
);
|
|
868
|
+
const isHypoHook = (h) =>
|
|
869
|
+
h.type === 'command' && typeof h.command === 'string' && expectedCmds.has(h.command);
|
|
870
|
+
|
|
819
871
|
const stripped = [];
|
|
820
872
|
let changed = false;
|
|
821
873
|
|
|
822
874
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
823
875
|
if (!Array.isArray(groups)) continue;
|
|
824
876
|
|
|
825
|
-
const managed = hookMap[event] ?? [];
|
|
826
|
-
const isHypoHook = (h) =>
|
|
827
|
-
h.type === 'command' &&
|
|
828
|
-
typeof h.command === 'string' &&
|
|
829
|
-
managed.some((file) => h.command === `node ${hooksDir.replace(HOME, '$HOME')}/${file}`);
|
|
830
|
-
|
|
831
877
|
const filtered = groups.flatMap((group) => {
|
|
832
878
|
if (!Array.isArray(group.hooks)) return [group];
|
|
833
879
|
|
|
@@ -873,13 +919,31 @@ if (args.hooksDir && !args.keepShell && !args.keepWikiHook) {
|
|
|
873
919
|
);
|
|
874
920
|
}
|
|
875
921
|
|
|
876
|
-
const { hookMap, hookFiles } = loadHookFiles();
|
|
877
|
-
|
|
878
922
|
const claudeHooksDir = args.hooksDir ?? join(HOME, '.claude', 'hooks');
|
|
879
923
|
const claudeSettings = join(HOME, '.claude', 'settings.json');
|
|
880
924
|
|
|
881
|
-
|
|
882
|
-
|
|
925
|
+
// A `source: 'none'` resolution (package unreadable AND no provenance record)
|
|
926
|
+
// means hook-file removal for that target was skipped entirely rather than
|
|
927
|
+
// guessed at — this run must not report plain success in that case. Collected
|
|
928
|
+
// across both targets and applied to the exit code once the whole report is
|
|
929
|
+
// printed, so the caller still sees everything else this run did (or would do).
|
|
930
|
+
let hardFailure = false;
|
|
931
|
+
function reportHookSetWarning(set) {
|
|
932
|
+
if (!set.warning) return;
|
|
933
|
+
console.error(`Warning: ${set.warning}`);
|
|
934
|
+
if (set.source === 'none') hardFailure = true;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const claudeHookSet = resolveHookFileSet(PKG_ROOT, claudeHooksDir);
|
|
938
|
+
reportHookSetWarning(claudeHookSet);
|
|
939
|
+
|
|
940
|
+
const hookResult = removeHookFiles(claudeHooksDir, claudeHookSet.files, args.apply);
|
|
941
|
+
const settingsResult = stripSettingsJson(
|
|
942
|
+
claudeSettings,
|
|
943
|
+
claudeHooksDir,
|
|
944
|
+
claudeHookSet.files,
|
|
945
|
+
args.apply,
|
|
946
|
+
);
|
|
883
947
|
const commandResult = removeCommands(args.apply, args.forceCommands);
|
|
884
948
|
|
|
885
949
|
// Wiki-side cleanup: the git pre-commit hook and the shell rc block init.mjs
|
|
@@ -946,8 +1010,15 @@ let codexExtSettings = { stripped: [] };
|
|
|
946
1010
|
if (args.codex) {
|
|
947
1011
|
const codexHooksDir = join(HOME, '.codex', 'hooks');
|
|
948
1012
|
const codexSettings = join(HOME, '.codex', 'settings.json');
|
|
949
|
-
|
|
950
|
-
|
|
1013
|
+
const codexHookSet = resolveHookFileSet(PKG_ROOT, codexHooksDir);
|
|
1014
|
+
reportHookSetWarning(codexHookSet);
|
|
1015
|
+
codexHookResult = removeHookFiles(codexHooksDir, codexHookSet.files, args.apply);
|
|
1016
|
+
codexSettingsResult = stripSettingsJson(
|
|
1017
|
+
codexSettings,
|
|
1018
|
+
codexHooksDir,
|
|
1019
|
+
codexHookSet.files,
|
|
1020
|
+
args.apply,
|
|
1021
|
+
);
|
|
951
1022
|
codexExtResult = removeExtensions('codex', args.apply, args.forceExtensions);
|
|
952
1023
|
codexExtSettings = stripExtensionSettings(
|
|
953
1024
|
codexSettings,
|
|
@@ -1105,3 +1176,10 @@ if (
|
|
|
1105
1176
|
}
|
|
1106
1177
|
|
|
1107
1178
|
console.log(lines.join('\n\n'));
|
|
1179
|
+
|
|
1180
|
+
// See resolveHookFileSet / reportHookSetWarning above (major D): neither the
|
|
1181
|
+
// package's own hooks.json nor a provenance record could identify what to
|
|
1182
|
+
// remove for at least one target. The report above already named what remains;
|
|
1183
|
+
// this is what tells an automated caller (or a script chaining onto this one)
|
|
1184
|
+
// that the run did not fully succeed, rather than exiting 0 on a silent skip.
|
|
1185
|
+
if (hardFailure) process.exit(1);
|