chapterhouse 0.1.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.
Files changed (119) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +363 -0
  3. package/agents/chapterhouse.agent.md +40 -0
  4. package/agents/coder.agent.md +38 -0
  5. package/agents/designer.agent.md +43 -0
  6. package/agents/general-purpose.agent.md +30 -0
  7. package/dist/api/auth.js +159 -0
  8. package/dist/api/auth.test.js +463 -0
  9. package/dist/api/errors.js +95 -0
  10. package/dist/api/errors.test.js +89 -0
  11. package/dist/api/rate-limit.js +85 -0
  12. package/dist/api/server-runtime.js +62 -0
  13. package/dist/api/server.js +651 -0
  14. package/dist/api/server.test.js +385 -0
  15. package/dist/api/sse.integration.test.js +270 -0
  16. package/dist/api/sse.js +7 -0
  17. package/dist/api/team.js +196 -0
  18. package/dist/api/team.test.js +466 -0
  19. package/dist/cli.js +102 -0
  20. package/dist/config.js +299 -0
  21. package/dist/config.phase3.test.js +20 -0
  22. package/dist/config.test.js +148 -0
  23. package/dist/copilot/agents.js +447 -0
  24. package/dist/copilot/agents.squad.test.js +72 -0
  25. package/dist/copilot/classifier.js +72 -0
  26. package/dist/copilot/client.js +32 -0
  27. package/dist/copilot/client.test.js +100 -0
  28. package/dist/copilot/episode-writer.js +219 -0
  29. package/dist/copilot/episode-writer.test.js +41 -0
  30. package/dist/copilot/mcp-config.js +22 -0
  31. package/dist/copilot/okr-mapper.js +196 -0
  32. package/dist/copilot/okr-mapper.test.js +114 -0
  33. package/dist/copilot/orchestrator.js +685 -0
  34. package/dist/copilot/orchestrator.test.js +523 -0
  35. package/dist/copilot/router.js +142 -0
  36. package/dist/copilot/router.test.js +119 -0
  37. package/dist/copilot/skills.js +125 -0
  38. package/dist/copilot/standup.js +138 -0
  39. package/dist/copilot/standup.test.js +132 -0
  40. package/dist/copilot/system-message.js +143 -0
  41. package/dist/copilot/system-message.test.js +17 -0
  42. package/dist/copilot/tools.js +1212 -0
  43. package/dist/copilot/tools.okr.test.js +260 -0
  44. package/dist/copilot/tools.squad.test.js +168 -0
  45. package/dist/daemon.js +235 -0
  46. package/dist/home-path.js +12 -0
  47. package/dist/home-path.test.js +11 -0
  48. package/dist/integrations/ado-analytics.js +178 -0
  49. package/dist/integrations/ado-analytics.test.js +284 -0
  50. package/dist/integrations/ado-client.js +227 -0
  51. package/dist/integrations/ado-client.test.js +176 -0
  52. package/dist/integrations/ado-schema.js +25 -0
  53. package/dist/integrations/ado-schema.test.js +39 -0
  54. package/dist/integrations/ado-skill.js +55 -0
  55. package/dist/integrations/report-generator.js +114 -0
  56. package/dist/integrations/report-generator.test.js +62 -0
  57. package/dist/integrations/team-push.js +144 -0
  58. package/dist/integrations/team-push.test.js +178 -0
  59. package/dist/integrations/teams-notify.js +108 -0
  60. package/dist/integrations/teams-notify.test.js +135 -0
  61. package/dist/paths.js +41 -0
  62. package/dist/setup.js +149 -0
  63. package/dist/shutdown-signals.js +13 -0
  64. package/dist/shutdown-signals.test.js +33 -0
  65. package/dist/squad/charter.js +108 -0
  66. package/dist/squad/charter.test.js +89 -0
  67. package/dist/squad/context.js +48 -0
  68. package/dist/squad/context.test.js +59 -0
  69. package/dist/squad/discovery.js +280 -0
  70. package/dist/squad/discovery.test.js +93 -0
  71. package/dist/squad/index.js +7 -0
  72. package/dist/squad/mirror.js +81 -0
  73. package/dist/squad/mirror.scheduler.js +78 -0
  74. package/dist/squad/mirror.scheduler.test.js +197 -0
  75. package/dist/squad/mirror.test.js +172 -0
  76. package/dist/squad/registry.js +162 -0
  77. package/dist/squad/registry.test.js +31 -0
  78. package/dist/squad/squad-coordinator-system-message.test.js +190 -0
  79. package/dist/squad/squad-session-routing.test.js +260 -0
  80. package/dist/squad/types.js +4 -0
  81. package/dist/status.js +25 -0
  82. package/dist/status.test.js +22 -0
  83. package/dist/store/db.js +290 -0
  84. package/dist/store/db.test.js +126 -0
  85. package/dist/store/squad-sessions.test.js +341 -0
  86. package/dist/test/setup-env.js +3 -0
  87. package/dist/update.js +112 -0
  88. package/dist/update.test.js +25 -0
  89. package/dist/wiki/context.js +138 -0
  90. package/dist/wiki/fs.js +195 -0
  91. package/dist/wiki/fs.test.js +39 -0
  92. package/dist/wiki/index-manager.js +359 -0
  93. package/dist/wiki/index-manager.test.js +129 -0
  94. package/dist/wiki/lock.js +26 -0
  95. package/dist/wiki/lock.test.js +30 -0
  96. package/dist/wiki/log-manager.js +20 -0
  97. package/dist/wiki/migrate.js +306 -0
  98. package/dist/wiki/okr.test.js +101 -0
  99. package/dist/wiki/path-utils.js +4 -0
  100. package/dist/wiki/path-utils.test.js +8 -0
  101. package/dist/wiki/seed-team-wiki.js +296 -0
  102. package/dist/wiki/seed-team-wiki.test.js +69 -0
  103. package/dist/wiki/team-sync.js +212 -0
  104. package/dist/wiki/team-sync.test.js +185 -0
  105. package/dist/wiki/templates/okr.js +98 -0
  106. package/package.json +72 -0
  107. package/skills/.gitkeep +0 -0
  108. package/skills/find-skills/SKILL.md +161 -0
  109. package/skills/find-skills/_meta.json +4 -0
  110. package/skills/frontend-design/LICENSE.txt +177 -0
  111. package/skills/frontend-design/SKILL.md +42 -0
  112. package/skills/squad/SKILL.md +76 -0
  113. package/web/dist/assets/index-D-e7K-fT.css +10 -0
  114. package/web/dist/assets/index-DAg9IrpO.js +142 -0
  115. package/web/dist/assets/index-DAg9IrpO.js.map +1 -0
  116. package/web/dist/chapterhouse-icon.png +0 -0
  117. package/web/dist/chapterhouse-icon.svg +42 -0
  118. package/web/dist/chapterhouse-logo.svg +46 -0
  119. package/web/dist/index.html +15 -0
