@dall337/lore 0.0.0-stage → 0.1.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 (53) hide show
  1. package/License.md +21 -0
  2. package/README.md +210 -2
  3. package/bin/lore.mjs +187 -0
  4. package/hud/dist/client/assets/index-BA5GHj6l.css +1 -0
  5. package/hud/dist/client/assets/index-C1P2dBAT.js +7 -0
  6. package/hud/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  7. package/hud/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  8. package/hud/dist/client/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  9. package/hud/dist/client/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  10. package/hud/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  11. package/hud/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  12. package/hud/dist/client/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  13. package/hud/dist/client/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  14. package/hud/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  15. package/hud/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  16. package/hud/dist/client/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  17. package/hud/dist/client/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  18. package/hud/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  19. package/hud/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  20. package/hud/dist/client/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  21. package/hud/dist/client/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  22. package/hud/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  23. package/hud/dist/client/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  24. package/hud/dist/client/index.html +17 -0
  25. package/hud/dist/client/licenses/JetBrainsMono-OFL.txt +93 -0
  26. package/hud/dist/server/main.js +1287 -0
  27. package/package.json +52 -4
  28. package/suite/AGENTS.md +228 -0
  29. package/suite/CLAUDE.md +19 -0
  30. package/suite/claude-settings-additions.json +16 -0
  31. package/suite/gitignore-additions.txt +17 -0
  32. package/suite/memory/INDEX.md +41 -0
  33. package/suite/policies/adversarial-check.md +169 -0
  34. package/suite/policies/board.md +103 -0
  35. package/suite/policies/creative.md +92 -0
  36. package/suite/policies/documentation.md +100 -0
  37. package/suite/policies/git.md +105 -0
  38. package/suite/policies/implementation.md +100 -0
  39. package/suite/policies/memory.md +453 -0
  40. package/suite/policies/orchestration.md +184 -0
  41. package/suite/policies/release.md +56 -0
  42. package/suite/policies/research.md +133 -0
  43. package/suite/policies/reserved/failure-and-recovery.md +6 -0
  44. package/suite/policies/reserved/migration.md +6 -0
  45. package/suite/policies/reserved/planning.md +6 -0
  46. package/suite/policies/reserved/review.md +6 -0
  47. package/suite/policies/reserved/test-authoring.md +6 -0
  48. package/suite/policies/security.md +85 -0
  49. package/suite/policies/tooling.md +153 -0
  50. package/suite/policies/verification.md +60 -0
  51. package/suite/scripts/agents-md-budget.mjs +71 -0
  52. package/suite/scripts/memory-lint.mjs +117 -0
  53. package/suite/scripts/provider-memory.mjs +298 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // memory-lint.mjs — the suite's first guard (AGENTS.md §1/§3).
