@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.
- package/License.md +21 -0
- package/README.md +210 -2
- package/bin/lore.mjs +187 -0
- package/hud/dist/client/assets/index-BA5GHj6l.css +1 -0
- package/hud/dist/client/assets/index-C1P2dBAT.js +7 -0
- package/hud/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/hud/dist/client/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/hud/dist/client/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/hud/dist/client/index.html +17 -0
- package/hud/dist/client/licenses/JetBrainsMono-OFL.txt +93 -0
- package/hud/dist/server/main.js +1287 -0
- package/package.json +52 -4
- package/suite/AGENTS.md +228 -0
- package/suite/CLAUDE.md +19 -0
- package/suite/claude-settings-additions.json +16 -0
- package/suite/gitignore-additions.txt +17 -0
- package/suite/memory/INDEX.md +41 -0
- package/suite/policies/adversarial-check.md +169 -0
- package/suite/policies/board.md +103 -0
- package/suite/policies/creative.md +92 -0
- package/suite/policies/documentation.md +100 -0
- package/suite/policies/git.md +105 -0
- package/suite/policies/implementation.md +100 -0
- package/suite/policies/memory.md +453 -0
- package/suite/policies/orchestration.md +184 -0
- package/suite/policies/release.md +56 -0
- package/suite/policies/research.md +133 -0
- package/suite/policies/reserved/failure-and-recovery.md +6 -0
- package/suite/policies/reserved/migration.md +6 -0
- package/suite/policies/reserved/planning.md +6 -0
- package/suite/policies/reserved/review.md +6 -0
- package/suite/policies/reserved/test-authoring.md +6 -0
- package/suite/policies/security.md +85 -0
- package/suite/policies/tooling.md +153 -0
- package/suite/policies/verification.md +60 -0
- package/suite/scripts/agents-md-budget.mjs +71 -0
- package/suite/scripts/memory-lint.mjs +117 -0
- 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
|
+
}
|