@pruddiman/hem 0.0.1-beta-5671db0

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agents/arbiter-agent.d.ts +72 -0
  3. package/dist/agents/arbiter-agent.js +149 -0
  4. package/dist/agents/architecture-agent.d.ts +148 -0
  5. package/dist/agents/architecture-agent.js +459 -0
  6. package/dist/agents/base-agent.d.ts +44 -0
  7. package/dist/agents/base-agent.js +57 -0
  8. package/dist/agents/crossref-agent.d.ts +140 -0
  9. package/dist/agents/crossref-agent.js +560 -0
  10. package/dist/agents/crossref-arbiter-agent.d.ts +72 -0
  11. package/dist/agents/crossref-arbiter-agent.js +147 -0
  12. package/dist/agents/documentation-agent.d.ts +55 -0
  13. package/dist/agents/documentation-agent.js +159 -0
  14. package/dist/agents/exploration-agent.d.ts +58 -0
  15. package/dist/agents/exploration-agent.js +102 -0
  16. package/dist/agents/grouping-agent.d.ts +167 -0
  17. package/dist/agents/grouping-agent.js +557 -0
  18. package/dist/agents/index-agent.d.ts +86 -0
  19. package/dist/agents/index-agent.js +360 -0
  20. package/dist/agents/organization-agent.d.ts +144 -0
  21. package/dist/agents/organization-agent.js +607 -0
  22. package/dist/auth.d.ts +372 -0
  23. package/dist/auth.js +1072 -0
  24. package/dist/broadcast-mcp.d.ts +21 -0
  25. package/dist/broadcast-mcp.js +59 -0
  26. package/dist/changelog.d.ts +85 -0
  27. package/dist/changelog.js +223 -0
  28. package/dist/decision-queue.d.ts +173 -0
  29. package/dist/decision-queue.js +265 -0
  30. package/dist/diff-scope.d.ts +24 -0
  31. package/dist/diff-scope.js +28 -0
  32. package/dist/discovery.d.ts +54 -0
  33. package/dist/discovery.js +405 -0
  34. package/dist/grouping.d.ts +37 -0
  35. package/dist/grouping.js +343 -0
  36. package/dist/helpers/format.d.ts +5 -0
  37. package/dist/helpers/format.js +13 -0
  38. package/dist/helpers/index.d.ts +11 -0
  39. package/dist/helpers/index.js +11 -0
  40. package/dist/helpers/parsing.d.ts +52 -0
  41. package/dist/helpers/parsing.js +128 -0
  42. package/dist/helpers/paths.d.ts +41 -0
  43. package/dist/helpers/paths.js +67 -0
  44. package/dist/helpers/strings.d.ts +45 -0
  45. package/dist/helpers/strings.js +97 -0
  46. package/dist/index.d.ts +135 -0
  47. package/dist/index.js +1087 -0
  48. package/dist/merge-utils.d.ts +22 -0
  49. package/dist/merge-utils.js +34 -0
  50. package/dist/orchestrator.d.ts +194 -0
  51. package/dist/orchestrator.js +1169 -0
  52. package/dist/output.d.ts +106 -0
  53. package/dist/output.js +243 -0
  54. package/dist/progress.d.ts +228 -0
  55. package/dist/progress.js +644 -0
  56. package/dist/providers/copilot.d.ts +247 -0
  57. package/dist/providers/copilot.js +598 -0
  58. package/dist/providers/index.d.ts +15 -0
  59. package/dist/providers/index.js +12 -0
  60. package/dist/providers/opencode.d.ts +156 -0
  61. package/dist/providers/opencode.js +416 -0
  62. package/dist/providers/types.d.ts +156 -0
  63. package/dist/providers/types.js +16 -0
  64. package/dist/resources.d.ts +76 -0
  65. package/dist/resources.js +151 -0
  66. package/dist/search-index.d.ts +71 -0
  67. package/dist/search-index.js +187 -0
  68. package/dist/search-mcp.d.ts +25 -0
  69. package/dist/search-mcp.js +100 -0
  70. package/dist/server-utils.d.ts +56 -0
  71. package/dist/server-utils.js +135 -0
  72. package/dist/session.d.ts +227 -0
  73. package/dist/session.js +370 -0
  74. package/dist/types.d.ts +272 -0
  75. package/dist/types.js +5 -0
  76. package/dist/worktree.d.ts +82 -0
  77. package/dist/worktree.js +187 -0
  78. package/package.json +45 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * LLM-assisted architecture overview generation agent for Hem.
