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.
- package/LICENSE +23 -0
- package/README.md +363 -0
- package/agents/chapterhouse.agent.md +40 -0
- package/agents/coder.agent.md +38 -0
- package/agents/designer.agent.md +43 -0
- package/agents/general-purpose.agent.md +30 -0
- package/dist/api/auth.js +159 -0
- package/dist/api/auth.test.js +463 -0
- package/dist/api/errors.js +95 -0
- package/dist/api/errors.test.js +89 -0
- package/dist/api/rate-limit.js +85 -0
- package/dist/api/server-runtime.js +62 -0
- package/dist/api/server.js +651 -0
- package/dist/api/server.test.js +385 -0
- package/dist/api/sse.integration.test.js +270 -0
- package/dist/api/sse.js +7 -0
- package/dist/api/team.js +196 -0
- package/dist/api/team.test.js +466 -0
- package/dist/cli.js +102 -0
- package/dist/config.js +299 -0
- package/dist/config.phase3.test.js +20 -0
- package/dist/config.test.js +148 -0
- package/dist/copilot/agents.js +447 -0
- package/dist/copilot/agents.squad.test.js +72 -0
- package/dist/copilot/classifier.js +72 -0
- package/dist/copilot/client.js +32 -0
- package/dist/copilot/client.test.js +100 -0
- package/dist/copilot/episode-writer.js +219 -0
- package/dist/copilot/episode-writer.test.js +41 -0
- package/dist/copilot/mcp-config.js +22 -0
- package/dist/copilot/okr-mapper.js +196 -0
- package/dist/copilot/okr-mapper.test.js +114 -0
- package/dist/copilot/orchestrator.js +685 -0
- package/dist/copilot/orchestrator.test.js +523 -0
- package/dist/copilot/router.js +142 -0
- package/dist/copilot/router.test.js +119 -0
- package/dist/copilot/skills.js +125 -0
- package/dist/copilot/standup.js +138 -0
- package/dist/copilot/standup.test.js +132 -0
- package/dist/copilot/system-message.js +143 -0
- package/dist/copilot/system-message.test.js +17 -0
- package/dist/copilot/tools.js +1212 -0
- package/dist/copilot/tools.okr.test.js +260 -0
- package/dist/copilot/tools.squad.test.js +168 -0
- package/dist/daemon.js +235 -0
- package/dist/home-path.js +12 -0
- package/dist/home-path.test.js +11 -0
- package/dist/integrations/ado-analytics.js +178 -0
- package/dist/integrations/ado-analytics.test.js +284 -0
- package/dist/integrations/ado-client.js +227 -0
- package/dist/integrations/ado-client.test.js +176 -0
- package/dist/integrations/ado-schema.js +25 -0
- package/dist/integrations/ado-schema.test.js +39 -0
- package/dist/integrations/ado-skill.js +55 -0
- package/dist/integrations/report-generator.js +114 -0
- package/dist/integrations/report-generator.test.js +62 -0
- package/dist/integrations/team-push.js +144 -0
- package/dist/integrations/team-push.test.js +178 -0
- package/dist/integrations/teams-notify.js +108 -0
- package/dist/integrations/teams-notify.test.js +135 -0
- package/dist/paths.js +41 -0
- package/dist/setup.js +149 -0
- package/dist/shutdown-signals.js +13 -0
- package/dist/shutdown-signals.test.js +33 -0
- package/dist/squad/charter.js +108 -0
- package/dist/squad/charter.test.js +89 -0
- package/dist/squad/context.js +48 -0
- package/dist/squad/context.test.js +59 -0
- package/dist/squad/discovery.js +280 -0
- package/dist/squad/discovery.test.js +93 -0
- package/dist/squad/index.js +7 -0
- package/dist/squad/mirror.js +81 -0
- package/dist/squad/mirror.scheduler.js +78 -0
- package/dist/squad/mirror.scheduler.test.js +197 -0
- package/dist/squad/mirror.test.js +172 -0
- package/dist/squad/registry.js +162 -0
- package/dist/squad/registry.test.js +31 -0
- package/dist/squad/squad-coordinator-system-message.test.js +190 -0
- package/dist/squad/squad-session-routing.test.js +260 -0
- package/dist/squad/types.js +4 -0
- package/dist/status.js +25 -0
- package/dist/status.test.js +22 -0
- package/dist/store/db.js +290 -0
- package/dist/store/db.test.js +126 -0
- package/dist/store/squad-sessions.test.js +341 -0
- package/dist/test/setup-env.js +3 -0
- package/dist/update.js +112 -0
- package/dist/update.test.js +25 -0
- package/dist/wiki/context.js +138 -0
- package/dist/wiki/fs.js +195 -0
- package/dist/wiki/fs.test.js +39 -0
- package/dist/wiki/index-manager.js +359 -0
- package/dist/wiki/index-manager.test.js +129 -0
- package/dist/wiki/lock.js +26 -0
- package/dist/wiki/lock.test.js +30 -0
- package/dist/wiki/log-manager.js +20 -0
- package/dist/wiki/migrate.js +306 -0
- package/dist/wiki/okr.test.js +101 -0
- package/dist/wiki/path-utils.js +4 -0
- package/dist/wiki/path-utils.test.js +8 -0
- package/dist/wiki/seed-team-wiki.js +296 -0
- package/dist/wiki/seed-team-wiki.test.js +69 -0
- package/dist/wiki/team-sync.js +212 -0
- package/dist/wiki/team-sync.test.js +185 -0
- package/dist/wiki/templates/okr.js +98 -0
- package/package.json +72 -0
- package/skills/.gitkeep +0 -0
- package/skills/find-skills/SKILL.md +161 -0
- package/skills/find-skills/_meta.json +4 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/squad/SKILL.md +76 -0
- package/web/dist/assets/index-D-e7K-fT.css +10 -0
- package/web/dist/assets/index-DAg9IrpO.js +142 -0
- package/web/dist/assets/index-DAg9IrpO.js.map +1 -0
- package/web/dist/chapterhouse-icon.png +0 -0
- package/web/dist/chapterhouse-icon.svg +42 -0
- package/web/dist/chapterhouse-logo.svg +46 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
async function loadAdoClientModule() {
|
|
4
|
+
try {
|
|
5
|
+
return await import(new URL(`./ado-client.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
test("getKeyResults returns the expected KR shape", async () => {
|
|
12
|
+
const ado = await loadAdoClientModule();
|
|
13
|
+
assert.ok(ado, "ado client module should exist");
|
|
14
|
+
const queries = [];
|
|
15
|
+
const client = new ado.AdoClient({
|
|
16
|
+
org: "https://dev.azure.com/example-org",
|
|
17
|
+
project: "example-project",
|
|
18
|
+
pat: "test-pat",
|
|
19
|
+
workItemTrackingApi: {
|
|
20
|
+
async queryByWiql(wiql, teamContext) {
|
|
21
|
+
queries.push(`${teamContext?.project ?? ""}:${wiql.query ?? ""}`);
|
|
22
|
+
return { workItems: [{ id: 101 }] };
|
|
23
|
+
},
|
|
24
|
+
async getWorkItems(ids) {
|
|
25
|
+
assert.deepEqual(ids, [101]);
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
id: 101,
|
|
29
|
+
fields: {
|
|
30
|
+
"System.Title": "Ship SSO to all tenants",
|
|
31
|
+
"Custom.CurrentValue": 75,
|
|
32
|
+
"Custom.TargetValue": 100,
|
|
33
|
+
"Microsoft.VSTS.Common.BusinessValue": "%",
|
|
34
|
+
"Custom.OKROwner": "ada@example.com",
|
|
35
|
+
"System.Parent": 7,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
},
|
|
40
|
+
async updateWorkItem() {
|
|
41
|
+
throw new Error("not used in this test");
|
|
42
|
+
},
|
|
43
|
+
async addComment() {
|
|
44
|
+
throw new Error("not used in this test");
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const keyResults = await client.getKeyResults("2026-Q2");
|
|
49
|
+
assert.equal(queries.length, 1);
|
|
50
|
+
assert.match(queries[0] ?? "", /Feature/);
|
|
51
|
+
assert.match(queries[0] ?? "", /Custom\.OKRPeriod/);
|
|
52
|
+
assert.deepEqual(keyResults, [
|
|
53
|
+
{
|
|
54
|
+
id: 101,
|
|
55
|
+
title: "Ship SSO to all tenants",
|
|
56
|
+
currentValue: 75,
|
|
57
|
+
targetValue: 100,
|
|
58
|
+
unit: "%",
|
|
59
|
+
owner: "ada@example.com",
|
|
60
|
+
parentId: 7,
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
test("updateKRProgress sends the expected patch payload", async () => {
|
|
65
|
+
const ado = await loadAdoClientModule();
|
|
66
|
+
assert.ok(ado, "ado client module should exist");
|
|
67
|
+
const updates = [];
|
|
68
|
+
const comments = [];
|
|
69
|
+
const client = new ado.AdoClient({
|
|
70
|
+
org: "https://dev.azure.com/example-org",
|
|
71
|
+
project: "example-project",
|
|
72
|
+
pat: "test-pat",
|
|
73
|
+
workItemTrackingApi: {
|
|
74
|
+
async queryByWiql() {
|
|
75
|
+
throw new Error("not used in this test");
|
|
76
|
+
},
|
|
77
|
+
async getWorkItems() {
|
|
78
|
+
throw new Error("not used in this test");
|
|
79
|
+
},
|
|
80
|
+
async updateWorkItem(_customHeaders, document, id, project) {
|
|
81
|
+
updates.push({ id, project, document });
|
|
82
|
+
return {};
|
|
83
|
+
},
|
|
84
|
+
async addComment(request, project, workItemId) {
|
|
85
|
+
comments.push({ project, workItemId, text: request.text });
|
|
86
|
+
return {};
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
await client.updateKRProgress(42, 88, "Recovered from rollout issues");
|
|
91
|
+
assert.equal(updates.length, 1);
|
|
92
|
+
assert.deepEqual(updates[0], {
|
|
93
|
+
id: 42,
|
|
94
|
+
project: "example-project",
|
|
95
|
+
document: [
|
|
96
|
+
{ op: "add", path: "/fields/Custom.CurrentValue", value: 88 },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
assert.deepEqual(comments, [
|
|
100
|
+
{
|
|
101
|
+
project: "example-project",
|
|
102
|
+
workItemId: 42,
|
|
103
|
+
text: "Recovered from rollout issues",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
test("getOKRSummary computes percent complete from KR progress", async () => {
|
|
108
|
+
const ado = await loadAdoClientModule();
|
|
109
|
+
assert.ok(ado, "ado client module should exist");
|
|
110
|
+
const client = new ado.AdoClient({
|
|
111
|
+
org: "https://dev.azure.com/example-org",
|
|
112
|
+
project: "example-project",
|
|
113
|
+
pat: "test-pat",
|
|
114
|
+
workItemTrackingApi: {
|
|
115
|
+
async queryByWiql(wiql) {
|
|
116
|
+
if ((wiql.query ?? "").includes("[System.WorkItemType] = 'Epic'")) {
|
|
117
|
+
return { workItems: [{ id: 7 }] };
|
|
118
|
+
}
|
|
119
|
+
if ((wiql.query ?? "").includes("[System.WorkItemType] = 'Feature'")) {
|
|
120
|
+
return { workItems: [{ id: 101 }, { id: 102 }] };
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Unexpected query: ${wiql.query ?? ""}`);
|
|
123
|
+
},
|
|
124
|
+
async getWorkItems(ids) {
|
|
125
|
+
if (ids.includes(7)) {
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
id: 7,
|
|
129
|
+
fields: {
|
|
130
|
+
"System.Title": "Improve identity coverage",
|
|
131
|
+
"Custom.OKROwner": "ada@example.com",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
id: 101,
|
|
139
|
+
fields: {
|
|
140
|
+
"System.Title": "Ship SSO to all tenants",
|
|
141
|
+
"Custom.CurrentValue": 75,
|
|
142
|
+
"Custom.TargetValue": 100,
|
|
143
|
+
"Microsoft.VSTS.Common.BusinessValue": "%",
|
|
144
|
+
"Custom.OKROwner": "ada@example.com",
|
|
145
|
+
"System.Parent": 7,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 102,
|
|
150
|
+
fields: {
|
|
151
|
+
"System.Title": "Migrate legacy auth flows",
|
|
152
|
+
"Custom.CurrentValue": 20,
|
|
153
|
+
"Custom.TargetValue": 40,
|
|
154
|
+
"Microsoft.VSTS.Common.BusinessValue": "%",
|
|
155
|
+
"Custom.OKROwner": "ada@example.com",
|
|
156
|
+
"System.Parent": 7,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
},
|
|
161
|
+
async updateWorkItem() {
|
|
162
|
+
throw new Error("not used in this test");
|
|
163
|
+
},
|
|
164
|
+
async addComment() {
|
|
165
|
+
throw new Error("not used in this test");
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const summary = await client.getOKRSummary("2026-Q2");
|
|
170
|
+
assert.equal(summary.period, "2026-Q2");
|
|
171
|
+
assert.equal(summary.objectives.length, 1);
|
|
172
|
+
assert.equal(summary.objectives[0]?.percentComplete, 63);
|
|
173
|
+
assert.equal(summary.objectives[0]?.keyResults[0]?.percentComplete, 75);
|
|
174
|
+
assert.equal(summary.objectives[0]?.keyResults[1]?.percentComplete, 50);
|
|
175
|
+
});
|
|
176
|
+
//# sourceMappingURL=ado-client.test.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
export const ADO_ORG = config.ADO_ORG;
|
|
3
|
+
export const ADO_PROJECT = config.ADO_PROJECT;
|
|
4
|
+
// Work item type mapping
|
|
5
|
+
export const WIT = {
|
|
6
|
+
OBJECTIVE: "Epic",
|
|
7
|
+
KEY_RESULT: "Feature",
|
|
8
|
+
};
|
|
9
|
+
// Custom field names (these need to be created in ADO project settings)
|
|
10
|
+
export const FIELDS = {
|
|
11
|
+
CURRENT_VALUE: "Custom.CurrentValue",
|
|
12
|
+
TARGET_VALUE: "Custom.TargetValue",
|
|
13
|
+
OKR_PERIOD: "Custom.OKRPeriod", // e.g. "2026-Q2"
|
|
14
|
+
OKR_OWNER: "Custom.OKROwner", // email
|
|
15
|
+
};
|
|
16
|
+
// Standard fields we use
|
|
17
|
+
export const STD_FIELDS = {
|
|
18
|
+
TITLE: "System.Title",
|
|
19
|
+
STATE: "System.State",
|
|
20
|
+
PARENT: "System.Parent",
|
|
21
|
+
ASSIGNED_TO: "System.AssignedTo",
|
|
22
|
+
TAGS: "System.Tags",
|
|
23
|
+
};
|
|
24
|
+
export const UNIT_FIELD = "Microsoft.VSTS.Common.BusinessValue";
|
|
25
|
+
//# sourceMappingURL=ado-schema.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
6
|
+
test("exports empty ADO settings when runtime config is unset", async () => {
|
|
7
|
+
const adoSchemaModule = await import(new URL(`./ado-schema.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
8
|
+
assert.equal(adoSchemaModule.ADO_ORG, "");
|
|
9
|
+
assert.equal(adoSchemaModule.ADO_PROJECT, "");
|
|
10
|
+
});
|
|
11
|
+
test("exports overridden ADO settings from runtime config", async () => {
|
|
12
|
+
const result = spawnSync(process.execPath, [
|
|
13
|
+
"--import",
|
|
14
|
+
"tsx",
|
|
15
|
+
"--input-type=module",
|
|
16
|
+
"--eval",
|
|
17
|
+
`
|
|
18
|
+
const mod = await import("./src/integrations/ado-schema.ts");
|
|
19
|
+
console.log("__RESULT__" + JSON.stringify({ org: mod.ADO_ORG, project: mod.ADO_PROJECT }));
|
|
20
|
+
`,
|
|
21
|
+
], {
|
|
22
|
+
cwd: REPO_ROOT,
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
ADO_ORG: "https://dev.azure.com/example-org",
|
|
26
|
+
ADO_PROJECT: "example-project",
|
|
27
|
+
},
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
});
|
|
30
|
+
assert.equal(result.status, 0, result.stderr);
|
|
31
|
+
const outputLine = result.stdout
|
|
32
|
+
.split("\n")
|
|
33
|
+
.find((line) => line.startsWith("__RESULT__"));
|
|
34
|
+
assert.ok(outputLine, "child process should print the imported ADO settings");
|
|
35
|
+
const output = JSON.parse(outputLine.replace("__RESULT__", ""));
|
|
36
|
+
assert.equal(output.org, "https://dev.azure.com/example-org");
|
|
37
|
+
assert.equal(output.project, "example-project");
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=ado-schema.test.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AdoClient } from "./ado-client.js";
|
|
2
|
+
function getCurrentQuarter(now = new Date()) {
|
|
3
|
+
return `${now.getUTCFullYear()}-Q${Math.floor(now.getUTCMonth() / 3) + 1}`;
|
|
4
|
+
}
|
|
5
|
+
function formatValue(value, unit) {
|
|
6
|
+
return unit === "%" ? `${value}%` : `${value}${unit ? ` ${unit}` : ""}`;
|
|
7
|
+
}
|
|
8
|
+
function renderRow(objectiveTitle, keyResult) {
|
|
9
|
+
return `| ${objectiveTitle} | ${keyResult.title} | ${keyResult.owner || "—"} | ${formatValue(keyResult.currentValue, keyResult.unit)} / ${formatValue(keyResult.targetValue, keyResult.unit)} | ${keyResult.percentComplete}% |`;
|
|
10
|
+
}
|
|
11
|
+
export function formatOKRSummaryMarkdown(summary) {
|
|
12
|
+
if (summary.objectives.length === 0) {
|
|
13
|
+
return `## Azure DevOps OKRs for ${summary.period}\n\nNo objectives found.`;
|
|
14
|
+
}
|
|
15
|
+
const lines = [
|
|
16
|
+
`## Azure DevOps OKRs for ${summary.period}`,
|
|
17
|
+
"",
|
|
18
|
+
"| Objective | Key Result | Owner | Progress | Complete |",
|
|
19
|
+
"| --- | --- | --- | --- | --- |",
|
|
20
|
+
];
|
|
21
|
+
for (const objective of summary.objectives) {
|
|
22
|
+
if (objective.keyResults.length === 0) {
|
|
23
|
+
lines.push(`| ${objective.title} | _No key results_ | ${objective.owner || "—"} | — | ${objective.percentComplete}% |`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
for (const keyResult of objective.keyResults) {
|
|
27
|
+
lines.push(renderRow(objective.title, keyResult));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
export async function adoGetOkrs(period) {
|
|
33
|
+
const resolvedPeriod = period?.trim() || getCurrentQuarter();
|
|
34
|
+
const client = new AdoClient();
|
|
35
|
+
const summary = await client.getOKRSummary(resolvedPeriod);
|
|
36
|
+
return formatOKRSummaryMarkdown(summary);
|
|
37
|
+
}
|
|
38
|
+
export async function adoUpdateKr(workItemId, currentValue, notes) {
|
|
39
|
+
const client = new AdoClient();
|
|
40
|
+
await client.updateKRProgress(workItemId, currentValue, notes);
|
|
41
|
+
const keyResult = (await client.getKeyResults()).find((item) => item.id === workItemId);
|
|
42
|
+
if (!keyResult) {
|
|
43
|
+
return `Updated Azure DevOps key result #${workItemId}.`;
|
|
44
|
+
}
|
|
45
|
+
const percentComplete = keyResult.targetValue > 0
|
|
46
|
+
? Math.round((keyResult.currentValue / keyResult.targetValue) * 100)
|
|
47
|
+
: 0;
|
|
48
|
+
return `Updated Azure DevOps key result #${workItemId} to ${keyResult.currentValue}/${keyResult.targetValue} (${percentComplete}%).`;
|
|
49
|
+
}
|
|
50
|
+
export async function adoOkrSummary(period) {
|
|
51
|
+
const resolvedPeriod = period?.trim() || getCurrentQuarter();
|
|
52
|
+
const client = new AdoClient();
|
|
53
|
+
return await client.getOKRSummary(resolvedPeriod);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=ado-skill.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ensureWikiStructure, listPages, readPage } from "../wiki/fs.js";
|
|
2
|
+
import { sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
3
|
+
import { AdoAnalytics } from "./ado-analytics.js";
|
|
4
|
+
const RECENT_UPDATE_DAYS = 31;
|
|
5
|
+
function getQuarterMonths(period) {
|
|
6
|
+
const match = /^(\d{4})-Q([1-4])$/.exec(period.trim());
|
|
7
|
+
if (!match) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const year = match[1];
|
|
11
|
+
const quarter = Number(match[2]);
|
|
12
|
+
const startMonth = (quarter - 1) * 3 + 1;
|
|
13
|
+
return Array.from({ length: 3 }, (_unused, index) => `${year}-${String(startMonth + index).padStart(2, "0")}`);
|
|
14
|
+
}
|
|
15
|
+
function getRecentQuarterUpdatePaths(period, now, listPagePaths) {
|
|
16
|
+
ensureWikiStructure();
|
|
17
|
+
const quarterMonths = new Set(getQuarterMonths(period));
|
|
18
|
+
const cutoff = new Date(now);
|
|
19
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - (RECENT_UPDATE_DAYS - 1));
|
|
20
|
+
const cutoffDate = cutoff.toISOString().slice(0, 10);
|
|
21
|
+
return listPagePaths()
|
|
22
|
+
.filter((path) => path.startsWith("pages/okrs/updates/"))
|
|
23
|
+
.filter((path) => {
|
|
24
|
+
const pageDate = path.slice("pages/okrs/updates/".length).replace(/\.md$/, "");
|
|
25
|
+
return pageDate >= cutoffDate && quarterMonths.has(pageDate.slice(0, 7));
|
|
26
|
+
})
|
|
27
|
+
.sort()
|
|
28
|
+
.reverse();
|
|
29
|
+
}
|
|
30
|
+
function renderObjectiveTable(reportData) {
|
|
31
|
+
const lines = [
|
|
32
|
+
"| Objective | Owner | Complete | KR Count |",
|
|
33
|
+
"| --- | --- | --- | --- |",
|
|
34
|
+
...reportData.objectives.map((objective) => (`| ${objective.title} | ${objective.owner || "—"} | ${objective.percentComplete}% | ${objective.krs.length} |`)),
|
|
35
|
+
];
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function renderKrTable(reportData) {
|
|
39
|
+
const lines = [
|
|
40
|
+
"| Objective | KR | Owner | State | Progress | Complete |",
|
|
41
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
42
|
+
];
|
|
43
|
+
for (const objective of reportData.objectives) {
|
|
44
|
+
for (const kr of objective.krs) {
|
|
45
|
+
lines.push(`| ${objective.title} | ${kr.title} | ${kr.owner || "—"} | ${kr.state || "—"} | ${kr.currentValue}/${kr.targetValue} | ${kr.percentComplete}% |`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
function buildPrompt(reportData, updateLogs) {
|
|
51
|
+
const highlights = reportData.highlights.length > 0
|
|
52
|
+
? reportData.highlights.map((item) => `- ${item}`).join("\n")
|
|
53
|
+
: "_No highlights extracted from recent updates._";
|
|
54
|
+
return [
|
|
55
|
+
"You are generating a monthly OKR report for the team.",
|
|
56
|
+
"Here is the current data:",
|
|
57
|
+
"",
|
|
58
|
+
`Period: ${reportData.period}`,
|
|
59
|
+
`Generated At: ${reportData.generatedAt}`,
|
|
60
|
+
`Total KRs: ${reportData.totalKRs}`,
|
|
61
|
+
`Completed KRs: ${reportData.completedKRs}`,
|
|
62
|
+
"",
|
|
63
|
+
"## Objective Summary",
|
|
64
|
+
renderObjectiveTable(reportData),
|
|
65
|
+
"",
|
|
66
|
+
"## Key Result Detail",
|
|
67
|
+
renderKrTable(reportData),
|
|
68
|
+
"",
|
|
69
|
+
"## Highlight Candidates",
|
|
70
|
+
highlights,
|
|
71
|
+
"",
|
|
72
|
+
"Here are the team's recent activity updates:",
|
|
73
|
+
updateLogs || "_No recent daily update logs found._",
|
|
74
|
+
"",
|
|
75
|
+
"Please write a concise executive-style OKR report covering: overall health, objective-by-objective summary, highlights, risks, and next steps.",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
async function defaultGenerateNarrative(prompt) {
|
|
79
|
+
return await new Promise((resolve, reject) => {
|
|
80
|
+
void sendToOrchestrator(prompt, { type: "background" }, (text, done) => {
|
|
81
|
+
if (!done) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (text.startsWith("Error: ")) {
|
|
85
|
+
reject(new Error(text.slice("Error: ".length).trim() || text));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
resolve(text);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export class ReportGenerator {
|
|
93
|
+
analytics;
|
|
94
|
+
now;
|
|
95
|
+
listPagesImpl;
|
|
96
|
+
readPageImpl;
|
|
97
|
+
generateNarrative;
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
this.analytics = options.analytics ?? new AdoAnalytics({ now: options.now });
|
|
100
|
+
this.now = options.now ?? (() => new Date());
|
|
101
|
+
this.listPagesImpl = options.listPages ?? listPages;
|
|
102
|
+
this.readPageImpl = options.readPage ?? readPage;
|
|
103
|
+
this.generateNarrative = options.generateNarrative ?? defaultGenerateNarrative;
|
|
104
|
+
}
|
|
105
|
+
async generateMonthlyReport(period) {
|
|
106
|
+
const reportData = await this.analytics.buildReportData(period);
|
|
107
|
+
const updateLogs = getRecentQuarterUpdatePaths(period, this.now(), this.listPagesImpl)
|
|
108
|
+
.map((path) => this.readPageImpl(path) ?? "")
|
|
109
|
+
.filter((content) => content.trim().length > 0)
|
|
110
|
+
.join("\n\n");
|
|
111
|
+
return await this.generateNarrative(buildPrompt(reportData, updateLogs));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=report-generator.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
async function loadReportGeneratorModule() {
|
|
4
|
+
try {
|
|
5
|
+
return await import(new URL(`./report-generator.js?cachebust=${Date.now()}-${Math.random()}`, import.meta.url).href);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
test("generateMonthlyReport formats analytics data and recent updates into the orchestrator prompt", async () => {
|
|
12
|
+
const reportGeneratorModule = await loadReportGeneratorModule();
|
|
13
|
+
assert.ok(reportGeneratorModule, "report generator module should exist");
|
|
14
|
+
const prompts = [];
|
|
15
|
+
const generator = new reportGeneratorModule.ReportGenerator({
|
|
16
|
+
analytics: {
|
|
17
|
+
async buildReportData() {
|
|
18
|
+
return {
|
|
19
|
+
period: "2026-Q2",
|
|
20
|
+
generatedAt: "2026-05-06T00:00:00.000Z",
|
|
21
|
+
totalKRs: 2,
|
|
22
|
+
completedKRs: 1,
|
|
23
|
+
highlights: ["Finished SSO rollout dry-run"],
|
|
24
|
+
objectives: [
|
|
25
|
+
{
|
|
26
|
+
workItemId: 7,
|
|
27
|
+
title: "Improve identity coverage",
|
|
28
|
+
owner: "ada@example.com",
|
|
29
|
+
percentComplete: 63,
|
|
30
|
+
krs: [
|
|
31
|
+
{
|
|
32
|
+
workItemId: 101,
|
|
33
|
+
title: "Ship SSO to all tenants",
|
|
34
|
+
currentValue: 75,
|
|
35
|
+
targetValue: 100,
|
|
36
|
+
percentComplete: 75,
|
|
37
|
+
owner: "ada@example.com",
|
|
38
|
+
state: "Active",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
listPages: () => ["pages/okrs/updates/2026-05-02.md"],
|
|
47
|
+
readPage: () => "## 2026-05-02T10:00:00.000Z — eng-1\n\n**Activity**: Finished SSO rollout dry-run \n",
|
|
48
|
+
generateNarrative: async (prompt) => {
|
|
49
|
+
prompts.push(prompt);
|
|
50
|
+
return "Overall health is improving.";
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const report = await generator.generateMonthlyReport("2026-Q2");
|
|
54
|
+
assert.equal(report, "Overall health is improving.");
|
|
55
|
+
assert.equal(prompts.length, 1);
|
|
56
|
+
assert.match(prompts[0] ?? "", /You are generating a monthly OKR report for the team\./);
|
|
57
|
+
assert.match(prompts[0] ?? "", /\| Objective \| Owner \| Complete \| KR Count \|/);
|
|
58
|
+
assert.match(prompts[0] ?? "", /Improve identity coverage/);
|
|
59
|
+
assert.match(prompts[0] ?? "", /Finished SSO rollout dry-run/);
|
|
60
|
+
assert.match(prompts[0] ?? "", /overall health, objective-by-objective summary, highlights, risks, and next steps/i);
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=report-generator.test.js.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
export class TeamPushClient {
|
|
3
|
+
teamChapterhouseUrl;
|
|
4
|
+
teamChapterhouseToken;
|
|
5
|
+
standaloneMode;
|
|
6
|
+
fetchImpl;
|
|
7
|
+
getAuthorizationHeader;
|
|
8
|
+
getCurrentUser;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.teamChapterhouseUrl = (options.teamChapterhouseUrl ?? config.teamChapterhouseUrl).trim().replace(/\/+$/, "");
|
|
11
|
+
this.teamChapterhouseToken = (options.teamChapterhouseToken ?? config.teamChapterhouseToken).trim();
|
|
12
|
+
this.standaloneMode = options.standaloneMode ?? config.standaloneMode;
|
|
13
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
14
|
+
this.getAuthorizationHeader = options.getAuthorizationHeader;
|
|
15
|
+
this.getCurrentUser = options.getCurrentUser;
|
|
16
|
+
}
|
|
17
|
+
async pushUpdate(payload) {
|
|
18
|
+
if (!this.isEnabled()) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
entry: {
|
|
22
|
+
disabled: true,
|
|
23
|
+
...payload,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await this.fetchImpl(`${this.teamChapterhouseUrl}/api/team/update`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
...this.buildHeaders(),
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
engineerId: this.resolveEngineerId(),
|
|
37
|
+
activity: payload.activity,
|
|
38
|
+
krId: payload.krId,
|
|
39
|
+
delta: payload.delta,
|
|
40
|
+
notes: payload.notes,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new Error(`Failed to push OKR update: network request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to push OKR update: ${describeHttpFailure(response.status)}`);
|
|
49
|
+
}
|
|
50
|
+
return await response.json();
|
|
51
|
+
}
|
|
52
|
+
async fetchOKRs(period) {
|
|
53
|
+
if (!this.isEnabled()) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
const url = new URL(`${this.teamChapterhouseUrl}/api/team/okrs`);
|
|
57
|
+
if (period) {
|
|
58
|
+
url.searchParams.set("period", period);
|
|
59
|
+
}
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await this.fetchImpl(url, {
|
|
63
|
+
headers: this.buildHeaders(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new Error(`Failed to fetch OKRs: network request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`Failed to fetch OKRs: ${describeHttpFailure(response.status)}`);
|
|
71
|
+
}
|
|
72
|
+
const payload = await response.json();
|
|
73
|
+
const pages = Array.isArray(payload.pages) ? payload.pages : [];
|
|
74
|
+
const filtered = period
|
|
75
|
+
? pages.filter((page) => page.path.includes(period))
|
|
76
|
+
: pages;
|
|
77
|
+
return filtered.map((page) => page.content).join("\n\n---\n\n");
|
|
78
|
+
}
|
|
79
|
+
async writePage(path, content) {
|
|
80
|
+
if (!this.isEnabled()) {
|
|
81
|
+
return { ok: false, path };
|
|
82
|
+
}
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await this.fetchImpl(`${this.teamChapterhouseUrl}/api/team/wiki/${encodeURIComponent(path)}`, {
|
|
86
|
+
method: "PUT",
|
|
87
|
+
headers: {
|
|
88
|
+
...this.buildHeaders(),
|
|
89
|
+
"content-type": "application/json",
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ content }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new Error(`Failed to write team wiki page: network request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`Failed to write team wiki page: ${describeHttpFailure(response.status)}`);
|
|
99
|
+
}
|
|
100
|
+
return await response.json();
|
|
101
|
+
}
|
|
102
|
+
buildHeaders() {
|
|
103
|
+
const headers = {
|
|
104
|
+
accept: "application/json",
|
|
105
|
+
};
|
|
106
|
+
const authorizationHeader = this.resolveAuthorizationHeader();
|
|
107
|
+
if (authorizationHeader) {
|
|
108
|
+
headers.authorization = authorizationHeader;
|
|
109
|
+
}
|
|
110
|
+
return headers;
|
|
111
|
+
}
|
|
112
|
+
resolveAuthorizationHeader() {
|
|
113
|
+
const currentHeader = this.getAuthorizationHeader?.()?.trim();
|
|
114
|
+
if (currentHeader) {
|
|
115
|
+
return currentHeader;
|
|
116
|
+
}
|
|
117
|
+
if (!this.teamChapterhouseToken) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return this.teamChapterhouseToken.startsWith("Bearer ")
|
|
121
|
+
? this.teamChapterhouseToken
|
|
122
|
+
: `Bearer ${this.teamChapterhouseToken}`;
|
|
123
|
+
}
|
|
124
|
+
resolveEngineerId() {
|
|
125
|
+
const user = this.getCurrentUser?.();
|
|
126
|
+
if (user?.id) {
|
|
127
|
+
return user.id;
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Failed to push OKR update: no authenticated engineer identity is available");
|
|
130
|
+
}
|
|
131
|
+
isEnabled() {
|
|
132
|
+
return !this.standaloneMode && this.teamChapterhouseUrl.length > 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function describeHttpFailure(status) {
|
|
136
|
+
if (status === 401) {
|
|
137
|
+
return "unauthorized (HTTP 401)";
|
|
138
|
+
}
|
|
139
|
+
if (status === 403) {
|
|
140
|
+
return "forbidden (HTTP 403)";
|
|
141
|
+
}
|
|
142
|
+
return `HTTP ${status}`;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=team-push.js.map
|