hypomnema 1.8.0 → 1.8.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.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.ko.md +8 -6
  5. package/README.md +8 -6
  6. package/commands/crystallize.md +32 -6
  7. package/commands/graph.md +7 -4
  8. package/commands/lint.md +8 -1
  9. package/commands/query.md +6 -4
  10. package/commands/resume.md +1 -0
  11. package/commands/verify.md +12 -1
  12. package/docs/ARCHITECTURE.md +26 -12
  13. package/docs/CONTRIBUTING.md +15 -6
  14. package/hooks/hypo-compact-guard.mjs +126 -23
  15. package/hooks/hypo-cwd-change.mjs +20 -19
  16. package/hooks/hypo-first-prompt.mjs +31 -16
  17. package/hooks/hypo-lookup.mjs +10 -5
  18. package/hooks/hypo-session-start.mjs +180 -2
  19. package/hooks/hypo-shared.mjs +410 -83
  20. package/hooks/hypo-web-fetch-ingest.mjs +9 -13
  21. package/package.json +2 -1
  22. package/scripts/doctor.mjs +32 -3
  23. package/scripts/graph.mjs +22 -2
  24. package/scripts/init.mjs +5 -1
  25. package/scripts/lib/crystallize-args.mjs +38 -2
  26. package/scripts/lib/crystallize-close-apply.mjs +745 -450
  27. package/scripts/lint.mjs +242 -39
  28. package/scripts/query.mjs +22 -2
  29. package/scripts/resume.mjs +177 -2
  30. package/scripts/upgrade.mjs +2 -2
  31. package/scripts/verify.mjs +22 -2
  32. package/templates/SCHEMA.md +23 -1
  33. package/templates/hypo-automation.md +4 -2
  34. package/templates/hypo-config.md +1 -1
  35. package/templates/hypo-guide.md +1 -1
  36. package/skills/crystallize/SKILL.md +0 -189
  37. package/skills/graph/SKILL.md +0 -58
  38. package/skills/ingest/SKILL.md +0 -107
  39. package/skills/lint/SKILL.md +0 -59
  40. package/skills/query/SKILL.md +0 -62
  41. package/skills/verify/SKILL.md +0 -96
@@ -15,11 +15,11 @@
15
15
  * graph before adding a new source.
16
16
  *
17
17
  * Output contract — Claude Code docs, "Add context for Claude":
18
- * PostToolUse uses **nested** hookSpecificOutput.additionalContext, NOT
19
- * the top-level additionalContext that UserPromptSubmit hooks use.
20
- * buildOutput() in hypo-shared.mjs is the top-level helper used by
21
- * hypo-first-prompt / hypo-lookup; intentionally not reused here. See
22
- * commit 515458f for the per-event matrix this codebase follows.
18
+ * PostToolUse uses **nested** hookSpecificOutput.additionalContext, the
19
+ * same shape every other injecting event in this codebase now uses.
20
+ * buildOutput() in hypo-shared.mjs emits that nested shape given the
21
+ * hookEventName, so this hook calls it too instead of building the
22
+ * object by hand.
23
23
  *
24
24
  * URL redaction:
25
25
  * additionalContext lands in the transcript. Query strings and
@@ -41,7 +41,7 @@
41
41
  * in-hook failure branch is needed.
42
42
  */
43
43
 
44
- import { isGateSkipped } from './hypo-shared.mjs';
44
+ import { isGateSkipped, buildOutput } from './hypo-shared.mjs';
45
45
 
46
46
  let input = {};