3
+ *
4
+ * Post-processing agent that runs AFTER cross-ref and organization agents.
5
+ * Uses an OpenCode session to generate the top-level `architecture.md`
6
+ * document by reading all generated documentation files and exploration
7
+ * findings.
8
+ *
9
+ * Architecture (v2):
10
+ * - The agent writes `architecture.md` directly via the edit tool.
11
+ * - The pipeline discovers what was written by scanning disk afterward.
12
+ * - Reads existing generated docs from disk for context.
13
+ * - Runs as the last content agent in the pipeline (after org + xref).
14
+ */
15
+ import pLimit from "p-limit";
16
+ import { extractJSON } from "../helpers/parsing.js";
17
+ import { BaseAgent } from "./base-agent.js";
18
+ import { computeMaxConcurrency } from "../resources.js";
19
+ import { AuthExpiredError } from "../auth.js";
20
+ // ── Constants ───────────────────────────────────────────────────────────
21
+ /**
22
+ * Maximum character count for the full architecture prompt before
23
+ * switching to chunked mode. Set conservatively below typical LLM
24
+ * context windows (~200K tokens ≈ ~800K chars) to leave room for
25
+ * the agent's tool interactions and responses.
26
+ */
27
+ export const ARCH_PROMPT_CHAR_LIMIT = 400_000;
28
+ /**
29
+ * Target character count per chunk when splitting findings and group
30
+ * summaries for chunked architecture generation. Each chunk should
31
+ * comfortably fit in a single prompt alongside instructions.
32
+ */
33
+ export const ARCH_CHUNK_TARGET_CHARS = 150_000;
34
+ // ── Agent ───────────────────────────────────────────────────────────────
35
+ /**
36
+ * An agent that uses an LLM to generate the architecture overview
37
+ * document for a project's documentation.
38
+ *
39
+ * Writes `architecture.md` directly via the edit tool. The pipeline
40
+ * discovers what was written by scanning disk afterward.
41
+ */
42
+ export class ArchitectureAgent extends BaseAgent {
43
+ constructor(provider) {
44
+ super(provider);
45
+ }
46
+ /**
47
+ * Run the architecture overview generation pipeline.
48
+ *
49
+ * When the full prompt would exceed `ARCH_PROMPT_CHAR_LIMIT`, the agent
50
+ * automatically switches to chunked mode: findings and group summaries
51
+ * are split into manageable batches, each batch is summarised by a
52
+ * dedicated session, and a final synthesis session combines the chunk
53
+ * summaries into the architecture overview.
54
+ *
55
+ * @param params - All inputs needed for the architecture prompt.
56
+ * @param verbose - Optional logging callback (writes to stderr).
57
+ * @throws If session creation or prompting fails.
58
+ */
59
+ async run(params, verbose) {
60
+ const tag = "arch-agent";
61
+ // 1. Build prompt and check size
62
+ const prompt = ArchitectureAgent.buildPrompt(params);
63
+ if (verbose) {
64
+ verbose(`[${tag}] Prompt: ${prompt.length.toLocaleString()} chars`);
65
+ }
66
+ // 2. If prompt exceeds limit, use chunked mode
67
+ if (prompt.length > ARCH_PROMPT_CHAR_LIMIT) {
68
+ if (verbose) {
69
+ verbose(`[${tag}] Prompt exceeds ${ARCH_PROMPT_CHAR_LIMIT.toLocaleString()} char limit, switching to chunked mode`);
70
+ }
71
+ return this.runChunked(params, verbose);
72
+ }
73
+ // 3. Create a new session (normal mode)
74
+ const sessionId = await this.createSession("Hem: architecture overview");
75
+ if (verbose) {
76
+ verbose(`[${tag}] Session created: ${sessionId}`);
77
+ }
78
+ // 4. Send prompt and wait for response — use hem-arch agent
79
+ await this.provider.prompt(sessionId, prompt, { agent: "hem-arch" });
80
+ if (verbose) {
81
+ verbose(`[${tag}] Agent completed`);
82
+ }
83
+ }
84
+ /**
85
+ * Chunked architecture generation for large projects.
86
+ *
87
+ * 1. Splits exploration findings and group summaries into chunks that
88
+ * fit within context limits.
89
+ * 2. Runs a sequential summarisation session for each chunk.
90
+ * 3. Runs a final synthesis session that combines all chunk summaries
91
+ * and writes `architecture.md` via the edit tool.
92
+ */
93
+ async runChunked(params, verbose) {
94
+ const tag = "arch-agent";
95
+ // 1. Chunk findings + group summaries
96
+ const chunks = ArchitectureAgent.chunkFindings(params.allFindings, params.allGroupSummaries, ARCH_CHUNK_TARGET_CHARS);
97
+ if (verbose) {
98
+ verbose(`[${tag}] Split into ${chunks.length} chunks for summarisation`);
99
+ }
100
+ // 2. Summarise chunks in parallel (bounded by resource concurrency)
101
+ const concurrency = computeMaxConcurrency();
102
+ const limit = pLimit(concurrency);
103
+ const settled = await Promise.allSettled(chunks.map((chunk, i) => limit(async () => {
104
+ const chunkLabel = `chunk-${i + 1}/${chunks.length}`;
105
+ if (verbose) {
106
+ verbose(`[${tag}] Summarising ${chunkLabel} (${chunk.findings.length} findings, ${chunk.groups.length} groups)`);
107
+ }
108
+ const chunkPrompt = ArchitectureAgent.buildChunkSummaryPrompt(params.projectName, chunk.findings, chunk.groups, i, chunks.length);
109
+ if (verbose) {
110
+ verbose(`[${tag}] ${chunkLabel} prompt: ${chunkPrompt.length.toLocaleString()} chars`);
111
+ }
112
+ const sessionId = await this.createSession(`Hem: arch ${chunkLabel}`);
113
+ const response = await this.provider.prompt(sessionId, chunkPrompt, { agent: "hem-arch" }) ?? "";
114
+ const summary = ArchitectureAgent.parseChunkSummary(response, i);
115
+ if (verbose) {
116
+ verbose(`[${tag}] ${chunkLabel} summarised: ${summary.patterns.length} patterns, ${summary.components.length} components`);
117
+ }
118
+ return summary;
119
+ })));
120
+ // Collect results; use empty fallback for failed chunks so synthesis still runs.
121
+ // Re-throw AuthExpiredError immediately.
122
+ const chunkSummaries = [];
123
+ for (let i = 0; i < settled.length; i++) {
124
+ const result = settled[i];
125
+ if (result.status === "fulfilled") {
126
+ chunkSummaries.push(result.value);
127
+ }
128
+ else {
129
+ if (result.reason instanceof AuthExpiredError) {
130
+ throw result.reason;
131
+ }
132
+ if (verbose) {
133
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
134
+ verbose(`[${tag}] chunk-${i + 1}/${chunks.length} failed: ${msg}`);
135
+ }
136
+ // Push an empty fallback so the synthesis index is not skewed
137
+ chunkSummaries.push(ArchitectureAgent.parseChunkSummary("", i));
138
+ }
139
+ }
140
+ // 3. Final synthesis — combine chunk summaries into architecture.md
141
+ if (verbose) {
142
+ verbose(`[${tag}] Running synthesis from ${chunkSummaries.length} chunk summaries`);
143
+ }
144
+ const synthesisPrompt = ArchitectureAgent.buildSynthesisPrompt(params, chunkSummaries);
145
+ if (verbose) {
146
+ verbose(`[${tag}] Synthesis prompt: ${synthesisPrompt.length.toLocaleString()} chars`);
147
+ }
148
+ const synthSessionId = await this.createSession("Hem: architecture synthesis");
149
+ if (verbose) {
150
+ verbose(`[${tag}] Synthesis session created: ${synthSessionId}`);
151
+ }
152
+ await this.provider.prompt(synthSessionId, synthesisPrompt, { agent: "hem-arch" });
153
+ if (verbose) {
154
+ verbose(`[${tag}] Chunked architecture generation completed`);
155
+ }
156
+ }
157
+ // ── Static helpers (pure functions, easy to unit test) ───────────────
158
+ /**
159
+ * Builds the architecture overview prompt from exploration findings
160
+ * and group summaries.
161
+ */
162
+ static buildPrompt(params) {
163
+ const { projectName, sourceRoot, destinationPath, allFindings, allGroupSummaries, allDocFiles, } = params;
164
+ const parts = [];
165
+ // 1. System-level instructions
166
+ parts.push(`Generate a comprehensive architecture overview that gives readers a mental model`, `of the entire system before they dive into individual feature or module pages.`, "", `**Write files directly using the edit tool.** Do NOT return Markdown content`, `in the response text. Instead, use the edit tool to create \`architecture.md\``, `in the destination directory. When done, stop.`, "");
167
+ // 2. Where to write
168
+ parts.push("## Destination", "", `Write the architecture overview to: \`${destinationPath}/architecture.md\``, "");
169
+ // 3. Quality standards
170
+ parts.push("## Quality Standard: Synthesize Questions Across the System", "", `Your primary quality standard is: **the architecture overview must synthesize`, `operational questions from ALL exploration groups into a system-wide view,`, `identify cross-cutting patterns, and link to feature pages for detailed answers.**`, "", `### Synthesize operational questions across groups`, "", `- Review the questions and integrations from EVERY group's exploration findings.`, `- Identify system-wide patterns (shared monitoring, common databases, etc.).`, `- Surface these patterns in dedicated architecture sections.`, `- Link to specific feature pages where full answers live — do NOT duplicate.`, "", `### Address cross-cutting concerns`, "", `- Secrets rotation, monitoring, shared databases, error handling, auth, etc.`, "");
171
+ parts.push("## Documentation quality standards", "");
172
+ parts.push(`1. **WHAT and WHY first**: Start with what the application is and why it exists.`, "", `2. **System-level Mermaid diagrams**: Always generate a system topology diagram.`, ` Add more diagrams based on complexity signals.`, "", `3. **No verbatim source code**: Reference files by path only.`, "", `4. **Google Markdown style guide**: ATX headings, single H1, informative links.`, "", `5. **Link to detail pages, don't duplicate**: Reference feature pages.`, "");
173
+ // 4. Task description
174
+ parts.push("## Your task", "");
175
+ parts.push(`Generate the architecture overview for **${projectName}**.`, "", `**Source root**: ${sourceRoot}`, "");
176
+ // 5. Read existing generated docs for context
177
+ if (allDocFiles.length > 0) {
178
+ parts.push("## Generated documentation files", "");
179
+ parts.push("These documentation files have already been generated. Read them using tools", "to understand what content exists, then link to them from the architecture overview.", "");
180
+ for (const docFile of allDocFiles) {
181
+ parts.push(`- \`${destinationPath}/${docFile}\``);
182
+ }
183
+ parts.push("");
184
+ }
185
+ // 6. Exploration findings
186
+ parts.push("## Exploration findings", "");
187
+ if (allFindings.length > 0) {
188
+ parts.push("The exploration phase discovered these findings across all file groups:", "");
189
+ parts.push(ArchitectureAgent.formatAllFindings(allFindings));
190
+ }
191
+ else {
192
+ parts.push("No exploration findings available. Use tools to read source files directly.", "");
193
+ }
194
+ // 7. File groups
195
+ parts.push("## File groups in this project", "");
196
+ parts.push(ArchitectureAgent.formatGroupSummaries(allGroupSummaries));
197
+ parts.push("");
198
+ // 8. How to work
199
+ parts.push("## How to work", "");
200
+ parts.push(`1. **Read generated docs**: Use tools to read the generated documentation files`, ` for context about what has already been documented.`, `2. **Read key source files**: Use tools to read entry points and config files.`, `3. **Synthesize questions across groups**: Identify system-wide patterns.`, `4. **Generate diagrams**: Always generate a system topology Mermaid diagram.`, `5. **Write the file**: Use the edit tool to write \`architecture.md\`.`, `6. **Link to detail pages**: Use relative paths to link to generated doc pages.`, "");
201
+ // 9. Document structure
202
+ parts.push("## Document structure", "");
203
+ parts.push(`Follow this general structure for \`architecture.md\`:`, "", `- \`# ${projectName} — Architecture Overview\``, `- What is ${projectName}?`, `- Why does it exist?`, `- System Architecture (with Mermaid diagram)`, `- Data Flow (with diagram if applicable)`, `- Infrastructure`, `- Key Design Decisions`, `- Cross-Cutting Concerns`, `- Component Index (links to generated pages)`, "");
204
+ // 10. Done
205
+ parts.push("## When you are done", "");
206
+ parts.push(`After writing \`architecture.md\` using the edit tool, simply stop.`, `Do NOT return a JSON manifest or any other structured output.`, `The pipeline will scan the destination directory to discover what you wrote.`);
207
+ return parts.join("\n");
208
+ }
209
+ /**
210
+ * Formats all exploration findings for the architecture prompt.
211
+ */
212
+ static formatAllFindings(allFindings) {
213
+ return allFindings
214
+ .map((f) => `### Group: ${f.groupId}\n\n${f.text}`)
215
+ .join("\n\n");
216
+ }
217
+ /**
218
+ * Formats file group summaries as a list for the prompt.
219
+ */
220
+ static formatGroupSummaries(groups) {
221
+ if (groups.length === 0)
222
+ return "No file groups defined.";
223
+ const parts = [];
224
+ for (const group of groups) {
225
+ parts.push(`- **${group.label}** (${group.id})`);
226
+ parts.push(` Files: ${group.files.join(", ")}`);
227
+ }
228
+ return parts.join("\n");
229
+ }
230
+ // ── Chunked-mode helpers ──────────────────────────────────────────────
231
+ /**
232
+ * Split exploration findings and group summaries into chunks that each
233
+ * stay under `targetChars` when formatted.
234
+ *
235
+ * Findings and their corresponding group summaries are kept together.
236
+ * Groups without findings are distributed across chunks to balance
237
+ * size.
238
+ *
239
+ * @returns Array of chunks, each with a subset of findings and groups.
240
+ */
241
+ static chunkFindings(allFindings, allGroupSummaries, targetChars) {
242
+ // Build a lookup of group summaries by id for quick access
243
+ const groupById = new Map(allGroupSummaries.map((g) => [g.id, g]));
244
+ // Pair each finding with its group summary and compute formatted size
245
+ const items = allFindings.map((f) => {
246
+ const group = groupById.get(f.groupId);
247
+ const groupText = group
248
+ ? ArchitectureAgent.formatGroupSummaries([group])
249
+ : "";
250
+ return {
251
+ finding: f,
252
+ group,
253
+ charSize: f.text.length + groupText.length,
254
+ };
255
+ });
256
+ // Collect group summaries that have no corresponding findings
257
+ const findingGroupIds = new Set(allFindings.map((f) => f.groupId));
258
+ const orphanGroups = allGroupSummaries.filter((g) => !findingGroupIds.has(g.id));
259
+ // Greedily pack items into chunks
260
+ const chunks = [];
261
+ let currentChunk = { findings: [], groups: [] };
262
+ let currentSize = 0;
263
+ for (const item of items) {
264
+ // Start a new chunk if adding this item would exceed the target
265
+ // (unless the current chunk is empty — always add at least one item)
266
+ if (currentSize > 0 && currentSize + item.charSize > targetChars) {
267
+ chunks.push(currentChunk);
268
+ currentChunk = { findings: [], groups: [] };
269
+ currentSize = 0;
270
+ }
271
+ currentChunk.findings.push(item.finding);
272
+ if (item.group) {
273
+ currentChunk.groups.push(item.group);
274
+ }
275
+ currentSize += item.charSize;
276
+ }
277
+ // Push last chunk if non-empty
278
+ if (currentChunk.findings.length > 0 || orphanGroups.length > 0) {
279
+ chunks.push(currentChunk);
280
+ }
281
+ // Distribute orphan groups across chunks (round-robin)
282
+ if (orphanGroups.length > 0 && chunks.length > 0) {
283
+ for (let i = 0; i < orphanGroups.length; i++) {
284
+ chunks[i % chunks.length].groups.push(orphanGroups[i]);
285
+ }
286
+ }
287
+ // Edge case: no findings and no groups — return a single empty chunk
288
+ if (chunks.length === 0) {
289
+ chunks.push({ findings: [], groups: [] });
290
+ }
291
+ return chunks;
292
+ }
293
+ /**
294
+ * Build a prompt for summarising a single chunk of findings and groups.
295
+ *
296
+ * The LLM should return a structured JSON summary that can be fed
297
+ * into the synthesis phase.
298
+ */
299
+ static buildChunkSummaryPrompt(projectName, findings, groups, chunkIndex, totalChunks) {
300
+ const parts = [];
301
+ parts.push(`Summarise chunk ${chunkIndex + 1} of ${totalChunks} for the **${projectName}**`, `project as part of an architecture analysis.`, "", `Read the exploration findings and group summaries below, then produce a`, `structured JSON summary of the architectural insights.`, "", `Respond with ONLY a JSON object inside a \`\`\`json code fence. No other text.`, "", `The JSON must match this schema:`, "```json", JSON.stringify({
302
+ patterns: ["Architectural pattern descriptions"],
303
+ crossCuttingConcerns: ["Cross-cutting concern descriptions"],
304
+ integrations: ["External integration descriptions"],
305
+ components: ["Component name: responsibility"],
306
+ dataFlows: ["Data flow or interaction pattern descriptions"],
307
+ rawSummary: "2-3 paragraph summary of this chunk's architecture",
308
+ }, null, 2), "```", "");
309
+ // Findings
310
+ parts.push("## Exploration findings for this chunk", "");
311
+ if (findings.length > 0) {
312
+ parts.push(ArchitectureAgent.formatAllFindings(findings));
313
+ }
314
+ else {
315
+ parts.push("No exploration findings in this chunk.", "");
316
+ }
317
+ // Groups
318
+ parts.push("## File groups in this chunk", "");
319
+ if (groups.length > 0) {
320
+ parts.push(ArchitectureAgent.formatGroupSummaries(groups));
321
+ }
322
+ else {
323
+ parts.push("No file groups in this chunk.");
324
+ }
325
+ parts.push("");
326
+ return parts.join("\n");
327
+ }
328
+ /**
329
+ * Parse the LLM response from a chunk summarisation session into an
330
+ * `ArchChunkSummary`.
331
+ *
332
+ * Falls back to a minimal summary containing just the raw response
333
+ * text if JSON extraction fails — chunked mode should be resilient.
334
+ */
335
+ static parseChunkSummary(response, chunkIndex) {
336
+ const fallback = {
337
+ chunkIndex,
338
+ patterns: [],
339
+ crossCuttingConcerns: [],
340
+ integrations: [],
341
+ components: [],
342
+ dataFlows: [],
343
+ rawSummary: response.slice(0, 5000),
344
+ };
345
+ try {
346
+ const json = extractJSON(response);
347
+ const parsed = JSON.parse(json);
348
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
349
+ return fallback;
350
+ }
351
+ const obj = parsed;
352
+ const toStringArray = (val) => {
353
+ if (!Array.isArray(val))
354
+ return [];
355
+ return val.filter((v) => typeof v === "string");
356
+ };
357
+ return {
358
+ chunkIndex,
359
+ patterns: toStringArray(obj.patterns),
360
+ crossCuttingConcerns: toStringArray(obj.crossCuttingConcerns),
361
+ integrations: toStringArray(obj.integrations),
362
+ components: toStringArray(obj.components),
363
+ dataFlows: toStringArray(obj.dataFlows),
364
+ rawSummary: typeof obj.rawSummary === "string"
365
+ ? obj.rawSummary
366
+ : response.slice(0, 5000),
367
+ };
368
+ }
369
+ catch {
370
+ return fallback;
371
+ }
372
+ }
373
+ /**
374
+ * Build the final synthesis prompt that combines all chunk summaries
375
+ * and instructs the agent to write `architecture.md`.
376
+ *
377
+ * Reuses the same quality standards and document structure as the
378
+ * normal-mode `buildPrompt()`, but substitutes chunk summaries for
379
+ * the raw findings.
380
+ */
381
+ static buildSynthesisPrompt(params, chunkSummaries) {
382
+ const { projectName, sourceRoot, destinationPath, allDocFiles } = params;
383
+ const parts = [];
384
+ // 1. System-level instructions
385
+ parts.push(`Generate a comprehensive architecture overview that gives readers a mental model`, `of the entire system before they dive into individual feature or module pages.`, "", `**Write files directly using the edit tool.** Do NOT return Markdown content`, `in the response text. Instead, use the edit tool to create \`architecture.md\``, `in the destination directory. When done, stop.`, "", `**NOTE**: This project is too large for a single-pass analysis. The findings`, `below are pre-summarised from ${chunkSummaries.length} analysis chunks.`, `Synthesize them into a unified architecture overview.`, "");
386
+ // 2. Where to write
387
+ parts.push("## Destination", "", `Write the architecture overview to: \`${destinationPath}/architecture.md\``, "");
388
+ // 3. Quality standards (same as buildPrompt)
389
+ parts.push("## Quality Standard: Synthesize Questions Across the System", "", `Your primary quality standard is: **the architecture overview must synthesize`, `operational questions from ALL exploration groups into a system-wide view,`, `identify cross-cutting patterns, and link to feature pages for detailed answers.**`, "");
390
+ parts.push("## Documentation quality standards", "");
391
+ parts.push(`1. **WHAT and WHY first**: Start with what the application is and why it exists.`, "", `2. **System-level Mermaid diagrams**: Always generate a system topology diagram.`, ` Add more diagrams based on complexity signals.`, "", `3. **No verbatim source code**: Reference files by path only.`, "", `4. **Google Markdown style guide**: ATX headings, single H1, informative links.`, "", `5. **Link to detail pages, don't duplicate**: Reference feature pages.`, "");
392
+ // 4. Task description
393
+ parts.push("## Your task", "");
394
+ parts.push(`Generate the architecture overview for **${projectName}**.`, "", `**Source root**: ${sourceRoot}`, "");
395
+ // 5. Read existing generated docs for context
396
+ if (allDocFiles.length > 0) {
397
+ parts.push("## Generated documentation files", "");
398
+ parts.push("These documentation files have already been generated. Read them using tools", "to understand what content exists, then link to them from the architecture overview.", "");
399
+ for (const docFile of allDocFiles) {
400
+ parts.push(`- \`${destinationPath}/${docFile}\``);
401
+ }
402
+ parts.push("");
403
+ }
404
+ // 6. Chunk summaries (replacing raw findings)
405
+ parts.push("## Pre-analysed architecture summaries", "");
406
+ parts.push(`The following summaries were generated from ${chunkSummaries.length} chunks`, `of exploration findings. Synthesize them into a unified view.`, "");
407
+ for (const summary of chunkSummaries) {
408
+ parts.push(`### Chunk ${summary.chunkIndex + 1}`, "");
409
+ if (summary.patterns.length > 0) {
410
+ parts.push("**Architectural patterns**:");
411
+ for (const p of summary.patterns) {
412
+ parts.push(`- ${p}`);
413
+ }
414
+ parts.push("");
415
+ }
416
+ if (summary.crossCuttingConcerns.length > 0) {
417
+ parts.push("**Cross-cutting concerns**:");
418
+ for (const c of summary.crossCuttingConcerns) {
419
+ parts.push(`- ${c}`);
420
+ }
421
+ parts.push("");
422
+ }
423
+ if (summary.integrations.length > 0) {
424
+ parts.push("**Integrations**:");
425
+ for (const i of summary.integrations) {
426
+ parts.push(`- ${i}`);
427
+ }
428
+ parts.push("");
429
+ }
430
+ if (summary.components.length > 0) {
431
+ parts.push("**Components**:");
432
+ for (const c of summary.components) {
433
+ parts.push(`- ${c}`);
434
+ }
435
+ parts.push("");
436
+ }
437
+ if (summary.dataFlows.length > 0) {
438
+ parts.push("**Data flows**:");
439
+ for (const d of summary.dataFlows) {
440
+ parts.push(`- ${d}`);
441
+ }
442
+ parts.push("");
443
+ }
444
+ if (summary.rawSummary) {
445
+ parts.push("**Summary**:", "", summary.rawSummary, "");
446
+ }
447
+ }
448
+ // 7. How to work
449
+ parts.push("## How to work", "");
450
+ parts.push(`1. **Read generated docs**: Use tools to read the generated documentation files`, ` for context about what has already been documented.`, `2. **Read key source files**: Use tools to read entry points and config files.`, `3. **Synthesize the chunk summaries**: Combine all chunk summaries into a unified view.`, `4. **Generate diagrams**: Always generate a system topology Mermaid diagram.`, `5. **Write the file**: Use the edit tool to write \`architecture.md\`.`, `6. **Link to detail pages**: Use relative paths to link to generated doc pages.`, "");
451
+ // 8. Document structure
452
+ parts.push("## Document structure", "");
453
+ parts.push(`Follow this general structure for \`architecture.md\`:`, "", `- \`# ${projectName} — Architecture Overview\``, `- What is ${projectName}?`, `- Why does it exist?`, `- System Architecture (with Mermaid diagram)`, `- Data Flow (with diagram if applicable)`, `- Infrastructure`, `- Key Design Decisions`, `- Cross-Cutting Concerns`, `- Component Index (links to generated pages)`, "");
454
+ // 9. Done
455
+ parts.push("## When you are done", "");
456
+ parts.push(`After writing \`architecture.md\` using the edit tool, simply stop.`, `Do NOT return a JSON manifest or any other structured output.`, `The pipeline will scan the destination directory to discover what you wrote.`);
457
+ return parts.join("\n");
458
+ }
459
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Abstract base class for all Hem agents.
3
+ *
4
+ * Encapsulates the shared session-creation and auth-error-handling
5
+ * boilerplate that was previously duplicated across every agent.
6
+ * Subclasses store their own `run()` signatures and prompt logic —
7
+ * the base class only provides infrastructure.
8
+ *
9
+ * Design decisions:
10
+ * - Abstract class (not interface) — the value is shared *implementation*
11
+ * (session creation with auth-error handling), not just a type contract.
12
+ * - No abstract `run()` — agents have intentionally different parameter
13
+ * lists and return types. The base class does not constrain them.
14
+ * - Low-level session ops (`promptAsync`, `session.abort`, `event.subscribe`)
15
+ * are accessed via `this.provider.session.*` and `this.provider.event.*`.
16
+ */
17
+ import type { Provider } from "../providers/types.js";
18
+ /**
19
+ * Abstract base class providing shared session lifecycle management
20
+ * for all Hem agents.
21
+ *
22
+ * Subclasses receive `provider` via the constructor and use
23
+ * `createSession()` to create new sessions with built-in auth-error
24
+ * detection and handling.
25
+ */
26
+ export declare abstract class BaseAgent {
27
+ protected provider: Provider;
28
+ constructor(provider: Provider);
29
+ /**
30
+ * Create a new session with auth-error handling.
31
+ *
32
+ * Encapsulates the session-creation boilerplate:
33
+ * 1. Calls `this.provider.createSession()`.
34
+ * 2. Catches thrown errors and checks for auth expiry.
35
+ * 3. Returns the session ID on success.
36
+ *
37
+ * @param _title - Ignored (kept for call-site compatibility; Dispatch
38
+ * pattern has no session titles).
39
+ * @returns The created session's ID.
40
+ * @throws {AuthExpiredError} If the creation fails due to expired auth.
41
+ * @throws {Error} If creation fails for any other reason.
42
+ */
43
+ protected createSession(_title?: string): Promise<string>;
44
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Abstract base class for all Hem agents.
3
+ *
4
+ * Encapsulates the shared session-creation and auth-error-handling
5
+ * boilerplate that was previously duplicated across every agent.
6
+ * Subclasses store their own `run()` signatures and prompt logic —
7
+ * the base class only provides infrastructure.
8
+ *
9
+ * Design decisions:
10
+ * - Abstract class (not interface) — the value is shared *implementation*
11
+ * (session creation with auth-error handling), not just a type contract.
12
+ * - No abstract `run()` — agents have intentionally different parameter
13
+ * lists and return types. The base class does not constrain them.
14
+ * - Low-level session ops (`promptAsync`, `session.abort`, `event.subscribe`)
15
+ * are accessed via `this.provider.session.*` and `this.provider.event.*`.
16
+ */
17
+ import { isAuthExpired, AuthExpiredError } from "../auth.js";
18
+ // ── Base Agent ──────────────────────────────────────────────────────────
19
+ /**
20
+ * Abstract base class providing shared session lifecycle management
21
+ * for all Hem agents.
22
+ *
23
+ * Subclasses receive `provider` via the constructor and use
24
+ * `createSession()` to create new sessions with built-in auth-error
25
+ * detection and handling.
26
+ */
27
+ export class BaseAgent {
28
+ provider;
29
+ constructor(provider) {
30
+ this.provider = provider;
31
+ }
32
+ /**
33
+ * Create a new session with auth-error handling.
34
+ *
35
+ * Encapsulates the session-creation boilerplate:
36
+ * 1. Calls `this.provider.createSession()`.
37
+ * 2. Catches thrown errors and checks for auth expiry.
38
+ * 3. Returns the session ID on success.
39
+ *
40
+ * @param _title - Ignored (kept for call-site compatibility; Dispatch
41
+ * pattern has no session titles).
42
+ * @returns The created session's ID.
43
+ * @throws {AuthExpiredError} If the creation fails due to expired auth.
44
+ * @throws {Error} If creation fails for any other reason.
45
+ */
46
+ async createSession(_title) {
47
+ try {
48
+ return await this.provider.createSession();
49
+ }
50
+ catch (err) {
51
+ if (isAuthExpired(err)) {
52
+ throw new AuthExpiredError("the configured provider", err);
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ }