chapterhouse 0.13.0 → 0.13.1
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/copilot/agents.js +12 -3
- package/dist/wiki/frontmatter.js +18 -6
- package/dist/wiki/frontmatter.test.js +40 -0
- package/package.json +1 -1
package/dist/copilot/agents.js
CHANGED
|
@@ -23,6 +23,7 @@ const agentFrontmatterSchema = z.object({
|
|
|
23
23
|
model: z.string().min(1),
|
|
24
24
|
skills: z.array(z.string()).optional(),
|
|
25
25
|
tools: z.array(z.string()).optional(),
|
|
26
|
+
management_tools: z.array(z.string()).optional(),
|
|
26
27
|
mcpServers: z.array(z.string()).optional(),
|
|
27
28
|
allowed_paths: z.array(z.string()).optional(),
|
|
28
29
|
persistent: z.union([z.boolean(), z.string()]).optional().transform((value) => {
|
|
@@ -83,6 +84,7 @@ export function parseAgentMdOrThrow(content, slug) {
|
|
|
83
84
|
scope: fm.scope,
|
|
84
85
|
skills: fm.skills,
|
|
85
86
|
tools: fm.tools,
|
|
87
|
+
managementTools: fm.management_tools,
|
|
86
88
|
mcpServers: fm.mcpServers,
|
|
87
89
|
allowedPaths: fm.allowed_paths,
|
|
88
90
|
systemMessage: body,
|
|
@@ -374,13 +376,20 @@ export function filterToolsForAgent(agent, allTools) {
|
|
|
374
376
|
if (agent.tools && agent.tools.length > 0) {
|
|
375
377
|
// Agent specifies an explicit allowlist — give those + wiki tools
|
|
376
378
|
const allowed = new Set([...agent.tools, ...WIKI_TOOL_NAMES]);
|
|
377
|
-
return allTools.filter((t) => allowed.has(t.name)
|
|
379
|
+
return allTools.filter((t) => allowed.has(t.name));
|
|
378
380
|
}
|
|
379
|
-
// Default: all tools except management (only @chapterhouse gets those)
|
|
381
|
+
// Default: all tools except management (only @chapterhouse gets those by default)
|
|
380
382
|
if (agent.slug === "chapterhouse") {
|
|
381
383
|
return allTools;
|
|
382
384
|
}
|
|
383
|
-
|
|
385
|
+
const baseTools = allTools.filter((t) => !MANAGEMENT_TOOL_NAMES.has(t.name));
|
|
386
|
+
// Agents can opt into specific management tools via management_tools: in their config
|
|
387
|
+
if (agent.managementTools && agent.managementTools.length > 0) {
|
|
388
|
+
const optedIn = new Set(agent.managementTools);
|
|
389
|
+
const extra = allTools.filter((t) => MANAGEMENT_TOOL_NAMES.has(t.name) && optedIn.has(t.name));
|
|
390
|
+
return [...baseTools, ...extra];
|
|
391
|
+
}
|
|
392
|
+
return baseTools;
|
|
384
393
|
}
|
|
385
394
|
/** Filter MCP servers based on agent config. */
|
|
386
395
|
export function filterMcpServersForAgent(agent, allMcpServers) {
|
package/dist/wiki/frontmatter.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { normalizeWikiPath } from "./path-utils.js";
|
|
2
2
|
import { DEFAULT_SCHEMA, load } from "js-yaml";
|
|
3
3
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
4
|
-
|
|
4
|
+
// Markdown formatting that must not appear in a one-line plain-text summary.
|
|
5
|
+
// Detects *actual* formatting — paired or line-anchored — not lone characters:
|
|
6
|
+
// a bare underscore/asterisk inside a word (e.g. `ask_user_question`) is plain
|
|
7
|
+
// text, not emphasis, and must not be rejected.
|
|
8
|
+
const SUMMARY_MARKDOWN_RE = /(\*\*[^*]+\*\*|__[^_]+__|\*[^\s*][^*]*\*|(?<![A-Za-z0-9])_[^\s_][^_]*_(?![A-Za-z0-9])|`[^`]+`|~~[^~]+~~|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
|
|
5
9
|
const FRONTMATTER_TEMPLATE = `---\ntitle: <title>\nsummary: <plain-text one-line summary, max 200 chars>\nupdated: YYYY-MM-DD\ntags: []\nrelated: []\n---`;
|
|
6
10
|
const PROJECT_RULES_HARD_DEFAULTS = {
|
|
7
11
|
auto_pr: true,
|
|
@@ -159,14 +163,22 @@ export function validateWikiFrontmatter(content, options = {}) {
|
|
|
159
163
|
}
|
|
160
164
|
else {
|
|
161
165
|
const summary = parsed.summary.trim();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const markdownMatch = summary.match(SUMMARY_MARKDOWN_RE);
|
|
167
|
+
let reason;
|
|
168
|
+
if (summary.length > 200) {
|
|
169
|
+
reason = `invalid 'summary' — it is ${summary.length} characters; the maximum is 200`;
|
|
170
|
+
}
|
|
171
|
+
else if (summary.includes("\n") || summary.includes("\r")) {
|
|
172
|
+
reason = "invalid 'summary' — it must be a single line";
|
|
173
|
+
}
|
|
174
|
+
else if (markdownMatch) {
|
|
175
|
+
reason = `invalid 'summary' — it must be plain text, but contains markdown formatting (${JSON.stringify(markdownMatch[0])})`;
|
|
176
|
+
}
|
|
177
|
+
if (reason) {
|
|
166
178
|
errors.push({
|
|
167
179
|
rule: "invalid-summary",
|
|
168
180
|
field: "summary",
|
|
169
|
-
message: formatFrontmatterMessage(
|
|
181
|
+
message: formatFrontmatterMessage(reason),
|
|
170
182
|
});
|
|
171
183
|
}
|
|
172
184
|
}
|
|
@@ -293,4 +293,44 @@ test("validateAndBackfillFrontmatter backfilled last_updated is valid ISO timest
|
|
|
293
293
|
const ts = match[1].trim();
|
|
294
294
|
assert.doesNotThrow(() => new Date(ts).toISOString(), `${ts} should be valid ISO timestamp`);
|
|
295
295
|
});
|
|
296
|
+
// ── Issue #458: summary markdown detection must not flag plain text ──────────
|
|
297
|
+
test("validateWikiFrontmatter accepts a plain-text summary containing technical identifiers", async () => {
|
|
298
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
299
|
+
const result = validateWikiFrontmatter(`---
|
|
300
|
+
title: Ask User Question
|
|
301
|
+
summary: Design notes for the ask_user_question tool, stored under ~/.chapterhouse
|
|
302
|
+
updated: 2026-05-19
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
# Ask User Question
|
|
306
|
+
`);
|
|
307
|
+
assert.equal(result.valid, true, JSON.stringify(result.errors));
|
|
308
|
+
});
|
|
309
|
+
test("validateWikiFrontmatter still rejects genuine markdown formatting in summaries", async () => {
|
|
310
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
311
|
+
const markdownSummaries = [
|
|
312
|
+
"**bold text**",
|
|
313
|
+
"__bold text__",
|
|
314
|
+
"an *italic* word",
|
|
315
|
+
"an _italic_ word",
|
|
316
|
+
"a `code` span",
|
|
317
|
+
"a ~~struck~~ phrase",
|
|
318
|
+
"[a link](https://example.com)",
|
|
319
|
+
];
|
|
320
|
+
for (const summary of markdownSummaries) {
|
|
321
|
+
const result = validateWikiFrontmatter(`---\ntitle: T\nsummary: ${summary}\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
322
|
+
assert.equal(result.valid, false, `expected "${summary}" to be rejected`);
|
|
323
|
+
assert.equal(result.errors[0]?.rule, "invalid-summary", `"${summary}" should fail invalid-summary`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
test("validateWikiFrontmatter invalid-summary message identifies the specific problem", async () => {
|
|
327
|
+
const { validateWikiFrontmatter } = await loadFrontmatterModule();
|
|
328
|
+
const markdown = validateWikiFrontmatter(`---\ntitle: T\nsummary: a \`code\` span\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
329
|
+
assert.equal(markdown.valid, false);
|
|
330
|
+
assert.match(markdown.errors[0].message, /markdown formatting/i);
|
|
331
|
+
assert.match(markdown.errors[0].message, /`code`/);
|
|
332
|
+
const tooLong = validateWikiFrontmatter(`---\ntitle: T\nsummary: ${"x".repeat(201)}\nupdated: 2026-05-19\n---\n\n# T\n`);
|
|
333
|
+
assert.equal(tooLong.valid, false);
|
|
334
|
+
assert.match(tooLong.errors[0].message, /200/);
|
|
335
|
+
});
|
|
296
336
|
//# sourceMappingURL=frontmatter.test.js.map
|
package/package.json
CHANGED