chapterhouse 0.3.16 → 0.3.18
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/dist/api/server.js +18 -0
- package/dist/api/server.test.js +57 -1
- package/dist/copilot/agents.js +2 -0
- package/dist/copilot/agents.test.js +22 -0
- package/dist/copilot/skills.test.js +10 -0
- package/dist/copilot/system-message.js +2 -0
- package/dist/copilot/system-message.test.js +11 -0
- package/dist/wiki/fs.js +16 -0
- package/dist/wiki/fs.test.js +19 -1
- package/dist/wiki/index-manager.test.js +13 -1
- package/dist/wiki/lint.test.js +17 -20
- package/dist/wiki/topic-structure.js +8 -2
- package/dist/wiki/topic-structure.test.js +12 -0
- package/package.json +1 -1
- package/skills/wiki-conventions/SKILL.md +145 -0
- package/web/dist/assets/{index-BYuMgJ36.js → index-Bjaa3b4i.js} +63 -63
- package/web/dist/assets/{index-BYuMgJ36.js.map → index-Bjaa3b4i.js.map} +1 -1
- package/web/dist/index.html +1 -1
package/dist/api/server.js
CHANGED
|
@@ -203,6 +203,20 @@ function assertValidPagePath(path) {
|
|
|
203
203
|
function getWikiPageScope(path) {
|
|
204
204
|
return teamWikiSync.isTeamPath(path) ? "team" : "personal";
|
|
205
205
|
}
|
|
206
|
+
function getEmptyWikiWelcomeContent(today = new Date()) {
|
|
207
|
+
return `---
|
|
208
|
+
title: Wiki
|
|
209
|
+
summary: Empty wiki — get started.
|
|
210
|
+
updated: ${today.toISOString().slice(0, 10)}
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
# Wiki
|
|
214
|
+
|
|
215
|
+
Your wiki is empty. Pages are organized by category — projects, people, tools, topics, areas, orgs, facts, preferences, routines.
|
|
216
|
+
|
|
217
|
+
Create your first page via the wiki UI or by editing files under \`pages/\`.
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
206
220
|
// Active SSE connections
|
|
207
221
|
const sseClients = new Map();
|
|
208
222
|
const pendingSseMessages = [];
|
|
@@ -746,6 +760,10 @@ app.get("/api/wiki/page", async (req, res) => {
|
|
|
746
760
|
: undefined;
|
|
747
761
|
const content = await readWikiPage(path, { authorizationHeader });
|
|
748
762
|
if (content === undefined) {
|
|
763
|
+
if (path === "pages/index.md") {
|
|
764
|
+
res.json({ path, content: getEmptyWikiWelcomeContent() });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
749
767
|
throw new NotFoundError("Page not found");
|
|
750
768
|
}
|
|
751
769
|
res.json({ path, content });
|
package/dist/api/server.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -293,6 +293,62 @@ test("server wiki routes support authenticated CRUD", async () => {
|
|
|
293
293
|
assert.deepEqual(await missingResponse.json(), { error: "Page not found" });
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
|
+
test("server wiki route synthesizes a welcome page when pages/index.md is missing", async () => {
|
|
297
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
298
|
+
rmSync(join(serverTestRoot, ".chapterhouse", "wiki", "pages", "index.md"), { force: true });
|
|
299
|
+
const response = await fetch(`${baseUrl}/api/wiki/page?path=pages/index.md`, {
|
|
300
|
+
headers: { authorization: authHeader },
|
|
301
|
+
});
|
|
302
|
+
assert.equal(response.status, 200);
|
|
303
|
+
assert.deepEqual(await response.json(), {
|
|
304
|
+
path: "pages/index.md",
|
|
305
|
+
content: `---
|
|
306
|
+
title: Wiki
|
|
307
|
+
summary: Empty wiki — get started.
|
|
308
|
+
updated: ${new Date().toISOString().slice(0, 10)}
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
# Wiki
|
|
312
|
+
|
|
313
|
+
Your wiki is empty. Pages are organized by category — projects, people, tools, topics, areas, orgs, facts, preferences, routines.
|
|
314
|
+
|
|
315
|
+
Create your first page via the wiki UI or by editing files under \`pages/\`.
|
|
316
|
+
`,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
test("server wiki route returns stored content for pages/index.md when it exists", async () => {
|
|
321
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
322
|
+
const indexPath = join(serverTestRoot, ".chapterhouse", "wiki", "pages", "index.md");
|
|
323
|
+
const content = `---
|
|
324
|
+
title: Wiki
|
|
325
|
+
summary: Existing home page.
|
|
326
|
+
updated: 2026-05-12
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
# Custom Wiki Home
|
|
330
|
+
`;
|
|
331
|
+
mkdirSync(join(serverTestRoot, ".chapterhouse", "wiki", "pages"), { recursive: true });
|
|
332
|
+
writeFileSync(indexPath, content, "utf-8");
|
|
333
|
+
const response = await fetch(`${baseUrl}/api/wiki/page?path=pages/index.md`, {
|
|
334
|
+
headers: { authorization: authHeader },
|
|
335
|
+
});
|
|
336
|
+
assert.equal(response.status, 200);
|
|
337
|
+
assert.deepEqual(await response.json(), {
|
|
338
|
+
path: "pages/index.md",
|
|
339
|
+
content,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
test("server wiki route still returns 404 for other missing wiki pages", async () => {
|
|
344
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
345
|
+
const response = await fetch(`${baseUrl}/api/wiki/page?path=pages/projects/nonexistent/index.md`, {
|
|
346
|
+
headers: { authorization: authHeader },
|
|
347
|
+
});
|
|
348
|
+
assert.equal(response.status, 404);
|
|
349
|
+
assert.deepEqual(await response.json(), { error: "Page not found" });
|
|
350
|
+
});
|
|
351
|
+
});
|
|
296
352
|
test("server message route validates the SSE connection id", async () => {
|
|
297
353
|
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
298
354
|
const response = await fetch(`${baseUrl}/api/message`, {
|
package/dist/copilot/agents.js
CHANGED
|
@@ -222,6 +222,8 @@ You are an agent within Chapterhouse, a team-level AI assistant for engineering
|
|
|
222
222
|
### Shared Wiki
|
|
223
223
|
All agents share a wiki knowledge base for persistent memory. Use \`wiki_read\` and \`wiki_search\` to find existing knowledge, and \`wiki_update\` to save important findings.
|
|
224
224
|
|
|
225
|
+
Invoke \`wiki-conventions\` before wiki writes or restructuring work. Treat \`wiki_update\`, \`remember\`, \`forget\`, \`wiki_ingest\`, \`wiki_lint\`, and \`wiki_rebuild_index\` as write-sensitive workflows. Before using them, read \`pages/index.md\`, scan the last 20-30 entries of \`pages/_meta/log.md\`, and run \`wiki_search\` for the topic when the wiki is large or the topic is ambiguous.
|
|
226
|
+
|
|
225
227
|
### Communication
|
|
226
228
|
- You receive tasks from @chapterhouse (the orchestrator) or directly from the user
|
|
227
229
|
- Your results are relayed back to the user by @chapterhouse
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { composeAgentSystemMessage, } from "./agents.js";
|
|
4
|
+
function makeAgent(slug) {
|
|
5
|
+
return {
|
|
6
|
+
slug,
|
|
7
|
+
name: slug,
|
|
8
|
+
description: `${slug} test agent`,
|
|
9
|
+
model: "claude-sonnet-4.6",
|
|
10
|
+
systemMessage: `You are ${slug}.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
test("composeAgentSystemMessage steers wiki-capable agents to wiki-conventions", () => {
|
|
14
|
+
for (const slug of ["coder", "general-purpose"]) {
|
|
15
|
+
const message = composeAgentSystemMessage(makeAgent(slug));
|
|
16
|
+
assert.match(message, /invoke `wiki-conventions` before wiki writes/i);
|
|
17
|
+
assert.match(message, /wiki_update[\s\S]{0,80}remember[\s\S]{0,80}forget[\s\S]{0,80}wiki_ingest[\s\S]{0,80}wiki_lint[\s\S]{0,80}wiki_rebuild_index/i);
|
|
18
|
+
assert.match(message, /read `pages\/index\.md`/i);
|
|
19
|
+
assert.match(message, /scan the last 20-30 entries of `pages\/_meta\/log\.md`/i);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=agents.test.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { listSkills } from "./skills.js";
|
|
4
|
+
test("listSkills includes bundled wiki-conventions skill metadata", () => {
|
|
5
|
+
const skill = listSkills().find((entry) => entry.slug === "wiki-conventions" && entry.source === "bundled");
|
|
6
|
+
assert.ok(skill);
|
|
7
|
+
assert.equal(skill.name, "wiki-conventions");
|
|
8
|
+
assert.match(skill.description, /creating, editing, linting, restructuring, or reviewing Chapterhouse wiki content/i);
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=skills.test.js.map
|
|
@@ -122,6 +122,8 @@ You can delegate **multiple tasks simultaneously**. Different agents can work in
|
|
|
122
122
|
- With \`remember\`, pass \`entity\` for entity categories (and \`facet\` to target a sub-page). With \`wiki_update\`, give the full canonical path — bad paths are rejected with a suggested correction; just retry with the suggestion.
|
|
123
123
|
- If the structure ever looks wrong, call \`wiki_rebuild_index\` to regenerate the index from disk.
|
|
124
124
|
|
|
125
|
+
**Wiki writes and restructuring**: Before writing or restructuring wiki content, invoke the \`wiki-conventions\` skill. Treat \`wiki_update\`, \`remember\`, \`forget\`, \`wiki_ingest\`, \`wiki_lint\`, and \`wiki_rebuild_index\` as write-sensitive workflows. Before using them, read \`pages/index.md\`, scan the last 20-30 entries of \`pages/_meta/log.md\`, and run \`wiki_search\` for the topic when the wiki is large or the topic is ambiguous.
|
|
126
|
+
|
|
125
127
|
**Learning workflow**: When the user asks you to do something you don't have a skill for:
|
|
126
128
|
1. **Search skills.sh first**: Use the find-skills skill to search for existing community skills.
|
|
127
129
|
2. **Present what you found**: Tell the user the skill name, what it does, and its security status.
|
|
@@ -22,4 +22,15 @@ test("orchestrator prompt omits version banner when version is not provided", ()
|
|
|
22
22
|
const message = getOrchestratorSystemMessage();
|
|
23
23
|
assert.doesNotMatch(message, /chapterhouse v\d/);
|
|
24
24
|
});
|
|
25
|
+
test("orchestrator prompt requires wiki-conventions before write-sensitive wiki work", () => {
|
|
26
|
+
const message = getOrchestratorSystemMessage();
|
|
27
|
+
assert.match(message, /wiki-conventions[\s\S]{0,500}wiki_update[\s\S]{0,200}remember[\s\S]{0,200}forget[\s\S]{0,200}wiki_ingest[\s\S]{0,200}wiki_lint[\s\S]{0,200}wiki_rebuild_index/i);
|
|
28
|
+
assert.match(message, /before writing or restructuring wiki content/i);
|
|
29
|
+
});
|
|
30
|
+
test("orchestrator prompt describes the wiki orientation ritual", () => {
|
|
31
|
+
const message = getOrchestratorSystemMessage();
|
|
32
|
+
assert.match(message, /read `pages\/index\.md`/i);
|
|
33
|
+
assert.match(message, /scan the last 20-30 entries of `pages\/_meta\/log\.md`/i);
|
|
34
|
+
assert.match(message, /run `wiki_search` for the topic/i);
|
|
35
|
+
});
|
|
25
36
|
//# sourceMappingURL=system-message.test.js.map
|
package/dist/wiki/fs.js
CHANGED
|
@@ -7,6 +7,7 @@ import { WIKI_DIR, WIKI_PAGES_DIR, WIKI_SOURCES_DIR } from "../paths.js";
|
|
|
7
7
|
import { normalizeWikiPath } from "./path-utils.js";
|
|
8
8
|
import { topicPathError } from "./topic-structure.js";
|
|
9
9
|
const INDEX_PATH = join(WIKI_DIR, "index.md");
|
|
10
|
+
const PAGES_INDEX_PATH = join(WIKI_PAGES_DIR, "index.md");
|
|
10
11
|
const LOG_PATH = join(WIKI_PAGES_DIR, "_meta", "log.md");
|
|
11
12
|
/**
|
|
12
13
|
* Write a file atomically: write to a temp file in the same directory, fsync,
|
|
@@ -63,6 +64,18 @@ Last updated: ${new Date().toISOString().slice(0, 10)}
|
|
|
63
64
|
_(No pages yet.)_
|
|
64
65
|
`;
|
|
65
66
|
}
|
|
67
|
+
function getInitialPagesIndex() {
|
|
68
|
+
return `---
|
|
69
|
+
title: Wiki
|
|
70
|
+
summary: Index of all wiki pages.
|
|
71
|
+
updated: ${new Date().toISOString().slice(0, 10)}
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# Wiki
|
|
75
|
+
|
|
76
|
+
Welcome to your Chapterhouse wiki. Pages are organized by category — projects, people, tools, topics, areas, orgs, facts, preferences, routines. Use the navigation to explore, or search.
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
66
79
|
const INITIAL_LOG = `# Wiki Action Log
|
|
67
80
|
|
|
68
81
|
_Append-only record of wiki operations._
|
|
@@ -79,6 +92,9 @@ export function ensureWikiStructure() {
|
|
|
79
92
|
if (!existsSync(INDEX_PATH)) {
|
|
80
93
|
writeFileAtomic(INDEX_PATH, getInitialIndex());
|
|
81
94
|
}
|
|
95
|
+
if (!existsSync(PAGES_INDEX_PATH)) {
|
|
96
|
+
writeFileAtomic(PAGES_INDEX_PATH, getInitialPagesIndex());
|
|
97
|
+
}
|
|
82
98
|
if (!existsSync(LOG_PATH)) {
|
|
83
99
|
writeFileAtomic(LOG_PATH, INITIAL_LOG);
|
|
84
100
|
}
|
package/dist/wiki/fs.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
5
|
const repoRoot = process.cwd();
|
|
@@ -20,6 +20,7 @@ test("wiki fs creates the wiki structure and supports page CRUD", async () => {
|
|
|
20
20
|
assert.equal(wiki.ensureWikiStructure(), true);
|
|
21
21
|
assert.equal(wiki.ensureWikiStructure(), false);
|
|
22
22
|
assert.equal(existsSync(join(wikiDir, "index.md")), true);
|
|
23
|
+
assert.equal(existsSync(join(wikiDir, "pages", "index.md")), true);
|
|
23
24
|
assert.equal(existsSync(join(wikiDir, "pages", "_meta", "log.md")), true);
|
|
24
25
|
wiki.writePage("pages/shared/runbooks/deploy.md", "# Deploy\n");
|
|
25
26
|
assert.equal(wiki.pageExists("pages/shared/runbooks/deploy.md"), true);
|
|
@@ -30,6 +31,23 @@ test("wiki fs creates the wiki structure and supports page CRUD", async () => {
|
|
|
30
31
|
assert.equal(wiki.deletePage("pages/shared/runbooks/deploy.md"), false);
|
|
31
32
|
assert.equal(wiki.readPage("pages/shared/runbooks/deploy.md"), undefined);
|
|
32
33
|
});
|
|
34
|
+
test("wiki fs seeds pages/index.md when missing", async () => {
|
|
35
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
36
|
+
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
37
|
+
const wiki = await loadFsModule();
|
|
38
|
+
wiki.ensureWikiStructure();
|
|
39
|
+
const pagesIndex = readFileSync(join(wikiDir, "pages", "index.md"), "utf-8");
|
|
40
|
+
assert.match(pagesIndex, /^---\n/m);
|
|
41
|
+
assert.match(pagesIndex, /^title: Wiki$/m);
|
|
42
|
+
});
|
|
43
|
+
test("wiki fs does not overwrite an existing pages/index.md", async () => {
|
|
44
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
45
|
+
mkdirSync(join(wikiDir, "pages"), { recursive: true });
|
|
46
|
+
writeFileSync(join(wikiDir, "pages", "index.md"), "# Custom Wiki Home\n", "utf-8");
|
|
47
|
+
const wiki = await loadFsModule();
|
|
48
|
+
wiki.ensureWikiStructure();
|
|
49
|
+
assert.equal(readFileSync(join(wikiDir, "pages", "index.md"), "utf-8"), "# Custom Wiki Home\n");
|
|
50
|
+
});
|
|
33
51
|
test("wiki fs rejects unsafe page paths", async () => {
|
|
34
52
|
const wiki = await loadFsModule();
|
|
35
53
|
assert.doesNotThrow(() => wiki.assertPagePath("pages/shared/runbooks/deploy.md"));
|
|
@@ -58,10 +58,19 @@ test("buildIndexEntryForPage treats frontmatter summary as the canonical index s
|
|
|
58
58
|
});
|
|
59
59
|
test("parseIndex self-heals an empty index from on-disk pages", async () => {
|
|
60
60
|
const { indexManager, wikiFs } = await loadModules();
|
|
61
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
61
62
|
wikiFs.writePage("pages/team/vision.md", "# Vision\n\nShared direction for the team.\n");
|
|
62
63
|
wikiFs.writeIndexFile("# Wiki Index\n\n");
|
|
63
64
|
const entries = indexManager.parseIndex();
|
|
64
65
|
assert.deepEqual(entries, [
|
|
66
|
+
{
|
|
67
|
+
path: "pages/index.md",
|
|
68
|
+
title: "Wiki",
|
|
69
|
+
summary: "Index of all wiki pages.",
|
|
70
|
+
section: "Knowledge",
|
|
71
|
+
tags: undefined,
|
|
72
|
+
updated: today,
|
|
73
|
+
},
|
|
65
74
|
{
|
|
66
75
|
path: "pages/team/vision.md",
|
|
67
76
|
title: "Vision",
|
|
@@ -119,6 +128,7 @@ test("searchIndex ranks strong metadata matches and falls back to page bodies",
|
|
|
119
128
|
});
|
|
120
129
|
test("addToIndex, removeFromIndex, and getIndexSummary keep the catalog in sync", async () => {
|
|
121
130
|
const { indexManager } = await loadModules();
|
|
131
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
122
132
|
indexManager.addToIndex({
|
|
123
133
|
path: "pages/people/ada.md",
|
|
124
134
|
title: "Ada Lovelace",
|
|
@@ -143,6 +153,8 @@ test("addToIndex, removeFromIndex, and getIndexSummary keep the catalog in sync"
|
|
|
143
153
|
});
|
|
144
154
|
assert.equal(indexManager.removeFromIndex("pages/projects/launch.md"), true);
|
|
145
155
|
assert.equal(indexManager.removeFromIndex("pages/projects/missing.md"), false);
|
|
146
|
-
|
|
156
|
+
const summary = indexManager.getIndexSummary();
|
|
157
|
+
assert.match(summary, /\*\*People\*\*: Ada Lovelace: Owns regression coverage \[qa, testing\] \(2026-05-06\)/);
|
|
158
|
+
assert.match(summary, new RegExp(`\\*\\*Index\\*\\*: Wiki: Index of all wiki pages\\. \\(${today}\\)`));
|
|
147
159
|
});
|
|
148
160
|
//# sourceMappingURL=index-manager.test.js.map
|
package/dist/wiki/lint.test.js
CHANGED
|
@@ -25,6 +25,18 @@ test.after(() => {
|
|
|
25
25
|
function wikiPage(frontmatter, body) {
|
|
26
26
|
return `---\n${frontmatter.join("\n")}\n---\n\n${body.trim()}\n`;
|
|
27
27
|
}
|
|
28
|
+
function wikiIndexWithSharedEntries(...entries) {
|
|
29
|
+
return `# Wiki Index
|
|
30
|
+
|
|
31
|
+
## Index
|
|
32
|
+
|
|
33
|
+
- [Wiki](pages/index.md) — Index of all wiki pages.
|
|
34
|
+
|
|
35
|
+
## Shared
|
|
36
|
+
|
|
37
|
+
${entries.join("\n")}
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
28
40
|
function setPageAge(relativePath, ageInDays) {
|
|
29
41
|
const stamped = new Date(Date.now() - ageInDays * 24 * 60 * 60 * 1000);
|
|
30
42
|
const fullPath = join(wikiDir, relativePath);
|
|
@@ -39,14 +51,9 @@ test("lintWiki reports orphan pages and index entries pointing to missing pages"
|
|
|
39
51
|
"updated: 2026-05-12",
|
|
40
52
|
"tags: [engineering]",
|
|
41
53
|
], "# Orphan\n\nThis orphan page has enough descriptive body text to avoid the premature-page lint while still remaining absent from the index."));
|
|
42
|
-
wikiFs.writeIndexFile(
|
|
43
|
-
|
|
44
|
-
## Shared
|
|
45
|
-
|
|
46
|
-
- [Missing](pages/shared/missing.md) — Not on disk
|
|
47
|
-
`);
|
|
54
|
+
wikiFs.writeIndexFile(wikiIndexWithSharedEntries("- [Missing](pages/shared/missing.md) — Not on disk"));
|
|
48
55
|
const report = lint.lintWiki();
|
|
49
|
-
assert.equal(report.pageCount,
|
|
56
|
+
assert.equal(report.pageCount, 2);
|
|
50
57
|
assert.equal(report.sourceCount, 0);
|
|
51
58
|
assert.deepEqual(report.issues.map((issue) => `${issue.rule}:${issue.severity}:${issue.path ?? "-"}`).sort(), [
|
|
52
59
|
"missing-page:warning:pages/shared/missing.md",
|
|
@@ -62,14 +69,9 @@ test("renderWikiLintReport preserves the current wiki_lint report format", async
|
|
|
62
69
|
"updated: 2026-05-12",
|
|
63
70
|
"tags: [engineering]",
|
|
64
71
|
], "# Orphan\n\nThis orphan page has enough descriptive body text to avoid the premature-page lint while still remaining absent from the index."));
|
|
65
|
-
wikiFs.writeIndexFile(
|
|
66
|
-
|
|
67
|
-
## Shared
|
|
68
|
-
|
|
69
|
-
- [Tracked](pages/shared/tracked.md) — Present only in the index
|
|
70
|
-
`);
|
|
72
|
+
wikiFs.writeIndexFile(wikiIndexWithSharedEntries("- [Tracked](pages/shared/tracked.md) — Present only in the index"));
|
|
71
73
|
const rendered = lint.renderWikiLintReport(lint.lintWiki());
|
|
72
|
-
assert.match(rendered, /^Wiki health report \(
|
|
74
|
+
assert.match(rendered, /^Wiki health report \(2 pages, 0 sources\):/);
|
|
73
75
|
assert.match(rendered, /warning\s+\|\s+orphan-page\s+\|\s+pages\/shared\/orphan\.md/);
|
|
74
76
|
assert.match(rendered, /warning\s+\|\s+missing-page\s+\|\s+pages\/shared\/tracked\.md/);
|
|
75
77
|
assert.match(rendered, /\*\*Suggestions\*\*: Look for pages that should link to each other/);
|
|
@@ -83,12 +85,7 @@ test("renderWikiLintReport reports the healthy wiki message when no issues are f
|
|
|
83
85
|
"updated: 2026-05-12",
|
|
84
86
|
"tags: [engineering]",
|
|
85
87
|
], "# Indexed\n\n## Overview\n\nTracked page with enough detail to stay above the premature-page threshold and remain clearly established as a real page in the healthy-wiki case."));
|
|
86
|
-
wikiFs.writeIndexFile(
|
|
87
|
-
|
|
88
|
-
## Shared
|
|
89
|
-
|
|
90
|
-
- [Indexed](pages/shared/indexed.md) — Tracked page
|
|
91
|
-
`);
|
|
88
|
+
wikiFs.writeIndexFile(wikiIndexWithSharedEntries("- [Indexed](pages/shared/indexed.md) — Tracked page"));
|
|
92
89
|
const rendered = lint.renderWikiLintReport(lint.lintWiki());
|
|
93
90
|
assert.match(rendered, /✅ No issues found\. Index and pages are in sync\./);
|
|
94
91
|
});
|
|
@@ -21,12 +21,16 @@ import { normalizeWikiPath } from "./path-utils.js";
|
|
|
21
21
|
export const FLAT_CATEGORIES = ["preferences", "facts", "routines"];
|
|
22
22
|
/** Path prefixes (relative to the wiki root) that follow their own conventions. */
|
|
23
23
|
export const EXEMPT_PREFIXES = [
|
|
24
|
+
"pages/_meta/",
|
|
24
25
|
"pages/conversations/",
|
|
25
26
|
"pages/team/",
|
|
26
27
|
"pages/okrs/",
|
|
27
28
|
"pages/kpis/",
|
|
28
29
|
"pages/shared/",
|
|
29
30
|
];
|
|
31
|
+
export const EXEMPT_PAGES = [
|
|
32
|
+
"pages/index.md",
|
|
33
|
+
];
|
|
30
34
|
/** Map a `remember`-style category name to its directory under pages/. */
|
|
31
35
|
const CATEGORY_DIR_MAP = {
|
|
32
36
|
person: "people",
|
|
@@ -65,7 +69,8 @@ function isFlatCategory(dir) {
|
|
|
65
69
|
return FLAT_CATEGORIES.includes(dir);
|
|
66
70
|
}
|
|
67
71
|
function isExemptPath(relativePath) {
|
|
68
|
-
return
|
|
72
|
+
return EXEMPT_PAGES.includes(relativePath)
|
|
73
|
+
|| EXEMPT_PREFIXES.some((p) => relativePath.startsWith(p));
|
|
69
74
|
}
|
|
70
75
|
/** Slugify a free-text name into a wiki-safe path segment. */
|
|
71
76
|
export function slugify(name) {
|
|
@@ -154,7 +159,8 @@ export function validateTopicPath(relativePath) {
|
|
|
154
159
|
ok: false,
|
|
155
160
|
reason: `'${topDir}' is not a recognized wiki category. ` +
|
|
156
161
|
`Use one of: ${valid} (entity categories take a topic directory: pages/<category>/<topic>/<page>.md). ` +
|
|
157
|
-
`Conversations and team pages are written by the system
|
|
162
|
+
`Conversations and team pages are written by the system. ` +
|
|
163
|
+
`(pages/_meta/ is reserved for system metadata.)`,
|
|
158
164
|
};
|
|
159
165
|
}
|
|
160
166
|
/** Convenience: returns the error message string for an invalid path, or undefined if valid. */
|
|
@@ -25,6 +25,7 @@ test("topicPagePath builds canonical paths", () => {
|
|
|
25
25
|
assert.equal(topicPagePath("tool", "Kubernetes"), "pages/tools/kubernetes/index.md");
|
|
26
26
|
});
|
|
27
27
|
test("validateTopicPath accepts canonical and exempt paths", () => {
|
|
28
|
+
assert.deepEqual(validateTopicPath("pages/index.md"), { ok: true });
|
|
28
29
|
assert.deepEqual(validateTopicPath("pages/projects/chapterhouse/index.md"), { ok: true });
|
|
29
30
|
assert.deepEqual(validateTopicPath("pages/projects/chapterhouse/decisions.md"), { ok: true });
|
|
30
31
|
assert.deepEqual(validateTopicPath("pages/people/brian/index.md"), { ok: true });
|
|
@@ -34,6 +35,16 @@ test("validateTopicPath accepts canonical and exempt paths", () => {
|
|
|
34
35
|
assert.deepEqual(validateTopicPath("pages/team/onboarding.md"), { ok: true });
|
|
35
36
|
assert.deepEqual(validateTopicPath("pages/okrs/2026-Q2.md"), { ok: true });
|
|
36
37
|
});
|
|
38
|
+
test("validateTopicPath accepts system-managed pages/_meta subtree paths", () => {
|
|
39
|
+
assert.deepEqual(validateTopicPath("pages/_meta/log.md"), { ok: true });
|
|
40
|
+
assert.deepEqual(validateTopicPath("pages/_meta/taxonomy.md"), { ok: true });
|
|
41
|
+
assert.deepEqual(validateTopicPath("pages/_meta/log-2026.md"), { ok: true });
|
|
42
|
+
assert.deepEqual(validateTopicPath("pages/_meta/sub/file.md"), { ok: true });
|
|
43
|
+
});
|
|
44
|
+
test("validateTopicPath rejects pages/_meta without a page file", () => {
|
|
45
|
+
const r = validateTopicPath("pages/_meta");
|
|
46
|
+
assert.equal(r.ok, false);
|
|
47
|
+
});
|
|
37
48
|
test("validateTopicPath rejects a bare entity-category file and suggests the topic index", () => {
|
|
38
49
|
const r = validateTopicPath("pages/projects/chapterhouse.md");
|
|
39
50
|
assert.equal(r.ok, false);
|
|
@@ -60,6 +71,7 @@ test("validateTopicPath rejects unknown top-level categories", () => {
|
|
|
60
71
|
const r = validateTopicPath("pages/randomstuff/foo.md");
|
|
61
72
|
assert.equal(r.ok, false);
|
|
62
73
|
assert.match(r.ok === false ? r.reason : "", /not a recognized wiki category/);
|
|
74
|
+
assert.match(r.ok === false ? r.reason : "", /pages\/_meta\/ is reserved for system metadata/);
|
|
63
75
|
});
|
|
64
76
|
test("validateTopicPath rejects non-slug topic or page names", () => {
|
|
65
77
|
const r = validateTopicPath("pages/projects/Chapter House/index.md");
|
package/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-conventions
|
|
3
|
+
description: Use when creating, editing, linting, restructuring, or reviewing Chapterhouse wiki content.
|
|
4
|
+
when_to_load: Wiki tools, remember/forget workflows, or direct edits under the Chapterhouse wiki.
|
|
5
|
+
related_skills:
|
|
6
|
+
- squad
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Wiki Conventions
|
|
10
|
+
|
|
11
|
+
Use this skill before creating, editing, linting, restructuring, archiving, or reviewing Chapterhouse wiki content.
|
|
12
|
+
|
|
13
|
+
## Orientation Ritual — Mandatory Before Any Wiki Write
|
|
14
|
+
|
|
15
|
+
Perform this checklist before using `wiki_update`, `remember`, `forget`, `wiki_ingest`, `wiki_lint`, or `wiki_rebuild_index`:
|
|
16
|
+
|
|
17
|
+
1. Read `pages/index.md` with `wiki_read`.
|
|
18
|
+
2. Scan the last 20-30 entries of `pages/_meta/log.md` to see recent edits, archives, and reorganizations.
|
|
19
|
+
3. If the wiki is large, the topic is ambiguous, or you might be creating a page, run `wiki_search` for the topic first.
|
|
20
|
+
|
|
21
|
+
Do not skip the ritual because the task feels small. The point is to avoid duplicate pages, category mistakes, stale contradictions, and taxonomy drift.
|
|
22
|
+
|
|
23
|
+
## Wiki Structure Recap
|
|
24
|
+
|
|
25
|
+
- **Entity categories** live at `pages/<category>/<topic>/index.md` with optional facet pages beside the overview.
|
|
26
|
+
- **Flat categories** live in a single page such as `pages/preferences.md`, `pages/facts.md`, or `pages/routines.md`.
|
|
27
|
+
- **Decisions** always belong to an entity and should land in `pages/<entity-category>/<topic>/decisions.md`.
|
|
28
|
+
|
|
29
|
+
## Naming Conventions
|
|
30
|
+
|
|
31
|
+
- Use lowercase only.
|
|
32
|
+
- Use kebab-case slugs.
|
|
33
|
+
- Do not put dates in slugs.
|
|
34
|
+
- Keep facet names predictable and durable, such as `decisions`, `feature-ideas`, or `architecture`.
|
|
35
|
+
- If a name feels ad hoc, it probably belongs as a section on an existing page instead of a new facet.
|
|
36
|
+
|
|
37
|
+
## `index.md` vs Facet Pages
|
|
38
|
+
|
|
39
|
+
- Use `index.md` for the durable overview of a topic, entity, or area.
|
|
40
|
+
- Use a facet page only when the subtopic has ongoing value and a stable identity.
|
|
41
|
+
- Do not create a facet page as random overflow for a page that merely feels long today.
|
|
42
|
+
- If a page is growing because it contains several durable subtopics, split those into facets and keep `index.md` as the orientation page.
|
|
43
|
+
|
|
44
|
+
## Page Creation Thresholds
|
|
45
|
+
|
|
46
|
+
- Create a new page when a topic appears in two or more distinct sources or interactions.
|
|
47
|
+
- Create a new page when the topic is central to one major source, even if it is only one source so far.
|
|
48
|
+
- Add to an existing page when coverage already exists.
|
|
49
|
+
- Do not create a page for a passing mention, transient detail, or one-off aside.
|
|
50
|
+
- Split pages once they approach roughly 300 lines.
|
|
51
|
+
- Archive fully superseded pages into `pages/_archive/` and remove them from the active index rather than letting stale content linger in place.
|
|
52
|
+
|
|
53
|
+
## Decision Recording Protocol
|
|
54
|
+
|
|
55
|
+
- With `remember`, use `category: "decision"` plus both `about` and `entity`.
|
|
56
|
+
- Record the decision against the actual entity that owns it.
|
|
57
|
+
- Keep decision facets canonical; prefer `decisions.md` over inventing one-off decision page names.
|
|
58
|
+
|
|
59
|
+
## Cross-Reference Standards
|
|
60
|
+
|
|
61
|
+
- Prefer wiki-relative paths over raw URLs when a wiki page exists.
|
|
62
|
+
- Avoid raw local absolute paths in the body when a canonical wiki page can be linked instead.
|
|
63
|
+
- Add reciprocal links when the relationship is durable enough that future readers should discover it from either side.
|
|
64
|
+
|
|
65
|
+
## Tag Usage
|
|
66
|
+
|
|
67
|
+
- Treat the merged taxonomy as the source of truth: code-defined defaults plus optional `pages/_meta/taxonomy.md` extensions.
|
|
68
|
+
- Read `pages/_meta/taxonomy.md` before inventing a new tag.
|
|
69
|
+
- If a new durable tag is needed, extend `pages/_meta/taxonomy.md` first instead of inventing the tag inline on a page.
|
|
70
|
+
|
|
71
|
+
## Contradiction Update Policy
|
|
72
|
+
|
|
73
|
+
When two sources disagree:
|
|
74
|
+
|
|
75
|
+
1. Compare dates first.
|
|
76
|
+
2. If the conflict is real, preserve both claims instead of collapsing them into one "truth".
|
|
77
|
+
3. Keep dates, sources, or citations in the page body so the disagreement is auditable.
|
|
78
|
+
4. Add reciprocal `contradictions:` frontmatter references.
|
|
79
|
+
5. Set `contested: true` while the issue remains unresolved.
|
|
80
|
+
6. Use `confidence` to describe certainty; do not smooth over disagreement with vague prose.
|
|
81
|
+
|
|
82
|
+
## Tone
|
|
83
|
+
|
|
84
|
+
- Be terse.
|
|
85
|
+
- Be factual.
|
|
86
|
+
- Be dated when the timing matters.
|
|
87
|
+
- Avoid speculative voice such as "I think" unless the uncertainty itself is the fact being recorded.
|
|
88
|
+
- Prefer explicit source attribution over implied certainty.
|
|
89
|
+
|
|
90
|
+
## Frontmatter Shape
|
|
91
|
+
|
|
92
|
+
Required fields:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
---
|
|
96
|
+
title: Chapterhouse
|
|
97
|
+
summary: Team-level AI assistant for engineering teams.
|
|
98
|
+
---
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Recommended fields for most durable pages:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
---
|
|
105
|
+
title: Chapterhouse
|
|
106
|
+
summary: Team-level AI assistant for engineering teams.
|
|
107
|
+
updated: 2026-05-12
|
|
108
|
+
tags: [engineering, orchestration]
|
|
109
|
+
related: [pages/projects/chapterhouse/decisions.md]
|
|
110
|
+
---
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use contradiction-supporting fields when needed:
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
---
|
|
117
|
+
title: Chapterhouse
|
|
118
|
+
summary: Coordination notes about the runtime architecture.
|
|
119
|
+
updated: 2026-05-12
|
|
120
|
+
tags: [engineering, architecture]
|
|
121
|
+
related: [pages/projects/chapterhouse/index.md]
|
|
122
|
+
confidence: low
|
|
123
|
+
contested: true
|
|
124
|
+
contradictions: [pages/projects/chapterhouse/decisions.md]
|
|
125
|
+
---
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Use `autostub: true` only for genuine stubs that are intentionally thin and expected to grow later.
|
|
129
|
+
|
|
130
|
+
## Common Mistakes
|
|
131
|
+
|
|
132
|
+
- **Wrong category:** move the content to the correct entity or flat page instead of stretching the path rules.
|
|
133
|
+
- **Page created too early:** merge it into an existing page or section until the topic earns a standalone page.
|
|
134
|
+
- **Invented tag:** update `pages/_meta/taxonomy.md` first.
|
|
135
|
+
- **Skipped orientation ritual:** stop, orient, then retry.
|
|
136
|
+
- **Collapsed contradiction:** preserve both claims, date them, and mark the relationship explicitly.
|
|
137
|
+
|
|
138
|
+
## Routing Guide — "I want to record X. Where does it go?"
|
|
139
|
+
|
|
140
|
+
- A durable fact about a named project, tool, person, org, topic, or area -> `pages/<category>/<slug>/index.md`
|
|
141
|
+
- A durable subtopic of an existing entity -> `pages/<category>/<slug>/<facet>.md`
|
|
142
|
+
- A decision about an entity -> `remember` with `category: "decision"`, `about`, and `entity`, or write to that entity's `decisions.md`
|
|
143
|
+
- A user preference or routine -> the corresponding flat category page
|
|
144
|
+
- Source material you want preserved before synthesis -> `wiki_ingest`
|
|
145
|
+
- A passing mention with no durable value yet -> do not create a new page; add it to existing context or leave it out
|