llm-wiki-kit 0.1.6 → 0.1.8
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/README.md +26 -29
- package/docs/concepts.md +8 -5
- package/docs/integrations/claude-code.md +7 -3
- package/docs/integrations/codex.md +9 -3
- package/docs/operations.md +36 -18
- package/docs/research/baseline.md +2 -0
- package/docs/research/future-large-scale.md +27 -0
- package/docs/troubleshooting.md +22 -11
- package/package.json +1 -1
- package/src/cli.js +32 -9
- package/src/doctor.js +49 -19
- package/src/hook.js +9 -3
- package/src/install.js +98 -8
- package/src/project-state.js +265 -9
- package/src/project.js +6 -3
- package/src/projects.js +16 -4
- package/src/state.js +5 -0
- package/src/templates.js +133 -29
- package/src/update.js +73 -12
- package/src/wiki-lint.js +11 -1
- package/src/wiki-search.js +3 -0
package/src/doctor.js
CHANGED
|
@@ -9,27 +9,27 @@ function nodeMajor() {
|
|
|
9
9
|
return Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async function runDoctor() {
|
|
12
|
+
export async function runDoctor(options = {}) {
|
|
13
13
|
const checks = [];
|
|
14
|
-
const add = (name, ok, detail = '') => checks.push({ name, ok, detail });
|
|
15
|
-
const stat = await status();
|
|
14
|
+
const add = (id, name, ok, detail = '') => checks.push({ id, name, ok, detail });
|
|
15
|
+
const stat = await status(options);
|
|
16
16
|
|
|
17
|
-
add('node >= 20', nodeMajor() >= 20, process.version);
|
|
18
|
-
add('runtime version detected', Boolean(stat.runtimeVersion), stat.runtimeVersion || 'unknown');
|
|
19
|
-
add('llm-wiki bin exists', await exists(stat.binPath), stat.binPath);
|
|
20
|
-
add('runtime installed from npm', stat.installSource === 'npm', `${stat.installSource}; npm install -g does not update source checkouts`);
|
|
21
|
-
add('llm-wiki command resolves to current runtime', stat.commandMatchesRuntime, stat.commandPath ? `command=${stat.commandPath}; runtime=${stat.binPath}` : 'command not found on PATH');
|
|
22
|
-
add('Codex hook installed', stat.codexInstalled, stat.codexHooksPath);
|
|
23
|
-
add('Claude hook installed', stat.claudeInstalled, stat.claudeSettingsPath);
|
|
24
|
-
add('project templates current', stat.project.managedFilesCurrent, stat.project.statePath);
|
|
25
|
-
add('codex command available', spawnSync('codex', ['--version'], { encoding: 'utf8' }).status === 0, 'codex --version');
|
|
26
|
-
add('claude command available', spawnSync('claude', ['--version'], { encoding: 'utf8' }).status === 0, 'claude --version');
|
|
27
|
-
add('state directory writable', await canWrite(join(kitDataDir(), '.doctor')), kitDataDir());
|
|
28
|
-
add('docs present', await docsPresent(), 'README.md and docs/');
|
|
29
|
-
add('sample hook roundtrip', await sampleHookRoundtrip(stat.binPath), 'UserPromptSubmit fixture');
|
|
17
|
+
add('node', 'node >= 20', nodeMajor() >= 20, process.version);
|
|
18
|
+
add('runtime-version', 'runtime version detected', Boolean(stat.runtimeVersion), stat.runtimeVersion || 'unknown');
|
|
19
|
+
add('runtime-bin', 'llm-wiki bin exists', await exists(stat.binPath), stat.binPath);
|
|
20
|
+
add('install-source', 'runtime installed from npm', stat.installSource === 'npm', `${stat.installSource}; npm install -g does not update source checkouts`);
|
|
21
|
+
add('command-path', 'llm-wiki command resolves to current runtime', stat.commandMatchesRuntime, stat.commandPath ? `command=${stat.commandPath}; runtime=${stat.binPath}` : 'command not found on PATH');
|
|
22
|
+
add('codex-hook', 'Codex hook installed', stat.codexInstalled, stat.codexHooksPath);
|
|
23
|
+
add('claude-hook', 'Claude hook installed', stat.claudeInstalled, stat.claudeSettingsPath);
|
|
24
|
+
add('project-templates', 'project templates current', stat.project.managedFilesCurrent, stat.project.statePath);
|
|
25
|
+
add('codex-command', 'codex command available', spawnSync('codex', ['--version'], { encoding: 'utf8' }).status === 0, 'codex --version');
|
|
26
|
+
add('claude-command', 'claude command available', spawnSync('claude', ['--version'], { encoding: 'utf8' }).status === 0, 'claude --version');
|
|
27
|
+
add('state-writable', 'state directory writable', await canWrite(join(kitDataDir(), '.doctor')), kitDataDir());
|
|
28
|
+
add('docs', 'docs present', await docsPresent(), 'README.md and docs/');
|
|
29
|
+
add('sample-hook', 'sample hook roundtrip', await sampleHookRoundtrip(stat.binPath), 'UserPromptSubmit fixture');
|
|
30
30
|
|
|
31
31
|
const allOk = checks.every((check) => check.ok);
|
|
32
|
-
return { ok: allOk, checks };
|
|
32
|
+
return { ok: allOk, checks, workspace: stat.workspace, status: stat };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function docsPresent() {
|
|
@@ -84,8 +84,38 @@ export function formatDoctor(result) {
|
|
|
84
84
|
lines.push(`- ${check.ok ? 'PASS' : 'WARN'} ${check.name}${check.detail ? ` (${check.detail})` : ''}`);
|
|
85
85
|
}
|
|
86
86
|
if (!result.ok) {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const remediation = doctorRemediation(result);
|
|
88
|
+
if (remediation.length > 0) {
|
|
89
|
+
lines.push('', 'Suggested fix:');
|
|
90
|
+
for (const item of remediation) lines.push(`- ${item}`);
|
|
91
|
+
}
|
|
89
92
|
}
|
|
90
93
|
return lines.join('\n');
|
|
91
94
|
}
|
|
95
|
+
|
|
96
|
+
function failed(result, id) {
|
|
97
|
+
return (result.checks || []).some((check) => check.id === id && !check.ok);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function doctorRemediation(result) {
|
|
101
|
+
const workspace = result.workspace || process.cwd();
|
|
102
|
+
const suggestions = [];
|
|
103
|
+
if (failed(result, 'install-source')) {
|
|
104
|
+
suggestions.push(`normal install: npm install -g llm-wiki-kit@latest && llm-wiki install --workspace ${workspace} --profile standard`);
|
|
105
|
+
suggestions.push(`source checkout development: ./install.sh --workspace ${workspace} --profile standard`);
|
|
106
|
+
}
|
|
107
|
+
if (failed(result, 'command-path')) {
|
|
108
|
+
const local = result.status?.localBin;
|
|
109
|
+
if (result.status?.installSource === 'npm' && local?.exists && local.managed) {
|
|
110
|
+
suggestions.push(`remove stale managed local shim if it still shadows npm: rm -f ${local.path} && hash -r`);
|
|
111
|
+
}
|
|
112
|
+
suggestions.push(`reconnect command and hooks: llm-wiki install --workspace ${workspace} --profile standard`);
|
|
113
|
+
}
|
|
114
|
+
if (failed(result, 'codex-hook') || failed(result, 'claude-hook')) {
|
|
115
|
+
suggestions.push(`install hooks: llm-wiki install --workspace ${workspace} --profile standard, then restart Codex/Claude Code sessions`);
|
|
116
|
+
}
|
|
117
|
+
if (failed(result, 'project-templates')) {
|
|
118
|
+
suggestions.push(`refresh managed project templates: llm-wiki post-update --workspace ${workspace}`);
|
|
119
|
+
}
|
|
120
|
+
return [...new Set(suggestions)];
|
|
121
|
+
}
|
package/src/hook.js
CHANGED
|
@@ -3,7 +3,8 @@ import { bootstrapProject, appendContextNote, appendLiveQa, appendSessionEnvelop
|
|
|
3
3
|
import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
|
|
4
4
|
import { recordProject } from './projects.js';
|
|
5
5
|
import { summarizeForStorage } from './redaction.js';
|
|
6
|
-
import { buildEntryFromState, rememberQuestion, rememberTool } from './state.js';
|
|
6
|
+
import { buildEntryFromState, clearTurnState, rememberQuestion, rememberTool } from './state.js';
|
|
7
|
+
import { relative } from 'path';
|
|
7
8
|
|
|
8
9
|
async function readStdinJson() {
|
|
9
10
|
const chunks = [];
|
|
@@ -45,7 +46,9 @@ async function autoUpdateManagedProject(projectRoot, eventName) {
|
|
|
45
46
|
const state = await inspectProjectState(projectRoot);
|
|
46
47
|
if (state.runtimeUpToDateWithProject && state.managedFilesCurrent) return;
|
|
47
48
|
const result = await applyProjectTemplateUpdate(projectRoot);
|
|
48
|
-
|
|
49
|
+
if (result.changed.length > 0) {
|
|
50
|
+
await appendWikiLog(projectRoot, `llm-wiki-kit auto-update applied runtime ${result.runtimeVersion}; changed templates: ${result.changed.length}; skipped templates: ${result.skipped.length}`);
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export async function handleHook(provider, explicitEvent) {
|
|
@@ -96,7 +99,10 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
96
99
|
await appendLiveQa(projectRoot, entry);
|
|
97
100
|
const queryPath = await writeQueryPage(projectRoot, entry);
|
|
98
101
|
const decisionPath = await writeDecisionPage(projectRoot, entry);
|
|
99
|
-
await appendWikiLog(projectRoot, `captured ${eventName}${queryPath ? `; query=${queryPath}` : ''}${decisionPath ? `; decision=${decisionPath}` : ''}`);
|
|
102
|
+
await appendWikiLog(projectRoot, `captured ${eventName}${queryPath ? `; query=${relative(projectRoot, queryPath)}` : ''}${decisionPath ? `; decision=${relative(projectRoot, decisionPath)}` : ''}`);
|
|
103
|
+
}
|
|
104
|
+
if (eventName === 'Stop' || eventName === 'SessionEnd') {
|
|
105
|
+
await clearTurnState(projectRoot, payload).catch(() => {});
|
|
100
106
|
}
|
|
101
107
|
return {};
|
|
102
108
|
}
|
package/src/install.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { realpathSync } from 'fs';
|
|
2
|
-
import { chmod } from 'fs/promises';
|
|
2
|
+
import { chmod, lstat, readlink, unlink } from 'fs/promises';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
5
5
|
import { CLAUDE_EVENTS, CODEX_EVENTS, KIT_NAME } from './constants.js';
|
|
6
|
-
import { backupFile,
|
|
6
|
+
import { backupFile, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
|
|
7
7
|
import { inspectProjectState } from './project-state.js';
|
|
8
8
|
import { bootstrapProject } from './project.js';
|
|
9
9
|
import { recordProject } from './projects.js';
|
|
@@ -34,6 +34,91 @@ function realpathOrOriginal(path) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function sameResolvedPath(left, right) {
|
|
38
|
+
const resolvedLeft = realpathOrOriginal(left);
|
|
39
|
+
const resolvedRight = realpathOrOriginal(right);
|
|
40
|
+
return Boolean(resolvedLeft && resolvedRight && resolvedLeft === resolvedRight);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function samePath(left, right) {
|
|
44
|
+
if (!left || !right) return false;
|
|
45
|
+
return resolve(left) === resolve(right);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isKitPath(path) {
|
|
49
|
+
return String(path || '').replace(/\\/g, '/').includes('/llm-wiki-kit/');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function inspectLocalBin(localBinPath) {
|
|
53
|
+
try {
|
|
54
|
+
const stat = await lstat(localBinPath);
|
|
55
|
+
const symlink = stat.isSymbolicLink();
|
|
56
|
+
const target = symlink ? await readlink(localBinPath).catch(() => null) : null;
|
|
57
|
+
const resolved = realpathOrOriginal(localBinPath);
|
|
58
|
+
return {
|
|
59
|
+
path: localBinPath,
|
|
60
|
+
exists: true,
|
|
61
|
+
symlink,
|
|
62
|
+
target,
|
|
63
|
+
resolved,
|
|
64
|
+
managed: symlink && (isKitPath(target) || isKitPath(resolved)),
|
|
65
|
+
matchesRuntime: sameResolvedPath(localBinPath, binPath),
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return {
|
|
69
|
+
path: localBinPath,
|
|
70
|
+
exists: false,
|
|
71
|
+
symlink: false,
|
|
72
|
+
target: null,
|
|
73
|
+
resolved: null,
|
|
74
|
+
managed: false,
|
|
75
|
+
matchesRuntime: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function reconcileLocalBin(localBinPath) {
|
|
81
|
+
const commandPathsBefore = llmWikiCommandPaths();
|
|
82
|
+
const localBefore = await inspectLocalBin(localBinPath);
|
|
83
|
+
const alternateRuntimeCommand = commandPathsBefore.some((path) => (
|
|
84
|
+
!samePath(path, localBinPath) && sameResolvedPath(path, binPath)
|
|
85
|
+
));
|
|
86
|
+
let action = 'kept';
|
|
87
|
+
|
|
88
|
+
if (alternateRuntimeCommand) {
|
|
89
|
+
if (localBefore.exists && localBefore.managed) {
|
|
90
|
+
await backupFile(localBinPath, 'local-bin-llm-wiki');
|
|
91
|
+
await unlink(localBinPath).catch(() => {});
|
|
92
|
+
action = 'removed-shadowing-shim';
|
|
93
|
+
} else if (localBefore.exists) {
|
|
94
|
+
action = 'left-unmanaged-local-bin';
|
|
95
|
+
} else {
|
|
96
|
+
action = 'skipped-npm-command-available';
|
|
97
|
+
}
|
|
98
|
+
} else if (localBefore.matchesRuntime) {
|
|
99
|
+
action = 'kept-current-shim';
|
|
100
|
+
} else {
|
|
101
|
+
if (localBefore.exists) {
|
|
102
|
+
await backupFile(localBinPath, 'local-bin-llm-wiki');
|
|
103
|
+
action = 'replaced-local-shim';
|
|
104
|
+
} else {
|
|
105
|
+
action = 'created-local-shim';
|
|
106
|
+
}
|
|
107
|
+
await safeSymlink(binPath, localBinPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const commandPathsAfter = llmWikiCommandPaths();
|
|
111
|
+
const localAfter = await inspectLocalBin(localBinPath);
|
|
112
|
+
return {
|
|
113
|
+
...localAfter,
|
|
114
|
+
action,
|
|
115
|
+
alternateRuntimeCommand,
|
|
116
|
+
commandPathsBefore,
|
|
117
|
+
commandPathsAfter,
|
|
118
|
+
commandPath: commandPathsAfter[0] || null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
37
122
|
function addHook(hooks, eventName, command, options = {}) {
|
|
38
123
|
hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
39
124
|
const already = JSON.stringify(hooks[eventName]).includes(command);
|
|
@@ -72,12 +157,8 @@ export async function install(options = {}) {
|
|
|
72
157
|
const workspace = resolve(options.workspace || process.cwd());
|
|
73
158
|
await chmod(binPath, 0o755).catch(() => {});
|
|
74
159
|
const localBin = join(homeDir(), '.local', 'bin');
|
|
75
|
-
await ensureDir(localBin);
|
|
76
160
|
const localBinPath = join(localBin, 'llm-wiki');
|
|
77
|
-
|
|
78
|
-
await backupFile(localBinPath, 'local-bin-llm-wiki');
|
|
79
|
-
}
|
|
80
|
-
await safeSymlink(binPath, localBinPath);
|
|
161
|
+
const localBinResult = await reconcileLocalBin(localBinPath);
|
|
81
162
|
if (!options.noProject) {
|
|
82
163
|
await bootstrapProject(workspace, { profile: options.profile || 'standard', recordState: true });
|
|
83
164
|
await recordProject(workspace, 'install');
|
|
@@ -133,7 +214,12 @@ export async function install(options = {}) {
|
|
|
133
214
|
workspace,
|
|
134
215
|
runtimeVersion: runtimeVersion(),
|
|
135
216
|
binPath,
|
|
136
|
-
localBin:
|
|
217
|
+
localBin: localBinPath,
|
|
218
|
+
localBinAction: localBinResult.action,
|
|
219
|
+
localBinManaged: localBinResult.managed,
|
|
220
|
+
localBinMatchesRuntime: localBinResult.matchesRuntime,
|
|
221
|
+
commandPath: localBinResult.commandPath,
|
|
222
|
+
commandPaths: localBinResult.commandPathsAfter,
|
|
137
223
|
changed,
|
|
138
224
|
};
|
|
139
225
|
}
|
|
@@ -176,7 +262,10 @@ export async function status(options = {}) {
|
|
|
176
262
|
const commandPath = commandPaths[0] || null;
|
|
177
263
|
const resolvedCommandPath = realpathOrOriginal(commandPath);
|
|
178
264
|
const resolvedBinPath = realpathOrOriginal(binPath);
|
|
265
|
+
const localBinPath = join(homeDir(), '.local', 'bin', 'llm-wiki');
|
|
266
|
+
const localBin = await inspectLocalBin(localBinPath);
|
|
179
267
|
return {
|
|
268
|
+
workspace,
|
|
180
269
|
runtimeVersion: runtimeVersion(),
|
|
181
270
|
packageRoot,
|
|
182
271
|
installSource: detectInstallSource(),
|
|
@@ -184,6 +273,7 @@ export async function status(options = {}) {
|
|
|
184
273
|
commandPath,
|
|
185
274
|
commandPaths,
|
|
186
275
|
commandMatchesRuntime: Boolean(resolvedCommandPath && resolvedBinPath && resolvedCommandPath === resolvedBinPath),
|
|
276
|
+
localBin,
|
|
187
277
|
codexInstalled,
|
|
188
278
|
claudeInstalled,
|
|
189
279
|
hooksCurrent: codexInstalled && claudeInstalled,
|
package/src/project-state.js
CHANGED
|
@@ -27,6 +27,14 @@ function templateDescriptors() {
|
|
|
27
27
|
path: join('llm-wiki', 'AGENTS.md'),
|
|
28
28
|
content: llmWikiAgents(),
|
|
29
29
|
mode: 'generated-file',
|
|
30
|
+
legacyContents: legacyLlmWikiAgentsContents(),
|
|
31
|
+
legacySignals: [
|
|
32
|
+
/^# LLM Wiki Agent Rules/m,
|
|
33
|
+
/Generated by llm-wiki-kit/m,
|
|
34
|
+
/## Purpose/m,
|
|
35
|
+
/## Core Rules/m,
|
|
36
|
+
/## Operations/m,
|
|
37
|
+
],
|
|
30
38
|
},
|
|
31
39
|
{
|
|
32
40
|
id: 'wiki-memory',
|
|
@@ -39,10 +47,179 @@ function templateDescriptors() {
|
|
|
39
47
|
path: join('llm-wiki', 'procedures', name),
|
|
40
48
|
content: procedure(name),
|
|
41
49
|
mode: 'generated-file',
|
|
50
|
+
legacyContents: legacyProcedureContents(name),
|
|
51
|
+
legacySignals: legacyProcedureSignals(name),
|
|
42
52
|
})),
|
|
43
53
|
];
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
function legacyLlmWikiAgentsContents() {
|
|
57
|
+
return [
|
|
58
|
+
`# LLM Wiki Agent Rules
|
|
59
|
+
|
|
60
|
+
Generated by llm-wiki-kit <version>.
|
|
61
|
+
|
|
62
|
+
## Purpose
|
|
63
|
+
Maintain a living Markdown LLM Wiki from immutable source files and redacted Codex/Claude Code session events.
|
|
64
|
+
These rules supersede older OMX/OMC/\`omx_wiki/\` LLM Wiki rules for this project.
|
|
65
|
+
|
|
66
|
+
## Directories
|
|
67
|
+
- \`raw/\`: immutable or redacted source material. Never edit original source captures.
|
|
68
|
+
- \`wiki/\`: AI-maintained knowledge pages.
|
|
69
|
+
- \`outputs/\`: live Q&A summaries, reports, and generated briefs.
|
|
70
|
+
- \`procedures/\`: detailed operating rules for ingest, query, lint, and security.
|
|
71
|
+
|
|
72
|
+
## Core Rules
|
|
73
|
+
- Never modify \`raw/\` source material except to append redacted session envelopes generated by hooks.
|
|
74
|
+
- Do not state unsupported claims as facts.
|
|
75
|
+
- Mark inference explicitly.
|
|
76
|
+
- Add \`source_ids\` or file references for important claims.
|
|
77
|
+
- Update \`wiki/index.md\` and \`wiki/log.md\` whenever new durable knowledge is created.
|
|
78
|
+
- Preserve contradictions in \`Contradictions\` or \`Open Questions\` instead of overwriting them.
|
|
79
|
+
- Preserve useful work context and redact authentication values before writing durable notes.
|
|
80
|
+
|
|
81
|
+
## Page Format
|
|
82
|
+
Use YAML frontmatter when creating wiki pages:
|
|
83
|
+
|
|
84
|
+
\`\`\`yaml
|
|
85
|
+
---
|
|
86
|
+
title: ""
|
|
87
|
+
type: "source | concept | entity | decision | architecture | debugging | context | query | session-log | convention"
|
|
88
|
+
source_ids: []
|
|
89
|
+
status: "draft | reviewed | stale"
|
|
90
|
+
last_updated: "YYYY-MM-DD"
|
|
91
|
+
confidence: "high | medium | low"
|
|
92
|
+
---
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## Operations
|
|
96
|
+
- ingest: read new raw files, create or update wiki pages, then update \`wiki/index.md\` and \`wiki/log.md\`.
|
|
97
|
+
- query: start from \`wiki/index.md\`, read relevant wiki pages, answer with source references, and save reusable answers.
|
|
98
|
+
- lint: find stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, contradictions, and missing links.`,
|
|
99
|
+
`# LLM Wiki Agent Rules
|
|
100
|
+
|
|
101
|
+
Generated by llm-wiki-kit <version>.
|
|
102
|
+
|
|
103
|
+
## Purpose
|
|
104
|
+
Maintain a living Markdown LLM Wiki from immutable source files and redacted Codex/Claude Code session events.
|
|
105
|
+
These rules supersede older OMX/OMC/\`omx_wiki/\` LLM Wiki rules for this project.
|
|
106
|
+
|
|
107
|
+
## Directories
|
|
108
|
+
- \`raw/\`: immutable or redacted source material. Never edit original source captures.
|
|
109
|
+
- \`wiki/\`: AI-maintained knowledge pages. \`wiki/memory.md\` is the short hot index injected into hook context.
|
|
110
|
+
- \`outputs/\`: live Q&A summaries, reports, and generated briefs.
|
|
111
|
+
- \`procedures/\`: detailed operating rules for ingest, query, lint, and security.
|
|
112
|
+
|
|
113
|
+
## Core Rules
|
|
114
|
+
- Never modify \`raw/\` source material except to append redacted session envelopes generated by hooks.
|
|
115
|
+
- Do not state unsupported claims as facts.
|
|
116
|
+
- Mark inference explicitly.
|
|
117
|
+
- Add \`source_ids\` or file references for important claims.
|
|
118
|
+
- Update \`wiki/memory.md\`, \`wiki/index.md\`, and \`wiki/log.md\` whenever new durable knowledge changes the active project map.
|
|
119
|
+
- Preserve contradictions in \`Contradictions\` or \`Open Questions\` instead of overwriting them.
|
|
120
|
+
- Preserve useful work context and redact authentication values before writing durable notes.
|
|
121
|
+
|
|
122
|
+
## Page Format
|
|
123
|
+
Use YAML frontmatter when creating wiki pages:
|
|
124
|
+
|
|
125
|
+
\`\`\`yaml
|
|
126
|
+
---
|
|
127
|
+
title: ""
|
|
128
|
+
type: "source | concept | entity | decision | architecture | debugging | context | query | session-log | convention"
|
|
129
|
+
source_ids: []
|
|
130
|
+
status: "draft | reviewed | stale"
|
|
131
|
+
last_updated: "YYYY-MM-DD"
|
|
132
|
+
confidence: "high | medium | low"
|
|
133
|
+
memory_type: "semantic | episodic | procedural"
|
|
134
|
+
importance: 1
|
|
135
|
+
last_verified: "YYYY-MM-DD | unknown"
|
|
136
|
+
supersedes: []
|
|
137
|
+
superseded_by: []
|
|
138
|
+
---
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
## Operations
|
|
142
|
+
- ingest: read \`wiki/memory.md\` and \`wiki/index.md\`, ingest new raw files, create or update wiki pages, then update the active map and \`wiki/log.md\`.
|
|
143
|
+
- query: start from \`llm-wiki context "<query>"\` or \`wiki/memory.md\`, read relevant wiki pages, answer with source references, and save reusable answers.
|
|
144
|
+
- lint: run \`llm-wiki lint --workspace <project>\` to find stale pages, orphan pages, broken wiki/Markdown links, unsafe source IDs, secret-like content, missing sources, duplicate concepts, contradictions, and missing links.
|
|
145
|
+
- consolidate: run \`llm-wiki consolidate --workspace <project>\` after meaningful wiki growth to refresh generated memory/index blocks without overwriting hand-written notes. Default query/context/session-log pages are excluded unless explicitly durable.`,
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function legacyProcedureContents(name) {
|
|
150
|
+
const early = {
|
|
151
|
+
'ingest.md': `# Ingest Procedure
|
|
152
|
+
|
|
153
|
+
1. Read \`wiki/index.md\` first.
|
|
154
|
+
2. Inspect new material under \`raw/inbox/\` or \`raw/sources/\`.
|
|
155
|
+
3. Create or update \`wiki/sources/<slug>.md\` for each source.
|
|
156
|
+
4. Update related concept, entity, decision, architecture, debugging, or context pages.
|
|
157
|
+
5. Prefer updating existing pages over creating duplicates.
|
|
158
|
+
6. Add source references and confidence.
|
|
159
|
+
7. Update \`wiki/index.md\` and append to \`wiki/log.md\`.`,
|
|
160
|
+
'query.md': `# Query Procedure
|
|
161
|
+
|
|
162
|
+
1. Start from \`wiki/index.md\`.
|
|
163
|
+
2. Search \`wiki/\` for relevant pages.
|
|
164
|
+
3. Read the smallest useful set of pages first.
|
|
165
|
+
4. Use raw sources only when exact evidence matters.
|
|
166
|
+
5. Separate verified facts from inference.
|
|
167
|
+
6. Save reusable answers into \`wiki/queries/\` and link them from related pages.`,
|
|
168
|
+
'lint.md': `# Lint Procedure
|
|
169
|
+
|
|
170
|
+
Check for stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, unsupported claims, and unresolved contradictions. Prefer producing a review report before automatic edits.`,
|
|
171
|
+
'security.md': `# Security Procedure
|
|
172
|
+
|
|
173
|
+
- Never store credentials, tokens, private keys, \`.env\` contents, customer identifiers, or private personal data.
|
|
174
|
+
- Redact before writing hook payloads or summaries.
|
|
175
|
+
- Full raw transcript capture is disabled by default and must be explicitly enabled by project policy.
|
|
176
|
+
- If a file or prompt looks secret-bearing, do not persist it and ask for confirmation before reading further.`,
|
|
177
|
+
};
|
|
178
|
+
const layered = {
|
|
179
|
+
'ingest.md': `# Ingest Procedure
|
|
180
|
+
|
|
181
|
+
1. Read \`wiki/memory.md\` and \`wiki/index.md\` first.
|
|
182
|
+
2. Inspect new material under \`raw/inbox/\` or \`raw/sources/\`.
|
|
183
|
+
3. Create or update \`wiki/sources/<slug>.md\` for each source.
|
|
184
|
+
4. Update related concept, entity, decision, architecture, debugging, or context pages.
|
|
185
|
+
5. Prefer updating existing pages over creating duplicates.
|
|
186
|
+
6. Add source references, confidence, memory type, importance, and verification status.
|
|
187
|
+
7. Update \`wiki/memory.md\` or run \`llm-wiki consolidate\` when durable entry points change.
|
|
188
|
+
8. Update \`wiki/index.md\` and append to \`wiki/log.md\`.`,
|
|
189
|
+
'query.md': `# Query Procedure
|
|
190
|
+
|
|
191
|
+
1. Start from \`llm-wiki context "<query>"\` or read \`wiki/memory.md\` and \`wiki/index.md\`.
|
|
192
|
+
2. Use \`--limit\` or \`--no-expand\` when you need a narrower context pack.
|
|
193
|
+
3. Search \`wiki/\` for relevant pages.
|
|
194
|
+
4. Read the smallest useful set of pages first.
|
|
195
|
+
5. Use raw sources only when exact evidence matters.
|
|
196
|
+
6. Separate verified facts from inference.
|
|
197
|
+
7. Save reusable answers into \`wiki/queries/\` and link them from related pages.`,
|
|
198
|
+
'lint.md': `# Lint Procedure
|
|
199
|
+
|
|
200
|
+
Run \`llm-wiki lint --workspace <project>\` to check for stale pages, orphan pages, broken wiki/Markdown links, unsafe source IDs, secret-like content, missing sources, duplicate concepts, unsupported claims, and unresolved contradictions. Prefer producing a review report before automatic edits.`,
|
|
201
|
+
'security.md': `# Security Procedure
|
|
202
|
+
|
|
203
|
+
- Preserve useful work context for the local project wiki.
|
|
204
|
+
- Do not block reads or tool calls only because they look sensitive.
|
|
205
|
+
- Redact authentication values such as tokens, passwords, bearer credentials, and private keys before writing hook payloads, summaries, or context packs.
|
|
206
|
+
- Run \`llm-wiki lint --workspace <project>\` when sensitive material might have entered wiki pages; secret-like content is reported as an error.
|
|
207
|
+
- Full raw transcript capture is disabled by default and must be explicitly enabled by project policy.`,
|
|
208
|
+
};
|
|
209
|
+
return [early[name], layered[name]].filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function legacyProcedureSignals(name) {
|
|
213
|
+
const common = [/^# .+ Procedure/m];
|
|
214
|
+
const byName = {
|
|
215
|
+
'ingest.md': [/^# Ingest Procedure/m, /wiki\/memory\.md/, /raw\/inbox|raw\/sources/],
|
|
216
|
+
'query.md': [/^# Query Procedure/m, /llm-wiki context/, /Save reusable answers|reusable answers/],
|
|
217
|
+
'lint.md': [/^# Lint Procedure/m, /llm-wiki lint/, /stale pages|orphan pages/],
|
|
218
|
+
'security.md': [/^# Security Procedure/m, /Redact authentication values|token|private keys/, /raw transcript capture/],
|
|
219
|
+
};
|
|
220
|
+
return byName[name] || common;
|
|
221
|
+
}
|
|
222
|
+
|
|
46
223
|
function emptyState() {
|
|
47
224
|
return {
|
|
48
225
|
schemaVersion: PROJECT_STATE_SCHEMA_VERSION,
|
|
@@ -79,18 +256,24 @@ export async function writeProjectState(projectRoot, state) {
|
|
|
79
256
|
}
|
|
80
257
|
|
|
81
258
|
function replaceMarkedBlock(current, replacement) {
|
|
259
|
+
if (markerStatus(current) !== 'complete') return null;
|
|
82
260
|
const start = current.indexOf(ROOT_POLICY_START);
|
|
83
261
|
const end = current.indexOf(ROOT_POLICY_END);
|
|
84
|
-
if (start === -1 || end === -1 || end < start) return null;
|
|
85
262
|
const afterEnd = end + ROOT_POLICY_END.length;
|
|
86
263
|
return `${current.slice(0, start)}${replacement.trimStart().trimEnd()}${current.slice(afterEnd)}`;
|
|
87
264
|
}
|
|
88
265
|
|
|
266
|
+
function markerCount(current, marker) {
|
|
267
|
+
return current.split(marker).length - 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
89
270
|
function markerStatus(current) {
|
|
90
271
|
const start = current.indexOf(ROOT_POLICY_START);
|
|
91
272
|
const end = current.indexOf(ROOT_POLICY_END);
|
|
92
|
-
|
|
93
|
-
|
|
273
|
+
const starts = markerCount(current, ROOT_POLICY_START);
|
|
274
|
+
const ends = markerCount(current, ROOT_POLICY_END);
|
|
275
|
+
if (starts === 0 && ends === 0) return 'missing';
|
|
276
|
+
if (starts !== 1 || ends !== 1 || end < start) return 'malformed';
|
|
94
277
|
return 'complete';
|
|
95
278
|
}
|
|
96
279
|
|
|
@@ -108,10 +291,39 @@ function isKnownGeneratedContent(text, descriptor) {
|
|
|
108
291
|
return templateHash(text) === templateHash(descriptor.content);
|
|
109
292
|
}
|
|
110
293
|
|
|
294
|
+
function normalizeGeneratedText(text) {
|
|
295
|
+
return String(text || '')
|
|
296
|
+
.replace(/\r\n/g, '\n')
|
|
297
|
+
.replace(/Generated by llm-wiki-kit [^\n]+/g, 'Generated by llm-wiki-kit <version>.')
|
|
298
|
+
.trim();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isLegacyGeneratedContent(text, descriptor) {
|
|
302
|
+
if (descriptor.mode !== 'generated-file') return false;
|
|
303
|
+
if (!Array.isArray(descriptor.legacyContents) || descriptor.legacyContents.length === 0) return false;
|
|
304
|
+
const normalized = normalizeGeneratedText(text);
|
|
305
|
+
return descriptor.legacyContents.some((content) => normalizeGeneratedText(content) === normalized);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasLegacyGeneratedSignal(text, descriptor) {
|
|
309
|
+
if (!Array.isArray(descriptor.legacySignals) || descriptor.legacySignals.length === 0) return false;
|
|
310
|
+
return descriptor.legacySignals.every((pattern) => pattern.test(text));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasKitManagedSignal(text, descriptor) {
|
|
314
|
+
if (descriptor.mode === 'marker') return markerStatus(text) !== 'missing';
|
|
315
|
+
if (descriptor.mode === 'generated-file') {
|
|
316
|
+
return isLegacyGeneratedContent(text, descriptor) || hasLegacyGeneratedSignal(text, descriptor) || /Generated by llm-wiki-kit/.test(text);
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
111
321
|
function canPatchDescriptor(state, descriptor, currentText, fileExists = true) {
|
|
112
322
|
if (descriptor.mode === 'create-only') return !fileExists;
|
|
113
323
|
if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content) !== null;
|
|
114
|
-
return isRecordedManaged(state, descriptor, currentText) ||
|
|
324
|
+
return isRecordedManaged(state, descriptor, currentText) ||
|
|
325
|
+
isKnownGeneratedContent(currentText, descriptor) ||
|
|
326
|
+
isLegacyGeneratedContent(currentText, descriptor);
|
|
115
327
|
}
|
|
116
328
|
|
|
117
329
|
function desiredTextForDescriptor(descriptor, currentText, fileExists = true) {
|
|
@@ -125,6 +337,10 @@ async function createOnlyParentExists(projectRoot, descriptor) {
|
|
|
125
337
|
return exists(dirname(join(projectRoot, descriptor.path)));
|
|
126
338
|
}
|
|
127
339
|
|
|
340
|
+
async function descriptorParentExists(projectRoot, descriptor) {
|
|
341
|
+
return exists(dirname(join(projectRoot, descriptor.path)));
|
|
342
|
+
}
|
|
343
|
+
|
|
128
344
|
export async function inspectProjectState(projectRoot) {
|
|
129
345
|
const state = await readProjectState(projectRoot);
|
|
130
346
|
const runtime = runtimeVersion();
|
|
@@ -139,10 +355,16 @@ export async function inspectProjectState(projectRoot) {
|
|
|
139
355
|
const currentHash = fileExists ? templateHash(currentText) : null;
|
|
140
356
|
const recorded = state.managedTemplates[descriptor.id] || null;
|
|
141
357
|
const markerState = descriptor.mode === 'marker' && fileExists ? markerStatus(currentText) : null;
|
|
142
|
-
const parentExists = await
|
|
358
|
+
const parentExists = await descriptorParentExists(projectRoot, descriptor);
|
|
143
359
|
const patchable = descriptor.mode === 'create-only' && !fileExists && !parentExists
|
|
144
360
|
? false
|
|
145
|
-
: (
|
|
361
|
+
: (!fileExists && descriptor.mode === 'generated-file'
|
|
362
|
+
? parentExists
|
|
363
|
+
: (markerState === 'malformed' ? false : canPatchDescriptor(state, descriptor, currentText, fileExists)));
|
|
364
|
+
const needsAttention = (fileExists || recorded) &&
|
|
365
|
+
!patchable &&
|
|
366
|
+
currentText !== (desiredText || '') &&
|
|
367
|
+
(fileExists ? hasKitManagedSignal(currentText, descriptor) : descriptor.mode !== 'create-only');
|
|
146
368
|
const current = descriptor.mode === 'create-only'
|
|
147
369
|
? fileExists
|
|
148
370
|
: fileExists && desiredText !== null && currentHash === templateHash(desiredText);
|
|
@@ -153,11 +375,12 @@ export async function inspectProjectState(projectRoot) {
|
|
|
153
375
|
exists: fileExists,
|
|
154
376
|
patchable,
|
|
155
377
|
current,
|
|
378
|
+
needsAttention,
|
|
156
379
|
currentHash,
|
|
157
380
|
desiredHash,
|
|
158
381
|
recordedHash: recorded?.hash || null,
|
|
159
382
|
recordedVersion: recorded?.version || null,
|
|
160
|
-
reason: markerState === 'malformed' ? 'malformed-marker' : null,
|
|
383
|
+
reason: markerState === 'malformed' ? 'malformed-marker' : (!fileExists ? 'missing' : null),
|
|
161
384
|
});
|
|
162
385
|
}
|
|
163
386
|
|
|
@@ -167,11 +390,31 @@ export async function inspectProjectState(projectRoot) {
|
|
|
167
390
|
schemaVersion: state.schemaVersion,
|
|
168
391
|
lastRuntimeVersionApplied: state.lastRuntimeVersionApplied,
|
|
169
392
|
runtimeUpToDateWithProject: state.lastRuntimeVersionApplied === runtime,
|
|
170
|
-
managedFilesCurrent: files.every((file) => file.current || !file.patchable),
|
|
393
|
+
managedFilesCurrent: files.every((file) => file.current || (!file.patchable && !file.needsAttention)),
|
|
171
394
|
managedFiles: files,
|
|
172
395
|
};
|
|
173
396
|
}
|
|
174
397
|
|
|
398
|
+
export function formatProjectMaintenanceContext(inspection) {
|
|
399
|
+
const files = inspection?.managedFiles || [];
|
|
400
|
+
const attention = files.filter((file) => file.needsAttention);
|
|
401
|
+
const outdated = files.filter((file) => !file.current && file.patchable);
|
|
402
|
+
if (attention.length === 0 && outdated.length === 0) return '';
|
|
403
|
+
|
|
404
|
+
const lines = [
|
|
405
|
+
'LLM Wiki maintenance note:',
|
|
406
|
+
'- 이전 버전의 llm-wiki-kit 규칙/문서가 남아 있을 수 있다.',
|
|
407
|
+
'- 확실히 kit가 관리하는 영역은 자동 갱신된다. 사용자 편집 가능성이 있는 문서는 덮어쓰지 말고 기존 내용을 보존한 채 현재 규칙에 맞게 자연스럽게 정리한다.',
|
|
408
|
+
];
|
|
409
|
+
if (outdated.length > 0) {
|
|
410
|
+
lines.push(`- 자동 갱신 대상: ${outdated.map((file) => file.path).join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
if (attention.length > 0) {
|
|
413
|
+
lines.push(`- agent 확인 필요: ${attention.map((file) => file.path).join(', ')}`);
|
|
414
|
+
}
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
175
418
|
export async function recordManagedTemplates(projectRoot) {
|
|
176
419
|
const state = await readProjectState(projectRoot);
|
|
177
420
|
const runtime = runtimeVersion();
|
|
@@ -211,7 +454,7 @@ export async function applyProjectTemplateUpdate(projectRoot, options = {}) {
|
|
|
211
454
|
const absolutePath = join(projectRoot, descriptor.path);
|
|
212
455
|
if (!(await exists(absolutePath))) {
|
|
213
456
|
if (descriptor.mode === 'create-only') {
|
|
214
|
-
if (!(await
|
|
457
|
+
if (!(await descriptorParentExists(projectRoot, descriptor))) {
|
|
215
458
|
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing-wiki' });
|
|
216
459
|
continue;
|
|
217
460
|
}
|
|
@@ -227,6 +470,19 @@ export async function applyProjectTemplateUpdate(projectRoot, options = {}) {
|
|
|
227
470
|
};
|
|
228
471
|
continue;
|
|
229
472
|
}
|
|
473
|
+
if (descriptor.mode === 'generated-file' && await descriptorParentExists(projectRoot, descriptor)) {
|
|
474
|
+
if (!options.dryRun) {
|
|
475
|
+
await ensureDir(dirname(absolutePath));
|
|
476
|
+
await writeText(absolutePath, descriptor.content, 'utf8');
|
|
477
|
+
}
|
|
478
|
+
changed.push({ id: descriptor.id, path: descriptor.path });
|
|
479
|
+
state.managedTemplates[descriptor.id] = {
|
|
480
|
+
path: descriptor.path,
|
|
481
|
+
version: runtime,
|
|
482
|
+
hash: templateHash(descriptor.content),
|
|
483
|
+
};
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
230
486
|
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing' });
|
|
231
487
|
continue;
|
|
232
488
|
}
|
package/src/project.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { LLM_WIKI_DIRS } from './constants.js';
|
|
12
12
|
import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
|
|
13
13
|
import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
14
|
-
import { recordManagedTemplates } from './project-state.js';
|
|
14
|
+
import { formatProjectMaintenanceContext, inspectProjectState, recordManagedTemplates } from './project-state.js';
|
|
15
15
|
import { buildContextPack, formatContextPack, searchWiki as searchWikiWithIndex } from './wiki-search.js';
|
|
16
16
|
|
|
17
17
|
export async function bootstrapProject(projectRoot, options = {}) {
|
|
@@ -117,7 +117,7 @@ export async function appendLiveQa(projectRoot, entry) {
|
|
|
117
117
|
entry.verification || '(not captured)',
|
|
118
118
|
'',
|
|
119
119
|
'### Follow-up',
|
|
120
|
-
entry.followUp || '
|
|
120
|
+
entry.followUp || '다음 작업에서 이 turn의 reusable fact가 있으면 기존 wiki 문서에 합치고, 일회성 기록은 이 Q&A에만 보존한다.',
|
|
121
121
|
'',
|
|
122
122
|
].join('\n');
|
|
123
123
|
await appendText(path, redactText(block, 12000));
|
|
@@ -163,5 +163,8 @@ export async function buildContextBrief(projectRoot, eventName, query = '') {
|
|
|
163
163
|
includeLog: eventName === 'SessionStart',
|
|
164
164
|
limit: 5,
|
|
165
165
|
});
|
|
166
|
-
|
|
166
|
+
const maintenance = await inspectProjectState(projectRoot)
|
|
167
|
+
.then(formatProjectMaintenanceContext)
|
|
168
|
+
.catch(() => '');
|
|
169
|
+
return [formatContextPack(pack), maintenance].filter(Boolean).join('\n\n');
|
|
167
170
|
}
|