chapterhouse 0.9.1 → 0.10.0

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.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/agents/korg.agent.md +20 -0
  3. package/dist/api/auth.js +11 -1
  4. package/dist/api/auth.test.js +29 -0
  5. package/dist/api/errors.js +23 -0
  6. package/dist/api/route-coverage.test.js +61 -21
  7. package/dist/api/routes/agents.js +472 -0
  8. package/dist/api/routes/memory.js +299 -0
  9. package/dist/api/routes/projects.js +170 -0
  10. package/dist/api/routes/sessions.js +347 -0
  11. package/dist/api/routes/system.js +82 -0
  12. package/dist/api/routes/wiki.js +455 -0
  13. package/dist/api/routes/wiki.test.js +49 -0
  14. package/dist/api/send-json.js +16 -0
  15. package/dist/api/send-json.test.js +18 -0
  16. package/dist/api/server-runtime.js +45 -3
  17. package/dist/api/server.js +34 -1764
  18. package/dist/api/server.test.js +239 -8
  19. package/dist/api/sse-hub.js +37 -0
  20. package/dist/cli.js +1 -1
  21. package/dist/config.js +151 -58
  22. package/dist/config.test.js +29 -0
  23. package/dist/copilot/okr-mapper.js +2 -11
  24. package/dist/copilot/orchestrator.js +358 -352
  25. package/dist/copilot/orchestrator.test.js +139 -4
  26. package/dist/copilot/prompt-date.js +2 -1
  27. package/dist/copilot/session-manager.js +25 -23
  28. package/dist/copilot/session-manager.test.js +35 -1
  29. package/dist/copilot/standup.js +2 -2
  30. package/dist/copilot/task-event-log.js +7 -1
  31. package/dist/copilot/task-event-log.test.js +13 -0
  32. package/dist/copilot/tools/agent.js +608 -0
  33. package/dist/copilot/tools/index.js +19 -0
  34. package/dist/copilot/tools/memory.js +678 -0
  35. package/dist/copilot/tools/models.js +2 -0
  36. package/dist/copilot/tools/okr.js +171 -0
  37. package/dist/copilot/tools/wiki.js +333 -0
  38. package/dist/copilot/tools-deps.js +4 -0
  39. package/dist/copilot/tools.agent.test.js +10 -8
  40. package/dist/copilot/tools.inventory.test.js +76 -0
  41. package/dist/copilot/tools.js +1 -1725
  42. package/dist/copilot/tools.okr.test.js +31 -0
  43. package/dist/copilot/tools.wiki.test.js +358 -6
  44. package/dist/copilot/turn-event-log.js +31 -4
  45. package/dist/copilot/turn-event-log.test.js +24 -2
  46. package/dist/copilot/workiq-installer.test.js +2 -2
  47. package/dist/daemon-install.js +3 -2
  48. package/dist/daemon.js +9 -17
  49. package/dist/integrations/ado-client.js +90 -9
  50. package/dist/integrations/ado-client.test.js +56 -0
  51. package/dist/integrations/team-push.js +1 -0
  52. package/dist/integrations/team-push.test.js +6 -0
  53. package/dist/integrations/teams-notify.js +1 -0
  54. package/dist/integrations/teams-notify.test.js +5 -0
  55. package/dist/memory/active-scope.test.js +0 -1
  56. package/dist/memory/checkpoint.js +89 -72
  57. package/dist/memory/checkpoint.test.js +23 -3
  58. package/dist/memory/eot.js +194 -89
  59. package/dist/memory/eot.test.js +186 -3
  60. package/dist/memory/hooks.js +2 -4
  61. package/dist/memory/housekeeping-scheduler.js +1 -1
  62. package/dist/memory/housekeeping-scheduler.test.js +1 -2
  63. package/dist/memory/housekeeping.js +100 -3
  64. package/dist/memory/housekeeping.test.js +33 -2
  65. package/dist/memory/reflect.test.js +2 -0
  66. package/dist/memory/scope-lock.js +26 -0
  67. package/dist/memory/scope-lock.test.js +118 -0
  68. package/dist/memory/scopes.test.js +0 -1
  69. package/dist/mode-context.js +58 -5
  70. package/dist/mode-context.test.js +68 -0
  71. package/dist/paths.js +1 -0
  72. package/dist/setup.js +3 -2
  73. package/dist/shared/api-schemas.js +48 -5
  74. package/dist/store/connection.js +96 -0
  75. package/dist/store/db.js +5 -1498
  76. package/dist/store/db.test.js +182 -1
  77. package/dist/store/migrations.js +460 -0
  78. package/dist/store/repositories/memory.js +281 -0
  79. package/dist/store/repositories/okr.js +3 -0
  80. package/dist/store/repositories/projects.js +5 -0
  81. package/dist/store/repositories/sessions.js +284 -0
  82. package/dist/store/repositories/wiki.js +60 -0
  83. package/dist/store/schema.js +501 -0
  84. package/dist/util/logger.js +3 -2
  85. package/dist/wiki/consolidation.js +50 -9
  86. package/dist/wiki/consolidation.test.js +45 -0
  87. package/dist/wiki/frontmatter.js +45 -14
  88. package/dist/wiki/frontmatter.test.js +26 -1
  89. package/dist/wiki/fs.js +16 -4
  90. package/dist/wiki/fs.test.js +84 -0
  91. package/dist/wiki/index-manager.js +30 -2
  92. package/dist/wiki/index-manager.test.js +43 -12
  93. package/dist/wiki/ingest.js +17 -1
  94. package/dist/wiki/lock.js +11 -1
  95. package/dist/wiki/log-manager.js +2 -7
  96. package/dist/wiki/migrate.js +44 -17
  97. package/dist/wiki/project-registry.js +10 -5
  98. package/dist/wiki/project-registry.test.js +14 -0
  99. package/dist/wiki/scheduler.js +1 -1
  100. package/dist/wiki/seed-team-wiki.js +2 -1
  101. package/dist/wiki/team-sync.js +31 -6
  102. package/dist/wiki/team-sync.test.js +81 -0
  103. package/package.json +1 -1
  104. package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
  105. package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
  106. package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
  107. package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
  108. package/web/dist/assets/index-CUm2Wbuh.js +250 -0
  109. package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
  110. package/web/dist/index.html +1 -1
  111. package/web/dist/assets/index-iQrv3lQN.js +0 -286
  112. package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
