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.
- package/README.md +1 -1
- package/agents/korg.agent.md +20 -0
- package/dist/api/auth.js +11 -1
- package/dist/api/auth.test.js +29 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/route-coverage.test.js +61 -21
- package/dist/api/routes/agents.js +472 -0
- package/dist/api/routes/memory.js +299 -0
- package/dist/api/routes/projects.js +170 -0
- package/dist/api/routes/sessions.js +347 -0
- package/dist/api/routes/system.js +82 -0
- package/dist/api/routes/wiki.js +455 -0
- package/dist/api/routes/wiki.test.js +49 -0
- package/dist/api/send-json.js +16 -0
- package/dist/api/send-json.test.js +18 -0
- package/dist/api/server-runtime.js +45 -3
- package/dist/api/server.js +34 -1764
- package/dist/api/server.test.js +239 -8
- package/dist/api/sse-hub.js +37 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +151 -58
- package/dist/config.test.js +29 -0
- package/dist/copilot/okr-mapper.js +2 -11
- package/dist/copilot/orchestrator.js +358 -352
- package/dist/copilot/orchestrator.test.js +139 -4
- package/dist/copilot/prompt-date.js +2 -1
- package/dist/copilot/session-manager.js +25 -23
- package/dist/copilot/session-manager.test.js +35 -1
- package/dist/copilot/standup.js +2 -2
- package/dist/copilot/task-event-log.js +7 -1
- package/dist/copilot/task-event-log.test.js +13 -0
- package/dist/copilot/tools/agent.js +608 -0
- package/dist/copilot/tools/index.js +19 -0
- package/dist/copilot/tools/memory.js +678 -0
- package/dist/copilot/tools/models.js +2 -0
- package/dist/copilot/tools/okr.js +171 -0
- package/dist/copilot/tools/wiki.js +333 -0
- package/dist/copilot/tools-deps.js +4 -0
- package/dist/copilot/tools.agent.test.js +10 -8
- package/dist/copilot/tools.inventory.test.js +76 -0
- package/dist/copilot/tools.js +1 -1725
- package/dist/copilot/tools.okr.test.js +31 -0
- package/dist/copilot/tools.wiki.test.js +358 -6
- package/dist/copilot/turn-event-log.js +31 -4
- package/dist/copilot/turn-event-log.test.js +24 -2
- package/dist/copilot/workiq-installer.test.js +2 -2
- package/dist/daemon-install.js +3 -2
- package/dist/daemon.js +9 -17
- package/dist/integrations/ado-client.js +90 -9
- package/dist/integrations/ado-client.test.js +56 -0
- package/dist/integrations/team-push.js +1 -0
- package/dist/integrations/team-push.test.js +6 -0
- package/dist/integrations/teams-notify.js +1 -0
- package/dist/integrations/teams-notify.test.js +5 -0
- package/dist/memory/active-scope.test.js +0 -1
- package/dist/memory/checkpoint.js +89 -72
- package/dist/memory/checkpoint.test.js +23 -3
- package/dist/memory/eot.js +194 -89
- package/dist/memory/eot.test.js +186 -3
- package/dist/memory/hooks.js +2 -4
- package/dist/memory/housekeeping-scheduler.js +1 -1
- package/dist/memory/housekeeping-scheduler.test.js +1 -2
- package/dist/memory/housekeeping.js +100 -3
- package/dist/memory/housekeeping.test.js +33 -2
- package/dist/memory/reflect.test.js +2 -0
- package/dist/memory/scope-lock.js +26 -0
- package/dist/memory/scope-lock.test.js +118 -0
- package/dist/memory/scopes.test.js +0 -1
- package/dist/mode-context.js +58 -5
- package/dist/mode-context.test.js +68 -0
- package/dist/paths.js +1 -0
- package/dist/setup.js +3 -2
- package/dist/shared/api-schemas.js +48 -5
- package/dist/store/connection.js +96 -0
- package/dist/store/db.js +5 -1498
- package/dist/store/db.test.js +182 -1
- package/dist/store/migrations.js +460 -0
- package/dist/store/repositories/memory.js +281 -0
- package/dist/store/repositories/okr.js +3 -0
- package/dist/store/repositories/projects.js +5 -0
- package/dist/store/repositories/sessions.js +284 -0
- package/dist/store/repositories/wiki.js +60 -0
- package/dist/store/schema.js +501 -0
- package/dist/util/logger.js +3 -2
- package/dist/wiki/consolidation.js +50 -9
- package/dist/wiki/consolidation.test.js +45 -0
- package/dist/wiki/frontmatter.js +45 -14
- package/dist/wiki/frontmatter.test.js +26 -1
- package/dist/wiki/fs.js +16 -4
- package/dist/wiki/fs.test.js +84 -0
- package/dist/wiki/index-manager.js +30 -2
- package/dist/wiki/index-manager.test.js +43 -12
- package/dist/wiki/ingest.js +17 -1
- package/dist/wiki/lock.js +11 -1
- package/dist/wiki/log-manager.js +2 -7
- package/dist/wiki/migrate.js +44 -17
- package/dist/wiki/project-registry.js +10 -5
- package/dist/wiki/project-registry.test.js +14 -0
- package/dist/wiki/scheduler.js +1 -1
- package/dist/wiki/seed-team-wiki.js +2 -1
- package/dist/wiki/team-sync.js +31 -6
- package/dist/wiki/team-sync.test.js +81 -0
- package/package.json +1 -1
- package/web/dist/assets/WikiEdit-BZXAdarz.js +30 -0
- package/web/dist/assets/WikiEdit-BZXAdarz.js.map +1 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js +2 -0
- package/web/dist/assets/WikiGraph-KrCYco4v.js.map +1 -0
- package/web/dist/assets/index-CUm2Wbuh.js +250 -0
- package/web/dist/assets/index-CUm2Wbuh.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-iQrv3lQN.js +0 -286
- package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
package/dist/wiki/migrate.js
CHANGED
|
@@ -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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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)
|
|
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 {
|
package/dist/wiki/scheduler.js
CHANGED
|
@@ -16,7 +16,7 @@ export class WikiConsolidationScheduler {
|
|
|
16
16
|
started = false;
|
|
17
17
|
running = false;
|
|
18
18
|
constructor(options = {}) {
|
|
19
|
-
this.env = options.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.";
|
package/dist/wiki/team-sync.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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