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.
@@ -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) && !(agent.persistent && MANAGEMENT_TOOL_NAMES.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
- return allTools.filter((t) => !MANAGEMENT_TOOL_NAMES.has(t.name));
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) {
@@ -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
- const SUMMARY_MARKDOWN_RE = /(\*\*|__|[_*`~]|^\s*#+\s|\[[^\]]+\]\([^)]+\)|!\[[^\]]*\]\([^)]+\)|^\s*>)/m;
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
- if (summary.length > 200 ||
163
- summary.includes("\n") ||
164
- summary.includes("\r") ||
165
- SUMMARY_MARKDOWN_RE.test(summary)) {
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("invalid 'summary' (use plain text, one line, max 200 chars)"),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Chapterhouse — a team-level AI assistant for engineering teams, built on the GitHub Copilot SDK. Web UI only.",
5
5
  "bin": {
6
6
  "chapterhouse": "dist/cli.js"