llm-wiki-kit 0.1.12 → 0.1.14
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 +10 -3
- package/docs/concepts.md +4 -2
- package/docs/integrations/claude-code.md +2 -2
- package/docs/integrations/codex.md +3 -3
- package/docs/operations.md +7 -0
- package/docs/troubleshooting.md +13 -0
- package/package.json +1 -1
- package/src/cli.js +9 -0
- package/src/constants.js +1 -0
- package/src/hook.js +12 -1
- package/src/install.js +6 -1
- package/src/maintenance.js +253 -0
- package/src/project.js +6 -1
- package/src/state.js +27 -5
- package/src/templates.js +3 -0
- package/src/wiki-lint.js +9 -0
package/README.md
CHANGED
|
@@ -58,10 +58,13 @@ llm-wiki/
|
|
|
58
58
|
│ ├── context/
|
|
59
59
|
│ └── queries/
|
|
60
60
|
├── outputs/
|
|
61
|
+
│ ├── questions/
|
|
62
|
+
│ ├── reports/
|
|
63
|
+
│ └── maintenance/
|
|
61
64
|
└── procedures/
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
`raw/` is the immutable or redacted evidence layer. `wiki/` is the LLM-maintained knowledge layer. `wiki/memory.md` is the short hot index injected into hook context before deeper search results. `outputs/` stores live Q&A and
|
|
67
|
+
`raw/` is the immutable or redacted evidence layer. `wiki/` is the LLM-maintained knowledge layer. `wiki/memory.md` is the short hot index injected into hook context before deeper search results. `outputs/` stores live Q&A, requested reports, and pending wiki maintenance candidates. `.kit-state.json` records which runtime version last applied managed templates to the project. Text written by the kit is normalized to NFC so Korean filenames and content stay composed.
|
|
65
68
|
|
|
66
69
|
## Normal Use
|
|
67
70
|
|
|
@@ -76,6 +79,8 @@ The installed hooks:
|
|
|
76
79
|
- update `llm-wiki/outputs/questions/YYYY-MM-DD-live-qa.md`
|
|
77
80
|
- capture reusable-answer candidates into `llm-wiki/wiki/queries/`
|
|
78
81
|
- promote decision-like turns into `llm-wiki/wiki/decisions/`
|
|
82
|
+
- queue durable cleanup candidates in `llm-wiki/outputs/maintenance/queue.md` at stop/session-end time
|
|
83
|
+
- recover stale per-turn state into that queue on the next session start or prompt submit when the previous stop hook did not complete
|
|
79
84
|
- nudge the active LLM to fold reusable facts into existing wiki pages instead of leaving everything as one-off Q&A
|
|
80
85
|
- automatically refresh managed rules/templates for older projects when the current runtime starts a session
|
|
81
86
|
|
|
@@ -87,10 +92,10 @@ Most users should not need these during daily Claude Code/Codex work. They exist
|
|
|
87
92
|
|
|
88
93
|
- Install/update: `llm-wiki install`, `llm-wiki update`, `llm-wiki post-update`, `llm-wiki projects`
|
|
89
94
|
- Diagnostics: `llm-wiki doctor`, `llm-wiki status`, `llm-wiki version`
|
|
90
|
-
- Agent maintenance helpers: `llm-wiki context`, `llm-wiki lint`, `llm-wiki consolidate`
|
|
95
|
+
- Agent maintenance helpers: `llm-wiki context`, `llm-wiki lint`, `llm-wiki consolidate`, `llm-wiki maintenance`
|
|
91
96
|
- Cleanup: `llm-wiki uninstall`
|
|
92
97
|
|
|
93
|
-
`llm-wiki status` is an offline consistency check. It reports the installed runtime version, hook targets, whether the `llm-wiki` command on `PATH` resolves to the current runtime, whether the current workspace has the current managed templates applied, how many rules are auto-updateable,
|
|
98
|
+
`llm-wiki status` is an offline consistency check. It reports the installed runtime version, hook targets, whether the `llm-wiki` command on `PATH` resolves to the current runtime, whether the current workspace has the current managed templates applied, how many rules are auto-updateable, how many managed-looking rules need agent cleanup, and how many wiki maintenance items are pending.
|
|
94
99
|
|
|
95
100
|
`llm-wiki update --check [--to <version-or-tag>]` is the online update check. It compares the installed package version with the npm registry target without changing files, and reports an available update only when the target version is newer than the installed version.
|
|
96
101
|
|
|
@@ -104,6 +109,8 @@ Most users should not need these during daily Claude Code/Codex work. They exist
|
|
|
104
109
|
|
|
105
110
|
`llm-wiki consolidate` refreshes only generated marker blocks in `wiki/memory.md` and `wiki/index.md`. It is an agent maintenance helper, not a command users should run after every turn.
|
|
106
111
|
|
|
112
|
+
`llm-wiki maintenance` prints the pending queue from `llm-wiki/outputs/maintenance/queue.md`. Hooks create candidates; the active agent should merge reusable items into existing durable wiki pages and mark queue items `done` or `skipped`.
|
|
113
|
+
|
|
107
114
|
`llm-wiki projects --workspace /apps` lists project roots that already have `llm-wiki-kit` state or an older `llm-wiki/wiki/index.md`, and shows the update commands to run. `llm-wiki update --workspace /apps` updates the global runtime once, then reapplies managed templates across every known or discovered project root under `/apps`.
|
|
108
115
|
|
|
109
116
|
After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working when they already point at the global npm package path. The next `SessionStart`/`InstructionsLoaded` hook automatically reapplies safe managed template updates for the active project root. Clearly generated older `llm-wiki/AGENTS.md` and procedure files are refreshed even when old state is missing; user-edited files are not overwritten and are surfaced to the active agent as cleanup work. If hooks point at a source checkout or stale shim, run `llm-wiki post-update --workspace <project>` or `llm-wiki install --workspace <project>` once to reconnect them.
|
package/docs/concepts.md
CHANGED
|
@@ -19,8 +19,9 @@ The important behavior is a loop:
|
|
|
19
19
|
3. The user works normally; no extra command loop is required.
|
|
20
20
|
4. Hooks gather redacted prompt/tool/result summaries.
|
|
21
21
|
5. At stop/session end, hooks append redacted live Q&A and create query or decision candidates when the turn has enough captured context.
|
|
22
|
-
6.
|
|
23
|
-
7.
|
|
22
|
+
6. At stop/session-end or the next start/prompt after an abrupt shutdown, hooks queue durable cleanup candidates in `outputs/maintenance/queue.md`.
|
|
23
|
+
7. When reusable knowledge appears, the active Claude Code/Codex agent folds it into existing durable wiki pages instead of leaving everything as one-off Q&A.
|
|
24
|
+
8. Future sessions start from the improved wiki instead of relying on long chat history.
|
|
24
25
|
|
|
25
26
|
The kit is a template/runtime repository. It must not centralize project wiki contents.
|
|
26
27
|
|
|
@@ -38,5 +39,6 @@ The maintenance loop is intentionally layered:
|
|
|
38
39
|
- `memory.md`: short hot index for current durable facts.
|
|
39
40
|
- `index.md`: broad navigation map.
|
|
40
41
|
- MiniSearch + wikilinks: retrieval over the rest of `wiki/**/*.md`, with substring fallback when MiniSearch is not installed in a source checkout.
|
|
42
|
+
- `outputs/maintenance/queue.md`: pending reminders for turns that need durable wiki review.
|
|
41
43
|
- `lint`: finds broken links, stale pages, duplicates, metadata gaps, secret-like content, and outdated managed rules.
|
|
42
44
|
- `consolidate`: agent helper that refreshes generated blocks in `memory.md` and `index.md` while preserving handwritten notes and keeping default query/context/session pages out of the durable generated maps.
|
|
@@ -40,9 +40,9 @@ when no project `CLAUDE.md` exists. Existing `CLAUDE.md` files are not overwritt
|
|
|
40
40
|
|
|
41
41
|
The hook records redacted turn summaries but does not deny tool calls only because an input looks sensitive. Hook payloads are stored as small redacted event envelopes rather than full transcripts, and context output is redacted field by field before it is returned to Claude Code.
|
|
42
42
|
|
|
43
|
-
At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, and any maintenance note for outdated or customized managed rules. At `UserPromptSubmit`, it searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set.
|
|
43
|
+
At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, pending queue items, and any maintenance note for outdated or customized managed rules. At `UserPromptSubmit`, it recovers stale turn state, searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set plus pending queue reminders.
|
|
44
44
|
|
|
45
|
-
`PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` records a compaction note, and `PostCompact` records the note and returns fresh wiki context. `SubagentStop`, `Stop`, and `SessionEnd` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` and `SessionEnd` clear the per-session turn buffer after recording; `SubagentStop` does not.
|
|
45
|
+
`PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` records a compaction note, and `PostCompact` records the note and returns fresh wiki context. `SubagentStop`, `Stop`, and `SessionEnd` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` and `SessionEnd` also queue pending maintenance items for durable wiki review, then clear the per-session turn buffer after recording; `SubagentStop` does not.
|
|
46
46
|
|
|
47
47
|
Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic managed-template refresh behavior.
|
|
48
48
|
|
|
@@ -29,12 +29,12 @@ Handled events:
|
|
|
29
29
|
|
|
30
30
|
Expected behavior:
|
|
31
31
|
|
|
32
|
-
- `SessionStart` first attempts a safe managed-template refresh, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, and any maintenance note for outdated or customized managed rules.
|
|
33
|
-
- `UserPromptSubmit` searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set.
|
|
32
|
+
- `SessionStart` first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, pending queue items, and any maintenance note for outdated or customized managed rules.
|
|
33
|
+
- `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set plus pending queue reminders.
|
|
34
34
|
- `PreToolUse` records redacted tool summaries without blocking tool calls.
|
|
35
35
|
- `PostToolUse` records redacted tool summaries in a turn buffer.
|
|
36
36
|
- `PreCompact` records a compaction note; `PostCompact` records the note and returns fresh wiki context.
|
|
37
|
-
- `SubagentStop` and `Stop` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like.
|
|
37
|
+
- `SubagentStop` and `Stop` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` also queues a pending maintenance item for durable wiki review.
|
|
38
38
|
- `Stop` clears the per-session turn buffer after recording. `SubagentStop` leaves the parent turn buffer available for the final stop event.
|
|
39
39
|
|
|
40
40
|
Hook payloads are stored as small redacted event envelopes rather than full transcripts. Context output is also redacted field by field before it is returned to Codex.
|
package/docs/operations.md
CHANGED
|
@@ -86,6 +86,7 @@ llm-wiki post-update --all --workspace /path/to/search-root
|
|
|
86
86
|
llm-wiki context "search phrase" --workspace /path/to/project
|
|
87
87
|
llm-wiki lint --workspace /path/to/project
|
|
88
88
|
llm-wiki consolidate --workspace /path/to/project
|
|
89
|
+
llm-wiki maintenance --workspace /path/to/project
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
`status` is offline and answers whether the local installation is internally consistent:
|
|
@@ -96,6 +97,7 @@ llm-wiki consolidate --workspace /path/to/project
|
|
|
96
97
|
- project template state from `llm-wiki/.kit-state.json`
|
|
97
98
|
- current managed file hashes
|
|
98
99
|
- auto-updateable managed rules and managed-looking rules that need agent cleanup
|
|
100
|
+
- pending wiki maintenance count
|
|
99
101
|
|
|
100
102
|
`update --check [--to <version-or-tag>]` is online and asks npm for the target version. It reports `update available` only when that registry target is newer than the installed version, so it does not suggest downgrades.
|
|
101
103
|
|
|
@@ -120,6 +122,8 @@ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working
|
|
|
120
122
|
|
|
121
123
|
Daily use should be Claude Code/Codex first. The user should not need to run a chain of `llm-wiki` commands while working. Hooks inject the context automatically, and the active agent is expected to update durable wiki pages when reusable project knowledge appears.
|
|
122
124
|
|
|
125
|
+
`Stop` and `SessionEnd` write pending cleanup candidates to `llm-wiki/outputs/maintenance/queue.md` after live Q&A/query/decision capture. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete, then inject up to five pending items into context. This is a recovery and reminder layer, not a full transcript capture path.
|
|
126
|
+
|
|
123
127
|
`llm-wiki context "<query>"` is the manual debug form of hook context injection. It reads:
|
|
124
128
|
|
|
125
129
|
- `llm-wiki/wiki/memory.md` as the short hot index
|
|
@@ -148,6 +152,7 @@ llm-wiki context "auth architecture" --workspace /path/to/project --limit 8 --no
|
|
|
148
152
|
- duplicate aliases or titles
|
|
149
153
|
- stale pages and orphan candidates
|
|
150
154
|
- outdated managed rules/templates from earlier `llm-wiki-kit` versions
|
|
155
|
+
- stale or oversized maintenance queues
|
|
151
156
|
|
|
152
157
|
Broken links, invalid source IDs, and secret-like content are errors and return exit code 1. Metadata and discoverability gaps are warnings.
|
|
153
158
|
|
|
@@ -163,6 +168,8 @@ Broken links, invalid source IDs, and secret-like content are errors and return
|
|
|
163
168
|
|
|
164
169
|
Agents may run `consolidate` after meaningful wiki growth. Users should not need to run it after every turn.
|
|
165
170
|
|
|
171
|
+
`llm-wiki maintenance --workspace <project>` prints queue counts and the first pending items. It does not merge wiki pages by itself; the active agent should review pending items, update the closest existing durable wiki document, then mark the queue item `done` or `skipped`.
|
|
172
|
+
|
|
166
173
|
When a new runtime sees an older project, `SessionStart`/`InstructionsLoaded` automatically reapplies safe managed template updates. Files that are clearly generated by older kit versions are refreshed. Files that look user-edited are preserved and surfaced to the active agent as cleanup context instead of being overwritten. Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic template refresh behavior.
|
|
167
174
|
|
|
168
175
|
## Updating User-Local Or nvm Installs
|
package/docs/troubleshooting.md
CHANGED
|
@@ -177,6 +177,19 @@ Check:
|
|
|
177
177
|
- project `CLAUDE.md` exists or imports `@AGENTS.md`
|
|
178
178
|
- the session was restarted after install
|
|
179
179
|
|
|
180
|
+
## Maintenance Queue Is Empty Or Stale
|
|
181
|
+
|
|
182
|
+
`llm-wiki/outputs/maintenance/queue.md` is created only after a captured `Stop`/`SessionEnd` turn, or when `SessionStart`/`UserPromptSubmit` recovers stale per-turn state from a session that did not stop cleanly.
|
|
183
|
+
|
|
184
|
+
Check the queue and health warnings:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
llm-wiki maintenance --workspace /path/to/project
|
|
188
|
+
llm-wiki lint --workspace /path/to/project
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If the queue is always empty, confirm hooks run and that the turn had a captured `UserPromptSubmit`. If pending items stay around, the active agent should merge reusable content into existing durable wiki pages and mark each queue item `done` or `skipped`.
|
|
192
|
+
|
|
180
193
|
## Authentication Values Were Redacted
|
|
181
194
|
|
|
182
195
|
The hook does not block tool calls only because inputs look sensitive. Durable summaries redact authentication values before writing, while ordinary work context such as dates, phone numbers, emails, and business identifiers is preserved by default.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolve } from 'path';
|
|
|
2
2
|
import { formatConsolidateResult, runConsolidate } from './consolidate.js';
|
|
3
3
|
import { handleHook } from './hook.js';
|
|
4
4
|
import { install, status, uninstall } from './install.js';
|
|
5
|
+
import { formatMaintenanceResult, maintenanceSummary } from './maintenance.js';
|
|
5
6
|
import { bootstrapProject } from './project.js';
|
|
6
7
|
import { inspectProjectState } from './project-state.js';
|
|
7
8
|
import { commandForProject, knownProjectRoots, recordProject } from './projects.js';
|
|
@@ -98,6 +99,7 @@ Usage:
|
|
|
98
99
|
llm-wiki context "<query>" --workspace <project> [--limit 5] [--no-expand]
|
|
99
100
|
llm-wiki lint --workspace <project>
|
|
100
101
|
llm-wiki consolidate --workspace <project> [--dry-run]
|
|
102
|
+
llm-wiki maintenance --workspace <project> [--json]
|
|
101
103
|
`);
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
@@ -195,6 +197,12 @@ Usage:
|
|
|
195
197
|
return;
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
if (command === 'maintenance') {
|
|
201
|
+
const projectRoot = resolve(options.workspace || process.cwd());
|
|
202
|
+
printJsonOrText(await maintenanceSummary(projectRoot, options), options, formatMaintenanceResult);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
198
206
|
if (command === 'hook') {
|
|
199
207
|
const provider = rest[0] || 'codex';
|
|
200
208
|
const eventName = rest[1];
|
|
@@ -250,6 +258,7 @@ function formatStatus(value) {
|
|
|
250
258
|
`- project templates current: ${project.managedFilesCurrent ? 'yes' : 'no'}`,
|
|
251
259
|
`- project auto-updateable rules: ${autoUpdateCount}`,
|
|
252
260
|
`- project rules needing agent cleanup: ${attentionCount}`,
|
|
261
|
+
`- project maintenance pending: ${value.maintenance?.pendingCount ?? 0}`,
|
|
253
262
|
].join('\n');
|
|
254
263
|
}
|
|
255
264
|
|
package/src/constants.js
CHANGED
package/src/hook.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findProjectRoot } from './fs-utils.js';
|
|
2
2
|
import { bootstrapProject, appendContextNote, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
|
|
3
|
+
import { recoverStaleTurnStates, recordMaintenanceForEntry } from './maintenance.js';
|
|
3
4
|
import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
|
|
4
5
|
import { recordProject } from './projects.js';
|
|
5
6
|
import { summarizeForStorage } from './redaction.js';
|
|
@@ -61,6 +62,9 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
61
62
|
await recordProject(projectRoot, 'hook').catch(() => {});
|
|
62
63
|
await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
|
|
63
64
|
await appendSessionEnvelope(projectRoot, eventName, payload).catch(() => {});
|
|
65
|
+
if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded' || eventName === 'UserPromptSubmit') {
|
|
66
|
+
await recoverStaleTurnStates(projectRoot).catch(() => []);
|
|
67
|
+
}
|
|
64
68
|
|
|
65
69
|
if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
|
|
66
70
|
const context = await buildContextBrief(projectRoot, 'SessionStart');
|
|
@@ -96,9 +100,16 @@ export async function handleHook(provider, explicitEvent) {
|
|
|
96
100
|
const assistantText = payload.last_assistant_message || payload.response || payload.assistant_response || '';
|
|
97
101
|
const entry = await buildEntryFromState(projectRoot, payload, assistantText);
|
|
98
102
|
if (entry.question !== '(not captured)' || entry.result !== '(not captured)') {
|
|
99
|
-
await appendLiveQa(projectRoot, entry);
|
|
103
|
+
const liveQaPath = await appendLiveQa(projectRoot, entry);
|
|
100
104
|
const queryPath = await writeQueryPage(projectRoot, entry);
|
|
101
105
|
const decisionPath = await writeDecisionPage(projectRoot, entry);
|
|
106
|
+
if (eventName === 'Stop' || eventName === 'SessionEnd') {
|
|
107
|
+
await recordMaintenanceForEntry(projectRoot, entry, {
|
|
108
|
+
source: decisionPath || queryPath || liveQaPath,
|
|
109
|
+
eventName,
|
|
110
|
+
reason: 'Captured turn needs durable wiki review.',
|
|
111
|
+
}).catch(() => {});
|
|
112
|
+
}
|
|
102
113
|
await appendWikiLog(projectRoot, `captured ${eventName}${queryPath ? `; query=${relative(projectRoot, queryPath)}` : ''}${decisionPath ? `; decision=${relative(projectRoot, decisionPath)}` : ''}`);
|
|
103
114
|
}
|
|
104
115
|
if (eventName === 'Stop' || eventName === 'SessionEnd') {
|
package/src/install.js
CHANGED
|
@@ -4,6 +4,7 @@ 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
6
|
import { backupFile, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
|
|
7
|
+
import { maintenanceSummary } from './maintenance.js';
|
|
7
8
|
import { inspectProjectState } from './project-state.js';
|
|
8
9
|
import { bootstrapProject } from './project.js';
|
|
9
10
|
import { recordProject } from './projects.js';
|
|
@@ -121,7 +122,10 @@ async function reconcileLocalBin(localBinPath) {
|
|
|
121
122
|
|
|
122
123
|
function addHook(hooks, eventName, command, options = {}) {
|
|
123
124
|
hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
|
|
124
|
-
const already =
|
|
125
|
+
const already = hooks[eventName].some((entry) => (
|
|
126
|
+
Array.isArray(entry?.hooks) &&
|
|
127
|
+
entry.hooks.some((hook) => hook?.type === 'command' && hook?.command === command)
|
|
128
|
+
));
|
|
125
129
|
if (already) return false;
|
|
126
130
|
const entry = {
|
|
127
131
|
hooks: [
|
|
@@ -280,5 +284,6 @@ export async function status(options = {}) {
|
|
|
280
284
|
codexHooksPath,
|
|
281
285
|
claudeSettingsPath,
|
|
282
286
|
project: await inspectProjectState(workspace),
|
|
287
|
+
maintenance: await maintenanceSummary(workspace),
|
|
283
288
|
};
|
|
284
289
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { readdir, unlink } from 'fs/promises';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { appendText, exists, kitDataDir, readJson, readText, sha256, writeTextIfMissing } from './fs-utils.js';
|
|
4
|
+
import { redactText, summarizeForStorage } from './redaction.js';
|
|
5
|
+
import { buildEntryFromTurnState, hasRecoverableTurnState } from './state.js';
|
|
6
|
+
|
|
7
|
+
export const MAINTENANCE_QUEUE_REL = 'llm-wiki/outputs/maintenance/queue.md';
|
|
8
|
+
const DEFAULT_STALE_TURN_MS = 10 * 60 * 1000;
|
|
9
|
+
const DEFAULT_STALE_PENDING_DAYS = 7;
|
|
10
|
+
const DEFAULT_PENDING_LIMIT = 20;
|
|
11
|
+
|
|
12
|
+
function queuePath(projectRoot) {
|
|
13
|
+
return join(projectRoot, MAINTENANCE_QUEUE_REL);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function queueHeader() {
|
|
17
|
+
return [
|
|
18
|
+
'# LLM Wiki Maintenance Queue',
|
|
19
|
+
'',
|
|
20
|
+
'정식 wiki 문서에 합칠 후보를 보관한다. Hook은 후보만 만들고, agent가 기존 durable wiki 문서를 찾아 병합한다.',
|
|
21
|
+
'',
|
|
22
|
+
'Status values: pending, done, skipped.',
|
|
23
|
+
'',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nowIso() {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function staleTurnMs() {
|
|
32
|
+
const value = Number(process.env.LLM_WIKI_KIT_STALE_TURN_MS || DEFAULT_STALE_TURN_MS);
|
|
33
|
+
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_STALE_TURN_MS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeSource(projectRoot, source) {
|
|
37
|
+
if (!source) return '';
|
|
38
|
+
const text = String(source);
|
|
39
|
+
if (text.startsWith('state:')) return text;
|
|
40
|
+
if (text.startsWith(projectRoot)) return relative(projectRoot, text).split('\\').join('/');
|
|
41
|
+
return text.split('\\').join('/');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function relativeSource(projectRoot, path) {
|
|
45
|
+
return normalizeSource(projectRoot, path);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizeField(value, maxLength = 500) {
|
|
49
|
+
return summarizeForStorage(String(value || '').replace(/\s+/g, ' ').trim(), maxLength);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferSuggestedTarget(entry, source) {
|
|
53
|
+
const text = `${entry?.question || ''}\n${entry?.result || ''}\n${entry?.work || ''}\n${source || ''}`.toLowerCase();
|
|
54
|
+
if (String(source || '').includes('wiki/decisions/') || /decision|decided|결정|선택|채택|확정/.test(text)) return 'wiki/decisions/';
|
|
55
|
+
if (/error|bug|fix|debug|failure|failed|실패|오류|버그|수정|원인/.test(text)) return 'wiki/debugging/';
|
|
56
|
+
if (/architecture|flow|runtime|hook|update|install|context|memory|구조|아키텍처|흐름|설치|업데이트/.test(text)) return 'wiki/architecture/';
|
|
57
|
+
if (/procedure|process|policy|rule|운영|절차|규칙|정책/.test(text)) return 'procedures/';
|
|
58
|
+
return 'wiki/concepts/';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function itemId(projectRoot, source, entry) {
|
|
62
|
+
const question = entry?.question || entry?.topic || '';
|
|
63
|
+
const firstInput = entry?.firstTimestamp || entry?.createdAt || source || '';
|
|
64
|
+
return sha256(`${projectRoot}\n${source}\n${question}\n${firstInput}`).slice(0, 24);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function itemBlock(item) {
|
|
68
|
+
return [
|
|
69
|
+
`## ${item.status || 'pending'} - ${item.topic || 'maintenance item'}`,
|
|
70
|
+
'',
|
|
71
|
+
`- id: ${item.id}`,
|
|
72
|
+
`- created_at: ${item.created_at}`,
|
|
73
|
+
`- last_seen_at: ${item.last_seen_at}`,
|
|
74
|
+
`- source: ${item.source}`,
|
|
75
|
+
`- suggested_target: ${item.suggested_target}`,
|
|
76
|
+
`- reason: ${item.reason}`,
|
|
77
|
+
`- result_missing: ${item.result_missing ? 'true' : 'false'}`,
|
|
78
|
+
'',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readMaintenanceQueue(projectRoot) {
|
|
83
|
+
const path = queuePath(projectRoot);
|
|
84
|
+
const text = await readText(path, '');
|
|
85
|
+
const items = [];
|
|
86
|
+
let current = null;
|
|
87
|
+
|
|
88
|
+
const pushCurrent = () => {
|
|
89
|
+
if (current) items.push(current);
|
|
90
|
+
current = null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const line of text.split(/\r?\n/)) {
|
|
94
|
+
const header = line.match(/^##\s+(pending|done|skipped)\s+-\s*(.*)$/);
|
|
95
|
+
if (header) {
|
|
96
|
+
pushCurrent();
|
|
97
|
+
current = {
|
|
98
|
+
status: header[1],
|
|
99
|
+
topic: header[2].trim(),
|
|
100
|
+
fields: {},
|
|
101
|
+
};
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!current) continue;
|
|
105
|
+
const field = line.match(/^-\s+([a-z_]+):\s*(.*)$/);
|
|
106
|
+
if (field) {
|
|
107
|
+
current.fields[field[1]] = field[2].trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
pushCurrent();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
path,
|
|
114
|
+
exists: await exists(path),
|
|
115
|
+
items: items.map((item) => ({
|
|
116
|
+
...item.fields,
|
|
117
|
+
status: item.status,
|
|
118
|
+
topic: item.topic,
|
|
119
|
+
result_missing: String(item.fields.result_missing || '').toLowerCase() === 'true',
|
|
120
|
+
})),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function summarizeMaintenanceQueue(queue, options = {}) {
|
|
125
|
+
const staleDays = options.staleDays ?? DEFAULT_STALE_PENDING_DAYS;
|
|
126
|
+
const pendingLimit = options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
|
|
127
|
+
const pending = queue.items.filter((item) => item.status === 'pending');
|
|
128
|
+
const staleCutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
|
|
129
|
+
const stalePending = pending.filter((item) => {
|
|
130
|
+
const time = Date.parse(item.created_at || item.last_seen_at || '');
|
|
131
|
+
return Number.isFinite(time) && time < staleCutoff;
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
path: queue.path,
|
|
135
|
+
exists: queue.exists,
|
|
136
|
+
items: queue.items,
|
|
137
|
+
pending,
|
|
138
|
+
done: queue.items.filter((item) => item.status === 'done'),
|
|
139
|
+
skipped: queue.items.filter((item) => item.status === 'skipped'),
|
|
140
|
+
pendingCount: pending.length,
|
|
141
|
+
doneCount: queue.items.filter((item) => item.status === 'done').length,
|
|
142
|
+
skippedCount: queue.items.filter((item) => item.status === 'skipped').length,
|
|
143
|
+
stalePending,
|
|
144
|
+
stalePendingDays: staleDays,
|
|
145
|
+
pendingLimit,
|
|
146
|
+
tooManyPending: pending.length > pendingLimit,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function maintenanceSummary(projectRoot, options = {}) {
|
|
151
|
+
return summarizeMaintenanceQueue(await readMaintenanceQueue(projectRoot), options);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function appendMaintenanceItem(projectRoot, item) {
|
|
155
|
+
const path = queuePath(projectRoot);
|
|
156
|
+
await writeTextIfMissing(path, queueHeader());
|
|
157
|
+
const queue = await readMaintenanceQueue(projectRoot);
|
|
158
|
+
if (queue.items.some((existing) => existing.id === item.id)) {
|
|
159
|
+
return { created: false, path, id: item.id };
|
|
160
|
+
}
|
|
161
|
+
await appendText(path, redactText(`\n${itemBlock(item)}`, 6000));
|
|
162
|
+
return { created: true, path, id: item.id };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function recordMaintenanceForEntry(projectRoot, entry, options = {}) {
|
|
166
|
+
const source = relativeSource(projectRoot, options.source || options.queryPath || options.decisionPath || options.liveQaPath || '');
|
|
167
|
+
if (!source && !options.resultMissing) return { created: false, reason: 'missing-source' };
|
|
168
|
+
if (!entry?.question || entry.question === '(not captured)') return { created: false, reason: 'missing-question' };
|
|
169
|
+
const created = nowIso();
|
|
170
|
+
const item = {
|
|
171
|
+
id: itemId(projectRoot, source, entry),
|
|
172
|
+
status: 'pending',
|
|
173
|
+
topic: sanitizeField(entry.topic || entry.question, 120),
|
|
174
|
+
created_at: created,
|
|
175
|
+
last_seen_at: created,
|
|
176
|
+
source,
|
|
177
|
+
suggested_target: options.suggestedTarget || inferSuggestedTarget(entry, source),
|
|
178
|
+
reason: sanitizeField(options.reason || `Captured ${options.eventName || 'turn'} needs durable wiki review.`, 300),
|
|
179
|
+
result_missing: Boolean(options.resultMissing),
|
|
180
|
+
};
|
|
181
|
+
return appendMaintenanceItem(projectRoot, item);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function stateDir() {
|
|
185
|
+
return join(kitDataDir(), 'state');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function stateFiles() {
|
|
189
|
+
try {
|
|
190
|
+
return (await readdir(stateDir()))
|
|
191
|
+
.filter((file) => file.endsWith('.json'))
|
|
192
|
+
.map((file) => join(stateDir(), file));
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function recoverStaleTurnStates(projectRoot, options = {}) {
|
|
199
|
+
const output = [];
|
|
200
|
+
const cutoffMs = options.staleMs ?? staleTurnMs();
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
for (const path of await stateFiles()) {
|
|
203
|
+
const state = await readJson(path, null);
|
|
204
|
+
if (!state || state.projectRoot !== projectRoot) continue;
|
|
205
|
+
const updated = Date.parse(state.updated_at || '');
|
|
206
|
+
if (Number.isFinite(updated) && now - updated < cutoffMs) continue;
|
|
207
|
+
if (!hasRecoverableTurnState(state)) continue;
|
|
208
|
+
const entry = buildEntryFromTurnState(state, '');
|
|
209
|
+
const result = await recordMaintenanceForEntry(projectRoot, entry, {
|
|
210
|
+
source: `state:${state.session || path.split('/').pop()?.replace(/\.json$/, '')}`,
|
|
211
|
+
eventName: 'recovered stale turn state',
|
|
212
|
+
reason: 'Recovered from stale turn state because Stop/SessionEnd did not complete.',
|
|
213
|
+
resultMissing: true,
|
|
214
|
+
});
|
|
215
|
+
await unlink(path).catch(() => {});
|
|
216
|
+
output.push({ ...result, statePath: path, session: state.session });
|
|
217
|
+
}
|
|
218
|
+
return output;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatMaintenanceContext(summary, options = {}) {
|
|
222
|
+
const limit = options.limit || 5;
|
|
223
|
+
const pending = summary.pending.slice(0, limit);
|
|
224
|
+
if (pending.length === 0) return '';
|
|
225
|
+
const lines = [
|
|
226
|
+
'LLM Wiki maintenance queue:',
|
|
227
|
+
'- Pending items should be merged into existing durable wiki pages when relevant; update queue status to done or skipped after review.',
|
|
228
|
+
];
|
|
229
|
+
for (const item of pending) {
|
|
230
|
+
lines.push(`- ${item.topic || item.id}: source=${item.source}; target=${item.suggested_target}; reason=${item.reason}${item.result_missing ? '; result_missing=true' : ''}`);
|
|
231
|
+
}
|
|
232
|
+
if (summary.pending.length > pending.length) {
|
|
233
|
+
lines.push(`- ${summary.pending.length - pending.length} more pending item(s) hidden from context.`);
|
|
234
|
+
}
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function formatMaintenanceResult(summary) {
|
|
239
|
+
const lines = [
|
|
240
|
+
'llm-wiki maintenance',
|
|
241
|
+
`- queue: ${summary.path}`,
|
|
242
|
+
`- pending: ${summary.pendingCount}`,
|
|
243
|
+
`- done: ${summary.doneCount}`,
|
|
244
|
+
`- skipped: ${summary.skippedCount}`,
|
|
245
|
+
];
|
|
246
|
+
if (summary.pending.length > 0) {
|
|
247
|
+
lines.push('', 'Pending:');
|
|
248
|
+
for (const item of summary.pending.slice(0, 10)) {
|
|
249
|
+
lines.push(`- ${item.topic || item.id}: ${item.source} -> ${item.suggested_target}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
package/src/project.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeTextIfMissing,
|
|
10
10
|
} from './fs-utils.js';
|
|
11
11
|
import { LLM_WIKI_DIRS } from './constants.js';
|
|
12
|
+
import { formatMaintenanceContext, maintenanceSummary } from './maintenance.js';
|
|
12
13
|
import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
|
|
13
14
|
import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
14
15
|
import { formatProjectMaintenanceContext, inspectProjectState, recordManagedTemplates } from './project-state.js';
|
|
@@ -121,6 +122,7 @@ export async function appendLiveQa(projectRoot, entry) {
|
|
|
121
122
|
'',
|
|
122
123
|
].join('\n');
|
|
123
124
|
await appendText(path, redactText(block, 12000));
|
|
125
|
+
return path;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export async function writeQueryPage(projectRoot, entry) {
|
|
@@ -166,5 +168,8 @@ export async function buildContextBrief(projectRoot, eventName, query = '') {
|
|
|
166
168
|
const maintenance = await inspectProjectState(projectRoot)
|
|
167
169
|
.then(formatProjectMaintenanceContext)
|
|
168
170
|
.catch(() => '');
|
|
169
|
-
|
|
171
|
+
const wikiMaintenance = await maintenanceSummary(projectRoot)
|
|
172
|
+
.then(formatMaintenanceContext)
|
|
173
|
+
.catch(() => '');
|
|
174
|
+
return [formatContextPack(pack), maintenance, wikiMaintenance].filter(Boolean).join('\n\n');
|
|
170
175
|
}
|
package/src/state.js
CHANGED
|
@@ -63,16 +63,38 @@ export async function rememberTool(projectRoot, payload, summary) {
|
|
|
63
63
|
|
|
64
64
|
export async function buildEntryFromState(projectRoot, payload, assistantText) {
|
|
65
65
|
const state = await readTurnState(projectRoot, payload);
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
return buildEntryFromTurnState(state, assistantText || payload.last_assistant_message || payload.response || '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hasRecoverableTurnState(state) {
|
|
70
|
+
return Boolean(
|
|
71
|
+
state &&
|
|
72
|
+
(
|
|
73
|
+
(Array.isArray(state.questions) && state.questions.length > 0) ||
|
|
74
|
+
(Array.isArray(state.tools) && state.tools.length > 0) ||
|
|
75
|
+
(Array.isArray(state.files) && state.files.length > 0) ||
|
|
76
|
+
(Array.isArray(state.verification) && state.verification.length > 0)
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildEntryFromTurnState(state, assistantText = '') {
|
|
82
|
+
const questions = Array.isArray(state.questions) ? state.questions : [];
|
|
83
|
+
const tools = Array.isArray(state.tools) ? state.tools : [];
|
|
84
|
+
const files = Array.isArray(state.files) ? state.files : [];
|
|
85
|
+
const verification = Array.isArray(state.verification) ? state.verification : [];
|
|
86
|
+
const question = questions.at(-1)?.text || '';
|
|
87
|
+
const result = summarizeForStorage(assistantText || '', 4000);
|
|
68
88
|
const topic = question ? question.replace(/\s+/g, ' ').slice(0, 80) : 'session turn';
|
|
69
89
|
return {
|
|
70
90
|
topic,
|
|
71
91
|
question: question || '(not captured)',
|
|
72
|
-
work:
|
|
92
|
+
work: tools.map((item) => `- ${item.text}`).join('\n') || '(not captured)',
|
|
73
93
|
result: result || '(not captured)',
|
|
74
|
-
changedFiles: [...new Set(
|
|
75
|
-
verification:
|
|
94
|
+
changedFiles: [...new Set(files)].map((item) => `- ${item}`).join('\n') || '(not captured)',
|
|
95
|
+
verification: verification.map((item) => `- ${item}`).join('\n') || '(not captured)',
|
|
76
96
|
followUp: '',
|
|
97
|
+
firstTimestamp: questions[0]?.at || tools[0]?.at || state.updated_at,
|
|
98
|
+
session: state.session,
|
|
77
99
|
};
|
|
78
100
|
}
|
package/src/templates.js
CHANGED
|
@@ -15,6 +15,7 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
|
|
|
15
15
|
- \`llm-wiki/wiki/memory.md\`는 짧은 핵심 기억이다. 긴 설명 대신 현재 상태와 중요한 문서 링크만 유지한다.
|
|
16
16
|
- hook이 주입한 context를 우선 사용한다. 수동 확인이나 정리에는 \`llm-wiki context\`, \`llm-wiki lint\`, \`llm-wiki consolidate\`를 agent 보조 도구로 사용한다.
|
|
17
17
|
- hook은 redacted live Q&A와 query/decision 후보를 기록한다. 재사용 가능한 지식은 agent가 기존 정식 wiki 문서에 합친다.
|
|
18
|
+
- hook은 종료/시작 경계에서 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. pending 항목은 agent가 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
|
|
18
19
|
- 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
|
|
19
20
|
- 일회성 작업 기록은 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 결론은 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
|
|
20
21
|
- 검증 명령, 근거 파일, 불확실한 점을 함께 남긴다. 추론은 추론이라고 표시하고, 모순은 지우지 말고 Open Questions 또는 Contradictions에 남긴다.
|
|
@@ -47,6 +48,7 @@ Codex와 Claude Code를 평소처럼 사용하는 동안 living Markdown LLM Wik
|
|
|
47
48
|
- 중요한 주장에는 \`source_ids\`, 파일 경로, 검증 명령 중 하나 이상을 남긴다.
|
|
48
49
|
- 오래 남길 내용이 생기면 새 문서부터 만들지 말고 기존 \`wiki/\` 문서를 먼저 찾아 갱신한다.
|
|
49
50
|
- 반복해서 쓸 지식은 \`outputs/questions/\`에만 두지 말고 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
|
|
51
|
+
- hook이 만든 \`outputs/maintenance/queue.md\` pending 항목은 다음 작업 시작 시 확인하고, 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
|
|
50
52
|
- \`wiki/memory.md\`는 짧게 유지한다. 긴 설명 대신 현재 상태와 중요한 문서 링크를 둔다.
|
|
51
53
|
- 모순은 덮어쓰지 말고 \`Contradictions\` 또는 \`Open Questions\`에 보존한다.
|
|
52
54
|
- 인증값, token, password, private key, \`.env\` 원문은 wiki에 저장하지 않는다.
|
|
@@ -75,6 +77,7 @@ superseded_by: []
|
|
|
75
77
|
- query: hook이 주입한 context를 우선 사용한다. 수동 명령은 점검용이며 일반 사용 흐름의 필수 단계가 아니다.
|
|
76
78
|
- lint: agent가 필요할 때 wiki 건강 상태를 점검하는 보조 도구다. 사용자가 매번 직접 실행해야 하는 명령이 아니다.
|
|
77
79
|
- consolidate: agent가 \`memory.md\`/\`index.md\` generated block을 안전하게 갱신할 때 쓰는 보조 도구다. 손글씨 영역과 정식 문서 본문은 덮어쓰지 않는다.
|
|
80
|
+
- maintenance: \`outputs/maintenance/queue.md\`는 종료/시작 경계에서 생긴 정리 후보 목록이다. agent가 기존 문서 병합 여부를 판단하고 상태를 갱신한다.
|
|
78
81
|
`;
|
|
79
82
|
}
|
|
80
83
|
|
package/src/wiki-lint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir } from 'fs/promises';
|
|
2
2
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'path';
|
|
3
3
|
import { exists, readText } from './fs-utils.js';
|
|
4
|
+
import { maintenanceSummary } from './maintenance.js';
|
|
4
5
|
import { inspectProjectState } from './project-state.js';
|
|
5
6
|
import { hasSecretLikeText } from './redaction.js';
|
|
6
7
|
import {
|
|
@@ -244,6 +245,14 @@ export async function runLint(projectRoot, options = {}) {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
const maintenance = await maintenanceSummary(projectRoot).catch(() => null);
|
|
249
|
+
if (maintenance?.tooManyPending) {
|
|
250
|
+
issues.push(issue('warning', 'maintenance-too-many-pending', 'outputs/maintenance/queue.md', `maintenance queue has ${maintenance.pendingCount} pending items; review and merge durable items into wiki pages`));
|
|
251
|
+
}
|
|
252
|
+
if ((maintenance?.stalePending || []).length > 0) {
|
|
253
|
+
issues.push(issue('warning', 'maintenance-stale-pending', 'outputs/maintenance/queue.md', `${maintenance.stalePending.length} pending maintenance item(s) are older than ${maintenance.stalePendingDays} days`));
|
|
254
|
+
}
|
|
255
|
+
|
|
247
256
|
return lintResult(projectRoot, pages, issues);
|
|
248
257
|
}
|
|
249
258
|
|