3
+ //
4
+ // Checks the FORM of the memory web defined in policies/memory.md:
5
+ // E1 memory/ exists but the hub (memory/INDEX.md) does not
6
+ // E2 struck-through content (~~…~~) without a YYYY-MM-DD stamp on the entry
7
+ // E3 malformed CLAIM (need "CLAIM YYYY-MM-DDTHH:MM — <id> — scope: …")
8
+ // E4 malformed RELEASE (need "RELEASE YYYY-MM-DD — <id> — …")
9
+ // W1 unknown status word (may be a repo extension — declare it in the hub)
10
+ // W2 multiple live claims in one node (allowed only with noted overlap,
11
+ // which a lint cannot verify — so it warns, humans and agents decide)
12
+ //
13
+ // What it cannot check: append-only itself. A stateless lint sees form,
14
+ // not history; that discipline remains on the agent.
15
+ //
16
+ // Zero dependencies. Usage: node scripts/memory-lint.mjs [repo-root] [--strict]
17
+ // Exit 1 on errors; --strict also fails warnings.
18
+ // The checks are the policy; the implementation is fungible — port this
19
+ // to your stack's language if Node isn't available.
20
+
21
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
22
+ import { join, relative } from 'node:path';
23
+
24
+ const args = process.argv.slice(2);
25
+ const STRICT = args.includes('--strict');
26
+ const ROOT = args.find((a) => !a.startsWith('--')) ?? '.';
27
+ const MEM = join(ROOT, 'memory');
28
+ const VOCAB = new Set(['planned', 'in-progress', 'blocked', 'done', 'deprecated']);
29
+ const DATE = /\d{4}-\d{2}-\d{2}/;
30
+
31
+ const errors = [];
32
+ const warnings = [];
33
+
34
+ if (!existsSync(MEM)) {
35
+ console.log('memory-lint: no memory/ directory — nothing to check.');
36
+ process.exit(0);
37
+ }
38
+ if (!existsSync(join(MEM, 'INDEX.md'))) {
39
+ errors.push(['E1', 'memory/INDEX.md', 0, 'memory/ exists but the hub (INDEX.md) does not']);
40
+ }
41
+
42
+ function* mdFiles(dir) {
43
+ for (const entry of readdirSync(dir)) {
44
+ const p = join(dir, entry);
45
+ const s = statSync(p);
46
+ if (s.isDirectory()) yield* mdFiles(p);
47
+ else if (entry.endsWith('.md')) yield p;
48
+ }
49
+ }
50
+
51
+ // Join wrapped bullet entries into logical lines: an indented line that is
52
+ // not itself a bullet continues the previous entry.
53
+ function logicalLines(text) {
54
+ const out = [];
55
+ text.split('\n').forEach((raw, i) => {
56
+ if (/^\s+\S/.test(raw) && !/^\s*[-*]\s/.test(raw) && out.length > 0) {
57
+ out[out.length - 1].text += ' ' + raw.trim();
58
+ } else {
59
+ out.push({ line: i + 1, text: raw });
60
+ }
61
+ });
62
+ return out;
63
+ }
64
+
65
+ for (const file of mdFiles(MEM)) {
66
+ const rel = relative(ROOT, file);
67
+ const lines = logicalLines(readFileSync(file, 'utf8'));
68
+ const claims = [];
69
+ const releases = [];
70
+
71
+ for (const { line, text } of lines) {
72
+ const struck = /~~[^~]+~~/.test(text);
73
+ if (struck && !DATE.test(text)) {
74
+ errors.push(['E2', rel, line, 'struck content without a date stamp']);
75
+ }
76
+
77
+ let m;
78
+ if ((m = text.match(/^\s*-\s*(~~\s*)?CLAIM\b(.*)$/))) {
79
+ const body = m[2];
80
+ const shape = body.match(/^\s*~{0,2}\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})\s+—\s+([^—]+?)\s+—\s+scope:/);
81
+ if (!shape) {
82
+ errors.push(['E3', rel, line, 'malformed CLAIM (need "CLAIM YYYY-MM-DDTHH:MM — <id> — scope: …")']);
83
+ } else {
84
+ claims.push({ line, struck: Boolean(m[1]) || struck, ts: shape[1], id: shape[2].trim() });
85
+ }
86
+ } else if ((m = text.match(/^\s*-\s*(~~\s*)?RELEASE\b(.*)$/))) {
87
+ const body = m[2];
88
+ const shape = body.match(/^\s*~{0,2}\s*(\d{4}-\d{2}-\d{2})(?:T\d{2}:\d{2})?\s+—\s+([^—]+?)(?:\s+—|$)/);
89
+ if (!shape) {
90
+ errors.push(['E4', rel, line, 'malformed RELEASE (need "RELEASE YYYY-MM-DD — <id> — …")']);
91
+ } else {
92
+ releases.push({ line, id: shape[2].trim() });
93
+ }
94
+ } else if ((m = text.match(/^\s*-\s*(~~\s*)?(\d{4}-\d{2}-\d{2})\s*—\s*([^—]+?)\s*—/))) {
95
+ // dated ledger line: "- YYYY-MM-DD — <status> — …"
96
+ const status = m[3].trim();
97
+ if (!VOCAB.has(status)) {
98
+ warnings.push(['W1', rel, line, `unknown status "${status}" — if a repo extension, declare it in the hub`]);
99
+ }
100
+ }
101
+ }
102
+
103
+ const live = claims.filter(
104
+ (c) => !c.struck && !releases.some((r) => r.id === c.id && r.line > c.line)
105
+ );
106
+ if (live.length > 1) {
107
+ warnings.push([
108
+ 'W2', rel, live[0].line,
109
+ `${live.length} live claims in one node (${live.map((c) => c.id).join(', ')}) — allowed only with noted overlap`,
110
+ ]);
111
+ }
112
+ }
113
+
114
+ for (const [code, f, l, msg] of errors) console.error(`ERROR ${code} ${f}:${l} — ${msg}`);
115
+ for (const [code, f, l, msg] of warnings) console.error(`warn ${code} ${f}:${l} — ${msg}`);
116
+ console.log(`memory-lint: ${errors.length} error(s), ${warnings.length} warning(s)${STRICT ? ' [strict]' : ''}`);
117
+ process.exit(errors.length > 0 || (STRICT && warnings.length > 0) ? 1 : 0);
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ // provider-memory.mjs — guard for a harness's own memory index
3
+ // (policies/memory.md §0, "Coexist with Provider-Native Memory").
4
+ //
5
+ // Where a provider lets the agent write its always-loaded index (Claude
6
+ // Code's MEMORY.md), LORE requires that index to be a links-only hub over
7
+ // area indexes: one per policy, reserved scopes included, plus the owner
8
+ // area and any repository areas. This script seeds that shape and checks it.
9
+ //
10
+ // init <memory-dir> [--repo <root>] seed the hub and missing areas;
11
+ // never overwrites an existing file
12
+ // check <memory-dir> [--repo <root>] [--strict]
13
+ // hook Claude Code PostToolUse mode: reads
14
+ // the hook payload on stdin and checks
15
+ // the memory directory a Write or Edit
16
+ // touched (claude-settings-additions.json)
17
+ //
18
+ // Checks:
19
+ // E1 the hub (MEMORY.md) is missing
20
+ // E2 hub line that is not a heading or a link line (prose, status, notes)
21
+ // E3 hub link to something other than an area index (area_*.md)
22
+ // E4 area line that is not a heading, its one scope line, or a link line
23
+ // E5 index link that does not resolve to a file in the memory directory
24
+ // E6 file not reachable from the hub through area indexes
25
+ // E7 hub over budget: half the provider's load cap
26
+ // E8 missing area: the owner area, or (with policies under --repo) a
27
+ // policy or reserved scope that has no area reachable from the hub
28
+ // E9 area whose scope line names a policy file that does not exist
29
+ // W1 area with more links than a walker can scan: split it into sub-areas
30
+ // W2 index link line long enough that its hook is turning into prose
31
+ //
32
+ // Topic files, the memories themselves, keep the provider's format; only
33
+ // their reachability is checked. --repo defaults to the current directory,
34
+ // and the policy checks run only when it contains policies/.
35
+ //
36
+ // Zero dependencies. Exit 1 on errors (--strict also fails warnings);
37
+ // hook mode exits 2 on any finding so Claude Code shows it to the agent.
38
+ // The checks are the policy; the implementation is fungible.
39
+
40
+ import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
41
+ import { join, dirname, basename, resolve } from 'node:path';
42
+
43
+ const HUB = 'MEMORY.md';
44
+ const OWNER = 'area_owner.md';
45
+ const AREA = /^area_[^/\\]+\.md$/;
46
+ // Claude Code loads the first 200 lines or 25KB of MEMORY.md; budget half.
47
+ const HUB_MAX_LINES = 100;
48
+ const HUB_MAX_BYTES = 12_500;
49
+ const AREA_MAX_LINKS = 30;
50
+ const LINK_LINE_MAX = 200;
51
+ // Claude Code's default auto-memory location; LORE_PROVIDER_MEMORY_DIR
52
+ // covers a relocated one (autoMemoryDirectory).
53
+ const PROVIDER_DIR = /[\\/]\.claude[\\/]projects[\\/][^\\/]+[\\/]memory$/;
54
+
55
+ const HEADING = /^#{1,6}\s/;
56
+ // A bullet that leads with a markdown link, after an optional short badge.
57
+ const LINK_LINE = /^\s*[-*]\s+(?:[^\s[]{1,4}\s+)?\[[^\]]+\]\([^)]+\)/;
58
+ const LINKS = /\]\(([^)\s]+)\)/g;
59
+ const POLICY_REF = /`(policies\/[^`]+\.md)`/;
60
+
61
+ const areaFile = (name) => `area_${name}.md`;
62
+ const titleOf = (name) => name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');
63
+ const snippet = (text) => (text.trim().length > 60 ? `${text.trim().slice(0, 57)}...` : text.trim());
64
+
65
+ // Lines after any leading YAML frontmatter, numbered as in the file.
66
+ function bodyLines(file) {
67
+ const all = readFileSync(file, 'utf8').split(/\r?\n/).map((text, i) => ({ line: i + 1, text }));
68
+ if (all[0]?.text.trim() === '---') {
69
+ const end = all.findIndex((l, i) => i > 0 && l.text.trim() === '---');
70
+ if (end > 0) return all.slice(end + 1);
71
+ }
72
+ return all;
73
+ }
74
+
75
+ function policies(repo) {
76
+ const out = [];
77
+ for (const [sub, reserved] of [['policies', false], ['policies/reserved', true]]) {
78
+ const dir = join(repo, sub);
79
+ if (!existsSync(dir)) continue;
80
+ for (const f of readdirSync(dir).sort()) {
81
+ if (f.endsWith('.md') && statSync(join(dir, f)).isFile()) {
82
+ out.push({ name: f.slice(0, -3), path: `${sub}/${f}`, reserved });
83
+ }
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ // `fresh` is a file the agent just wrote: in hook mode it gets until the
90
+ // next memory write to be linked, since writing a topic precedes linking it.
91
+ function check(dir, repo, fresh = null) {
92
+ const errors = [];
93
+ const warnings = [];
94
+ const hubPath = join(dir, HUB);
95
+ if (!existsSync(hubPath)) {
96
+ errors.push(['E1', HUB, 0, 'the hub is missing; seed it with `provider-memory.mjs init`']);
97
+ return { errors, warnings };
98
+ }
99
+
100
+ const raw = readFileSync(hubPath, 'utf8');
101
+ const rawLines = raw.split(/\r?\n/);
102
+ const lineCount = rawLines.at(-1) === '' ? rawLines.length - 1 : rawLines.length;
103
+ const bytes = Buffer.byteLength(raw);
104
+ if (lineCount > HUB_MAX_LINES || bytes > HUB_MAX_BYTES) {
105
+ errors.push(['E7', HUB, 0, `hub is ${lineCount} lines / ${bytes} bytes; budget is ${HUB_MAX_LINES} lines / ${HUB_MAX_BYTES} bytes`]);
106
+ }
107
+
108
+ // Walk from the hub through index files only; a topic file's own links
109
+ // do not make another topic reachable.
110
+ const reached = new Set([HUB]);
111
+ const scopes = new Map();
112
+ const queue = [HUB];
113
+ while (queue.length > 0) {
114
+ const name = queue.shift();
115
+ const isHub = name === HUB;
116
+ let links = 0;
117
+ let scope = null;
118
+ for (const { line, text } of bodyLines(join(dir, name))) {
119
+ if (!text.trim() || HEADING.test(text)) continue;
120
+ if (LINK_LINE.test(text)) {
121
+ links++;
122
+ if (text.length > LINK_LINE_MAX) {
123
+ warnings.push(['W2', name, line, `link line is ${text.length} chars; keep the hook to a short route description`]);
124
+ }
125
+ for (const [, target] of text.matchAll(LINKS)) {
126
+ const file = target.split('#')[0].replace(/^\.\//, '');
127
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || file.startsWith('/') || file.includes('..') || !file) {
128
+ errors.push(['E5', name, line, `link "${target}" must point to a file in the memory directory`]);
129
+ continue;
130
+ }
131
+ if (!existsSync(join(dir, file))) {
132
+ errors.push(['E5', name, line, `broken link: ${file} does not exist`]);
133
+ continue;
134
+ }
135
+ const isArea = AREA.test(file);
136
+ if (isHub && !isArea) {
137
+ errors.push(['E3', name, line, `hub links ${file}; the hub links area indexes only — move the link into an area`]);
138
+ }
139
+ if (!reached.has(file)) {
140
+ reached.add(file);
141
+ if (isArea) queue.push(file);
142
+ }
143
+ }
144
+ continue;
145
+ }
146
+ if (!isHub && scope === null && links === 0) {
147
+ scope = text.trim();
148
+ continue;
149
+ }
150
+ errors.push(isHub
151
+ ? ['E2', name, line, `hub line is not a heading or a link: "${snippet(text)}"`]
152
+ : ['E4', name, line, `area line is not a heading, its scope line, or a link: "${snippet(text)}"`]);
153
+ }
154
+ if (!isHub) scopes.set(name, scope ?? '');
155
+ if (!isHub && links > AREA_MAX_LINKS) {
156
+ warnings.push(['W1', name, 0, `${links} links; split this area into sub-areas linked from it`]);
157
+ }
158
+ }
159
+
160
+ for (const f of readdirSync(dir).sort()) {
161
+ if (f.endsWith('.md') && statSync(join(dir, f)).isFile() && !reached.has(f) && f !== fresh) {
162
+ errors.push(['E6', f, 0, 'not reachable from the hub; link it from an area']);
163
+ }
164
+ }
165
+
166
+ if (!reached.has(OWNER)) {
167
+ errors.push(['E8', HUB, 0, `no owner area: add ${OWNER} and link it from the hub (init does this)`]);
168
+ }
169
+ if (repo && existsSync(join(repo, 'policies'))) {
170
+ for (const p of policies(repo)) {
171
+ if (!reached.has(areaFile(p.name))) {
172
+ errors.push(['E8', HUB, 0, `no area for ${p.path}: add ${areaFile(p.name)} and link it from the hub (init does this)`]);
173
+ }
174
+ }
175
+ for (const [name, scope] of scopes) {
176
+ const m = scope.match(POLICY_REF);
177
+ if (m && !existsSync(join(repo, m[1]))) {
178
+ errors.push(['E9', name, 0, `scope names ${m[1]}, which does not exist in the repository`]);
179
+ }
180
+ }
181
+ }
182
+ return { errors, warnings };
183
+ }
184
+
185
+ function report({ errors, warnings }) {
186
+ for (const [code, f, l, msg] of errors) console.error(`ERROR ${code} ${f}:${l} — ${msg}`);
187
+ for (const [code, f, l, msg] of warnings) console.error(`warn ${code} ${f}:${l} — ${msg}`);
188
+ }
189
+
190
+ function init(dir, repo) {
191
+ mkdirSync(dir, { recursive: true });
192
+ const hubPath = join(dir, HUB);
193
+ const linked = new Set();
194
+ if (existsSync(hubPath)) {
195
+ let prose = 0;
196
+ for (const { text } of bodyLines(hubPath)) {
197
+ if (!text.trim() || HEADING.test(text)) continue;
198
+ if (LINK_LINE.test(text)) {
199
+ for (const [, target] of text.matchAll(LINKS)) linked.add(target.replace(/^\.\//, ''));
200
+ } else prose++;
201
+ }
202
+ if (prose > 0) {
203
+ console.error(`init: ${HUB} is not a links-only hub (${prose} line(s) of prose or status). ` +
204
+ 'Migrating it is an owner decision; init leaves the directory untouched.');
205
+ return 1;
206
+ }
207
+ }
208
+
209
+ const pols = policies(repo);
210
+ if (pols.length === 0) console.error(`init: no policies under ${resolve(repo)}; seeding the owner area only`);
211
+ const areas = [
212
+ {
213
+ file: OWNER, title: 'Owner', group: 'Owner',
214
+ scope: 'Who the owner is and how they work. Open on first contact.',
215
+ hook: 'who the owner is and how they work; open on first contact',
216
+ },
217
+ ...pols.map((p) => ({
218
+ file: areaFile(p.name), title: titleOf(p.name),
219
+ group: p.reserved ? 'Reserved scopes' : 'Policies',
220
+ scope: p.reserved
221
+ ? `Memories that apply when \`${p.path}\` is loaded. Reserved scope with no rules yet: a lesson filed here is a candidate for its first rule.`
222
+ : `Memories that apply when \`${p.path}\` is loaded.`,
223
+ hook: p.reserved ? `opened with ${p.path} (reserved, no rules yet)` : `opened with ${p.path}`,
224
+ })),
225
+ ];
226
+
227
+ let created = 0;
228
+ for (const a of areas) {
229
+ if (!existsSync(join(dir, a.file))) {
230
+ writeFileSync(join(dir, a.file), `# ${a.title}\n\n${a.scope}\n`);
231
+ created++;
232
+ }
233
+ }
234
+
235
+ const missing = areas.filter((a) => !linked.has(a.file));
236
+ const line = (a) => `- [${a.title}](${a.file}) — ${a.hook}`;
237
+ if (!existsSync(hubPath)) {
238
+ const groups = ['Owner', 'Policies', 'Reserved scopes'];
239
+ const sections = groups
240
+ .map((g) => [g, missing.filter((a) => a.group === g)])
241
+ .filter(([, as]) => as.length > 0)
242
+ .map(([g, as]) => `## ${g}\n\n${as.map(line).join('\n')}\n`);
243
+ writeFileSync(hubPath, `# Memory hub\n\n${sections.join('\n')}\n## Repository areas\n`);
244
+ } else if (missing.length > 0) {
245
+ appendFileSync(hubPath, `\n${missing.map(line).join('\n')}\n`);
246
+ }
247
+ console.log(`init: ${created} area file(s) created, ${missing.length} hub link(s) added; existing files untouched.`);
248
+ return 0;
249
+ }
250
+
251
+ function readStdin() {
252
+ return new Promise((done) => {
253
+ let data = '';
254
+ process.stdin.setEncoding('utf8');
255
+ process.stdin.on('data', (chunk) => { data += chunk; });
256
+ process.stdin.on('end', () => done(data));
257
+ });
258
+ }
259
+
260
+ async function hook() {
261
+ let input;
262
+ try {
263
+ input = JSON.parse(await readStdin());
264
+ } catch {
265
+ return 0; // not a hook payload; never break the session over it
266
+ }
267
+ const file = input?.tool_input?.file_path;
268
+ if (typeof file !== 'string' || !file.endsWith('.md')) return 0;
269
+ const dir = dirname(resolve(file));
270
+ const relocated = process.env.LORE_PROVIDER_MEMORY_DIR;
271
+ if (!PROVIDER_DIR.test(dir) && !(relocated && resolve(relocated) === dir)) return 0;
272
+
273
+ const result = check(dir, process.env.CLAUDE_PROJECT_DIR || input.cwd || '.', basename(file));
274
+ if (result.errors.length === 0 && result.warnings.length === 0) return 0;
275
+ console.error(`provider-memory: ${dir}`);
276
+ report(result);
277
+ console.error('Fix these now: the index is a links-only hub over areas (policies/memory.md §0).');
278
+ return 2;
279
+ }
280
+
281
+ const [cmd, ...rest] = process.argv.slice(2);
282
+ const repoAt = rest.indexOf('--repo');
283
+ const REPO = repoAt >= 0 ? rest[repoAt + 1] : '.';
284
+ const DIR = rest.find((a, i) => !a.startsWith('--') && (repoAt < 0 || i !== repoAt + 1));
285
+ const STRICT = rest.includes('--strict');
286
+
287
+ if (cmd === 'hook') {
288
+ process.exit(await hook());
289
+ } else if ((cmd === 'check' || cmd === 'init') && DIR && REPO) {
290
+ if (cmd === 'init') process.exit(init(DIR, REPO));
291
+ const result = check(DIR, REPO);
292
+ report(result);
293
+ console.log(`provider-memory: ${result.errors.length} error(s), ${result.warnings.length} warning(s)${STRICT ? ' [strict]' : ''}`);
294
+ process.exit(result.errors.length > 0 || (STRICT && result.warnings.length > 0) ? 1 : 0);
295
+ } else {
296
+ console.error('usage: provider-memory.mjs init|check <memory-dir> [--repo <root>] [--strict]\n provider-memory.mjs hook (Claude Code PostToolUse; payload on stdin)');
297
+ process.exit(1);
298
+ }