47
47
  try {
@@ -58,13 +58,9 @@ try {
58
58
  }
59
59
 
60
60
  const context = buildContext(input);
61
- const output = { continue: true, suppressOutput: true };
62
- if (context) {
63
- output.hookSpecificOutput = {
64
- hookEventName: 'PostToolUse',
65
- additionalContext: context,
66
- };
67
- }
61
+ const output = context
62
+ ? buildOutput('PostToolUse', context, { continue: true, suppressOutput: true })
63
+ : { continue: true, suppressOutput: true };
68
64
  console.log(JSON.stringify(output));
69
65
 
70
66
  function buildContext(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypomnema",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "LLM-native personal wiki system for Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -87,6 +87,7 @@
87
87
  "fix:verify": "node scripts/fix-status-verify.mjs",
88
88
  "graph": "node scripts/graph.mjs",
89
89
  "smoke-pack": "node scripts/smoke-pack.mjs",
90
+ "check:release-vehicle": "node scripts/check-release-vehicle.mjs",
90
91
  "check:versions": "node scripts/check-versions.mjs",
91
92
  "smoke:plugin": "node scripts/smoke-plugin.mjs",
92
93
  "check:bilingual": "node scripts/check-bilingual.mjs --changelog",
@@ -34,6 +34,7 @@ import {
34
34
  collectProjectWorkingDirs,
35
35
  detectSessionCloseArtifact,
36
36
  localAndUtcDates,
37
+ normalizeVerifiedScope,
37
38
  SESSION_CLOSED_MARKER_STALE_MS,
38
39
  isUsablePkgRootLocal,
39
40
  selfLocationPkgRootFrom,
@@ -916,14 +917,32 @@ function deriveCommitProjects(hypoDir, hash) {
916
917
  // an artifact with no verifiable scope, so this never matches, ever.
917
918
  // • 'projects' — a project file, or a commit that touched one or more
918
919
  // identifiable projects/<slug>/ paths. EVERY named project must appear
919
- // in the marker's own `projects` list.
920
+ // in the marker's own `projects` list, AND, when the marker carries a
921
+ // `verified_scope` (session-close-scope-boundary spec §3), in that
922
+ // scope's own `projects` too — an AND, never an OR. `projects` is
923
+ // evidence-based attribution; `verified_scope` is what the gate that
924
+ // wrote this marker actually checked, and the two can diverge (an
925
+ // unnarrowed gate run can attribute to more projects than it verified).
926
+ // A `verified_scope` of kind 'log-only' verified no project at all, so it
927
+ // can never cover a 'projects'-scoped artifact. A marker with NO
928
+ // `verified_scope` (every marker before this field existed) skips this
929
+ // extra check entirely — it neither tightens nor loosens the membership
930
+ // check above, so old markers read back byte-for-byte as before.
920
931
  function markerCoversArtifact(marker, artifact) {
921
932
  if (!marker.dates.includes(artifact.date)) return false;
922
933
  switch (artifact.scope.kind) {
923
934
  case 'root-universal':
924
935
  return true;
925
936
  case 'projects':
926
- return artifact.scope.projects.every((p) => marker.projects.includes(p));
937
+ if (!artifact.scope.projects.every((p) => marker.projects.includes(p))) return false;
938
+ if (!marker.verifiedScope) return true;
939
+ // Unreachable for a 'projects' artifact today: a log-only close writes
940
+ // `projects: []`, so the membership check above already returned false.
941
+ // Kept deliberately, not by oversight — it states the kind's meaning
942
+ // (log-only verified no project at all) so this stays correct if
943
+ // `projects` ever carries something for a log-only close.
944
+ if (marker.verifiedScope.kind === 'log-only') return false;
945
+ return artifact.scope.projects.every((p) => marker.verifiedScope.projects.includes(p));
927
946
  default:
928
947
  return false; // 'unscoped'
929
948
  }
@@ -1035,7 +1054,17 @@ function checkSessionCloseArtifacts(hypoDir) {
1035
1054
  // and doctor has no corroborating signal of its own to add, so it
1036
1055
  // must refuse it too rather than re-opening the same hole standalone.
1037
1056
  const projects = Array.isArray(data?.projects) ? data.projects.filter(Boolean) : [];
1038
- markers.push({ projects: [...new Set(projects)], dates: localAndUtcDates(new Date(ts)) });
1057
+ // verified_scope (session-close-scope-boundary spec §3): the SAME
1058
+ // collapse the writer runs (hooks/hypo-shared.mjs), not a hand-rolled
1059
+ // copy — a shape this reader doesn't recognize, or a 'project'/'global'
1060
+ // scope with an empty `projects`, reads back as "field absent" (`null`),
1061
+ // never as a false claim.
1062
+ const verifiedScope = normalizeVerifiedScope(data?.verified_scope);
1063
+ markers.push({
1064
+ projects: [...new Set(projects)],
1065
+ dates: localAndUtcDates(new Date(ts)),
1066
+ verifiedScope,
1067
+ });
1039
1068
  } catch {
1040
1069
  // corrupt marker — not this check's job to clean up
1041
1070
  }
package/scripts/graph.mjs CHANGED
@@ -22,12 +22,32 @@ import { collectPagesGraph, extractWikilinks } from './lib/wikilink.mjs';
22
22
 
23
23
  // ── arg parsing ───────────────────────────────────────────────────────────────
24
24
 
25
+ const ALLOWED_FLAGS = ['--hypo-dir=<path>', '--format=<fmt>', '--min-edges=<n>'];
26
+
25
27
  function parseArgs(argv) {
26
28
  const args = { hypoDir: null, format: 'json', minEdges: 0 };
27
29
  for (const arg of argv.slice(2)) {
28
- if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
29
- else if (arg.startsWith('--format=')) args.format = arg.slice(9);
30
+ if (arg.startsWith('--hypo-dir=')) {
31
+ // See lint.mjs's parseArgs for why an empty raw value (--hypo-dir=,
32
+ // or --hypo-dir="$VAULT" with VAULT unset) is rejected instead of
33
+ // falling through to the default-resolution branch below.
34
+ const raw = arg.slice(11);
35
+ if (!raw) {
36
+ console.error(`graph.mjs: --hypo-dir requires a non-empty value (got '${arg}')`);
37
+ console.error(`graph.mjs accepts: ${ALLOWED_FLAGS.join(', ')}`);
38
+ process.exit(2);
39
+ }
40
+ args.hypoDir = expandHome(raw);
41
+ } else if (arg.startsWith('--format=')) args.format = arg.slice(9);
30
42
  else if (arg.startsWith('--min-edges=')) args.minEdges = parseInt(arg.slice(12), 10) || 0;
43
+ else {
44
+ // No positional arguments here either, so an unmatched arg is rejected
45
+ // rather than dropped. See lint.mjs's parseArgs for the background
46
+ // this closes.
47
+ console.error(`graph.mjs: unrecognized argument '${arg}'`);
48
+ console.error(`graph.mjs accepts: ${ALLOWED_FLAGS.join(', ')}`);
49
+ process.exit(2);
50
+ }
31
51
  }
32
52
  if (!args.hypoDir) {
33
53
  const info = resolveHypoRootInfo();
package/scripts/init.mjs CHANGED
@@ -173,7 +173,11 @@ Init options:
173
173
  --lint-strict Opt-in: also gate the wiki pre-commit hook on
174
174
  \`lint --strict\` (promotes STRICT_PROMOTE_IDS warnings
175
175
  to a blocking error), sequenced after the .hypoignore
176
- guard. Off by default; re-run init to add or drop it.
176
+ guard. Exception: root hot.md/log.md's own
177
+ no-frontmatter warning stays a warn even under this
178
+ flag, so a legacy vault predating the frontmatter
179
+ convention still commits. Off by default; re-run init
180
+ to add or drop it.
177
181
  --dry-run Show what would be done without making changes
178
182
  --version Print the installed package version and exit
179
183
  --help, -h Show this help message
@@ -3,6 +3,22 @@ import { isValidProjectName } from './project-create.mjs';
3
3
 
4
4
  // ── arg parsing ──────────────────────────────────────────────────────────────
5
5
 
6
+ const ALLOWED_FLAGS = [
7
+ '--hypo-dir=<path>',
8
+ '--min-group=<n>',
9
+ '--check-session-close',
10
+ '--apply-session-close',
11
+ '--mark-session-closed',
12
+ '--log-only',
13
+ '--session-id=<id>',
14
+ '--payload=<path|->',
15
+ '--transcript-path=<path>',
16
+ '--session-cwd=<path>',
17
+ '--project=<slug>',
18
+ '--force',
19
+ '--json',
20
+ ];
21
+
6
22
  export function parseArgs(argv) {
7
23
  const args = {
8
24
  hypoDir: null,
@@ -19,8 +35,20 @@ export function parseArgs(argv) {
19
35
  project: null,
20
36
  };
21
37
  for (const arg of argv.slice(2)) {
22
- if (arg.startsWith('--hypo-dir=')) args.hypoDir = expandHome(arg.slice(11));
23
- else if (arg.startsWith('--min-group=')) args.minGroup = parseInt(arg.slice(12), 10) || 2;
38
+ if (arg.startsWith('--hypo-dir=')) {
39
+ // See scripts/lint.mjs's parseArgs for why an empty raw value
40
+ // (--hypo-dir=, or --hypo-dir="$VAULT" with VAULT unset) is rejected
41
+ // instead of falling through to resolveHypoRoot() below. For this
42
+ // script specifically, a silent fallback here means a session-close
43
+ // apply writes to the wrong vault.
44
+ const raw = arg.slice(11);
45
+ if (!raw) {
46
+ console.error(`crystallize.mjs: --hypo-dir requires a non-empty value (got '${arg}')`);
47
+ console.error(`crystallize.mjs accepts: ${ALLOWED_FLAGS.join(', ')}`);
48
+ process.exit(2);
49
+ }
50
+ args.hypoDir = expandHome(raw);
51
+ } else if (arg.startsWith('--min-group=')) args.minGroup = parseInt(arg.slice(12), 10) || 2;
24
52
  else if (arg === '--check-session-close') args.checkSessionClose = true;
25
53
  else if (arg === '--apply-session-close') args.applySessionClose = true;
26
54
  else if (arg === '--mark-session-closed') args.markSessionClosed = true;
@@ -32,6 +60,14 @@ export function parseArgs(argv) {
32
60
  else if (arg.startsWith('--project=')) args.project = arg.slice(10);
33
61
  else if (arg === '--force') args.force = true;
34
62
  else if (arg === '--json') args.json = true;
63
+ else {
64
+ // No positional arguments here either, so an unmatched arg is rejected
65
+ // rather than dropped. See scripts/lint.mjs's parseArgs for the
66
+ // background this closes.
67
+ console.error(`crystallize.mjs: unrecognized argument '${arg}'`);
68
+ console.error(`crystallize.mjs accepts: ${ALLOWED_FLAGS.join(', ')}`);
69
+ process.exit(2);
70
+ }
35
71
  }
36
72
  if (!args.hypoDir) args.hypoDir = resolveHypoRoot();
37
73
  // --project=<slug> override (check/mark only). Validate the SYNTAX here so a