hypomnema 1.4.2 → 1.5.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.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/commands/audit.md +1 -1
  6. package/commands/crystallize.md +8 -8
  7. package/commands/doctor.md +1 -1
  8. package/commands/feedback.md +2 -2
  9. package/commands/graph.md +1 -1
  10. package/commands/ingest.md +1 -1
  11. package/commands/init.md +2 -2
  12. package/commands/lint.md +1 -1
  13. package/commands/query.md +1 -1
  14. package/commands/rename.md +1 -1
  15. package/commands/resume.md +1 -1
  16. package/commands/stats.md +1 -1
  17. package/commands/uninstall.md +4 -2
  18. package/commands/upgrade.md +1 -1
  19. package/commands/verify.md +1 -1
  20. package/docs/ARCHITECTURE.md +2 -2
  21. package/docs/CONTRIBUTING.md +8 -7
  22. package/hooks/hypo-auto-commit.mjs +2 -2
  23. package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
  24. package/hooks/hypo-compact-guard.mjs +2 -2
  25. package/hooks/hypo-cwd-change.mjs +27 -37
  26. package/hooks/hypo-personal-check.mjs +37 -22
  27. package/hooks/hypo-session-end.mjs +1 -1
  28. package/hooks/hypo-session-record.mjs +2 -2
  29. package/hooks/hypo-session-start.mjs +34 -40
  30. package/hooks/hypo-shared.mjs +271 -71
  31. package/hooks/version-check.mjs +1 -1
  32. package/package.json +5 -1
  33. package/scripts/check-tracker-ids.mjs +69 -31
  34. package/scripts/crystallize.mjs +82 -38
  35. package/scripts/doctor.mjs +7 -7
  36. package/scripts/feedback-sync.mjs +5 -5
  37. package/scripts/feedback.mjs +9 -10
  38. package/scripts/init.mjs +7 -7
  39. package/scripts/lib/check-tracker-ids.mjs +90 -32
  40. package/scripts/lib/design-history-stale.mjs +1 -1
  41. package/scripts/lib/extensions.mjs +7 -7
  42. package/scripts/lib/failure-type.mjs +1 -1
  43. package/scripts/lib/feedback-scope.mjs +1 -1
  44. package/scripts/lib/project-create.mjs +2 -2
  45. package/scripts/lib/template-schema-version.mjs +1 -1
  46. package/scripts/lib/wd-match.mjs +181 -0
  47. package/scripts/lib/wikilink.mjs +1 -1
  48. package/scripts/lint.mjs +5 -5
  49. package/scripts/rename.mjs +1 -1
  50. package/scripts/resume.mjs +20 -22
  51. package/scripts/session-audit.mjs +1 -1
  52. package/scripts/stats.mjs +3 -3
  53. package/scripts/uninstall.mjs +3 -3
  54. package/scripts/upgrade.mjs +17 -18
  55. package/skills/crystallize/SKILL.md +7 -7
  56. package/skills/graph/SKILL.md +2 -2
  57. package/skills/ingest/SKILL.md +4 -4
  58. package/skills/lint/SKILL.md +2 -2
  59. package/skills/query/SKILL.md +2 -2
  60. package/skills/verify/SKILL.md +2 -2
  61. package/templates/SCHEMA.md +1 -1
  62. package/templates/hypo-config.md +1 -1
  63. package/templates/hypo-guide.md +20 -14
  64. package/templates/projects/_template/hot.md +1 -1
  65. package/scripts/fix-status-verify.mjs +0 -256
  66. package/scripts/lib/adr-corpus.mjs +0 -79
  67. package/scripts/lib/fix-manifest.mjs +0 -109
  68. package/scripts/lib/fix-status-verify.mjs +0 -439
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypomnema",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "LLM-native personal wiki system for Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,6 +14,10 @@
14
14
  "!scripts/bump-version.mjs",
15
15
  "!scripts/smoke-pack.mjs",
16
16
  "!scripts/smoke-plugin.mjs",
17
+ "!scripts/fix-status-verify.mjs",
18
+ "!scripts/lib/fix-status-verify.mjs",
19
+ "!scripts/lib/fix-manifest.mjs",
20
+ "!scripts/lib/adr-corpus.mjs",
17
21
  "commands/",
18
22
  "hooks/",
19
23
  "skills/",
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * check-tracker-ids.mjs — CLI gate: no wiki-internal tracker IDs (ISSUE-N,
4
- * fix #N) in OSS-public artifacts.
4
+ * fix #N, FEAT-N, IMPR-N, PRAC-N) in OSS-public artifacts.
5
5
  *
6
6
  * Rule source: CLAUDE.md learned_behaviors (no-internal-tracker-ids-in-oss-
7
7
  * artifacts, 2026-06-09). The repo ships through npm + a Claude Code plugin;
8
- * a `fix #N` / `ISSUE-N` in any shipped file or in README/CHANGELOG is a
9
- * dangling pointer into the maintainer's PRIVATE wiki tracker. GitHub refs
10
- * (`PR #N`, `(#N)`, `#N`, issue URLs) are legitimate and never flagged.
8
+ * an `ISSUE-N` / `fix #N` / `FEAT-N` / `IMPR-N` / `PRAC-N` in any shipped file or
9
+ * in README/CHANGELOG is a dangling pointer into the maintainer's PRIVATE wiki
10
+ * tracker. GitHub refs (`PR #N`, `(#N)`, `#N`, issue URLs) are legitimate and
11
+ * never flagged.
11
12
  *
12
13
  * Modes:
13
14
  * --all (default) Walk the public-artifact scope and scan every text
@@ -31,14 +32,16 @@
31
32
  * auto-ships it) + shipped trees commands/ hooks/ scripts/ skills/ templates/