@@ -0,0 +1,260 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ async function loadToolsModule() {
7
+ try {
8
+ return await import(new URL(`./tools.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ test.beforeEach(() => {
15
+ process.env.CHAPTERHOUSE_HOME = mkdtempSync(join(tmpdir(), "chapterhouse-tools-okr-"));
16
+ });
17
+ test.afterEach(async () => {
18
+ const home = process.env.CHAPTERHOUSE_HOME;
19
+ if (home) {
20
+ const dbModule = await import("../store/db.js");
21
+ dbModule.closeDb();
22
+ rmSync(home, { recursive: true, force: true });
23
+ }
24
+ });
25
+ test("createTools registers OKR progress tools", async () => {
26
+ const toolsModule = await loadToolsModule();
27
+ assert.ok(toolsModule, "tools module should exist");
28
+ const tools = toolsModule.createTools({
29
+ client: { async listModels() { return []; } },
30
+ onAgentTaskComplete: () => { },
31
+ });
32
+ assert.ok(tools.some((tool) => tool.name === "log_okr_progress"));
33
+ assert.ok(tools.some((tool) => tool.name === "get_my_okrs"));
34
+ assert.ok(tools.some((tool) => tool.name === "write_team_wiki"));
35
+ });
36
+ test("log_okr_progress suggests a KR when the user did not specify one", async () => {
37
+ const toolsModule = await loadToolsModule();
38
+ assert.ok(toolsModule, "tools module should exist");
39
+ const tools = toolsModule.createTools({
40
+ client: { async listModels() { return []; } },
41
+ onAgentTaskComplete: () => { },
42
+ createTeamPushClient: () => ({
43
+ async pushUpdate() {
44
+ throw new Error("should not push before confirmation");
45
+ },
46
+ async fetchOKRs() {
47
+ return "";
48
+ },
49
+ async writePage() {
50
+ throw new Error("should not write team wiki in this test");
51
+ },
52
+ }),
53
+ createOKRMapper: () => ({
54
+ async findMatchingKRs() {
55
+ return [
56
+ {
57
+ krId: "O1-KR2",
58
+ title: "Reduce auth service latency",
59
+ objectiveTitle: "Improve auth platform reliability",
60
+ confidence: "high",
61
+ },
62
+ ];
63
+ },
64
+ formatUpdatePrompt() {
65
+ return "Suggested prompt";
66
+ },
67
+ recordConfirmedMapping() {
68
+ throw new Error("should not record a mapping before confirmation");
69
+ },
70
+ }),
71
+ getCurrentUser: () => ({
72
+ id: "eng-1",
73
+ name: "Ada Lovelace",
74
+ email: "ada@example.com",
75
+ role: "engineer",
76
+ }),
77
+ });
78
+ const result = await tools.find((tool) => tool.name === "log_okr_progress")?.handler({
79
+ activity: "merged auth refactor PR",
80
+ });
81
+ assert.equal(result, "Suggested prompt");
82
+ });
83
+ test("log_okr_progress records a confirmed mapping after a successful push", async () => {
84
+ const toolsModule = await loadToolsModule();
85
+ assert.ok(toolsModule, "tools module should exist");
86
+ let recorded;
87
+ const tools = toolsModule.createTools({
88
+ client: { async listModels() { return []; } },
89
+ onAgentTaskComplete: () => { },
90
+ createTeamPushClient: () => ({
91
+ async pushUpdate() {
92
+ return {
93
+ ok: true,
94
+ entry: { delta: 5 },
95
+ };
96
+ },
97
+ async fetchOKRs() {
98
+ return "";
99
+ },
100
+ async writePage() {
101
+ throw new Error("should not write team wiki in this test");
102
+ },
103
+ }),
104
+ createOKRMapper: () => ({
105
+ async findMatchingKRs() {
106
+ return [];
107
+ },
108
+ formatUpdatePrompt() {
109
+ return "";
110
+ },
111
+ recordConfirmedMapping(activity, krId) {
112
+ recorded = { activity, krId };
113
+ },
114
+ }),
115
+ getCurrentUser: () => ({
116
+ id: "eng-1",
117
+ name: "Ada Lovelace",
118
+ email: "ada@example.com",
119
+ role: "engineer",
120
+ }),
121
+ });
122
+ const result = await tools.find((tool) => tool.name === "log_okr_progress")?.handler({
123
+ activity: "merged auth refactor PR",
124
+ krId: "O1-KR1",
125
+ delta: 5,
126
+ });
127
+ assert.equal(result, "Logged OKR progress for O1-KR1 (5% logged).");
128
+ assert.deepEqual(recorded, {
129
+ activity: "merged auth refactor PR",
130
+ krId: "O1-KR1",
131
+ });
132
+ });
133
+ test("get_my_okrs returns KRs owned by the current user", async () => {
134
+ const toolsModule = await loadToolsModule();
135
+ assert.ok(toolsModule, "tools module should exist");
136
+ const tools = toolsModule.createTools({
137
+ client: { async listModels() { return []; } },
138
+ onAgentTaskComplete: () => { },
139
+ createTeamPushClient: () => ({
140
+ async pushUpdate() {
141
+ throw new Error("not used in this test");
142
+ },
143
+ async fetchOKRs() {
144
+ return `# OKRs — 2026 Q2
145
+
146
+ ## O1: Improve auth platform reliability
147
+ **Owner**: Team Lead
148
+
149
+ ### O1-KR1: Reduce auth service latency
150
+ - **Owner**: Ada Lovelace
151
+ - **Target**: 100%
152
+ - **Current**: 40%
153
+ - **Unit**: %
154
+ - **Due**: 2026-06-30
155
+
156
+ ### O1-KR2: Migrate legacy auth flows
157
+ - **Owner**: Grace Hopper
158
+ - **Target**: 100%
159
+ - **Current**: 60%
160
+ - **Unit**: %
161
+ - **Due**: 2026-06-30
162
+ `;
163
+ },
164
+ async writePage() {
165
+ throw new Error("not used in this test");
166
+ },
167
+ }),
168
+ getCurrentUser: () => ({
169
+ id: "eng-1",
170
+ name: "Ada Lovelace",
171
+ email: "ada@example.com",
172
+ role: "engineer",
173
+ }),
174
+ });
175
+ const result = await tools.find((tool) => tool.name === "get_my_okrs")?.handler({ period: "2026-Q2" });
176
+ assert.match(result, /O1-KR1: Reduce auth service latency/);
177
+ assert.doesNotMatch(result, /O1-KR2: Migrate legacy auth flows/);
178
+ });
179
+ test("write_team_wiki writes shared wiki pages through the team push client", async () => {
180
+ const toolsModule = await loadToolsModule();
181
+ assert.ok(toolsModule, "tools module should exist");
182
+ const writes = [];
183
+ const tools = toolsModule.createTools({
184
+ client: { async listModels() { return []; } },
185
+ onAgentTaskComplete: () => { },
186
+ createTeamPushClient: () => ({
187
+ async pushUpdate() {
188
+ throw new Error("not used in this test");
189
+ },
190
+ async fetchOKRs() {
191
+ throw new Error("not used in this test");
192
+ },
193
+ async writePage(path, content) {
194
+ writes.push({ path, content });
195
+ return { ok: true, path };
196
+ },
197
+ }),
198
+ });
199
+ const result = await tools.find((tool) => tool.name === "write_team_wiki")?.handler({
200
+ path: "pages/shared/runbooks/deploy.md",
201
+ content: "# Deploy Runbook\n",
202
+ });
203
+ assert.equal(result, "Wrote shared team wiki page: pages/shared/runbooks/deploy.md");
204
+ assert.deepEqual(writes, [{
205
+ path: "pages/shared/runbooks/deploy.md",
206
+ content: "# Deploy Runbook\n",
207
+ }]);
208
+ });
209
+ test("write_team_wiki rejects paths outside pages/shared", async () => {
210
+ const toolsModule = await loadToolsModule();
211
+ assert.ok(toolsModule, "tools module should exist");
212
+ let writeAttempts = 0;
213
+ const tools = toolsModule.createTools({
214
+ client: { async listModels() { return []; } },
215
+ onAgentTaskComplete: () => { },
216
+ createTeamPushClient: () => ({
217
+ async pushUpdate() {
218
+ throw new Error("not used in this test");
219
+ },
220
+ async fetchOKRs() {
221
+ throw new Error("not used in this test");
222
+ },
223
+ async writePage() {
224
+ writeAttempts += 1;
225
+ return { ok: true, path: "pages/team/index.md" };
226
+ },
227
+ }),
228
+ });
229
+ const result = await tools.find((tool) => tool.name === "write_team_wiki")?.handler({
230
+ path: "pages/team/index.md",
231
+ content: "# Team Directory\n",
232
+ });
233
+ assert.match(result, /must start with "pages\/shared\/"/i);
234
+ assert.equal(writeAttempts, 0);
235
+ });
236
+ test("generate_okr_report is available with ADO_PAT and returns the generated narrative", async () => {
237
+ process.env.ADO_PAT = "test-pat";
238
+ try {
239
+ const toolsModule = await loadToolsModule();
240
+ assert.ok(toolsModule, "tools module should exist");
241
+ const tools = toolsModule.createTools({
242
+ client: { async listModels() { return []; } },
243
+ onAgentTaskComplete: () => { },
244
+ createReportGenerator: () => ({
245
+ async generateMonthlyReport(period) {
246
+ assert.equal(period, "2026-Q2");
247
+ return "Executive summary goes here.";
248
+ },
249
+ }),
250
+ });
251
+ const tool = tools.find((entry) => entry.name === "generate_okr_report");
252
+ assert.ok(tool, "generate_okr_report should be registered");
253
+ const result = await tool.handler({ period: "2026-Q2" });
254
+ assert.equal(result, "Executive summary goes here.");
255
+ }
256
+ finally {
257
+ delete process.env.ADO_PAT;
258
+ }
259
+ });
260
+ //# sourceMappingURL=tools.okr.test.js.map
@@ -0,0 +1,168 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ const fixtureProjectRoot = `${process.cwd()}/src/test/fixtures/mock-squad-repo`;
4
+ async function loadToolsModule(t, state) {
5
+ const fakeSession = {
6
+ on() {
7
+ return () => { };
8
+ },
9
+ async sendAndWait() {
10
+ return { data: { content: "Squad result" } };
11
+ },
12
+ async destroy() { },
13
+ };
14
+ t.mock.module("../config.js", {
15
+ namedExports: {
16
+ config: {
17
+ workerTimeoutMs: 1_000,
18
+ chapterhouseMode: "personal",
19
+ adoPat: "",
20
+ squadEnabled: true,
21
+ copilotModel: "claude-sonnet-4.6",
22
+ },
23
+ persistModel: () => { },
24
+ },
25
+ });
26
+ t.mock.module("./orchestrator.js", {
27
+ namedExports: {
28
+ getCurrentSourceChannel: () => "web",
29
+ getCurrentActivityCallback: () => undefined,
30
+ getCurrentAuthenticatedUser: () => undefined,
31
+ getLastAuthenticatedUser: () => undefined,
32
+ getCurrentAuthorizationHeader: () => undefined,
33
+ getCurrentChannelKey: () => "conn-squad",
34
+ getCurrentSessionKey: () => "default",
35
+ switchSessionModel: async () => { },
36
+ },
37
+ });
38
+ t.mock.module("./agents.js", {
39
+ namedExports: {
40
+ getAgentRegistry: () => [{ slug: "coder" }, { slug: "designer" }],
41
+ getAgent: () => undefined,
42
+ createEphemeralAgentSession: async () => {
43
+ throw new Error("should not create a Chapterhouse registry agent session");
44
+ },
45
+ createSquadAgentSession: async (slug, _client, allTools, systemMessagePrefix, modelOverride) => {
46
+ state.createSquadSessionCalls.push({
47
+ slug,
48
+ toolNames: allTools.map((tool) => tool.name),
49
+ systemMessagePrefix,
50
+ modelOverride,
51
+ });
52
+ return fakeSession;
53
+ },
54
+ getAgentSessionStatus: () => ({ taskCount: 0, tasks: [] }),
55
+ getActiveTasks: () => [],
56
+ getTask: () => undefined,
57
+ registerTask: (agentSlug, description, originChannel) => ({
58
+ taskId: "task-squad-1",
59
+ agentSlug,
60
+ description,
61
+ status: "running",
62
+ startedAt: Date.now(),
63
+ originChannel,
64
+ }),
65
+ completeTask: () => { },
66
+ failTask: () => { },
67
+ createAgentFile: () => null,
68
+ removeAgentFile: () => null,
69
+ loadAgents: () => [],
70
+ },
71
+ });
72
+ t.mock.module("../squad/context.js", {
73
+ namedExports: {
74
+ getChannelProject: () => fixtureProjectRoot,
75
+ },
76
+ });
77
+ t.mock.module("../squad/registry.js", {
78
+ namedExports: {
79
+ findSquadAgent: (_projectRoot, mention) => mention === "ripley"
80
+ ? {
81
+ slug: "ripley",
82
+ mention: "@ripley",
83
+ role: "Lead",
84
+ charterPath: `${fixtureProjectRoot}/.squad/agents/ripley/charter.md`,
85
+ origin: "project-squad",
86
+ }
87
+ : null,
88
+ },
89
+ });
90
+ t.mock.module("../squad/charter.js", {
91
+ namedExports: {
92
+ buildSquadSystemPrefix: (context, descriptor) => {
93
+ state.charterCalls.push({ projectRoot: context.projectRoot, slug: descriptor.slug });
94
+ return "SQUAD SYSTEM PREFIX";
95
+ },
96
+ },
97
+ });
98
+ t.mock.module("../squad/mirror.js", {
99
+ namedExports: {
100
+ mirrorDecisionToWiki: async (link, taskSummary, resultSummary) => {
101
+ state.mirrorCalls.push({ link, taskSummary, resultSummary });
102
+ return "pages/projects/mock-squad-repo/decisions.md";
103
+ },
104
+ syncDecisionsFileToWiki: async (_projectRoot) => null,
105
+ },
106
+ });
107
+ t.mock.module("../store/db.js", {
108
+ namedExports: {
109
+ getDb: () => ({
110
+ prepare: (sql) => ({
111
+ run: (...args) => {
112
+ state.dbWrites.push({ sql, args });
113
+ return {};
114
+ },
115
+ get: () => undefined,
116
+ all: () => [],
117
+ }),
118
+ }),
119
+ getState: () => undefined,
120
+ setState: () => { },
121
+ logConversation: () => { },
122
+ },
123
+ });
124
+ t.mock.module("../wiki/team-sync.js", {
125
+ namedExports: {
126
+ readWikiPage: async () => undefined,
127
+ teamWikiSync: {
128
+ isEnabled: () => false,
129
+ syncAll: async () => { },
130
+ },
131
+ },
132
+ });
133
+ return await import(new URL(`./tools.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
134
+ }
135
+ test("delegate_to_agent routes squad virtual agents with charter injection and mirrors results", async (t) => {
136
+ const state = {
137
+ taskCompleteCalls: [],
138
+ createSquadSessionCalls: [],
139
+ charterCalls: [],
140
+ mirrorCalls: [],
141
+ dbWrites: [],
142
+ };
143
+ const toolsModule = await loadToolsModule(t, state);
144
+ const tools = toolsModule.createTools({
145
+ client: {},
146
+ onAgentTaskComplete: (taskId, agentSlug, result) => {
147
+ state.taskCompleteCalls.push({ taskId, agentSlug, result });
148
+ },
149
+ });
150
+ const delegateTool = tools.find((tool) => tool.name === "delegate_to_agent");
151
+ assert.ok(delegateTool, "delegate_to_agent should exist");
152
+ const result = await delegateTool.handler({
153
+ agent_name: "ripley",
154
+ task: "Audit the squad routing",
155
+ summary: "Audit squad routing",
156
+ }, {});
157
+ await new Promise((resolve) => setTimeout(resolve, 0));
158
+ assert.match(String(result), /@ripley/);
159
+ assert.equal(state.createSquadSessionCalls.length, 1);
160
+ assert.deepEqual(state.charterCalls, [{ projectRoot: fixtureProjectRoot, slug: "ripley" }]);
161
+ assert.equal(state.taskCompleteCalls[0]?.result, "Squad result");
162
+ assert.equal(state.mirrorCalls.length, 1);
163
+ assert.equal(state.mirrorCalls[0]?.taskSummary, "Audit squad routing");
164
+ assert.equal(state.mirrorCalls[0]?.resultSummary, "Squad result");
165
+ assert.ok(state.dbWrites.some((write) => write.sql.includes("INSERT INTO agent_tasks")));
166
+ assert.ok(state.dbWrites.some((write) => write.sql.includes("squad_task_links")));
167
+ });
168
+ //# sourceMappingURL=tools.squad.test.js.map
package/dist/daemon.js ADDED
@@ -0,0 +1,235 @@
1
+ import { getClient, stopClient } from "./copilot/client.js";
2
+ import { initOrchestrator, setMessageLogger, setProactiveNotify, getAgentInfo, shutdownAgents } from "./copilot/orchestrator.js";
3
+ import { stopEpisodeWriter } from "./copilot/episode-writer.js";
4
+ import { startApiServer, broadcastToSSE } from "./api/server.js";
5
+ import { getDb, closeDb, getState } from "./store/db.js";
6
+ import { config } from "./config.js";
7
+ import { spawn } from "child_process";
8
+ import { readdirSync, statSync, rmSync } from "fs";
9
+ import { join } from "path";
10
+ import { checkForUpdate } from "./update.js";
11
+ import { ensureWikiStructure } from "./wiki/fs.js";
12
+ import { seedTeamWiki } from "./wiki/seed-team-wiki.js";
13
+ import { shouldMigrate, migrateMemoriesToWiki, shouldReorganize, reorganizeWiki } from "./wiki/migrate.js";
14
+ import { SESSIONS_DIR } from "./paths.js";
15
+ import { getDisplayHost } from "./api/server-runtime.js";
16
+ import { StandupScheduler } from "./copilot/standup.js";
17
+ import { DecisionsSyncScheduler } from "./squad/mirror.scheduler.js";
18
+ import { registerShutdownSignals } from "./shutdown-signals.js";
19
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
20
+ /** Remove orphaned session folders older than 7 days, preserving the current session. */
21
+ function pruneOldSessions() {
22
+ try {
23
+ const sessionStateDir = join(SESSIONS_DIR, "session-state");
24
+ const currentSessionId = getState("orchestrator_session_id");
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(sessionStateDir);
28
+ }
29
+ catch {
30
+ return; // directory may not exist yet
31
+ }
32
+ const cutoff = Date.now() - SEVEN_DAYS_MS;
33
+ let pruned = 0;
34
+ for (const entry of entries) {
35
+ if (entry === currentSessionId)
36
+ continue;
37
+ const fullPath = join(sessionStateDir, entry);
38
+ try {
39
+ const stat = statSync(fullPath);
40
+ if (stat.isDirectory() && stat.mtimeMs < cutoff) {
41
+ rmSync(fullPath, { recursive: true, force: true });
42
+ pruned++;
43
+ }
44
+ }
45
+ catch {
46
+ // skip entries we can't stat or remove
47
+ }
48
+ }
49
+ if (pruned > 0) {
50
+ console.log(`[chapterhouse] Pruned ${pruned} orphaned session folder(s)`);
51
+ }
52
+ }
53
+ catch (err) {
54
+ console.error("[chapterhouse] Session pruning failed (non-fatal):", err instanceof Error ? err.message : err);
55
+ }
56
+ }
57
+ function truncate(text, max = 200) {
58
+ const oneLine = text.replace(/\n/g, " ").trim();
59
+ return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
60
+ }
61
+ async function main() {
62
+ console.log("[chapterhouse] Starting Chapterhouse daemon...");
63
+ if (config.selfEditEnabled) {
64
+ console.log("[chapterhouse] ⚠ Self-edit mode enabled — Chapterhouse can modify his own source code");
65
+ }
66
+ // Set up message logging to daemon console
67
+ setMessageLogger((direction, source, text) => {
68
+ const arrow = direction === "in" ? "⟶" : "⟵";
69
+ const tag = source.padEnd(8);
70
+ console.log(`[chapterhouse] ${tag} ${arrow} ${truncate(text)}`);
71
+ });
72
+ // Initialize SQLite
73
+ getDb();
74
+ console.log("[chapterhouse] Database initialized");
75
+ // Initialize wiki knowledge base
76
+ const wikiIsNew = ensureWikiStructure();
77
+ if (wikiIsNew) {
78
+ console.log("[chapterhouse] Created wiki at ~/.chapterhouse/wiki/");
79
+ }
80
+ if (config.chapterhouseMode === "team") {
81
+ const seed = seedTeamWiki();
82
+ if (seed.created.length > 0) {
83
+ console.log(`[chapterhouse] Seeded ${seed.created.length} team wiki page(s): ${seed.created.join(", ")}`);
84
+ }
85
+ }
86
+ if (shouldMigrate()) {
87
+ console.log("[chapterhouse] Migrating SQLite memories to wiki...");
88
+ const count = migrateMemoriesToWiki();
89
+ console.log(`[chapterhouse] Migrated ${count} memories to wiki`);
90
+ }
91
+ if (shouldReorganize()) {
92
+ console.log("[chapterhouse] Reorganizing wiki pages into entity structure...");
93
+ const count = reorganizeWiki();
94
+ console.log(`[chapterhouse] Created ${count} entity pages`);
95
+ }
96
+ // Prune orphaned session folders older than 7 days
97
+ pruneOldSessions();
98
+ // One-time deprecation note for legacy Telegram users (v1 → v2)
99
+ if (process.env.TELEGRAM_BOT_TOKEN) {
100
+ console.log("[chapterhouse] ℹ TELEGRAM_BOT_TOKEN found in env — Telegram support was removed in v2. The web UI is now the only client. You can delete this line from ~/.chapterhouse/.env.");
101
+ }
102
+ // Start Copilot SDK client
103
+ console.log("[chapterhouse] Starting Copilot SDK client...");
104
+ const client = await getClient();
105
+ console.log("[chapterhouse] Copilot SDK client ready");
106
+ // Initialize orchestrator session
107
+ console.log("[chapterhouse] Creating orchestrator session...");
108
+ await initOrchestrator(client);
109
+ console.log("[chapterhouse] Orchestrator session ready");
110
+ // Wire up proactive notifications — broadcast to all connected web clients
111
+ setProactiveNotify((text) => {
112
+ console.log(`[chapterhouse] bg-notify ⟵ ${truncate(text)}`);
113
+ broadcastToSSE(text);
114
+ });
115
+ // Start HTTP API + serve the web UI
116
+ await startApiServer();
117
+ if (config.chapterhouseMode === "personal" && (config.adoPat || config.teamChapterhouseUrl)) {
118
+ new StandupScheduler().schedule();
119
+ }
120
+ // Start periodic decisions→wiki sync for all registered projects
121
+ decisionsSyncScheduler = new DecisionsSyncScheduler();
122
+ decisionsSyncScheduler.start();
123
+ const url = `http://${getDisplayHost(config.apiHost)}:${config.apiPort}`;
124
+ console.log(`[chapterhouse] Chapterhouse is fully operational — open ${url} in your browser`);
125
+ if (process.env.CHAPTERHOUSE_OPEN_BROWSER === "1") {
126
+ const opener = process.platform === "darwin" ? "open" :
127
+ process.platform === "win32" ? "explorer.exe" : "xdg-open";
128
+ try {
129
+ spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
130
+ }
131
+ catch (err) {
132
+ console.error(`[chapterhouse] Could not open browser:`, err instanceof Error ? err.message : err);
133
+ }
134
+ }
135
+ // Non-blocking update check
136
+ checkForUpdate()
137
+ .then(({ updateAvailable, current, latest }) => {
138
+ if (updateAvailable) {
139
+ console.log(`[chapterhouse] ⬆ Update available: v${current} → v${latest} — run 'chapterhouse update' to install`);
140
+ }
141
+ })
142
+ .catch(() => { }); // silent — network may be unavailable
143
+ if (process.env.CHAPTERHOUSE_RESTARTED === "1") {
144
+ delete process.env.CHAPTERHOUSE_RESTARTED;
145
+ }
146
+ }
147
+ // Graceful shutdown
148
+ let shutdownState = "idle";
149
+ let decisionsSyncScheduler;
150
+ async function shutdown() {
151
+ if (shutdownState === "shutting_down") {
152
+ console.log("\n[chapterhouse] Forced exit.");
153
+ process.exit(1);
154
+ }
155
+ // Check for running workers before shutting down
156
+ const workers = getAgentInfo();
157
+ if (workers.length > 0 && shutdownState === "idle") {
158
+ const names = workers.map(w => `@${w.slug}`).join(", ");
159
+ console.log(`\n[chapterhouse] ⚠ ${workers.length} running worker(s) will be stopped: ${names}`);
160
+ console.log("[chapterhouse] Press Ctrl+C again to shut down, or wait for workers to finish.");
161
+ shutdownState = "warned";
162
+ return;
163
+ }
164
+ shutdownState = "shutting_down";
165
+ console.log("\n[chapterhouse] Shutting down... (Ctrl+C again to force)");
166
+ // Force exit after 3 seconds no matter what
167
+ const forceTimer = setTimeout(() => {
168
+ console.log("[chapterhouse] Shutdown timed out — forcing exit.");
169
+ process.exit(1);
170
+ }, 3000);
171
+ forceTimer.unref();
172
+ // Destroy all active agent sessions
173
+ await shutdownAgents();
174
+ try {
175
+ decisionsSyncScheduler?.stop();
176
+ }
177
+ catch { /* best effort */ }
178
+ try {
179
+ stopEpisodeWriter();
180
+ }
181
+ catch { /* best effort */ }
182
+ try {
183
+ await stopClient();
184
+ }
185
+ catch { /* best effort */ }
186
+ closeDb();
187
+ console.log("[chapterhouse] Goodbye.");
188
+ process.exit(0);
189
+ }
190
+ /** Restart the daemon by spawning a new process and exiting. */
191
+ export async function restartDaemon() {
192
+ console.log("[chapterhouse] Restarting...");
193
+ const workers = getAgentInfo();
194
+ if (workers.length > 0) {
195
+ console.log(`[chapterhouse] ⚠ Stopping ${workers.length} running worker(s) for restart`);
196
+ }
197
+ // Destroy all active agent sessions
198
+ await shutdownAgents();
199
+ try {
200
+ decisionsSyncScheduler?.stop();
201
+ }
202
+ catch { /* best effort */ }
203
+ try {
204
+ stopEpisodeWriter();
205
+ }
206
+ catch { /* best effort */ }
207
+ try {
208
+ await stopClient();
209
+ }
210
+ catch { /* best effort */ }
211
+ closeDb();
212
+ // Spawn a detached replacement process with the same args (include execArgv for tsx/loaders)
213
+ const child = spawn(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
214
+ detached: true,
215
+ stdio: "inherit",
216
+ env: { ...process.env, CHAPTERHOUSE_RESTARTED: "1" },
217
+ });
218
+ child.unref();
219
+ console.log("[chapterhouse] New process spawned. Exiting old process.");
220
+ process.exit(0);
221
+ }
222
+ registerShutdownSignals(process, shutdown);
223
+ // Prevent unhandled errors from crashing the daemon
224
+ process.on("unhandledRejection", (reason) => {
225
+ console.error("[chapterhouse] Unhandled rejection (kept alive):", reason);
226
+ });
227
+ process.on("uncaughtException", (err) => {
228
+ console.error("[chapterhouse] Uncaught exception — shutting down:", err);
229
+ process.exit(1);
230
+ });
231
+ main().catch((err) => {
232
+ console.error("[chapterhouse] Fatal error:", err);
233
+ process.exit(1);
234
+ });
235
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,12 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ export function getChapterhouseHomeDisplayPath() {
4
+ return join(homedir(), ".chapterhouse");
5
+ }
6
+ export function getChapterhouseEnvDisplayPath() {
7
+ return join(getChapterhouseHomeDisplayPath(), ".env");
8
+ }
9
+ export function getExampleProjectPath() {
10
+ return join(homedir(), "dev", "myapp");
11
+ }
12
+ //# sourceMappingURL=home-path.js.map
@@ -0,0 +1,11 @@
1
+ import assert from "node:assert/strict";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { getChapterhouseHomeDisplayPath, getChapterhouseEnvDisplayPath, getExampleProjectPath, } from "./home-path.js";
6
+ test("home-path helpers build cross-platform display paths", () => {
7
+ assert.equal(getChapterhouseHomeDisplayPath(), join(homedir(), ".chapterhouse"));
8
+ assert.equal(getChapterhouseEnvDisplayPath(), join(homedir(), ".chapterhouse", ".env"));
9
+ assert.equal(getExampleProjectPath(), join(homedir(), "dev", "myapp"));
10
+ });
11
+ //# sourceMappingURL=home-path.test.js.map