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,119 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ async function loadRouterModule(t, options = {}) {
4
+ const state = new Map();
5
+ if (options.storedConfig !== undefined) {
6
+ state.set("router_config", options.storedConfig);
7
+ }
8
+ t.mock.module("../store/db.js", {
9
+ namedExports: {
10
+ getState: (key) => state.get(key),
11
+ setState: (key, value) => {
12
+ state.set(key, value);
13
+ },
14
+ },
15
+ });
16
+ t.mock.module("./classifier.js", {
17
+ namedExports: {
18
+ classifyWithLLM: async (_client, prompt) => await (options.classify?.(prompt) ?? null),
19
+ },
20
+ });
21
+ const router = await import(new URL(`./router.js?case=${Date.now()}-${Math.random()}`, import.meta.url).href);
22
+ return { router, state };
23
+ }
24
+ test("router config falls back safely and deep-merges tier model updates", async (t) => {
25
+ const { router, state } = await loadRouterModule(t, {
26
+ storedConfig: "{not-json}",
27
+ });
28
+ assert.deepEqual(router.getRouterConfig(), {
29
+ enabled: false,
30
+ tierModels: {
31
+ fast: "gpt-4.1",
32
+ standard: "claude-sonnet-4.6",
33
+ premium: "claude-opus-4.6",
34
+ },
35
+ overrides: [
36
+ {
37
+ name: "design",
38
+ keywords: [
39
+ "design", "ui", "ux", "css", "layout", "styling", "visual",
40
+ "mockup", "wireframe", "frontend design", "tailwind", "responsive",
41
+ ],
42
+ model: "claude-opus-4.6",
43
+ },
44
+ ],
45
+ cooldownMessages: 2,
46
+ });
47
+ const updated = router.updateRouterConfig({
48
+ enabled: true,
49
+ tierModels: {
50
+ ...router.getRouterConfig().tierModels,
51
+ premium: "gpt-5.5",
52
+ },
53
+ });
54
+ assert.equal(updated.enabled, true);
55
+ assert.deepEqual(updated.tierModels, {
56
+ fast: "gpt-4.1",
57
+ standard: "claude-sonnet-4.6",
58
+ premium: "gpt-5.5",
59
+ });
60
+ assert.equal(JSON.parse(state.get("router_config") || "{}").tierModels.premium, "gpt-5.5");
61
+ });
62
+ test("resolveModel stays in manual mode when the router is disabled", async (t) => {
63
+ const { router } = await loadRouterModule(t);
64
+ const result = await router.resolveModel("Ship it", "claude-sonnet-4.6", []);
65
+ assert.deepEqual(result, {
66
+ model: "claude-sonnet-4.6",
67
+ tier: null,
68
+ switched: false,
69
+ routerMode: "manual",
70
+ });
71
+ });
72
+ test("resolveModel applies overrides by whole word and ignores partial-word matches", async (t) => {
73
+ const { router } = await loadRouterModule(t, {
74
+ classify: async () => "fast",
75
+ });
76
+ router.updateRouterConfig({ enabled: true });
77
+ const override = await router.resolveModel("Need a UI refresh", "gpt-4.1", [], {});
78
+ const noOverride = await router.resolveModel("Fruit salad", "gpt-4.1", [], {});
79
+ assert.equal(override.overrideName, "design");
80
+ assert.equal(override.model, "claude-opus-4.6");
81
+ assert.equal(override.switched, true);
82
+ assert.equal(noOverride.overrideName, undefined);
83
+ assert.equal(noOverride.model, "gpt-4.1");
84
+ assert.equal(noOverride.tier, "fast");
85
+ });
86
+ test("short follow-ups inherit the previous tier", async (t) => {
87
+ const { router } = await loadRouterModule(t);
88
+ router.updateRouterConfig({ enabled: true });
89
+ const result = await router.resolveModel("yes", "claude-sonnet-4.6", ["premium"]);
90
+ assert.deepEqual(result, {
91
+ model: "claude-opus-4.6",
92
+ tier: "premium",
93
+ switched: true,
94
+ routerMode: "auto",
95
+ });
96
+ });
97
+ test("cooldown prevents rapid model flapping until enough messages pass", async (t) => {
98
+ const responses = ["premium", "fast", "fast", "fast"];
99
+ const { router } = await loadRouterModule(t, {
100
+ classify: async () => responses.shift() || "standard",
101
+ });
102
+ router.updateRouterConfig({
103
+ enabled: true,
104
+ cooldownMessages: 2,
105
+ });
106
+ const first = await router.resolveModel("Deep architecture review", "gpt-4.1", [], {});
107
+ const second = await router.resolveModel("thanks", "claude-opus-4.6", [], {});
108
+ const third = await router.resolveModel("thanks again", "claude-opus-4.6", [], {});
109
+ const fourth = await router.resolveModel("one more follow-up", "claude-opus-4.6", [], {});
110
+ assert.equal(first.model, "claude-opus-4.6");
111
+ assert.equal(first.switched, true);
112
+ assert.equal(second.model, "claude-opus-4.6");
113
+ assert.equal(second.switched, false);
114
+ assert.equal(third.model, "claude-opus-4.6");
115
+ assert.equal(third.switched, false);
116
+ assert.equal(fourth.model, "gpt-4.1");
117
+ assert.equal(fourth.switched, true);
118
+ });
119
+ //# sourceMappingURL=router.test.js.map
@@ -0,0 +1,125 @@
1
+ import { readdirSync, readFileSync, mkdirSync, writeFileSync, existsSync, rmSync } from "fs";
2
+ import { join, dirname, sep } from "path";
3
+ import { homedir } from "os";
4
+ import { fileURLToPath } from "url";
5
+ import { SKILLS_DIR } from "../paths.js";
6
+ /** User-local skills directory (~/.chapterhouse/skills/) */
7
+ const LOCAL_SKILLS_DIR = SKILLS_DIR;
8
+ /** Global shared skills directory */
9
+ const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
10
+ /** Skills bundled with the Chapterhouse package (e.g. find-skills) */
11
+ const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
12
+ /** Returns all skill directories that exist on disk. */
13
+ export function getSkillDirectories() {
14
+ const dirs = [];
15
+ if (existsSync(BUNDLED_SKILLS_DIR))
16
+ dirs.push(BUNDLED_SKILLS_DIR);
17
+ if (existsSync(LOCAL_SKILLS_DIR))
18
+ dirs.push(LOCAL_SKILLS_DIR);
19
+ if (existsSync(GLOBAL_SKILLS_DIR))
20
+ dirs.push(GLOBAL_SKILLS_DIR);
21
+ return dirs;
22
+ }
23
+ /** Scan all skill directories and return metadata for each skill found. */
24
+ export function listSkills() {
25
+ const skills = [];
26
+ for (const [dir, source] of [
27
+ [BUNDLED_SKILLS_DIR, "bundled"],
28
+ [LOCAL_SKILLS_DIR, "local"],
29
+ [GLOBAL_SKILLS_DIR, "global"],
30
+ ]) {
31
+ if (!existsSync(dir))
32
+ continue;
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(dir);
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ for (const entry of entries) {
41
+ const skillDir = join(dir, entry);
42
+ const skillMd = join(skillDir, "SKILL.md");
43
+ if (!existsSync(skillMd))
44
+ continue;
45
+ try {
46
+ const content = readFileSync(skillMd, "utf-8");
47
+ const { name, description } = parseFrontmatter(content);
48
+ skills.push({
49
+ slug: entry,
50
+ name: name || entry,
51
+ description: description || "(no description)",
52
+ directory: skillDir,
53
+ source,
54
+ });
55
+ }
56
+ catch {
57
+ skills.push({
58
+ slug: entry,
59
+ name: entry,
60
+ description: "(could not read SKILL.md)",
61
+ directory: skillDir,
62
+ source,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ return skills;
68
+ }
69
+ /** Create a new skill in the local skills directory. */
70
+ export function createSkill(slug, name, description, instructions) {
71
+ const skillDir = join(LOCAL_SKILLS_DIR, slug);
72
+ // Guard against path traversal
73
+ if (!skillDir.startsWith(LOCAL_SKILLS_DIR + sep)) {
74
+ return `Invalid slug '${slug}': must be a simple kebab-case name without path separators.`;
75
+ }
76
+ if (existsSync(skillDir)) {
77
+ return `Skill '${slug}' already exists at ${skillDir}. Edit it directly or delete it first.`;
78
+ }
79
+ mkdirSync(skillDir, { recursive: true });
80
+ writeFileSync(join(skillDir, "_meta.json"), JSON.stringify({ slug, version: "1.0.0" }, null, 2) + "\n");
81
+ const skillMd = `---
82
+ name: ${name}
83
+ description: ${description}
84
+ ---
85
+
86
+ ${instructions}
87
+ `;
88
+ writeFileSync(join(skillDir, "SKILL.md"), skillMd);
89
+ return `Skill '${name}' created at ${skillDir}. It will be available on your next message.`;
90
+ }
91
+ /** Remove a skill from the local skills directory (~/.chapterhouse/skills/). */
92
+ export function removeSkill(slug) {
93
+ const skillDir = join(LOCAL_SKILLS_DIR, slug);
94
+ // Guard against path traversal
95
+ if (!skillDir.startsWith(LOCAL_SKILLS_DIR + sep)) {
96
+ return { ok: false, message: `Invalid slug '${slug}': must be a simple kebab-case name without path separators.` };
97
+ }
98
+ if (!existsSync(skillDir)) {
99
+ return { ok: false, message: `Skill '${slug}' not found in ${LOCAL_SKILLS_DIR}.` };
100
+ }
101
+ rmSync(skillDir, { recursive: true, force: true });
102
+ return { ok: true, message: `Skill '${slug}' removed from ${skillDir}. It will no longer be available on your next message.` };
103
+ }
104
+ /** Parse YAML frontmatter from a SKILL.md file. */
105
+ function parseFrontmatter(content) {
106
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
107
+ if (!match)
108
+ return { name: "", description: "" };
109
+ const frontmatter = match[1];
110
+ let name = "";
111
+ let description = "";
112
+ for (const line of frontmatter.split("\n")) {
113
+ const idx = line.indexOf(": ");
114
+ if (idx <= 0)
115
+ continue;
116
+ const key = line.slice(0, idx).trim();
117
+ const value = line.slice(idx + 2).trim();
118
+ if (key === "name")
119
+ name = value;
120
+ if (key === "description")
121
+ description = value;
122
+ }
123
+ return { name, description };
124
+ }
125
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1,138 @@
1
+ import { config } from "../config.js";
2
+ import { sendToOrchestrator, getCurrentAuthorizationHeader, getCurrentAuthenticatedUser, getLastAuthenticatedUser, } from "./orchestrator.js";
3
+ import { TeamPushClient } from "../integrations/team-push.js";
4
+ import { getMyOkrsSummary } from "./tools.js";
5
+ import { readPage, writePage } from "../wiki/fs.js";
6
+ const DAY_MS = 24 * 60 * 60 * 1000;
7
+ export class StandupScheduler {
8
+ getMyOkrs;
9
+ sendSystemMessage;
10
+ readStandupPage;
11
+ writeStandupPage;
12
+ now;
13
+ standupTime;
14
+ setTimeoutImpl;
15
+ setIntervalImpl;
16
+ timeoutHandle;
17
+ intervalHandle;
18
+ constructor(options = {}) {
19
+ this.now = options.now ?? (() => new Date());
20
+ this.standupTime = options.standupTime ?? config.standupTime;
21
+ this.getMyOkrs = options.getMyOkrs ?? defaultGetMyOkrs;
22
+ this.sendSystemMessage = options.sendSystemMessage ?? defaultSendSystemMessage;
23
+ this.readStandupPage = options.readStandupPage ?? readPage;
24
+ this.writeStandupPage = options.writeStandupPage ?? writePage;
25
+ this.setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
26
+ this.setIntervalImpl = options.setIntervalImpl ?? setInterval;
27
+ }
28
+ schedule() {
29
+ const now = this.now();
30
+ if (shouldFireMissedStandup(now, this.standupTime, this.readStandupPage)) {
31
+ void this.sendStandupPrompt();
32
+ }
33
+ const delayMs = getDelayUntilNextStandup(now, this.standupTime);
34
+ this.timeoutHandle = this.setTimeoutImpl(() => {
35
+ void this.sendStandupPrompt();
36
+ this.intervalHandle = this.setIntervalImpl(() => {
37
+ if (!isWeekday(this.now())) {
38
+ return;
39
+ }
40
+ void this.sendStandupPrompt();
41
+ }, DAY_MS);
42
+ this.intervalHandle?.unref?.();
43
+ }, delayMs);
44
+ this.timeoutHandle?.unref?.();
45
+ }
46
+ async sendStandupPrompt() {
47
+ const now = this.now();
48
+ if (!isWeekday(now)) {
49
+ return;
50
+ }
51
+ const period = getQuarter(now);
52
+ const okrSummary = await this.getMyOkrs(period);
53
+ const prompt = [
54
+ `Good morning! Here's your OKR ownership for ${period}:`,
55
+ okrSummary,
56
+ "",
57
+ "What did you work on yesterday? What are you focusing on today?",
58
+ "(I'll help you log any progress against your key results)",
59
+ ].join("\n");
60
+ this.writeStandupPage(`pages/standups/${formatDate(now)}.md`, `${prompt}\n`);
61
+ await this.sendSystemMessage(prompt);
62
+ }
63
+ }
64
+ function getDelayUntilNextStandup(now, standupTime) {
65
+ const target = getNextStandupDate(now, standupTime);
66
+ return Math.max(0, target.getTime() - now.getTime());
67
+ }
68
+ function shouldFireMissedStandup(now, standupTime, readStandupPage) {
69
+ if (!isWeekday(now) || !isPastStandupTime(now, standupTime)) {
70
+ return false;
71
+ }
72
+ const standupPage = readStandupPage(`pages/standups/${formatDate(now)}.md`);
73
+ return !standupPage?.trim();
74
+ }
75
+ function getNextStandupDate(now, standupTime) {
76
+ const [hoursText, minutesText] = standupTime.split(":");
77
+ const hours = Number(hoursText);
78
+ const minutes = Number(minutesText);
79
+ const next = new Date(now);
80
+ next.setHours(hours, minutes, 0, 0);
81
+ while (!isWeekday(next) || next.getTime() <= now.getTime()) {
82
+ next.setDate(next.getDate() + 1);
83
+ next.setHours(hours, minutes, 0, 0);
84
+ }
85
+ return next;
86
+ }
87
+ function isPastStandupTime(now, standupTime) {
88
+ const [hoursText, minutesText] = standupTime.split(":");
89
+ const hours = Number(hoursText);
90
+ const minutes = Number(minutesText);
91
+ const standupDate = new Date(now);
92
+ standupDate.setHours(hours, minutes, 0, 0);
93
+ return now.getTime() > standupDate.getTime();
94
+ }
95
+ function isWeekday(now) {
96
+ const day = now.getDay();
97
+ return day !== 0 && day !== 6;
98
+ }
99
+ function getQuarter(now) {
100
+ return `${now.getFullYear()}-Q${Math.floor(now.getMonth() / 3) + 1}`;
101
+ }
102
+ function formatDate(now) {
103
+ const year = now.getFullYear();
104
+ const month = String(now.getMonth() + 1).padStart(2, "0");
105
+ const day = String(now.getDate()).padStart(2, "0");
106
+ return `${year}-${month}-${day}`;
107
+ }
108
+ async function defaultGetMyOkrs(period) {
109
+ const getCurrentUser = () => getCurrentAuthenticatedUser() ?? getLastAuthenticatedUser();
110
+ return await getMyOkrsSummary({
111
+ createTeamPushClient: () => new TeamPushClient({
112
+ getAuthorizationHeader: getCurrentAuthorizationHeader,
113
+ getCurrentUser,
114
+ }),
115
+ getCurrentUser,
116
+ period,
117
+ });
118
+ }
119
+ async function defaultSendSystemMessage(prompt) {
120
+ await new Promise((resolve, reject) => {
121
+ void sendToOrchestrator([
122
+ "Send the following standup prompt to the user exactly as written.",
123
+ "Do not add commentary, explanation, or markdown fences.",
124
+ "",
125
+ prompt,
126
+ ].join("\n"), { type: "background" }, (text, done) => {
127
+ if (!done) {
128
+ return;
129
+ }
130
+ if (text.startsWith("Error: ")) {
131
+ reject(new Error(text.slice("Error: ".length).trim() || text));
132
+ return;
133
+ }
134
+ resolve();
135
+ });
136
+ });
137
+ }
138
+ //# sourceMappingURL=standup.js.map
@@ -0,0 +1,132 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ async function loadStandupModule() {
4
+ try {
5
+ return await import(new URL(`./standup.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ test("sendStandupPrompt builds the morning prompt with owned KR progress", async () => {
12
+ const standupModule = await loadStandupModule();
13
+ assert.ok(standupModule, "standup module should exist");
14
+ let sentPrompt = "";
15
+ const scheduler = new standupModule.StandupScheduler({
16
+ now: () => new Date("2026-05-06T08:45:00"),
17
+ getMyOkrs: async () => [
18
+ "Current OKRs for Ada Lovelace:",
19
+ "• O1-KR1: Reduce auth service latency (Improve auth platform reliability) — 40/100 %",
20
+ "• O2-KR2: Cut flaky builds (Build confidence) — 3/5 runs",
21
+ ].join("\n"),
22
+ sendSystemMessage: async (prompt) => {
23
+ sentPrompt = prompt;
24
+ },
25
+ writeStandupPage: () => { },
26
+ });
27
+ await scheduler.sendStandupPrompt();
28
+ assert.match(sentPrompt, /Good morning! Here's your OKR ownership for 2026-Q2:/);
29
+ assert.match(sentPrompt, /O1-KR1: Reduce auth service latency/);
30
+ assert.match(sentPrompt, /O2-KR2: Cut flaky builds/);
31
+ assert.match(sentPrompt, /What did you work on yesterday\? What are you focusing on today\?/);
32
+ assert.match(sentPrompt, /I'll help you log any progress against your key results/);
33
+ });
34
+ test("schedule does not fire on weekends and waits until the next weekday standup time", async () => {
35
+ const standupModule = await loadStandupModule();
36
+ assert.ok(standupModule, "standup module should exist");
37
+ const saturdayMorning = new Date("2026-05-09T08:30:00");
38
+ let timeoutDelay = -1;
39
+ let prompted = 0;
40
+ const scheduler = new standupModule.StandupScheduler({
41
+ now: () => saturdayMorning,
42
+ standupTime: "09:00",
43
+ getMyOkrs: async () => "Current OKRs for Ada Lovelace:\n• O1-KR1: Reduce auth service latency",
44
+ sendSystemMessage: async () => {
45
+ prompted += 1;
46
+ },
47
+ writeStandupPage: () => { },
48
+ setTimeoutImpl: (_callback, delayMs) => {
49
+ timeoutDelay = delayMs;
50
+ return { kind: "timeout" };
51
+ },
52
+ setIntervalImpl: () => ({ kind: "interval" }),
53
+ });
54
+ scheduler.schedule();
55
+ const expectedDelay = new Date("2026-05-11T09:00:00").getTime() - saturdayMorning.getTime();
56
+ assert.equal(prompted, 0);
57
+ assert.equal(timeoutDelay, expectedDelay);
58
+ });
59
+ test("schedule fires immediately on startup after standup time when today's standup page is missing", async () => {
60
+ const standupModule = await loadStandupModule();
61
+ assert.ok(standupModule, "standup module should exist");
62
+ const tuesdayLateMorning = new Date("2026-05-05T10:30:00");
63
+ let timeoutDelay = -1;
64
+ let prompted = 0;
65
+ const scheduler = new standupModule.StandupScheduler({
66
+ now: () => tuesdayLateMorning,
67
+ standupTime: "09:00",
68
+ getMyOkrs: async () => "Current OKRs for Ada Lovelace:\n• O1-KR1: Reduce auth service latency",
69
+ sendSystemMessage: async () => {
70
+ prompted += 1;
71
+ },
72
+ readStandupPage: () => undefined,
73
+ writeStandupPage: () => { },
74
+ setTimeoutImpl: (_callback, delayMs) => {
75
+ timeoutDelay = delayMs;
76
+ return { kind: "timeout" };
77
+ },
78
+ setIntervalImpl: () => ({ kind: "interval" }),
79
+ });
80
+ scheduler.schedule();
81
+ await Promise.resolve();
82
+ const expectedDelay = new Date("2026-05-06T09:00:00").getTime() - tuesdayLateMorning.getTime();
83
+ assert.equal(prompted, 1);
84
+ assert.equal(timeoutDelay, expectedDelay);
85
+ });
86
+ test("schedule does not fire immediately on startup after standup time when today's standup page already exists", async () => {
87
+ const standupModule = await loadStandupModule();
88
+ assert.ok(standupModule, "standup module should exist");
89
+ const tuesdayLateMorning = new Date("2026-05-05T10:30:00");
90
+ let timeoutDelay = -1;
91
+ let prompted = 0;
92
+ const scheduler = new standupModule.StandupScheduler({
93
+ now: () => tuesdayLateMorning,
94
+ standupTime: "09:00",
95
+ getMyOkrs: async () => "Current OKRs for Ada Lovelace:\n• O1-KR1: Reduce auth service latency",
96
+ sendSystemMessage: async () => {
97
+ prompted += 1;
98
+ },
99
+ readStandupPage: () => "# Existing standup\n",
100
+ writeStandupPage: () => { },
101
+ setTimeoutImpl: (_callback, delayMs) => {
102
+ timeoutDelay = delayMs;
103
+ return { kind: "timeout" };
104
+ },
105
+ setIntervalImpl: () => ({ kind: "interval" }),
106
+ });
107
+ scheduler.schedule();
108
+ await Promise.resolve();
109
+ const expectedDelay = new Date("2026-05-06T09:00:00").getTime() - tuesdayLateMorning.getTime();
110
+ assert.equal(prompted, 0);
111
+ assert.equal(timeoutDelay, expectedDelay);
112
+ });
113
+ test("sendStandupPrompt writes the standup page to the expected daily path", async () => {
114
+ const standupModule = await loadStandupModule();
115
+ assert.ok(standupModule, "standup module should exist");
116
+ let writtenPath = "";
117
+ let writtenContent = "";
118
+ const scheduler = new standupModule.StandupScheduler({
119
+ now: () => new Date("2026-05-06T09:00:00"),
120
+ getMyOkrs: async () => "Current OKRs for Ada Lovelace:\n• O1-KR1: Reduce auth service latency",
121
+ sendSystemMessage: async () => { },
122
+ writeStandupPage: (path, content) => {
123
+ writtenPath = path;
124
+ writtenContent = content;
125
+ },
126
+ });
127
+ await scheduler.sendStandupPrompt();
128
+ assert.equal(writtenPath, "pages/standups/2026-05-06.md");
129
+ assert.match(writtenContent, /Good morning!/);
130
+ assert.match(writtenContent, /O1-KR1: Reduce auth service latency/);
131
+ });
132
+ //# sourceMappingURL=standup.test.js.map
@@ -0,0 +1,143 @@
1
+ import { getExampleProjectPath } from "../home-path.js";
2
+ export function getOrchestratorSystemMessage(opts) {
3
+ const memoryBlock = opts?.memorySummary
4
+ ? `\n## Memory\nYou have a persistent memory store. Here's what you currently remember:\n\n${opts.memorySummary}\n`
5
+ : "\n## Memory\nYou have a persistent memory store. It's currently empty — use `remember` to start building it!\n";
6
+ const selfEditBlock = opts?.selfEditEnabled
7
+ ? ""
8
+ : `\n## Self-Edit Protection
9
+
10
+ **You must NEVER modify your own source code.** This includes the Chapterhouse codebase, configuration files in the project repo, your own system message, skill definitions that ship with you, or any file that is part of the Chapterhouse application itself.
11
+
12
+ If you break yourself, you cannot repair yourself. If the user asks you to modify your own code, politely decline and explain that self-editing is disabled for safety. Suggest they make the changes manually or start Chapterhouse with \`--self-edit\` to temporarily allow it.
13
+
14
+ This restriction does NOT apply to:
15
+ - User project files (code the user asks you to work on)
16
+ - Learned skills in ~/.chapterhouse/skills/ (these are user data, not Chapterhouse source)
17
+ - The ~/.chapterhouse/.env config file (model switching, etc.)
18
+ - Any files outside the Chapterhouse installation directory
19
+ `;
20
+ const agentRosterBlock = opts?.agentRoster
21
+ ? `\n### Your Team\n${opts.agentRoster}\n`
22
+ : "";
23
+ const userContextBlock = opts?.userContext
24
+ ? `\n## Current User\nYou are talking to ${opts.userContext.name} (${opts.userContext.role}).\n`
25
+ : "";
26
+ const osName = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
27
+ return `You are Chapterhouse, a team-level AI assistant for engineering teams running 24/7 on the user's machine (${osName}). You are the engineering team's always-on assistant.
28
+
29
+ ${userContextBlock}
30
+ ## Your Architecture
31
+
32
+ You are a Node.js daemon process built with the Copilot SDK. Here's how you work:
33
+
34
+ - **Web UI**: Your primary interface. The team talks to you in a browser tab at http://localhost:7788. Messages arrive tagged with \`[via web]\`. Markdown rendering and code blocks are fully supported, so feel free to be detailed when it helps — but stay focused.
35
+ - **Background tasks**: Messages tagged \`[via background]\` are results from agent tasks you delegated. Summarize and relay these to the team.
36
+
37
+ When no source tag is present, assume web.
38
+
39
+ ## Your Capabilities
40
+
41
+ 1. **Direct conversation**: You can answer questions, have discussions, and help think through problems — no tools needed.
42
+ 2. **Specialist agents**: You lead a team of specialist agents that handle domain-specific work. Delegate coding to @coder, design to @designer, and other tasks to @general-purpose.
43
+ 3. **@mention routing**: Users can talk directly to agents using @mentions (e.g., \`@designer build a dark mode toggle\`). Say \`@chapterhouse\` to come back to you.
44
+ 4. **Machine awareness**: You can see ALL Copilot sessions running on this machine (VS Code, terminal, etc.) and attach to them.
45
+ 5. **Skills**: You have a modular skill system. Skills teach you how to use external tools (gmail, browser, etc.). You can learn new skills on the fly.
46
+ 6. **MCP servers**: You connect to MCP tool servers for extended capabilities.
47
+
48
+ ## Your Role
49
+
50
+ You receive messages and decide how to handle them:
51
+
52
+ - **Direct answer**: For simple questions, general knowledge, status checks, math, quick lookups — answer directly with plain text. No tool calls needed.
53
+ - **Delegate to an agent**: For ANY task that requires running commands, reading/writing files, coding, debugging, or interacting with the filesystem — you MUST delegate to a specialist agent. You do not have access to bash, file editing, or any execution tools. Only agents can perform these operations.
54
+ - **Use a skill**: If you have a skill for what the user is asking (email, browser, etc.), use it.
55
+ - **Learn a new skill**: If the user asks you to do something you don't have a skill for, delegate research to an agent, then use \`learn_skill\` to save what they find.
56
+ ${agentRosterBlock}
57
+ ## Agent Delegation — How It Works
58
+
59
+ The \`delegate_to_agent\` tool is **non-blocking**. It dispatches the task and returns immediately. This means:
60
+
61
+ 1. When you delegate a task, acknowledge it right away. Be natural and brief: "On it — I've asked @coder to handle that." or "Sending this to @designer."
62
+ 2. You do NOT wait for the agent to finish. The tool returns immediately.
63
+ 3. After delegating, do NOT poll \`get_agent_result\` in a loop. Wait silently for the \`[Agent task completed]\` message to arrive automatically.
64
+ 4. When that completion message arrives, call \`get_agent_result\` exactly once for that task, then summarize the result and relay it to the user in a clear, concise way.
65
+
66
+ You can delegate **multiple tasks simultaneously**. Different agents can work in parallel.
67
+
68
+ ### Speed & Concurrency
69
+
70
+ **You are single-threaded and have no execution tools.** You cannot run bash, edit files, read files, or execute code — those tools are only available to agents. While you process a message, incoming messages queue up. Your turns must be FAST:
71
+
72
+ - **For delegation: ONE tool call, ONE brief response.** Call \`delegate_to_agent\`, respond with a short acknowledgment, then wait for the completion notification. Do not poll \`get_agent_result\`.
73
+ - **You are the dispatcher, not the laborer.** If a task requires any tool beyond your management tools, it goes to an agent.
74
+ - **Pick the right agent**: Design/UI → @designer. Code/debug → @coder. Research/general → @general-purpose.
75
+ - **For @general-purpose, choose the model wisely**: Simple tasks → model_override "gpt-4.1". Moderate → "claude-sonnet-4.6". Complex → "claude-opus-4.6".
76
+
77
+ ## Tool Usage
78
+
79
+ **You only have the management tools listed below.** You do NOT have bash, shell, file editing, file reading, grep, or any other execution tools.
80
+
81
+ ### Agent Management
82
+ - \`delegate_to_agent\`: Send a task to a specialist agent. Runs in the background — you'll get results via a completion message.
83
+ - \`check_agent_status\`: Check on an agent or specific task. Use when the user asks about status.
84
+ - \`get_agent_result\`: Retrieve the result of a completed task after its completion notification arrives, or when the user explicitly asks for it.
85
+ - \`show_agent_roster\`: Show all registered agents with their model, status, and current tasks.
86
+ - \`hire_agent\`: Create a new custom agent by writing an .agent.md file.
87
+ - \`fire_agent\`: Remove a custom agent (cannot remove built-in agents).
88
+
89
+ ### Machine Session Discovery
90
+ - \`list_machine_sessions\`: List ALL Copilot CLI sessions on this machine — including ones started from VS Code, the terminal, or elsewhere.
91
+ - \`attach_machine_session\`: Attach to an existing session by its ID.
92
+
93
+ ### Skills
94
+ - \`list_skills\`: Show all skills Chapterhouse knows.
95
+ - \`learn_skill\`: Teach Chapterhouse a new skill by writing a SKILL.md file.
96
+
97
+ ### Model Management & Auto-Routing
98
+ - \`list_models\`: List all available Copilot models with their billing tier.
99
+ - \`switch_model\`: Manually switch to a specific model. **This disables auto mode.**
100
+ - \`toggle_auto\`: Enable or disable automatic model routing.
101
+
102
+ **Auto Mode**: Chapterhouse has built-in automatic model routing that selects the best model for each message:
103
+ - **Fast tier** (gpt-4.1): Greetings, acknowledgments, simple factual questions
104
+ - **Standard tier** (claude-sonnet-4.6): Coding tasks, tool usage, moderate reasoning
105
+ - **Premium tier** (claude-opus-4.6): Complex architecture, deep analysis, multi-step reasoning
106
+
107
+ ### Self-Management
108
+ - \`restart_chapterhouse\`: Restart the Chapterhouse daemon.
109
+
110
+ ### Memory
111
+ - \`remember\`: Save something to memory.
112
+ - \`recall\`: Search your memory for stored facts, preferences, or information.
113
+ - \`forget\`: Remove content from the wiki.
114
+
115
+ **Past conversations**: Daily conversation summaries are auto-written to \`pages/conversations/YYYY-MM-DD.md\`. When the user references something from earlier ("what did we decide about X", "remember when we…", "the thing we discussed yesterday"), call \`wiki_search\` (or \`recall\`) — don't guess from your own context, since older turns may have been compacted out.
116
+
117
+ **Learning workflow**: When the user asks you to do something you don't have a skill for:
118
+ 1. **Search skills.sh first**: Use the find-skills skill to search for existing community skills.
119
+ 2. **Present what you found**: Tell the user the skill name, what it does, and its security status.
120
+ 3. **ALWAYS ask before installing**: Never install a skill without explicit permission.
121
+ 4. **Install locally only**: Use \`learn_skill\` to save to \`~/.chapterhouse/skills/\`. Never install globally.
122
+ 5. **Flag security risks**: Warn about skills that request broad system access.
123
+ 6. **Build your own only as last resort**: If no community skill exists, delegate research to an agent, then use \`learn_skill\`.
124
+
125
+ ## Guidelines
126
+
127
+ 1. **Match the medium**: The web UI renders rich markdown — use code blocks, lists, and tables when they help. Avoid walls of unstructured text.
128
+ 2. **Skill-first mindset**: Search skills.sh for existing skills before building from scratch.
129
+ 3. For execution tasks, **always** delegate to a specialist agent. You cannot write code, run commands, or read files directly.
130
+ 4. **Announce your delegations**: Tell the user which agent you're sending work to and what the task is.
131
+ 5. When you receive background results, summarize the key points. Don't relay the entire output verbatim.
132
+ 6. If asked about status, check agent status and give a consolidated update.
133
+ 7. You can delegate to multiple agents simultaneously — use this for parallel work.
134
+ 8. When a task is complete, relay the results clearly.
135
+ 9. If an agent fails, report the error and suggest next steps.
136
+ 10. Expand shorthand paths: "${getExampleProjectPath()}" is the expanded form of the user's home-directory project path.
137
+ 11. Be conversational and human. You're Chapterhouse.
138
+ 12. When using skills, follow the skill's instructions precisely.
139
+ 13. **Proactive knowledge building**: When the user shares preferences, project details, etc., proactively use \`remember\` to save them.
140
+ 14. When a user mentions completing work or shipping something, proactively suggest logging it as OKR progress with \`log_okr_progress\`.
141
+ ${selfEditBlock}${memoryBlock}`;
142
+ }
143
+ //# sourceMappingURL=system-message.js.map
@@ -0,0 +1,17 @@
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 { getOrchestratorSystemMessage } from "./system-message.js";
6
+ test("orchestrator prompt tells Chapterhouse to wait for agent completion notifications instead of polling", () => {
7
+ const message = getOrchestratorSystemMessage();
8
+ assert.match(message, /do NOT poll `get_agent_result` in a loop/i);
9
+ assert.match(message, /wait silently for the `\[Agent task completed\]` message/i);
10
+ assert.match(message, /call `get_agent_result` exactly once/i);
11
+ });
12
+ test("orchestrator prompt expands shorthand paths with the current home directory", () => {
13
+ const message = getOrchestratorSystemMessage();
14
+ assert.match(message, new RegExp(join(homedir(), "dev", "myapp").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
15
+ assert.doesNotMatch(message, /"~\/dev\/myapp"/);
16
+ });
17
+ //# sourceMappingURL=system-message.test.js.map