32
33
  * docs/ .github/ .claude-plugin/. Excluded: tests/ and qa-runs/ (internal
33
34
  * maintainer artifacts — a tracker id in a test description aids traceability
34
- * and never reaches an installed user), node_modules/, .git/. The checker's OWN
35
- * sources are scanned (NOT exempt); their examples use `N` placeholders so they
36
- * stay clean.
35
+ * and never reaches an installed user), node_modules/, .git/, and the
36
+ * fix-status-verify subsystem (EXCLUDED_FILES maintainer-only, carries wiki
37
+ * `decisions/` paths as runtime data, un-shipped from the npm package). The
38
+ * checker's OWN sources are scanned (NOT exempt); their examples use `N`
39
+ * placeholders so they stay clean.
37
40
  *
38
- * ADR scope (narrower): README.md, README.ko.md, and docs/ additionally block
39
- * `ADR NNNN` / `decisions/NNNN` wiki-ADR pointers (USER_FACING_PATTERNS). Shipped
40
- * code comments and CHANGELOG history keep their ADR anchors, so this set is
41
- * applied per-file via patternsFor() only inside that doc surface.
41
+ * ADR scope: `ADR NNNN` / `decisions/NNNN` wiki-ADR pointers (DECISION_PATTERNS)
42
+ * are blocked everywhere in scope EXCEPT CHANGELOG.md, whose version history
43
+ * legitimately cites the decision behind a release line. patternsFor() applies the
44
+ * broader set to every in-scope file but the CHANGELOG.
42
45
  */
43
46
 
44
47
  import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
@@ -50,7 +53,7 @@ import {
50
53
  stripScissors,
51
54
  messageHasGitTemplate,
52
55
  BLOCKED_PATTERNS,
53
- USER_FACING_PATTERNS,
56
+ DECISION_PATTERNS,
54
57
  TAG_BODY_PATTERNS,
55
58
  } from './lib/check-tracker-ids.mjs';
56
59
  import {
@@ -70,7 +73,8 @@ const REPO_ROOT = process.env.CHECK_TRACKER_ROOT || join(__dirname, '..');
70
73
 
71
74
  const RULE_REF =
72
75
  'Rule: CLAUDE.md learned_behaviors (no-internal-tracker-ids-in-oss-artifacts). ' +
73
- 'OSS-public artifacts must not reference the private wiki tracker (ISSUE-N / fix #N). ' +
76
+ 'OSS-public artifacts must not reference the private wiki tracker ' +
77
+ '(ISSUE-N / fix #N / FEAT-N / IMPR-N / PRAC-N). ' +
74
78
  'Use a GitHub ref (#N / PR #N) or drop the reference.';
75
79
 
76
80
  // Top-level public-artifact entry points. Files are scanned directly; dirs are
@@ -94,15 +98,49 @@ const SCOPE_DIRS = [
94
98
  // and never reaches an installed user). The checker's OWN sources are NOT
95
99
  // excluded — they use `N` placeholders in their examples so they scan clean.
96
100
  const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'tests', 'qa-runs']);
97
- const EXCLUDED_FILES = new Set();
101
+ // The fix-status-verify subsystem (run only via `npm run fix:verify`, never by a
102
+ // shipped command/hook/skill) is a maintainer evidence-verification tool. Its
103
+ // manifest carries `decisions/NNNN` wiki paths as RUNTIME DATA (resolved against
104
+ // the maintainer's local wiki), so the anchors cannot be stripped the way a
105
+ // comment can. It is removed from the npm package (package.json `files`) and
106
+ // excluded here so the gate does not flag its load-bearing data.
107
+ // Stored POSIX-style ('/'); paths are normalized before lookup (see toPosix).
108
+ const EXCLUDED_FILES = new Set([
109
+ 'scripts/fix-status-verify.mjs',
110
+ 'scripts/lib/fix-status-verify.mjs',
111
+ 'scripts/lib/fix-manifest.mjs',
112
+ 'scripts/lib/adr-corpus.mjs',
113
+ ]);
98
114
 
99
115
  // Only text artifacts. Binary / lockfiles add noise and never carry prose refs.
100
- const TEXT_EXT = new Set(['.md', '.mjs', '.js', '.cjs', '.json', '.yml', '.yaml', '.sh', '.txt']);
116
+ // .svg is included because the shipped docs/assets/ logos are hand-authored XML
117
+ // text whose <title>/<desc>/comment fields could carry a dangling wiki pointer.
118
+ const TEXT_EXT = new Set([
119
+ '.md',
120
+ '.mjs',
121
+ '.js',
122
+ '.cjs',
123
+ '.json',
124
+ '.yml',
125
+ '.yaml',
126
+ '.sh',
127
+ '.txt',
128
+ '.svg',
129
+ ]);
130
+
131
+ // Normalize OS separators to '/' so the two scan modes classify a path the same
132
+ // way on every host: --all builds relPath via relative() (platform sep, so '\' on
133
+ // Windows) while --staged takes git's output (always '/'). Without this, a Windows
134
+ // --staged run would neither exclude the verifier files nor recognize scope dirs.
135
+ // No-op on POSIX (sep is already '/').
136
+ function toPosix(relPath) {
137
+ return sep === '/' ? relPath : relPath.split(sep).join('/');
138
+ }
101
139
 
