chapterhouse 0.9.2 → 0.11.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 (121) hide show
  1. package/README.md +1 -1
  2. package/dist/api/auth.js +11 -1
  3. package/dist/api/auth.test.js +29 -0
  4. package/dist/api/errors.js +23 -0
  5. package/dist/api/route-coverage.test.js +61 -21
  6. package/dist/api/routes/agents.js +472 -0
  7. package/dist/api/routes/memory.js +299 -0
  8. package/dist/api/routes/projects.js +170 -0
  9. package/dist/api/routes/sessions.js +347 -0
  10. package/dist/api/routes/system.js +82 -0
  11. package/dist/api/routes/wiki.js +455 -0
  12. package/dist/api/routes/wiki.test.js +49 -0
  13. package/dist/api/send-json.js +16 -0
  14. package/dist/api/send-json.test.js +18 -0
  15. package/dist/api/server-runtime.js +45 -3
  16. package/dist/api/server.js +34 -1764
  17. package/dist/api/server.test.js +239 -8
  18. package/dist/api/sse-hub.js +37 -0
  19. package/dist/cli.js +1 -1
  20. package/dist/config.js +151 -58
  21. package/dist/config.test.js +29 -0
  22. package/dist/copilot/okr-mapper.js +2 -11
  23. package/dist/copilot/orchestrator.js +358 -352
  24. package/dist/copilot/orchestrator.test.js +139 -4
  25. package/dist/copilot/prompt-date.js +2 -1
  26. package/dist/copilot/session-manager.js +25 -23
  27. package/dist/copilot/session-manager.test.js +35 -1
  28. package/dist/copilot/standup.js +2 -2
  29. package/dist/copilot/task-event-log.js +7 -1
  30. package/dist/copilot/task-event-log.test.js +13 -0
  31. package/dist/copilot/tools/agent.js +608 -0
  32. package/dist/copilot/tools/index.js +19 -0
  33. package/dist/copilot/tools/memory.js +678 -0
  34. package/dist/copilot/tools/models.js +2 -0
  35. package/dist/copilot/tools/okr.js +171 -0
  36. package/dist/copilot/tools/wiki.js +333 -0
  37. package/dist/copilot/tools-deps.js +4 -0
  38. package/dist/copilot/tools.agent.test.js +10 -8
  39. package/dist/copilot/tools.inventory.test.js +76 -0
  40. package/dist/copilot/tools.js +1 -1780
  41. package/dist/copilot/tools.okr.test.js +31 -0
  42. package/dist/copilot/tools.wiki.test.js +6 -3
  43. package/dist/copilot/turn-event-log.js +31 -4
  44. package/dist/copilot/turn-event-log.test.js +24 -2
  45. package/dist/copilot/workiq-installer.test.js +2 -2
  46. package/dist/daemon-install.js +3 -2
  47. package/dist/daemon.js +9 -17
  48. package/dist/integrations/ado-client.js +90 -9
  49. package/dist/integrations/ado-client.test.js +56 -0
  50. package/dist/integrations/team-push.js +1 -0
  51. package/dist/integrations/team-push.test.js +6 -0
  52. package/dist/integrations/teams-notify.js +1 -0
  53. package/dist/integrations/teams-notify.test.js +5 -0
  54. package/dist/memory/active-scope.test.js +0 -1
  55. package/dist/memory/checkpoint.js +89 -72
  56. package/dist/memory/checkpoint.test.js +23 -3
  57. package/dist/memory/eot.js +87 -85
  58. package/dist/memory/eot.test.js +71 -3
  59. package/dist/memory/hooks.js +2 -4
  60. package/dist/memory/housekeeping-scheduler.js +1 -1
  61. package/dist/memory/housekeeping-scheduler.test.js +1 -2
  62. package/dist/memory/housekeeping.js +100 -3
  63. package/dist/memory/housekeeping.test.js +33 -2
  64. package/dist/memory/reflect.test.js +2 -0
  65. package/dist/memory/scope-lock.js +26 -0
  66. package/dist/memory/scope-lock.test.js +118 -0
  67. package/dist/memory/scopes.test.js +0 -1
  68. package/dist/mode-context.js +58 -5
  69. package/dist/mode-context.test.js +68 -0
  70. package/dist/paths.js +1 -0
  71. package/dist/setup.js +3 -2
  72. package/dist/shared/api-schemas.js +48 -5
  73. package/dist/store/connection.js +96 -0
  74. package/dist/store/db.js +5 -1498
  75. package/dist/store/db.test.js +182 -1
  76. package/dist/store/migrations.js +460 -0
  77. package/dist/store/repositories/memory.js +281 -0
  78. package/dist/store/repositories/okr.js +3 -0
  79. package/dist/store/repositories/projects.js +5 -0
  80. package/dist/store/repositories/sessions.js +284 -0
  81. package/dist/store/repositories/wiki.js +60 -0
  82. package/dist/store/schema.js +501 -0
  83. package/dist/util/logger.js +3 -2
  84. package/dist/wiki/consolidation.js +50 -9
  85. package/dist/wiki/consolidation.test.js +45 -0
  86. package/dist/wiki/frontmatter.js +43 -13
  87. package/dist/wiki/frontmatter.test.js +24 -0
  88. package/dist/wiki/fs.js +16 -4
  89. package/dist/wiki/fs.test.js +84 -0
  90. package/dist/wiki/index-manager.js +30 -2
  91. package/dist/wiki/index-manager.test.js +43 -12
  92. package/dist/wiki/ingest.js +1 -1
  93. package/dist/wiki/lock.js +11 -1
  94. package/dist/wiki/log-manager.js +2 -7
  95. package/dist/wiki/migrate.js +44 -17
  96. package/dist/wiki/project-registry.js +10 -5
  97. package/dist/wiki/project-registry.test.js +14 -0
  98. package/dist/wiki/scheduler.js +1 -1
  99. package/dist/wiki/seed-team-wiki.js +2 -1
  100. package/dist/wiki/team-sync.js +31 -6
  101. package/dist/wiki/team-sync.test.js +81 -0
  102. package/package.json +1 -1
  103. package/web/dist/assets/WikiEdit-EBVoY1Pk.js +30 -0
  104. package/web/dist/assets/WikiEdit-EBVoY1Pk.js.map +1 -0
  105. package/web/dist/assets/WikiGraph-BUbbABq-.js +2 -0
  106. package/web/dist/assets/WikiGraph-BUbbABq-.js.map +1 -0
  107. package/web/dist/assets/icon-acolyte-cream.svg +10 -0
  108. package/web/dist/assets/icon-acolyte-dark.svg +10 -0
  109. package/web/dist/assets/icon-acolyte-gold.svg +10 -0
  110. package/web/dist/assets/icon-acolyte-ibad.svg +10 -0
  111. package/web/dist/assets/icon-acolyte-lit.svg +10 -0
  112. package/web/dist/assets/icon-acolyte-mono.svg +10 -0
  113. package/web/dist/assets/icon-acolyte.png +0 -0
  114. package/web/dist/assets/icon-acolyte.svg +10 -0
  115. package/web/dist/assets/index-BGLL9pgM.css +10 -0
  116. package/web/dist/assets/index-KFX8UmOb.js +250 -0
  117. package/web/dist/assets/index-KFX8UmOb.js.map +1 -0
  118. package/web/dist/index.html +6 -4
  119. package/web/dist/assets/index-5kz9aRU9.css +0 -10
  120. package/web/dist/assets/index-iQrv3lQN.js +0 -286
  121. package/web/dist/assets/index-iQrv3lQN.js.map +0 -1