@@ -33,6 +33,7 @@ const CATEGORY_MAP = {
33
33
  export function migrateMemoriesToWiki() {
34
34
  ensureWikiStructure();
35
35
  const db = getDb();
36
+ ensureWikiMigrationVersionTable(db);
36
37
  const rows = db.prepare(`SELECT id, category, content, source, created_at FROM memories ORDER BY category, id`).all();
37
38
  if (rows.length === 0) {
38
39
  setState(MIGRATION_KEY, "true");
@@ -73,30 +74,26 @@ export function migrateMemoriesToWiki() {
73
74
  lines.push("");
74
75
  // Check if a page already exists (avoid clobbering manual content)
75
76
  const existing = readPage(mapping.path);
76
- // Idempotency marker: if the migration block was already appended, skip the
77
- // append so re-runs don't duplicate bullets.
78
- const MIGRATE_MARKER = `<!-- migrate:${category}:v1 -->`;
77
+ backfillMemoryMigrationVersionFromLegacyMarker(db, category, mapping.path, existing);
78
+ if (hasWikiMigrationVersion(db, category, 1)) {
79
+ const entry = {
80
+ path: mapping.path,
81
+ title: mapping.title,
82
+ summary: `${items.length} ${category} memories (already migrated)`,
83
+ section: mapping.section,
84
+ };
85
+ addToIndex(entry);
86
+ continue;
87
+ }
79
88
  if (existing) {
80
- if (existing.includes(MIGRATE_MARKER)) {
81
- // Already migrated; just refresh the index entry.
82
- const entry = {
83
- path: mapping.path,
84
- title: mapping.title,
85
- summary: `${items.length} ${category} memories (already migrated)`,
86
- section: mapping.section,
87
- };
88
- addToIndex(entry);
89
- continue;
90
- }
91
89
  // Extract only the bullet-point items to append
92
90
  const bulletLines = lines.filter((l) => l.startsWith("- "));
93
- writePage(mapping.path, existing + `\n${MIGRATE_MARKER}\n## Migrated Memories\n\n` + bulletLines.join("\n") + "\n");
91
+ writePage(mapping.path, existing + `\n## Migrated Memories\n\n` + bulletLines.join("\n") + "\n");
94
92
  }
95
93
  else {
96
- // Embed the marker in fresh pages too so future re-runs are no-ops.
97
- lines.splice(lines.length - 1, 0, MIGRATE_MARKER);
98
94
  writePage(mapping.path, lines.join("\n"));
99
95
  }
96
+ setWikiMigrationVersion(db, category, 1);
100
97
  // Update index
101
98
  const entry = {
102
99
  path: mapping.path,
@@ -113,6 +110,36 @@ export function migrateMemoriesToWiki() {
113
110
  log.info({ total, pageCount: Object.keys(grouped).length }, "Wiki migration complete");
114
111
  return total;
115
112
  }
113
+ function ensureWikiMigrationVersionTable(db) {
114
+ db.exec(`
115
+ CREATE TABLE IF NOT EXISTS wiki_migration_versions (
116
+ name TEXT NOT NULL,
117
+ version INTEGER NOT NULL,
118
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
119
+ PRIMARY KEY (name, version)
120
+ )
121
+ `);
122
+ }
123
+ function hasWikiMigrationVersion(db, category, version) {
124
+ const row = db.prepare(`SELECT 1 FROM wiki_migration_versions WHERE name = ? AND version = ?`).get(`memory:${category}`, version);
125
+ return Boolean(row);
126
+ }
127
+ function setWikiMigrationVersion(db, category, version) {
128
+ db.prepare(`
129
+ INSERT OR IGNORE INTO wiki_migration_versions (name, version)
130
+ VALUES (?, ?)
131
+ `).run(`memory:${category}`, version);
132
+ }
133
+ function backfillMemoryMigrationVersionFromLegacyMarker(db, category, path, existing) {
134
+ if (existing?.includes(`<!-- migrate:${category}:v1 -->`)) {
135
+ setWikiMigrationVersion(db, category, 1);
136
+ return;
137
+ }
138
+ const markerPage = readPage(path);
139
+ if (markerPage?.includes(`<!-- migrate:${category}:v1 -->`)) {
140
+ setWikiMigrationVersion(db, category, 1);
141
+ }
142
+ }
116
143
  // ---------------------------------------------------------------------------
117
144
  // One-time reorganization: flat dump pages → entity pages
118
145
  // ---------------------------------------------------------------------------
@@ -1,4 +1,4 @@
1
- import { isAbsolute } from "node:path";
1
+ import { isAbsolute, resolve } from "node:path";
2
2
  import { getDb } from "../store/db.js";
3
3
  import { childLogger } from "../util/logger.js";
4
4
  import { deletePage, pageExists, readPage } from "./fs.js";
@@ -13,12 +13,14 @@ export function loadRegistry() {
13
13
  const rows = getDb()
14
14
  .prepare("SELECT slug, cwd FROM projects ORDER BY slug")
15
15
  .all();
16
- return Object.fromEntries(rows.map(({ slug, cwd }) => [slug, cwd]));
16
+ return Object.fromEntries(rows.map(({ slug, cwd }) => [slug, normalizeProjectPath(cwd)]));
17
17
  }
18
18
  export function saveRegistry(registry) {
19
19
  validateRegistry(registry);
20
20
  ensureRegistryMigrated();
21
- const entries = Object.entries(registry).sort(([left], [right]) => left.localeCompare(right));
21
+ const entries = Object.entries(registry)
22
+ .map(([slug, cwd]) => [slug, normalizeProjectPath(cwd)])
23
+ .sort(([left], [right]) => left.localeCompare(right));
22
24
  const db = getDb();
23
25
  const save = db.transaction(() => {
24
26
  db.prepare("DELETE FROM projects").run();
@@ -138,9 +140,8 @@ function parseRegistryBlock(lines) {
138
140
  throw new Error(`Project registry is malformed: malformed registry line '${rawLine}'.`);
139
141
  }
140
142
  const slug = line.slice(0, separatorIndex).trim();
141
- const path = line.slice(separatorIndex + 1).trim();
143
+ const path = normalizeProjectPath(line.slice(separatorIndex + 1).trim());
142
144
  assertValidProjectSlug(slug);
143
- validatePath(path);
144
145
  if (slug in registry) {
145
146
  throw new Error(`Project registry is malformed: duplicate project slug '${slug}'.`);
146
147
  }
@@ -154,6 +155,10 @@ function validateRegistry(registry) {
154
155
  validatePath(path);
155
156
  }
156
157
  }
158
+ export function normalizeProjectPath(path) {
159
+ validatePath(path);
160
+ return resolve(path);
161
+ }
157
162
  function validatePath(path) {
158
163
  if (!path || !isAbsolute(path)) {
159
164
  throw new Error(`Project registry path '${path}' must be an absolute path.`);
@@ -79,6 +79,20 @@ test("saveRegistry replaces prior SQLite-backed registry contents", async () =>
79
79
  dbModule.closeDb();
80
80
  }
81
81
  });
82
+ test("saveRegistry normalizes absolute project paths before persisting", async () => {
83
+ const { projectRegistry, dbModule } = await loadModules();
84
+ try {
85
+ projectRegistry.saveRegistry({
86
+ alpha: "/srv/projects/../alpha/",
87
+ });
88
+ assert.deepEqual(projectRegistry.loadRegistry(), {
89
+ alpha: "/srv/alpha",
90
+ });
91
+ }
92
+ finally {
93
+ dbModule.closeDb();
94
+ }
95
+ });
82
96
  test("loadRegistry migrates the legacy wiki registry into SQLite and removes the file", async () => {
83
97
  const { projectRegistry, wikiFs, dbModule } = await loadModules();
84
98
  try {
@@ -16,7 +16,7 @@ export class WikiConsolidationScheduler {
16
16
  started = false;
17
17
  running = false;
18
18
  constructor(options = {}) {
19
- this.env = options.env ?? process.env;
19
+ this.env = options.env ?? {};
20
20
  this.runConsolidationImpl = options.runConsolidation ?? (() => runConsolidation(getDb()));
21
21
  this.now = options.now ?? (() => new Date());
22
22
  this.log = options.log ?? childLogger("wiki.scheduler");
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
2
2
  import { resolve } from "node:path";
3
3
  import { ensureWikiStructure, readPage, writePage } from "./fs.js";
4
4
  import { addToIndex, buildIndexEntryForPage } from "./index-manager.js";
5
+ import { withWikiWrite } from "./lock.js";
5
6
  import { generateKPIPage, generateOKRQuarterPage, generateTeamIndexPage, } from "./templates/okr.js";
6
7
  import { childLogger } from "../util/logger.js";
7
8
  const log = childLogger("wiki:seed");
@@ -281,7 +282,7 @@ export function seedTeamWiki() {
281
282
  return { created, existing };
282
283
  }
283
284
  async function main() {
284
- const result = seedTeamWiki();
285
+ const result = await withWikiWrite(() => seedTeamWiki());
285
286
  const message = result.created.length > 0
286
287
  ? `Seeded ${result.created.length} team wiki page(s): ${result.created.join(", ")}`
287
288
  : "Team wiki seed already up to date; no pages created.";
@@ -5,8 +5,21 @@ import { ModeContext } from "../mode-context.js";
5
5
  import { WIKI_DIR } from "../paths.js";
6
6
  import { assertPagePath, readPage, writePage, writeFileAtomic } from "./fs.js";
7
7
  import { addToIndex, buildIndexEntryForPage } from "./index-manager.js";
8
+ import { withWikiWrite } from "./lock.js";
8
9
  import { childLogger } from "../util/logger.js";
9
10
  const log = childLogger("team-sync");
11
+ const DEFAULT_RATE_LIMIT_RETRY_MS = 1_000;
12
+ function parseRetryAfterMs(value) {
13
+ if (!value) {
14
+ return DEFAULT_RATE_LIMIT_RETRY_MS;
15
+ }
16
+ const seconds = Number(value);
17
+ if (Number.isFinite(seconds) && seconds >= 0) {
18
+ return seconds * 1000;
19
+ }
20
+ const dateMs = Date.parse(value);
21
+ return Number.isFinite(dateMs) ? Math.max(DEFAULT_RATE_LIMIT_RETRY_MS, dateMs - Date.now()) : DEFAULT_RATE_LIMIT_RETRY_MS;
22
+ }
10
23
  export class TeamWikiSync {
11
24
  teamChapterhouseUrl;
12
25
  teamChapterhouseToken;
@@ -36,6 +49,7 @@ export class TeamWikiSync {
36
49
  this.now = options.now ?? (() => new Date());
37
50
  this.modeContext = new ModeContext({
38
51
  ...config,
52
+ chapterhouseMode: options.chapterhouseMode ?? config.chapterhouseMode,
39
53
  teamChapterhouseUrl: this.teamChapterhouseUrl,
40
54
  standaloneMode: this.standaloneMode,
41
55
  });
@@ -58,7 +72,7 @@ export class TeamWikiSync {
58
72
  return cachedContent;
59
73
  }
60
74
  try {
61
- const response = await this.fetchImpl(this.buildPageUrl(path), {
75
+ const response = await this.fetchWithRateLimitBackoff(this.buildPageUrl(path), {
62
76
  headers: this.buildHeaders(options.authorizationHeader),
63
77
  });
64
78
  if (!response.ok) {
@@ -85,6 +99,15 @@ export class TeamWikiSync {
85
99
  return null;
86
100
  }
87
101
  }
102
+ async fetchWithRateLimitBackoff(input, init) {
103
+ const response = await this.fetchImpl(input, init);
104
+ if (response.status !== 429) {
105
+ return response;
106
+ }
107
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
108
+ await new Promise((resolve) => setTimeout(resolve, retryAfterMs));
109
+ return this.fetchImpl(input, init);
110
+ }
88
111
  async syncAll(options = {}) {
89
112
  if (!this.isEnabled()) {
90
113
  return [];
@@ -103,11 +126,13 @@ export class TeamWikiSync {
103
126
  }
104
127
  const content = await this.fetchPage(path, options);
105
128
  if (content !== null) {
106
- writePage(path, content);
107
- const entry = buildIndexEntryForPage(path, { section: "Team" });
108
- if (entry) {
109
- addToIndex(entry);
110
- }
129
+ await withWikiWrite(() => {
130
+ writePage(path, content);
131
+ const entry = buildIndexEntryForPage(path, { section: "Team" });
132
+ if (entry) {
133
+ addToIndex(entry);
134
+ }
135
+ });
111
136
  synced.push(path);
112
137
  }
113
138
  }
@@ -36,6 +36,7 @@ test("uses fresh cache when TTL has not expired", async () => {
36
36
  }));
37
37
  let fetchCalls = 0;
38
38
  const sync = new teamSyncModule.TeamWikiSync({
39
+ chapterhouseMode: "team",
39
40
  teamChapterhouseUrl: "https://team.example.com",
40
41
  standaloneMode: false,
41
42
  cacheTtlMinutes: 60,
@@ -64,6 +65,7 @@ test("re-fetches when cache is stale", async () => {
64
65
  },
65
66
  }));
66
67
  const sync = new teamSyncModule.TeamWikiSync({
68
+ chapterhouseMode: "team",
67
69
  teamChapterhouseUrl: "https://team.example.com",
68
70
  standaloneMode: false,
69
71
  cacheTtlMinutes: 60,
@@ -90,6 +92,41 @@ test("re-fetches when cache is stale", async () => {
90
92
  const manifest = JSON.parse(readFileSync(join(cacheRoot, "manifest.json"), "utf-8"));
91
93
  assert.equal(manifest[pagePath]?.etag, '"fresh-q2"');
92
94
  });
95
+ test("fetchPage retries once after a team wiki 429 response", async () => {
96
+ const teamSyncModule = await loadTeamSyncModule();
97
+ assert.ok(teamSyncModule, "team sync module should exist");
98
+ const wikiDir = makeWikiDir("rate-limit-retry");
99
+ const pagePath = "pages/team/rate-limit.md";
100
+ let fetchCalls = 0;
101
+ const sync = new teamSyncModule.TeamWikiSync({
102
+ chapterhouseMode: "team",
103
+ teamChapterhouseUrl: "https://team.example.com",
104
+ standaloneMode: false,
105
+ cacheTtlMinutes: 60,
106
+ teamWikiPaths: ["pages/team"],
107
+ wikiDir,
108
+ fetchImpl: async () => {
109
+ fetchCalls += 1;
110
+ if (fetchCalls === 1) {
111
+ return new Response("rate limited", {
112
+ status: 429,
113
+ headers: { "retry-after": "0" },
114
+ });
115
+ }
116
+ return new Response(JSON.stringify({
117
+ path: pagePath,
118
+ content: "# Recovered\n",
119
+ exists: true,
120
+ }), {
121
+ status: 200,
122
+ headers: { "content-type": "application/json" },
123
+ });
124
+ },
125
+ });
126
+ const content = await sync.fetchPage(pagePath);
127
+ assert.equal(content, "# Recovered\n");
128
+ assert.equal(fetchCalls, 2);
129
+ });
93
130
  test("returns stale cache on network failure with warning", async () => {
94
131
  const teamSyncModule = await loadTeamSyncModule();
95
132
  assert.ok(teamSyncModule, "team sync module should exist");
@@ -105,6 +142,7 @@ test("returns stale cache on network failure with warning", async () => {
105
142
  }));
106
143
  const warnings = [];
107
144
  const sync = new teamSyncModule.TeamWikiSync({
145
+ chapterhouseMode: "team",
108
146
  teamChapterhouseUrl: "https://team.example.com",
109
147
  standaloneMode: false,
110
148
  cacheTtlMinutes: 60,
@@ -125,6 +163,7 @@ test("returns null when no cache exists and network fails", async () => {
125
163
  assert.ok(teamSyncModule, "team sync module should exist");
126
164
  const wikiDir = makeWikiDir("cold-miss");
127
165
  const sync = new teamSyncModule.TeamWikiSync({
166
+ chapterhouseMode: "team",
128
167
  teamChapterhouseUrl: "https://team.example.com",
129
168
  standaloneMode: false,
130
169
  cacheTtlMinutes: 60,
@@ -141,6 +180,7 @@ test("isTeamPath matches configured team wiki prefixes", async () => {
141
180
  const teamSyncModule = await loadTeamSyncModule();
142
181
  assert.ok(teamSyncModule, "team sync module should exist");
143
182
  const sync = new teamSyncModule.TeamWikiSync({
183
+ chapterhouseMode: "team",
144
184
  teamChapterhouseUrl: "https://team.example.com",
145
185
  standaloneMode: false,
146
186
  teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis", "pages/shared"],
@@ -152,11 +192,52 @@ test("isTeamPath matches configured team wiki prefixes", async () => {
152
192
  assert.equal(sync.isTeamPath("pages/shared/runbooks/deploy.md"), true);
153
193
  assert.equal(sync.isTeamPath("pages/private/notes.md"), false);
154
194
  });
195
+ test("syncAll writes fetched team pages and refreshes the index", async () => {
196
+ const teamSyncModule = await loadTeamSyncModule();
197
+ assert.ok(teamSyncModule, "team sync module should exist");
198
+ const wikiDir = makeWikiDir("sync-all");
199
+ const sync = new teamSyncModule.TeamWikiSync({
200
+ chapterhouseMode: "team",
201
+ teamChapterhouseUrl: "https://team.example.com",
202
+ standaloneMode: false,
203
+ cacheTtlMinutes: 60,
204
+ teamWikiPaths: ["pages/team", "pages/okrs", "pages/kpis"],
205
+ wikiDir,
206
+ fetchImpl: async (input) => {
207
+ const url = String(input);
208
+ if (url === "https://team.example.com/api/team/wiki") {
209
+ return new Response(JSON.stringify({
210
+ pages: ["pages/team/vision.md", "pages/private/skip.md"],
211
+ }), {
212
+ status: 200,
213
+ headers: { "content-type": "application/json" },
214
+ });
215
+ }
216
+ if (url === "https://team.example.com/api/team/wiki/pages%2Fteam%2Fvision.md") {
217
+ return new Response(JSON.stringify({
218
+ path: "pages/team/vision.md",
219
+ content: "---\ntitle: Team Vision\nsummary: Shared direction\nupdated: 2026-05-15\n---\n\n# Team Vision\n",
220
+ exists: true,
221
+ }), {
222
+ status: 200,
223
+ headers: { "content-type": "application/json" },
224
+ });
225
+ }
226
+ throw new Error(`unexpected fetch: ${url}`);
227
+ },
228
+ });
229
+ const synced = await sync.syncAll();
230
+ const indexManager = await import(new URL(`./index-manager.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
231
+ assert.deepEqual(synced, ["pages/team/vision.md"]);
232
+ assert.equal(readFileSync(join(tmpRoot, ".chapterhouse", "wiki", "pages", "team", "vision.md"), "utf-8"), "---\ntitle: Team Vision\nsummary: Shared direction\nupdated: 2026-05-15\n---\n\n# Team Vision\n");
233
+ assert.ok(indexManager.parseIndex().some((entry) => entry.path === "pages/team/vision.md"));
234
+ });
155
235
  test("pushUpdate silently no-ops when team wiki sync is disabled", async () => {
156
236
  const teamSyncModule = await loadTeamSyncModule();
157
237
  assert.ok(teamSyncModule, "team sync module should exist");
158
238
  let called = false;
159
239
  const sync = new teamSyncModule.TeamWikiSync({
240
+ chapterhouseMode: "team",
160
241
  teamChapterhouseUrl: "",
161
242
  standaloneMode: false,
162
243
  wikiDir: makeWikiDir("disabled-push"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chapterhouse",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
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"