102
140
  function isExcludedRel(relPath) {
103
- const parts = relPath.split(sep);
104
- if (parts.some((p) => EXCLUDED_DIRS.has(p))) return true;
105
- if (EXCLUDED_FILES.has(relPath)) return true;
141
+ const p = toPosix(relPath);
142
+ if (p.split('/').some((seg) => EXCLUDED_DIRS.has(seg))) return true;
143
+ if (EXCLUDED_FILES.has(p)) return true;
106
144
  return false;
107
145
  }
108
146
 
@@ -116,22 +154,22 @@ function isTextFile(p) {
116
154
  function isInScope(relPath) {
117
155
  if (isExcludedRel(relPath)) return false;
118
156
  if (!isTextFile(relPath)) return false;
119
- if (SCOPE_FILES.includes(relPath)) return true;
120
- return SCOPE_DIRS.includes(relPath.split(sep)[0]);
157
+ const p = toPosix(relPath);
158
+ if (SCOPE_FILES.includes(p)) return true;
159
+ return SCOPE_DIRS.includes(p.split('/')[0]);
121
160
  }
122
161
 
123
- // User-facing prose surface: README.md, README.ko.md, and the docs/ tree. These
124
- // additionally block `ADR NNNN` / `decisions/NNNN` (dangling wiki-ADR pointers).
125
- // Everything else in scope shipped code, CHANGELOG, package.json — keeps its ADR
126
- // anchors, so the broader pattern set is applied here ONLY.
127
- const USER_FACING_DOCS = new Set(['README.md', 'README.ko.md']);
128
- function isUserFacingDoc(relPath) {
129
- return USER_FACING_DOCS.has(relPath) || relPath.split(sep)[0] === 'docs';
130
- }
162
+ // `ADR NNNN` / `decisions/NNNN` are dangling pointers into the maintainer's
163
+ // private wiki ADR set, so they are blocked everywhere in scope EXCEPT the
164
+ // CHANGELOG, whose version history legitimately cites the decision behind a
165
+ // release line (and never reaches an installed user as a live link). The verifier
166
+ // subsystem that carries `decisions/` paths as runtime data is already removed by
167
+ // EXCLUDED_FILES, so it is never reached here.
168
+ const ADR_EXEMPT_FILES = new Set(['CHANGELOG.md']);
131
169
  function patternsFor(relPath) {
132
- return isUserFacingDoc(relPath)
133
- ? [...BLOCKED_PATTERNS, ...USER_FACING_PATTERNS]
134
- : BLOCKED_PATTERNS;
170
+ return ADR_EXEMPT_FILES.has(toPosix(relPath))
171
+ ? BLOCKED_PATTERNS
172
+ : [...BLOCKED_PATTERNS, ...DECISION_PATTERNS];
135
173
  }
136
174
 
137
175
  // Recursively collect in-scope text files under an absolute dir.
@@ -83,6 +83,7 @@ import {
83
83
  sessionClosedMarkerPath,
84
84
  readSessionClosedMarker,
85
85
  partitionLintScope,
86
+ isUnderProjectDirs,
86
87
  sessionLogShardPath,
87
88
  sessionLogReadCandidates,
88
89
  sessionLogScopePath,
@@ -177,7 +178,7 @@ function parseArgs(argv) {
177
178
  return args;
178
179
  }
179
180
 
180
- // ── session-close hard gate (ADR 0055) ───────────────────────────────────────
181
+ // ── session-close hard gate ───────────────────────────────────────────────────
181
182
  // The marker attests "the USER closed this session". Its evidence transcript is
182
183
  // resolved STRICTLY from the session id (a globally-unique UUID) by globbing the
183
184
  // Claude project dirs — never from a CLI arg. A model owns the whole subprocess
@@ -208,7 +209,7 @@ function requireProjectDir(args, slug) {
208
209
  // flow can self-verify before /compact triggers PreCompact.
209
210
 
210
211
  function runSessionCloseCheck(args) {
211
- // ADR 0046: the check mirrors the FULL PreCompact gate via the shared
212
+ // The check mirrors the FULL PreCompact gate via the shared
212
213
  // precompactGateStatus (close files + lint + design-history + feedback
213
214
  // projection), not just the close files — so a green check means /compact
214
215
  // won't block on a human-fixable issue. Pass --transcript-path to widen the
@@ -221,9 +222,9 @@ function runSessionCloseCheck(args) {
221
222
  // (codex design Finding 2).
222
223
  //
223
224
  // --project=<slug> narrows BOTH the close status and the lint scope to that one
224
- // project: a project-scoped DIAGNOSTIC, NOT the global compact-ready verdict
225
- // (ADR 0046 caveat below). It is check-only the marker writers stay global so
226
- // the marker == compact-ready invariant holds (ADR 0047). When narrowed, the
225
+ // project: a project-scoped DIAGNOSTIC, NOT the global compact-ready verdict.
226
+ // It is check-only: the marker writers stay global so
227
+ // the marker == compact-ready invariant holds. When narrowed, the
227
228
  // transcript widening is suppressed: a transcript touch in some OTHER project
228
229
  // would re-add that project's files to the lint scope and re-block the scoped
229
230
  // check, defeating the point. The global (no --project) check keeps widening.
@@ -238,9 +239,9 @@ function runSessionCloseCheck(args) {
238
239
  });
239
240
  const close = status.close;
240
241
 
241
- // ADR 0047: when a --session-id is supplied, report whether THIS session's
242
+ // When a --session-id is supplied, report whether THIS session's
242
243
  // per-session marker (the Stop-chain completion signal) exists. This is a
243
- // separate field, NOT folded into `ok` `ok` stays the ADR 0046 compact-
244
+ // separate field, NOT folded into `ok`: `ok` stays the compact-
244
245
  // readiness verdict. A green gate with marker_present=false is exactly the
245
246
  // hand-edit close state: close is compact-ready but the Stop hook will
246
247
  // still block until the marker is written.
@@ -328,7 +329,7 @@ function runSessionCloseCheck(args) {
328
329
  }
329
330
  // Beyond the close files: the rest of the PreCompact gate (lint, design-history,
330
331
  // feedback over-cap/conflict). These are what made a "close-complete" check
331
- // disagree with the real /compact gate before ADR 0046.
332
+ // disagree with the real /compact gate before this check was added.
332
333
  for (const b of status.blockers) {
333
334
  if (b.type !== 'close') console.log(` ✗ ${b.reason}`);
334
335
  }
@@ -336,7 +337,7 @@ function runSessionCloseCheck(args) {
336
337
  console.log('');
337
338
  for (const n of status.notices) console.log(` · ${n.reason}`);
338
339
  }
339
- // ADR 0047: surface the per-session marker state (separate from compact-
340
+ // Surface the per-session marker state (separate from compact-
340
341
  // readiness) so a green-but-unmarked close is visible at verify time.
341
342
  if (args.sessionId) {
342
343
  console.log('');
@@ -450,7 +451,7 @@ function todayLocal() {
450
451
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
451
452
  }
452
453
 
453
- // Spec §5.2.7 / §8.3 + ADR 0029: 4 mandatory + 2 optional (`log`, `openQuestions`).
454
+ // Spec §5.2.7 / §8.3: 4 mandatory + 2 optional (`log`, `openQuestions`).
454
455
  // The payload shape MUST mirror that contract — missing a mandatory field is a
455
456
  // payload bug, not a no-op. Caller is the LLM session-close flow, which composes
456
457
  // the payload deliberately; partial payloads must fail loudly so caller fixes
@@ -501,14 +502,14 @@ function validatePayloadShape(payload) {
501
502
  return errs;
502
503
  }
503
504
 
504
- // ── session-close marker (ADR 0022 amendment 2026-05-19) ──────
505
+ // ── session-close marker (amendment 2026-05-19) ───────────────
505
506
  // Standalone marker writer. Used when the LLM closes the session via direct
506
507
  // Write tool calls (not --apply-session-close). Hook `hypo-auto-minimal-
507
508
  // crystallize` is the only Reader; writer authority is intentionally split
508
509
  // between this CLI and the auto-write at the tail of applySessionClose.
509
510
  //
510
511
  // Contract: the marker is written only when the FULL /compact gate
511
- // (precompactGateStatus, ADR 0046) is green. A failed gate exits 1 with no
512
+ // (precompactGateStatus) is green. A failed gate exits 1 with no
512
513
  // marker — the next Stop hook re-blocks.
513
514
 
514
515
  function runMarkSessionClosed(args) {
@@ -520,12 +521,12 @@ function runMarkSessionClosed(args) {
520
521
  // --project=<slug> on --mark is ATTRIBUTION ONLY (the marker's `project` field).
521
522
  // The gate stays GLOBAL — never narrowed — because a marker that narrowed its
522
523
  // gate could attest compact-ready while PreCompact re-checks all of today's
523
- // projects and stays red (the marker == compact-ready invariant, ADR 0047 /
524
+ // projects and stays red (the marker == compact-ready invariant,
524
525
  // codex design finding 1/2). Validate the attribution slug exists as a
525
526
  // directory, exactly as --check does, but only when it is actually used (a
526
527
  // --log-only mark attributes to no project, so --project is moot there).
527
528
  if (args.project && !args.logOnly) requireProjectDir(args, args.project);
528
- // ADR 0047: the per-session marker is the THIRD session-close completion
529
+ // The per-session marker is the THIRD session-close completion
529
530
  // signal (after the PreCompact gate and `--check-session-close`). It must use
530
531
  // the SAME gate that governs /compact — precompactGateStatus — so the marker
531
532
  // can never attest "closed" while /compact would still block. This subsumes
@@ -570,7 +571,7 @@ function runMarkSessionClosed(args) {
570
571
  }
571
572
  process.exit(1);
572
573
  }
573
- // User-close hard gate (ADR 0055): the compact gate above only proves the wiki
574
+ // User-close hard gate: the compact gate above only proves the wiki
574
575
  // is compact-ready; it does NOT prove the USER asked to close. Refuse the marker
575
576
  // unless the transcript carries a genuine user close signal (NL close phrase,
576
577
  // /compact, or an AskUserQuestion close answer). This is the hard backstop for
@@ -618,7 +619,7 @@ function runMarkSessionClosed(args) {
618
619
  scope: args.logOnly ? 'log-only' : 'project',
619
620
  date: status.dates[0],
620
621
  notices: gate.notices,
621
- // ADR 0047: pure feedback-projection drift is a non-blocker the marker
622
+ // pure feedback-projection drift is a non-blocker: the marker
622
623
  // attests "compact-ready (no human-fixable blocker)", and the PreCompact
623
624
  // hook self-heals the projection (feedback-sync --write) at /compact. Surface
624
625
  // the deferral so the caller knows MEMORY/CLAUDE sync is pending, not lost.
@@ -652,7 +653,7 @@ function applySessionClose(args) {
652
653
  // for any actual apply work (readPayload below surfaces "payload is
653
654
  // required" the same way it always has).
654
655
  if (!args.force && !args.payload) {
655
- // ADR 0043: no-payload "already complete?" probe uses the
656
+ // No-payload "already complete?" probe uses the
656
657
  // global invariant, not a recency pick.
657
658
  const probe = sessionCloseGlobalStatus(args.hypoDir);
658
659
  if (probe.ok) {
@@ -793,7 +794,7 @@ function applySessionClose(args) {
793
794
  // payloadScope = every file this apply writes or appends. Both lint passes are
794
795
  // judged against it; errors elsewhere are surfaced as notices, never blocking.
795
796
  //
796
- // session-log needs TWO entries (ADR 0050): the daily WRITE target (what this
797
+ // session-log needs TWO entries: the daily WRITE target (what this
797
798
  // apply creates/appends, judged by post-apply lint) AND the freshness EVIDENCE
798
799
  // file. They coincide except in the hybrid cutover month, where a fallback-
799
800
  // aware no-op (the identical entry already lives in the legacy monthly file)
@@ -837,7 +838,7 @@ function applySessionClose(args) {
837
838
  ok: false,
838
839
  stage: 'preflight-lint',
839
840
  error: 'lint preflight failed — apply aborted (no payload bytes written)',
840
- lint: { ...preflightLint, blockingErrors },
841
+ lint: { ...summarizeLintForOutput(preflightLint), blockingErrors },
841
842
  };
842
843
  if (args.json) {
843
844
  console.log(JSON.stringify(out, null, 2));
@@ -875,7 +876,7 @@ function applySessionClose(args) {
875
876
  const rel = join('projects', project, 'session-log', `${date}.md`);
876
877
  const full = join(args.hypoDir, rel);
877
878
  const isPresent = entryAlreadyPresent(payload.sessionLog.entry);
878
- // Fallback-aware idempotency (ADR 0050 hybrid cutover): during the month the
879
+ // Fallback-aware idempotency (hybrid cutover): during the month the
879
880
  // shard takes over, today's entry may already live in the legacy monthly
880
881
  // file from an earlier (pre-cutover) close. Treat presence in EITHER the
881
882
  // daily shard or the legacy monthly file as "already written" so a same-day
@@ -905,7 +906,7 @@ function applySessionClose(args) {
905
906
  // otherwise mistake it for the evidence file. The dated `## [date] ...`
906
907
  // heading lives inside the entry, so freshness / derive / design-history
907
908
  // are unchanged.
908
- // PRAC-17 audit fields. The shard frontmatter is git-tracked and synced, so
909
+ // Audit fields (device, session_id). The shard frontmatter is git-tracked and synced, so
909
910
  // `device` is an INTENTIONAL synced multi-machine identifier (privacy note:
910
911
  // docs/ARCHITECTURE.md). It is a CREATOR-only stamp — only the session/
911
912
  // machine that first seeds the daily shard is recorded; later same-day
@@ -1037,11 +1038,17 @@ function applySessionClose(args) {
1037
1038
  const postLintOk = !postApplyCrashed && postBlocking.length === 0;
1038
1039
  const ok = verification.ok && postLintOk;
1039
1040
 
1040
- // ADR 0022 amendment 2026-05-19: auto-write the per-session
1041
+ // Scope the non-blocking notice to the close-target project: debt under
1042
+ // projects/<project>/ stays listed; debt elsewhere folds to a count so the
1043
+ // same untouched-file debt does not re-list its filenames on every close.
1044
+ const closeScopeNotice = postNotice.filter((e) => isUnderProjectDirs(e.file, [project]));
1045
+ const otherDebtCount = postNotice.length - closeScopeNotice.length;
1046
+
1047
+ // Amendment 2026-05-19: auto-write the per-session
1041
1048
  // closed marker on a verified close. Hook authority is read-only; this is
1042
1049
  // one of the two writer paths (the other is --mark-session-closed standalone).
1043
1050
  //
1044
- // ADR 0047: the marker write is governed by the SAME gate as standalone
1051
+ // The marker write is governed by the SAME gate as standalone
1045
1052
  // --mark-session-closed and /compact (precompactGateStatus), NOT just apply's
1046
1053
  // `ok` + git-clean. Apply's payload preflight/post-apply lint and `ok` still
1047
1054
  // govern apply SUCCESS (exit code below), but the marker must additionally
@@ -1049,13 +1056,13 @@ function applySessionClose(args) {
1049
1056
  // path could issue a marker the standalone path would refuse (the second
1050
1057
  // divergence codex flagged).
1051
1058
  //
1052
- // ADR 0056: apply just wrote the payload, so the tree is dirty by its OWN
1053
- // writes the gate's `uncommitted` git blocker would always trip and the
1059
+ // Apply just wrote the payload, so the tree is dirty by its OWN
1060
+ // writes: the gate's `uncommitted` git blocker would always trip and the
1054
1061
  // marker would be skipped, deferring the close to a manual --mark-session-closed
1055
- // (the ADR 0047 "done but still blocked" regression). Commit the payload HERE, via
1062
+ // ("done but still blocked" regression). Commit the payload HERE, via
1056
1063
  // the SAME .hypoignore-aware helper the auto-commit Stop hook uses, so the gate sees
1057
1064
  // a committed tree. Push stays deferred to the Stop hook; the resulting
1058
- // committed-but-unpushed state is a gate notice, not a blocker (ADR 0056), so
1065
+ // committed-but-unpushed state is a gate notice, not a blocker, so
1059
1066
  // this still marks. A commit failure (not a repo / pre-commit reject / git error)
1060
1067
  // skips the marker WITH a surfaced reason — today's behavior was also "no marker",
1061
1068
  // but silently.
@@ -1077,14 +1084,14 @@ function applySessionClose(args) {
1077
1084
  // (feedback/W8/hot/lint — git is now committed) is resolved.
1078
1085
  markerSkipReason = 'compact-gate-not-ok';
1079
1086
  } else if (!closeTranscript || !hasUserCloseSignal(closeTranscript)) {
1080
- // User-close hard gate (ADR 0055): apply succeeded (payload files written)
1087
+ // User-close hard gate: apply succeeded (payload files written)
1081
1088
  // but the user never signalled session close, so the marker — which attests
1082
1089
  // "user closed" — is withheld. The wiki record stands; the session is simply
1083
1090
  // not marked closed. Surfaced (not silent) so the caller knows.
1084
1091
  markerSkipReason = closeTranscript ? 'no-user-close-signal' : 'transcript-unresolved';
1085
1092
  } else {
1086
1093
  writeSessionClosedMarker(args.hypoDir, args.sessionId, { project });
1087
- // Codex CONCERN (ADR 0055/0056): the writer swallows IO errors (best-effort).
1094
+ // Codex CONCERN: the writer swallows IO errors (best-effort).
1088
1095
  // Verify the file actually landed — mirroring the standalone path — instead of
1089
1096
  // asserting markerWritten=true, so a .cache permission/disk problem surfaces
1090
1097
  // rather than the caller reporting "closed" while the next Stop re-blocks.
@@ -1111,13 +1118,22 @@ function applySessionClose(args) {
1111
1118
  applied,
1112
1119
  skipped,
1113
1120
  verification,
1114
- // ADR 0055: surface the marker outcome instead of skipping silently, so the
1121
+ // Surface the marker outcome instead of skipping silently, so the
1115
1122
  // caller can tell "closed" from "applied but not marked".
1116
1123
  ...(args.sessionId ? { markerWritten, markerSkipReason } : {}),
1117
- lint: { preflight: preflightLint, postApply: postApplyLint },
1118
- // Pre-existing lint debt in files this close did not author (Bug B): surfaced
1119
- // for visibility, never gated. Empty on a clean vault.
1120
- notices: [...new Set(postNotice.map((e) => e.file))],
1124
+ lint: {
1125
+ preflight: summarizeLintForOutput(preflightLint),
1126
+ postApply: summarizeLintForOutput(postApplyLint),
1127
+ },
1128
+ // Pre-existing lint debt in files this close did not author: surfaced for
1129
+ // visibility, never gated. Empty on a clean vault. Scoped to the close-target
1130
+ // project's own dir — debt under projects/<project>/ is this close's
1131
+ // neighborhood and stays listed; debt elsewhere (other projects, shared
1132
+ // pages, root files) folds into otherDebtCount so the same untouched-file
1133
+ // debt does not re-list its filenames on every close (run `node
1134
+ // scripts/lint.mjs` for the full list).
1135
+ notices: [...new Set(closeScopeNotice.map((e) => e.file))],
1136
+ otherDebtCount,
1121
1137
  };
1122
1138
 
1123
1139
  if (args.json) {
@@ -1171,13 +1187,18 @@ function applySessionClose(args) {
1171
1187
  console.log(' Payload introduced a lint blocker — fix the payload content and retry.');
1172
1188
  }
1173
1189
  }
1174
- if (postNotice.length > 0) {
1190
+ if (closeScopeNotice.length > 0) {
1175
1191
  console.log(
1176
- `\n· ${postNotice.length} pre-existing lint issue(s) in untouched files (not blocking): ${[
1177
- ...new Set(postNotice.map((e) => e.file)),
1192
+ `\n· ${closeScopeNotice.length} pre-existing lint issue(s) in untouched files (not blocking): ${[
1193
+ ...new Set(closeScopeNotice.map((e) => e.file)),
1178
1194
  ]
1179
1195
  .slice(0, 5)
1180
- .join(', ')}${postNotice.length > 5 ? ', …' : ''}`,
1196
+ .join(', ')}${closeScopeNotice.length > 5 ? ', …' : ''}`,
1197
+ );
1198
+ }
1199
+ if (otherDebtCount > 0) {
1200
+ console.log(
1201
+ `\n· +${otherDebtCount} pre-existing lint issue(s) elsewhere in the vault (other projects / shared pages, not blocking) — run \`node scripts/lint.mjs\` for the full list.`,
1181
1202
  );
1182
1203
  }
1183
1204
  }
@@ -1186,6 +1207,29 @@ function applySessionClose(args) {
1186
1207
 
1187
1208
  // ── helpers ──────────────────────────────────────────────────────────────────
1188
1209
 
1210
+ // Cap the warn list in MODEL-FACING lint output. `result.lint` is serialized
1211
+ // into the --json apply result the documented close path reads, and lint runs
1212
+ // twice per close (preflight + post-apply), so an un-capped warn list (e.g.
1213
+ // hundreds of broken-wikilink warnings) lands in model context twice on every
1214
+ // close. Errors stay full (few, actionable); warns collapse to a count plus a
1215
+ // small sample for quick diagnosis. The INTERNAL preflightLint / postApplyLint
1216
+ // objects are untouched — the blocking filter and the pending-tag scan still
1217
+ // read the full warn list. lint.mjs --json itself also stays full: its
1218
+ // programmatic consumers (the pending-tag scan, the PreCompact W8 filter, tests)
1219
+ // need every warn.
1220
+ function summarizeLintForOutput(l) {
1221
+ const SAMPLE = 10;
1222
+ const warns = l.warns || [];
1223
+ const out = {
1224
+ ok: l.ok,
1225
+ errors: l.errors || [],
1226
+ warnCount: warns.length,
1227
+ warns: warns.slice(0, SAMPLE),
1228
+ };
1229
+ if (warns.length > SAMPLE) out.warnsTruncated = warns.length - SAMPLE;
1230
+ return out;
1231
+ }
1232
+
1189
1233
  function parseFrontmatter(content) {
1190
1234
  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1191
1235
  if (!m) return null;
@@ -221,7 +221,7 @@ function checkDirectories(hypoDir) {
221
221
  'pages',
222
222
  'projects',
223
223
  'sources',
224
- // Extensions baseline (ADR 0024). Existence only — SHA / settings /
224
+ // Extensions baseline. Existence only — SHA / settings /
225
225
  // manifest integrity is E5.
226
226
  'extensions/hooks',
227
227
  'extensions/commands',
@@ -308,7 +308,7 @@ function checkSettingsJson() {
308
308
  }
309
309
 
310
310
  // stale hypo-* entries (uninstall remnants).
311
- // hypo-ext-* commands are user-extension entries (ADR 0024) — not core hooks,
311
+ // hypo-ext-* commands are user-extension entries — not core hooks,
312
312
  // so they are intentionally absent from HOOK_MAP. Excluded here; their
313
313
  // integrity (SHA + manifest + entry match) is checked separately in E5.
314
314
  const isExtCommand = (cmd) => /(?:^|[/\s])hypo-ext-[^/\s]+\.mjs(?=$|["'\s])/.test(cmd);
@@ -545,7 +545,7 @@ function checkSyncState(hypoDir) {
545
545
  }
546
546
 
547
547
  function checkProjectSuggestions(hypoDir) {
548
- // ADR 0023: the auto-project skip-persistence store. Absent file is
548
+ // Auto-project skip-persistence store. Absent file is
549
549
  // healthy (no offers declined yet). Validate the RAW JSON shape here rather
550
550
  // than via readProjectSuggestions(): that helper deliberately normalizes a
551
551
  // non-array `skips` to [] for fail-open hook reads, which would mask a
@@ -665,7 +665,7 @@ function checkCodexPaths() {
665
665
  }
666
666
  }
667
667
 
668
- // ── extensions integrity (ADR 0024, E5) ───────────────────────────
668
+ // ── extensions integrity (E5) ─────────────────────────────────────
669
669
 
670
670
  // Detect drift between the user's `~/hypomnema/extensions/` source, the recorded
671
671
  // per-target SHA map (`hypo-pkg.json`), and the installed copies + settings.json
@@ -685,7 +685,7 @@ function checkCodexPaths() {
685
685
  //
686
686
  // E5 is doctor SURFACE for extensions integrity. The mixed-group surgical
687
687
  // *write* (preserve sibling-plugin hooks, swap only ours) used to be deferred
688
- // here; ADR 0024 amendment 2026-05-23 lifted that deferral
688
+ // here; amendment 2026-05-23 lifted that deferral:
689
689
  // registerSettings (extensions.mjs:478 docstring) now does occurrence-first +
690
690
  // 8-rank canonical write, and the (b) loop below mirrors that read-path via
691
691
  // collectOurOccurrences so a valid mixed-group occurrence is no longer warned
@@ -926,7 +926,7 @@ function checkExtensions(hypoDir, claudeHome, target = 'claude') {
926
926
  else warn(label, `${sample}${extra}`);
927
927
  }
928
928
 
929
- // ── feedback projection (ADR 0031) ──────────────────────────────
929
+ // ── feedback projection ──────────────────────────────────────────
930
930
 
931
931
  // Spawn feedback-sync.mjs --check --json and map its drift report onto doctor's
932
932
  // pass/warn/fail. Integrity violations (exit-3 class: conflict / unpaired marker
@@ -1015,7 +1015,7 @@ function checkFeedbackProjection(hypoDir, claudeHome, projectId) {
1015
1015
  }
1016
1016
  }
1017
1017
 
1018
- // ── stale sibling install (ADR 0038, D) ──────────────────────────────────────
1018
+ // ── stale sibling install (D) ────────────────────────────────────────────────
1019
1019
  //
1020
1020
  // Detect a SECOND, older Hypomnema that owns the `hypomnema` bin on PATH while a
1021
1021
  // newer copy owns the active hooks. That sibling is a footgun: `hypomnema init` /
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * feedback-sync.mjs — project wiki feedback as SoT, external memory as projection (ADR 0031)
3
+ * feedback-sync.mjs — project wiki feedback as SoT, external memory as projection
4
4
  *
5
5
  * Wiki `pages/feedback/<slug>.md` is the single source of truth for
6
6
  * learning/correction knowledge. Two Claude Code memory surfaces are derived
@@ -506,7 +506,7 @@ async function resolveProjectId(args, { prompt = defaultPrompt, isTTY } = {}) {
506
506
  //
507
507
  // Both modes are *reverse* one-time helpers that scaffold wiki DRAFTS under
508
508
  // pages/feedback/_drafts/ — they NEVER write pages/feedback/<slug>.md directly
509
- // (the single-direction invariant, ADR 0031 §6). A human reviews each draft,
509
+ // (the single-direction invariant). A human reviews each draft,
510
510
  // fills the decision fields (scope/tier/targets/promote_to_global), and moves
511
511
  // it into pages/feedback/. _drafts/ is excluded from sync candidates
512
512
  // (loadFeedbackPages) and from lint (collectPages skips `_`-dirs), so an
@@ -856,7 +856,7 @@ function run(args, resolvedPid = null) {
856
856
  // not actionable drift — contract §5 step 4 promises this never hard-fails,
857
857
  // so it must stay OUT of strictWarnings or the PreCompact gate (#3, which
858
858
  // runs `--check --strict`) would block every first-run user. A private-
859
- // sensitivity page IS a real SoT violation (ADR 0031 §7; lint #8 blocks it
859
+ // sensitivity page IS a real SoT violation (lint #8 blocks it
860
860
  // at the source) so it stays strict-escalatable as defense-in-depth.
861
861
  const warnings = [];
862
862
  const strictWarnings = [];
@@ -875,8 +875,8 @@ function run(args, resolvedPid = null) {
875
875
 
876
876
  // pass 1: preflight every target before touching disk — validates the write
877
877
  // plan (container/anchor, markers) and computes next content. A conflict /
878
- // over-cap / build error in ANY target blocks writes to ALL (atomicity — ADR
879
- // 0031 Decision §6 "no auto-merge"; avoids a partial write where one target
878
+ // over-cap / build error in ANY target blocks writes to ALL (atomicity:
879
+ // "no auto-merge"; avoids a partial write where one target
880
880
  // lands and another refuses).
881
881
  const evals = targets.map((target) => ({ target, res: evaluateTarget(pages, target) }));
882
882
  for (const { target, res } of evals) {
@@ -3,8 +3,8 @@
3
3
  * Hypomnema feedback script
4
4
  *
5
5
  * Creates or appends to pages/feedback/<topic>.md with a behaviour-correction
6
- * entry. The wiki feedback page is the single source of truth (ADR 0031 / fix
7
- * #37); MEMORY.md and CLAUDE.md <learned_behaviors> are one-way *projections*
6
+ * entry. The wiki feedback page is the single source of truth; MEMORY.md and
7
+ * CLAUDE.md <learned_behaviors> are one-way *projections*
8
8
  * derived from it. Never hand-edit those targets — this script writes the page
9
9
  * and then runs `feedback-sync --write` to refresh the projection automatically.
10
10
  *
@@ -123,7 +123,7 @@ function listTopics(hypoDir) {
123
123
  for (const f of files) console.log(` ${f.replace(/\.md$/, '')}`);
124
124
  }
125
125
 
126
- // ── classification validation (mirrors lint #8 / ADR 0031 §6) ──────────────────
126
+ // ── classification validation (mirrors lint #8) ─────────────────────────────────
127
127
 
128
128
  const TIER_ENUM = ['L1', 'L2'];
129
129
  const SENSITIVITY_ENUM = ['public', 'sanitized']; // private is forbidden (wiki is git-public)
@@ -136,7 +136,7 @@ function parseTargets(raw) {
136
136
  .filter(Boolean);
137
137
  }
138
138
 
139
- // FEAT-1: `failure_type` is OPTIONAL an unset value is always fine. A set value
139
+ // `failure_type` is OPTIONAL: an unset value is always fine. A set value
140
140
  // must be one of the eight enum members (same vocabulary lint enforces). Returns
141
141
  // an error string or null. Shared by create (validateClassification) and append.
142
142
  function failureTypeError(value) {
@@ -168,7 +168,7 @@ function validateClassification(args, targets) {
168
168
  const ftErr = failureTypeError(args.failureType);
169
169
  if (ftErr) errs.push(ftErr);
170
170
 
171
- // CLAUDE.md projection candidates must be global + L1 (ADR 0031 §6 filter), and
171
+ // CLAUDE.md projection candidates must be global + L1 (SoT filter), and
172
172
  // carry the two conditional fields (lint #8). Enforce here so we never write a
173
173
  // claude-learned page that lint rejects or that silently never projects.
174
174
  if (targets.includes('claude-learned')) {
@@ -176,9 +176,8 @@ function validateClassification(args, targets) {
176
176
  errs.push('--global-summary is required when --targets includes claude-learned');
177
177
  if (!args.promoteToGlobal)
178
178
  errs.push('--promote-to-global is required when --targets includes claude-learned');
179
- if (args.scope !== 'global')
180
- errs.push('claude-learned projection requires --scope=global (ADR 0031 §6)');
181
- if (args.tier !== 'L1') errs.push('claude-learned projection requires --tier=L1 (ADR 0031 §6)');
179
+ if (args.scope !== 'global') errs.push('claude-learned projection requires --scope=global');
180
+ if (args.tier !== 'L1') errs.push('claude-learned projection requires --tier=L1');
182
181
  }
183
182
  return errs;
184
183
  }
@@ -279,7 +278,7 @@ function writeFeedback(args, today) {
279
278
  // Append a dated entry; preserve existing frontmatter classification.
280
279
  mode = 'append';
281
280
  const existing = readFileSync(filePath, 'utf-8');
282
- // FEAT-1: failure_type is a per-page classification property. On append,
281
+ // failure_type is a per-page classification property. On append,
283
282
  // set it if the page has none, error if it conflicts with an existing value,
284
283
  // no-op if it matches. Without the flag, append is byte-for-byte unchanged.
285
284
  const existingFt = (parseFrontmatter(existing) || {}).failure_type || null;
@@ -348,7 +347,7 @@ function writeFeedback(args, today) {
348
347
  return { wrote: true };
349
348
  }
350
349
 
351
- // ── projection post-step (ADR 0031) ────────────────────────────────────────────
350
+ // ── projection post-step ────────────────────────────────────────────────────────
352
351
 
353
352
  // Refresh the MEMORY.md / CLAUDE.md projection from the just-written page. This
354
353
  // is best-effort and NON-blocking: a projection failure (over-cap, conflict,