package/dist/config.js CHANGED
@@ -18,6 +18,8 @@ function loadRuntimeEnv() {
18
18
  loadRuntimeEnv();
19
19
  const configSchema = z.object({
20
20
  NODE_ENV: z.string().optional(),
21
+ PATH: z.string().optional(),
22
+ CHAPTERHOUSE_DISABLE_DOTENV: z.string().optional(),
21
23
  CHAPTERHOUSE_MODE: z.string().optional(),
22
24
  API_HOST: z.string().optional(),
23
25
  API_PORT: z.string().optional(),
@@ -34,6 +36,7 @@ const configSchema = z.object({
34
36
  ENTRA_AUTH_ENABLED: z.string().optional(),
35
37
  CORS_ALLOWED_ORIGINS: z.string().optional(),
36
38
  API_TOKEN: z.string().optional(),
39
+ LOG_LEVEL: z.string().optional(),
37
40
  TEAM_CHAPTERHOUSE_URL: z.string().optional(),
38
41
  TEAM_CHAPTERHOUSE_TOKEN: z.string().optional(),
39
42
  TEAM_WIKI_CACHE_TTL_MINUTES: z.string().optional(),
@@ -43,6 +46,7 @@ const configSchema = z.object({
43
46
  TEAMS_NOTIFICATIONS_ENABLED: z.string().optional(),
44
47
  COPILOT_TOKEN: z.string().optional(),
45
48
  GITHUB_TOKEN: z.string().optional(),
49
+ TELEGRAM_BOT_TOKEN: z.string().optional(),
46
50
  API_RATE_LIMIT_WINDOW_MS: z.string().optional(),
47
51
  API_RATE_LIMIT_GENERAL_MAX: z.string().optional(),
48
52
  API_RATE_LIMIT_AUTH_MAX: z.string().optional(),
@@ -50,7 +54,20 @@ const configSchema = z.object({
50
54
  CHAPTERHOUSE_SSE_BUFFER_CAPACITY: z.string().optional(),
51
55
  CHAPTERHOUSE_SSE_REPLAY_LIMIT: z.string().optional(),
52
56
  CHAPTERHOUSE_WORKIQ_AUTO_INSTALL: z.string().optional(),
57
+ CHAPTERHOUSE_SELF_EDIT: z.string().optional(),
58
+ CHAPTERHOUSE_OPEN_BROWSER: z.string().optional(),
59
+ CHAPTERHOUSE_RESTARTED: z.string().optional(),
60
+ CHAPTERHOUSE_SHUTDOWN_TIMEOUT_MS: z.string().optional(),
53
61
  CHAPTERHOUSE_CHAT_SSE: z.string().optional(),
62
+ CHAPTERHOUSE_INJECT_DATE: z.string().optional(),
63
+ CHAPTERHOUSE_SESSION_AGENT_NAME: z.string().optional(),
64
+ CHAPTERHOUSE_AGENT_NAME: z.string().optional(),
65
+ COPILOT_AGENT_NAME: z.string().optional(),
66
+ AGENT_NAME: z.string().optional(),
67
+ CHAPTERHOUSE_SESSION_IDLE_TTL_MS: z.string().optional(),
68
+ CHAPTERHOUSE_SESSION_MAX_ACTIVE: z.string().optional(),
69
+ CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS: z.string().optional(),
70
+ CHAPTERHOUSE_JSON_LIMIT: z.string().optional(),
54
71
  CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED: z.string().optional(),
55
72
  CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS: z.string().optional(),
56
73
  CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE: z.string().optional(),
@@ -58,9 +75,12 @@ const configSchema = z.object({
58
75
  CHAPTERHOUSE_MEMORY_INJECT: z.string().optional(),
59
76
  CHAPTERHOUSE_MEMORY_AUTO_ACCEPT: z.string().optional(),
60
77
  CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED: z.string().optional(),
78
+ CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED: z.string().optional(),
61
79
  CHAPTERHOUSE_MEMORY_HOOKS_ENABLED: z.string().optional(),
62
80
  CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED: z.string().optional(),
63
81
  CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS: z.string().optional(),
82
+ CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD: z.string().optional(),
83
+ CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD: z.string().optional(),
64
84
  CHAPTERHOUSE_MEMORY_DECAY_DAYS: z.string().optional(),
65
85
  CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS: z.string().optional(),
66
86
  CHAPTERHOUSE_MEMORY_TIERING_ENABLED: z.string().optional(),
@@ -68,6 +88,7 @@ const configSchema = z.object({
68
88
  CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS: z.string().optional(),
69
89
  CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED: z.string().optional(),
70
90
  CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR: z.string().optional(),
91
+ CHAPTERHOUSE_PKB_TRUTH_REWRITE_BUDGET: z.string().optional(),
71
92
  });
72
93
  export const DEFAULT_MODEL = "claude-sonnet-4.6";
73
94
  export const DEFAULT_TEAM_WIKI_CACHE_TTL_MINUTES = 60;
@@ -82,13 +103,18 @@ export const DEFAULT_API_RATE_LIMIT_AUTH_MAX = 10;
82
103
  export const DEFAULT_API_RATE_LIMIT_SSE_MAX_CONNECTIONS = 5;
83
104
  export const DEFAULT_SSE_BUFFER_CAPACITY = 2_000;
84
105
  export const DEFAULT_SSE_REPLAY_LIMIT = 10_000;
106
+ export const DEFAULT_JSON_BODY_LIMIT = "2mb";
107
+ export const DEFAULT_PKB_TRUTH_REWRITE_BUDGET = 18;
85
108
  function parseZeroOneEnv(name, rawValue, defaultValue) {
86
109
  const normalized = rawValue?.trim();
87
110
  if (!normalized) {
88
111
  return defaultValue;
89
112
  }
113
+ if (normalized === "true" || normalized === "false") {
114
+ return normalized === "true";
115
+ }
90
116
  if (normalized !== "0" && normalized !== "1") {
91
- throw new Error(`${name} must be '0' or '1', got: "${rawValue}"`);
117
+ throw new Error(`${name} must be '0', '1', 'true', or 'false', got: "${rawValue}"`);
92
118
  }
93
119
  return normalized === "1";
94
120
  }
@@ -143,6 +169,17 @@ function parsePositiveNumberEnv(name, rawValue, defaultValue) {
143
169
  }
144
170
  return parsed;
145
171
  }
172
+ function parseUnitIntervalEnv(name, rawValue, defaultValue) {
173
+ const normalized = rawValue?.trim();
174
+ if (!normalized) {
175
+ return defaultValue;
176
+ }
177
+ const parsed = Number(normalized);
178
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
179
+ throw new Error(`${name} must be a number between 0 and 1, got: "${rawValue}"`);
180
+ }
181
+ return parsed;
182
+ }
146
183
  function parseHourEnv(name, rawValue, defaultValue) {
147
184
  const normalized = rawValue?.trim();
148
185
  if (!normalized) {
@@ -308,12 +345,20 @@ export function parseRuntimeConfig(env, options = {}) {
308
345
  const memoryCheckpointTurns = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_TURNS, 5);
309
346
  const memoryCheckpointMinTurnsForScopeFire = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_MIN_TURNS_FOR_SCOPE_FIRE, 2);
310
347
  const memoryHousekeepingTurns = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS, 50);
348
+ const memoryHousekeepingSimilarityThreshold = parseUnitIntervalEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD, 0.8);
349
+ const memoryCheckpointDuplicateThreshold = parseUnitIntervalEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD, 0.85);
311
350
  const memoryDecayDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_DECAY_DAYS", raw.CHAPTERHOUSE_MEMORY_DECAY_DAYS, 30);
312
351
  const memoryInboxRetentionDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS", raw.CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS, 7);
313
352
  const memoryHotRecallBoost = parsePositiveNumberEnv("CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST", raw.CHAPTERHOUSE_MEMORY_HOT_RECALL_BOOST, 1.5);
314
353
  const memoryHotAgeDays = parsePositiveIntegerEnv("CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS", raw.CHAPTERHOUSE_MEMORY_HOT_AGE_DAYS, 30);
315
354
  const pkbConsolidationEnabled = parseBooleanEnv("CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED", raw.CHAPTERHOUSE_PKB_CONSOLIDATION_ENABLED, true);
316
355
  const pkbConsolidationHour = parseHourEnv("CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR", raw.CHAPTERHOUSE_PKB_CONSOLIDATION_HOUR, 3);
356
+ const jsonBodyLimit = raw.CHAPTERHOUSE_JSON_LIMIT?.trim() || DEFAULT_JSON_BODY_LIMIT;
357
+ const shutdownTimeoutMs = parsePositiveIntegerEnv("CHAPTERHOUSE_SHUTDOWN_TIMEOUT_MS", raw.CHAPTERHOUSE_SHUTDOWN_TIMEOUT_MS, 60_000);
358
+ const sessionIdleTtlMs = parsePositiveIntegerEnv("CHAPTERHOUSE_SESSION_IDLE_TTL_MS", raw.CHAPTERHOUSE_SESSION_IDLE_TTL_MS, 1_800_000);
359
+ const sessionMaxActive = parsePositiveIntegerEnv("CHAPTERHOUSE_SESSION_MAX_ACTIVE", raw.CHAPTERHOUSE_SESSION_MAX_ACTIVE, 20);
360
+ const orchestratorTimeoutMs = parsePositiveIntegerEnv("CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS", raw.CHAPTERHOUSE_ORCHESTRATOR_TIMEOUT_MS, 1_800_000);
361
+ const pkbTruthRewriteBudget = parsePositiveIntegerEnv("CHAPTERHOUSE_PKB_TRUTH_REWRITE_BUDGET", raw.CHAPTERHOUSE_PKB_TRUTH_REWRITE_BUDGET, DEFAULT_PKB_TRUTH_REWRITE_BUDGET);
317
362
  if (effectiveEntraAuthEnabled && (!effectiveEntraTenantId || !effectiveEntraClientId)) {
318
363
  throw new Error("ENTRA_AUTH_ENABLED=true requires ENTRA_TENANT_ID and ENTRA_CLIENT_ID");
319
364
  }
@@ -323,7 +368,9 @@ export function parseRuntimeConfig(env, options = {}) {
323
368
  return {
324
369
  nodeEnv,
325
370
  isProduction,
371
+ disableDotenv: raw.CHAPTERHOUSE_DISABLE_DOTENV === "1",
326
372
  chapterhouseMode: parsedMode,
373
+ explicitChapterhouseMode: raw.CHAPTERHOUSE_MODE?.trim() || "",
327
374
  apiHost: parsedHost,
328
375
  apiPort: parsedPort,
329
376
  standupTime: parsedStandupTime,
@@ -332,16 +379,18 @@ export function parseRuntimeConfig(env, options = {}) {
332
379
  adoProject: raw.ADO_PROJECT?.trim() || DEFAULT_ADO_PROJECT,
333
380
  adoPat: raw.ADO_PAT?.trim() || "",
334
381
  workerTimeoutMs: parsedWorkerTimeout,
382
+ shellPath: raw.PATH?.trim() || "",
335
383
  entraTenantId: effectiveEntraTenantId,
336
384
  entraClientId: effectiveEntraClientId,
337
385
  entraRequiredRole: effectiveEntraRequiredRole,
338
386
  entraTeamLeadId: effectiveEntraTeamLeadId,
339
387
  entraAuthEnabled: effectiveEntraAuthEnabled,
388
+ apiToken,
340
389
  standaloneMode,
341
390
  corsAllowedOrigins: parseCorsAllowedOrigins(raw.CORS_ALLOWED_ORIGINS),
342
391
  copilotModel: raw.COPILOT_MODEL || DEFAULT_MODEL,
343
392
  copilotAuthToken: raw.COPILOT_TOKEN?.trim() || raw.GITHUB_TOKEN?.trim() || "",
344
- selfEditEnabled: env.CHAPTERHOUSE_SELF_EDIT === "1",
393
+ selfEditEnabled: raw.CHAPTERHOUSE_SELF_EDIT === "1",
345
394
  teamChapterhouseUrl: parseOptionalUrl("TEAM_CHAPTERHOUSE_URL", raw.TEAM_CHAPTERHOUSE_URL),
346
395
  teamChapterhouseToken: raw.TEAM_CHAPTERHOUSE_TOKEN?.trim() || "",
347
396
  teamWikiCacheTtlMinutes: parsedTeamWikiCacheTtlMinutes,
@@ -356,7 +405,23 @@ export function parseRuntimeConfig(env, options = {}) {
356
405
  sseBufferCapacity,
357
406
  sseReplayLimit,
358
407
  workiqAutoInstall: parseBooleanEnv("CHAPTERHOUSE_WORKIQ_AUTO_INSTALL", raw.CHAPTERHOUSE_WORKIQ_AUTO_INSTALL, true),
408
+ logLevel: raw.LOG_LEVEL?.trim() || undefined,
409
+ shutdownTimeoutMs,
410
+ telegramBotTokenConfigured: Boolean(raw.TELEGRAM_BOT_TOKEN?.trim()),
411
+ openBrowserOnStart: raw.CHAPTERHOUSE_OPEN_BROWSER === "1",
412
+ restarted: raw.CHAPTERHOUSE_RESTARTED === "1",
413
+ injectDate: raw.CHAPTERHOUSE_INJECT_DATE !== "0",
414
+ agentNameCandidates: [
415
+ raw.CHAPTERHOUSE_SESSION_AGENT_NAME,
416
+ raw.CHAPTERHOUSE_AGENT_NAME,
417
+ raw.COPILOT_AGENT_NAME,
418
+ raw.AGENT_NAME,
419
+ ].map((value) => value?.trim() || "").filter(Boolean),
420
+ sessionIdleTtlMs,
421
+ sessionMaxActive,
422
+ orchestratorTimeoutMs,
359
423
  chatSseEnabled: parseZeroOneEnv("CHAPTERHOUSE_CHAT_SSE", raw.CHAPTERHOUSE_CHAT_SSE, true),
424
+ jsonBodyLimit,
360
425
  memoryCheckpointEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_ENABLED, true),
361
426
  memoryCheckpointTurns,
362
427
  memoryCheckpointOnScopeChange: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE", raw.CHAPTERHOUSE_MEMORY_CHECKPOINT_ON_SCOPE_CHANGE, true),
@@ -364,9 +429,12 @@ export function parseRuntimeConfig(env, options = {}) {
364
429
  memoryInjectEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_INJECT", raw.CHAPTERHOUSE_MEMORY_INJECT, true),
365
430
  memoryAutoAcceptEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_AUTO_ACCEPT", raw.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT, true),
366
431
  memoryEndOfTaskHookEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED", raw.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED, true),
432
+ memoryEndOfTaskFrictionEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED", raw.CHAPTERHOUSE_MEMORY_EOT_FRICTION_ENABLED, false),
367
433
  memoryHooksEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_HOOKS_ENABLED", raw.CHAPTERHOUSE_MEMORY_HOOKS_ENABLED, true),
368
434
  memoryHousekeepingEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED", raw.CHAPTERHOUSE_MEMORY_HOUSEKEEPING_ENABLED, true),
369
435
  memoryHousekeepingTurns,
436
+ memoryHousekeepingSimilarityThreshold,
437
+ memoryCheckpointDuplicateThreshold,
370
438
  memoryDecayDays,
371
439
  memoryInboxRetentionDays,
372
440
  memoryTieringEnabled: parseZeroOneEnv("CHAPTERHOUSE_MEMORY_TIERING_ENABLED", raw.CHAPTERHOUSE_MEMORY_TIERING_ENABLED, true),
@@ -374,75 +442,100 @@ export function parseRuntimeConfig(env, options = {}) {
374
442
  memoryHotAgeDays,
375
443
  pkbConsolidationEnabled,
376
444
  pkbConsolidationHour,
445
+ pkbTruthRewriteBudget,
377
446
  modeCompatibilityWarnings,
378
447
  };
379
448
  }
380
449
  const runtimeConfig = parseRuntimeConfig(process.env);
381
450
  let _copilotModel = runtimeConfig.copilotModel;
451
+ let _adoPatOverride;
452
+ function currentRuntimeConfig() {
453
+ return parseRuntimeConfig(process.env);
454
+ }
382
455
  export const config = {
383
- nodeEnv: runtimeConfig.nodeEnv,
384
- isProduction: runtimeConfig.isProduction,
385
- chapterhouseMode: runtimeConfig.chapterhouseMode,
386
- apiHost: runtimeConfig.apiHost,
387
- apiPort: runtimeConfig.apiPort,
388
- standupTime: runtimeConfig.standupTime,
389
- adoOrg: runtimeConfig.adoOrg,
390
- adoProject: runtimeConfig.adoProject,
391
- adoPat: runtimeConfig.adoPat,
456
+ get nodeEnv() { return currentRuntimeConfig().nodeEnv; },
457
+ get isProduction() { return currentRuntimeConfig().isProduction; },
458
+ get disableDotenv() { return currentRuntimeConfig().disableDotenv; },
459
+ get chapterhouseMode() { return currentRuntimeConfig().chapterhouseMode; },
460
+ get explicitChapterhouseMode() { return currentRuntimeConfig().explicitChapterhouseMode; },
461
+ get apiHost() { return currentRuntimeConfig().apiHost; },
462
+ get apiPort() { return currentRuntimeConfig().apiPort; },
463
+ get standupTime() { return currentRuntimeConfig().standupTime; },
464
+ get adoOrg() { return currentRuntimeConfig().adoOrg; },
465
+ get adoProject() { return currentRuntimeConfig().adoProject; },
466
+ get adoPat() { return _adoPatOverride ?? currentRuntimeConfig().adoPat; },
467
+ set adoPat(value) { _adoPatOverride = value; },
392
468
  get ADO_ORG() {
393
- return runtimeConfig.adoOrg;
469
+ return currentRuntimeConfig().adoOrg;
394
470
  },
395
471
  get ADO_PROJECT() {
396
- return runtimeConfig.adoProject;
472
+ return currentRuntimeConfig().adoProject;
397
473
  },
398
- workerTimeoutMs: runtimeConfig.workerTimeoutMs,
399
- entraTenantId: runtimeConfig.entraTenantId,
400
- entraClientId: runtimeConfig.entraClientId,
401
- entraRequiredRole: runtimeConfig.entraRequiredRole,
402
- entraTeamLeadId: runtimeConfig.entraTeamLeadId,
403
- entraAuthEnabled: runtimeConfig.entraAuthEnabled,
404
- standaloneMode: runtimeConfig.standaloneMode,
405
- corsAllowedOrigins: runtimeConfig.corsAllowedOrigins,
406
- teamChapterhouseUrl: runtimeConfig.teamChapterhouseUrl,
407
- teamChapterhouseToken: runtimeConfig.teamChapterhouseToken,
408
- teamWikiCacheTtlMinutes: runtimeConfig.teamWikiCacheTtlMinutes,
409
- teamWikiPaths: runtimeConfig.teamWikiPaths,
410
- wikiEntityCategories: runtimeConfig.wikiEntityCategories,
411
- teamsWebhookUrl: runtimeConfig.teamsWebhookUrl,
412
- teamsNotificationsEnabled: runtimeConfig.teamsNotificationsEnabled,
413
- apiRateLimitWindowMs: runtimeConfig.apiRateLimitWindowMs,
414
- apiRateLimitGeneralMax: runtimeConfig.apiRateLimitGeneralMax,
415
- apiRateLimitAuthMax: runtimeConfig.apiRateLimitAuthMax,
416
- apiRateLimitSseMaxConnections: runtimeConfig.apiRateLimitSseMaxConnections,
417
- sseBufferCapacity: runtimeConfig.sseBufferCapacity,
418
- sseReplayLimit: runtimeConfig.sseReplayLimit,
419
- workiqAutoInstall: runtimeConfig.workiqAutoInstall,
420
- chatSseEnabled: runtimeConfig.chatSseEnabled,
421
- memoryCheckpointEnabled: runtimeConfig.memoryCheckpointEnabled,
422
- memoryCheckpointTurns: runtimeConfig.memoryCheckpointTurns,
423
- memoryCheckpointOnScopeChange: runtimeConfig.memoryCheckpointOnScopeChange,
424
- memoryCheckpointMinTurnsForScopeFire: runtimeConfig.memoryCheckpointMinTurnsForScopeFire,
425
- memoryInjectEnabled: runtimeConfig.memoryInjectEnabled,
426
- memoryAutoAcceptEnabled: runtimeConfig.memoryAutoAcceptEnabled,
427
- memoryEndOfTaskHookEnabled: runtimeConfig.memoryEndOfTaskHookEnabled,
428
- memoryHooksEnabled: runtimeConfig.memoryHooksEnabled,
429
- memoryHousekeepingEnabled: runtimeConfig.memoryHousekeepingEnabled,
430
- memoryHousekeepingTurns: runtimeConfig.memoryHousekeepingTurns,
431
- memoryDecayDays: runtimeConfig.memoryDecayDays,
432
- memoryInboxRetentionDays: runtimeConfig.memoryInboxRetentionDays,
433
- memoryTieringEnabled: runtimeConfig.memoryTieringEnabled,
434
- memoryHotRecallBoost: runtimeConfig.memoryHotRecallBoost,
435
- memoryHotAgeDays: runtimeConfig.memoryHotAgeDays,
436
- pkbConsolidationEnabled: runtimeConfig.pkbConsolidationEnabled,
437
- pkbConsolidationHour: runtimeConfig.pkbConsolidationHour,
474
+ get workerTimeoutMs() { return currentRuntimeConfig().workerTimeoutMs; },
475
+ get shellPath() { return currentRuntimeConfig().shellPath; },
476
+ get entraTenantId() { return currentRuntimeConfig().entraTenantId; },
477
+ get entraClientId() { return currentRuntimeConfig().entraClientId; },
478
+ get entraRequiredRole() { return currentRuntimeConfig().entraRequiredRole; },
479
+ get entraTeamLeadId() { return currentRuntimeConfig().entraTeamLeadId; },
480
+ get entraAuthEnabled() { return currentRuntimeConfig().entraAuthEnabled; },
481
+ get apiToken() { return currentRuntimeConfig().apiToken; },
482
+ get standaloneMode() { return currentRuntimeConfig().standaloneMode; },
483
+ get corsAllowedOrigins() { return currentRuntimeConfig().corsAllowedOrigins; },
484
+ get teamChapterhouseUrl() { return currentRuntimeConfig().teamChapterhouseUrl; },
485
+ get teamChapterhouseToken() { return currentRuntimeConfig().teamChapterhouseToken; },
486
+ get teamWikiCacheTtlMinutes() { return currentRuntimeConfig().teamWikiCacheTtlMinutes; },
487
+ get teamWikiPaths() { return currentRuntimeConfig().teamWikiPaths; },
488
+ get wikiEntityCategories() { return currentRuntimeConfig().wikiEntityCategories; },
489
+ get teamsWebhookUrl() { return currentRuntimeConfig().teamsWebhookUrl; },
490
+ get teamsNotificationsEnabled() { return currentRuntimeConfig().teamsNotificationsEnabled; },
491
+ get apiRateLimitWindowMs() { return currentRuntimeConfig().apiRateLimitWindowMs; },
492
+ get apiRateLimitGeneralMax() { return currentRuntimeConfig().apiRateLimitGeneralMax; },
493
+ get apiRateLimitAuthMax() { return currentRuntimeConfig().apiRateLimitAuthMax; },
494
+ get apiRateLimitSseMaxConnections() { return currentRuntimeConfig().apiRateLimitSseMaxConnections; },
495
+ get sseBufferCapacity() { return currentRuntimeConfig().sseBufferCapacity; },
496
+ get sseReplayLimit() { return currentRuntimeConfig().sseReplayLimit; },
497
+ get workiqAutoInstall() { return currentRuntimeConfig().workiqAutoInstall; },
498
+ get logLevel() { return currentRuntimeConfig().logLevel; },
499
+ get shutdownTimeoutMs() { return currentRuntimeConfig().shutdownTimeoutMs; },
500
+ get telegramBotTokenConfigured() { return currentRuntimeConfig().telegramBotTokenConfigured; },
501
+ get openBrowserOnStart() { return currentRuntimeConfig().openBrowserOnStart; },
502
+ get restarted() { return currentRuntimeConfig().restarted; },
503
+ get injectDate() { return currentRuntimeConfig().injectDate; },
504
+ get agentNameCandidates() { return currentRuntimeConfig().agentNameCandidates; },
505
+ get sessionIdleTtlMs() { return currentRuntimeConfig().sessionIdleTtlMs; },
506
+ get sessionMaxActive() { return currentRuntimeConfig().sessionMaxActive; },
507
+ get orchestratorTimeoutMs() { return currentRuntimeConfig().orchestratorTimeoutMs; },
508
+ get chatSseEnabled() { return currentRuntimeConfig().chatSseEnabled; },
509
+ get jsonBodyLimit() { return currentRuntimeConfig().jsonBodyLimit; },
510
+ get memoryCheckpointEnabled() { return currentRuntimeConfig().memoryCheckpointEnabled; },
511
+ get memoryCheckpointTurns() { return currentRuntimeConfig().memoryCheckpointTurns; },
512
+ get memoryCheckpointOnScopeChange() { return currentRuntimeConfig().memoryCheckpointOnScopeChange; },
513
+ get memoryCheckpointMinTurnsForScopeFire() { return currentRuntimeConfig().memoryCheckpointMinTurnsForScopeFire; },
514
+ get memoryInjectEnabled() { return currentRuntimeConfig().memoryInjectEnabled; },
515
+ get memoryAutoAcceptEnabled() { return currentRuntimeConfig().memoryAutoAcceptEnabled; },
516
+ get memoryEndOfTaskHookEnabled() { return currentRuntimeConfig().memoryEndOfTaskHookEnabled; },
517
+ get memoryEndOfTaskFrictionEnabled() { return currentRuntimeConfig().memoryEndOfTaskFrictionEnabled; },
518
+ get memoryHooksEnabled() { return currentRuntimeConfig().memoryHooksEnabled; },
519
+ get memoryHousekeepingEnabled() { return currentRuntimeConfig().memoryHousekeepingEnabled; },
520
+ get memoryHousekeepingTurns() { return currentRuntimeConfig().memoryHousekeepingTurns; },
521
+ get memoryHousekeepingSimilarityThreshold() { return currentRuntimeConfig().memoryHousekeepingSimilarityThreshold; },
522
+ get memoryCheckpointDuplicateThreshold() { return currentRuntimeConfig().memoryCheckpointDuplicateThreshold; },
523
+ get memoryDecayDays() { return currentRuntimeConfig().memoryDecayDays; },
524
+ get memoryInboxRetentionDays() { return currentRuntimeConfig().memoryInboxRetentionDays; },
525
+ get memoryTieringEnabled() { return currentRuntimeConfig().memoryTieringEnabled; },
526
+ get memoryHotRecallBoost() { return currentRuntimeConfig().memoryHotRecallBoost; },
527
+ get memoryHotAgeDays() { return currentRuntimeConfig().memoryHotAgeDays; },
528
+ get pkbConsolidationEnabled() { return currentRuntimeConfig().pkbConsolidationEnabled; },
529
+ get pkbConsolidationHour() { return currentRuntimeConfig().pkbConsolidationHour; },
530
+ get pkbTruthRewriteBudget() { return currentRuntimeConfig().pkbTruthRewriteBudget; },
438
531
  get PKB_CONSOLIDATION_ENABLED() {
439
- return runtimeConfig.pkbConsolidationEnabled;
532
+ return currentRuntimeConfig().pkbConsolidationEnabled;
440
533
  },
441
534
  get PKB_CONSOLIDATION_HOUR() {
442
- return runtimeConfig.pkbConsolidationHour;
535
+ return currentRuntimeConfig().pkbConsolidationHour;
443
536
  },
444
- modeCompatibilityWarnings: runtimeConfig.modeCompatibilityWarnings,
445
- copilotAuthToken: runtimeConfig.copilotAuthToken,
537
+ get modeCompatibilityWarnings() { return currentRuntimeConfig().modeCompatibilityWarnings; },
538
+ get copilotAuthToken() { return currentRuntimeConfig().copilotAuthToken; },
446
539
  get copilotModel() {
447
540
  return _copilotModel;
448
541
  },
@@ -450,7 +543,7 @@ export const config = {
450
543
  _copilotModel = model;
451
544
  },
452
545
  get selfEditEnabled() {
453
- return runtimeConfig.selfEditEnabled;
546
+ return currentRuntimeConfig().selfEditEnabled;
454
547
  },
455
548
  };
456
549
  /** Update or append an env var in ~/.chapterhouse/.env */
@@ -112,6 +112,19 @@ test("defaults chat SSE on and still honors explicit CHAPTERHOUSE_CHAT_SSE overr
112
112
  assert.equal(parsedDisabled.chatSseEnabled, false);
113
113
  assert.equal(parsedEnabled.chatSseEnabled, true);
114
114
  });
115
+ test("parses configurable JSON body and consolidation truth rewrite limits", async () => {
116
+ const configModule = await import("./config.js");
117
+ assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
118
+ const parsedDefault = configModule.parseRuntimeConfig({});
119
+ const parsedExplicit = configModule.parseRuntimeConfig({
120
+ CHAPTERHOUSE_JSON_LIMIT: "8mb",
121
+ CHAPTERHOUSE_PKB_TRUTH_REWRITE_BUDGET: "7",
122
+ });
123
+ assert.equal(parsedDefault.jsonBodyLimit, "2mb");
124
+ assert.equal(parsedDefault.pkbTruthRewriteBudget, 18);
125
+ assert.equal(parsedExplicit.jsonBodyLimit, "8mb");
126
+ assert.equal(parsedExplicit.pkbTruthRewriteBudget, 7);
127
+ });
115
128
  test("defaults memory checkpoint turns to 5 and parses integer overrides", async () => {
116
129
  const configModule = await import("./config.js");
117
130
  assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
@@ -197,15 +210,31 @@ test("parses memory housekeeping config defaults and overrides", async () => {
197
210
  CHAPTERHOUSE_MEMORY_HOUSEKEEPING_TURNS: "12",
198
211
  CHAPTERHOUSE_MEMORY_DECAY_DAYS: "45",
199
212
  CHAPTERHOUSE_MEMORY_INBOX_RETENTION_DAYS: "14",
213
+ CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD: "0.91",
214
+ CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD: "0.72",
200
215
  });
201
216
  assert.equal(parsedDefault.memoryHousekeepingEnabled, true);
202
217
  assert.equal(parsedDefault.memoryHousekeepingTurns, 50);
203
218
  assert.equal(parsedDefault.memoryDecayDays, 30);
204
219
  assert.equal(parsedDefault.memoryInboxRetentionDays, 7);
220
+ assert.equal(parsedDefault.memoryHousekeepingSimilarityThreshold, 0.8);
221
+ assert.equal(parsedDefault.memoryCheckpointDuplicateThreshold, 0.85);
205
222
  assert.equal(parsedOverride.memoryHousekeepingEnabled, false);
206
223
  assert.equal(parsedOverride.memoryHousekeepingTurns, 12);
207
224
  assert.equal(parsedOverride.memoryDecayDays, 45);
208
225
  assert.equal(parsedOverride.memoryInboxRetentionDays, 14);
226
+ assert.equal(parsedOverride.memoryHousekeepingSimilarityThreshold, 0.91);
227
+ assert.equal(parsedOverride.memoryCheckpointDuplicateThreshold, 0.72);
228
+ });
229
+ test("rejects memory similarity thresholds outside the 0..1 range", async () => {
230
+ const configModule = await import("./config.js");
231
+ assert.equal(typeof configModule.parseRuntimeConfig, "function", "parseRuntimeConfig should be exported");
232
+ assert.throws(() => configModule.parseRuntimeConfig({
233
+ CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD: "1.1",
234
+ }), /CHAPTERHOUSE_MEMORY_HOUSEKEEPING_SIMILARITY_THRESHOLD must be a number between 0 and 1/);
235
+ assert.throws(() => configModule.parseRuntimeConfig({
236
+ CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD: "-0.1",
237
+ }), /CHAPTERHOUSE_MEMORY_CHECKPOINT_DUPLICATE_THRESHOLD must be a number between 0 and 1/);
209
238
  });
210
239
  test("defaults automatic proposal acceptance on and still honors explicit CHAPTERHOUSE_MEMORY_AUTO_ACCEPT overrides", async () => {
211
240
  const configModule = await import("./config.js");
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
3
+ import { getChapterhouseHome } from "../paths.js";
4
4
  const STOP_WORDS = new Set([
5
5
  "about", "after", "against", "been", "from", "into", "just", "that", "their", "them",
6
6
  "then", "they", "this", "today", "work", "worked", "yesterday",
@@ -8,11 +8,9 @@ const STOP_WORDS = new Set([
8
8
  const MIN_HISTORY_CONFIDENCE = 2;
9
9
  export class OKRMapper {
10
10
  teamWikiSync;
11
- _adoClient;
12
11
  learnedMappings;
13
12
  constructor(teamWikiSync, _adoClient) {
14
13
  this.teamWikiSync = teamWikiSync;
15
- this._adoClient = _adoClient;
16
14
  this.learnedMappings = loadLearnedMappings();
17
15
  }
18
16
  recordConfirmedMapping(activity, krId) {
@@ -181,14 +179,7 @@ function saveLearnedMappings(store) {
181
179
  writeFileSync(join(wikiDir, ".okr-memory.json"), `${JSON.stringify(store, null, 2)}\n`);
182
180
  }
183
181
  function getWikiDir() {
184
- const configuredHome = process.env.CHAPTERHOUSE_HOME?.trim();
185
- if (!configuredHome) {
186
- return join(homedir(), ".chapterhouse", "wiki");
187
- }
188
- const chapterhouseHome = configuredHome.endsWith(".chapterhouse")
189
- ? configuredHome
190
- : join(configuredHome, ".chapterhouse");
191
- return join(chapterhouseHome, "wiki");
182
+ return join(getChapterhouseHome(), "wiki");
192
183
  }
193
184
  function getOkrMemoryPath() {
194
185
  return join(getWikiDir(), ".okr-memory.json");