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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/commands/audit.md +1 -1
- package/commands/crystallize.md +8 -8
- package/commands/doctor.md +1 -1
- package/commands/feedback.md +2 -2
- package/commands/graph.md +1 -1
- package/commands/ingest.md +1 -1
- package/commands/init.md +2 -2
- package/commands/lint.md +1 -1
- package/commands/query.md +1 -1
- package/commands/rename.md +1 -1
- package/commands/resume.md +1 -1
- package/commands/stats.md +1 -1
- package/commands/uninstall.md +4 -2
- package/commands/upgrade.md +1 -1
- package/commands/verify.md +1 -1
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/CONTRIBUTING.md +8 -7
- package/hooks/hypo-auto-commit.mjs +2 -2
- package/hooks/hypo-auto-minimal-crystallize.mjs +7 -7
- package/hooks/hypo-compact-guard.mjs +2 -2
- package/hooks/hypo-cwd-change.mjs +27 -37
- package/hooks/hypo-personal-check.mjs +37 -22
- package/hooks/hypo-session-end.mjs +1 -1
- package/hooks/hypo-session-record.mjs +2 -2
- package/hooks/hypo-session-start.mjs +34 -40
- package/hooks/hypo-shared.mjs +271 -71
- package/hooks/version-check.mjs +1 -1
- package/package.json +5 -1
- package/scripts/check-tracker-ids.mjs +69 -31
- package/scripts/crystallize.mjs +82 -38
- package/scripts/doctor.mjs +7 -7
- package/scripts/feedback-sync.mjs +5 -5
- package/scripts/feedback.mjs +9 -10
- package/scripts/init.mjs +7 -7
- package/scripts/lib/check-tracker-ids.mjs +90 -32
- package/scripts/lib/design-history-stale.mjs +1 -1
- package/scripts/lib/extensions.mjs +7 -7
- package/scripts/lib/failure-type.mjs +1 -1
- package/scripts/lib/feedback-scope.mjs +1 -1
- package/scripts/lib/project-create.mjs +2 -2
- package/scripts/lib/template-schema-version.mjs +1 -1
- package/scripts/lib/wd-match.mjs +181 -0
- package/scripts/lib/wikilink.mjs +1 -1
- package/scripts/lint.mjs +5 -5
- package/scripts/rename.mjs +1 -1
- package/scripts/resume.mjs +20 -22
- package/scripts/session-audit.mjs +1 -1
- package/scripts/stats.mjs +3 -3
- package/scripts/uninstall.mjs +3 -3
- package/scripts/upgrade.mjs +17 -18
- package/skills/crystallize/SKILL.md +7 -7
- package/skills/graph/SKILL.md +2 -2
- package/skills/ingest/SKILL.md +4 -4
- package/skills/lint/SKILL.md +2 -2
- package/skills/query/SKILL.md +2 -2
- package/skills/verify/SKILL.md +2 -2
- package/templates/SCHEMA.md +1 -1
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +20 -14
- package/templates/projects/_template/hot.md +1 -1
- package/scripts/fix-status-verify.mjs +0 -256
- package/scripts/lib/adr-corpus.mjs +0 -79
- package/scripts/lib/fix-manifest.mjs +0 -109
- 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.
|
|
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
|
-
*
|
|
9
|
-
* dangling pointer into the maintainer's PRIVATE wiki
|
|
10
|
-
* (`PR #N`, `(#N)`, `#N`, issue URLs) are legitimate and
|
|
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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
if (
|
|
105
|
-
if (EXCLUDED_FILES.has(
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
133
|
-
?
|
|
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.
|
package/scripts/crystallize.mjs
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
226
|
-
// the marker == compact-ready invariant holds
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1053
|
-
// writes
|
|
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
|
-
// (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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: {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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 (
|
|
1190
|
+
if (closeScopeNotice.length > 0) {
|
|
1175
1191
|
console.log(
|
|
1176
|
-
`\n· ${
|
|
1177
|
-
...new Set(
|
|
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(', ')}${
|
|
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;
|
package/scripts/doctor.mjs
CHANGED
|
@@ -221,7 +221,7 @@ function checkDirectories(hypoDir) {
|
|
|
221
221
|
'pages',
|
|
222
222
|
'projects',
|
|
223
223
|
'sources',
|
|
224
|
-
// Extensions baseline
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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;
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
879
|
-
//
|
|
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) {
|
package/scripts/feedback.mjs
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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,
|