@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,147 @@
1
+ /**
2
+ * Arbiter agent for Hem's parallel cross-reference pass.
3
+ *
4
+ * The cross-ref arbiter is a lightweight coordinator that runs alongside N
5
+ * parallel cross-ref workers. It receives all broadcast messages, evaluates
6
+ * SUGGESTION messages, and issues DECISION directives telling specific
7
+ * workers what to do for link consistency. It does NOT edit files itself.
8
+ *
9
+ * Lifecycle (two-phase, driven by CrossRefAgent.runParallel):
10
+ * 1. `run()` — creates the session, sends the initial prompt via
11
+ * promptAsync (fire-and-forget). Returns { sessionId }.
12
+ * 2. `wrapUp()` — called after all workers complete. Sends a final
13
+ * prompt so the arbiter can issue any remaining
14
+ * DECISION messages. Non-fatal on failure.
15
+ */
16
+ import { isAuthExpired, AuthExpiredError } from "../auth.js";
17
+ import { BaseAgent } from "./base-agent.js";
18
+ // ── Agent ───────────────────────────────────────────────────────────────
19
+ /**
20
+ * A coordinator agent that monitors cross-ref worker broadcasts and issues
21
+ * DECISION directives to resolve cross-worker link consistency issues.
22
+ *
23
+ * Does NOT edit files — directs workers to make all changes.
24
+ */
25
+ export class CrossRefArbiterAgent extends BaseAgent {
26
+ constructor(provider) {
27
+ super(provider);
28
+ }
29
+ /**
30
+ * Create the arbiter session and send the initial prompt.
31
+ *
32
+ * The initial prompt is sent via `promptAsync` (fire-and-forget) because
33
+ * the arbiter sits idle until it receives broadcasts from workers.
34
+ *
35
+ * @returns The session ID so the caller can register it in the SSE
36
+ * relay map and later call `wrapUp()`.
37
+ * @throws {AuthExpiredError} If session creation or the initial prompt
38
+ * fails due to authentication expiry.
39
+ */
40
+ async run(params, verbose) {
41
+ const tag = "xref-arbiter";
42
+ // 1. Build prompt
43
+ const prompt = CrossRefArbiterAgent.buildPrompt(params);
44
+ if (verbose) {
45
+ verbose(`[${tag}] prompt ${prompt.length.toLocaleString()} chars`);
46
+ }
47
+ // 2. Create session
48
+ const sessionId = await this.createSession("Hem: xref-arbiter");
49
+ if (verbose) {
50
+ verbose(`[${tag}] session ${sessionId}`);
51
+ }
52
+ // 3. Fire the initial prompt (fire-and-forget — arbiter waits for broadcasts)
53
+ try {
54
+ await this.provider.session.promptAsync({
55
+ path: { id: sessionId },
56
+ body: {
57
+ parts: [{ type: "text", text: prompt }],
58
+ agent: "hem-xref",
59
+ },
60
+ });
61
+ }
62
+ catch (err) {
63
+ if (isAuthExpired(err)) {
64
+ throw new AuthExpiredError("the configured provider", err);
65
+ }
66
+ throw err;
67
+ }
68
+ if (verbose) {
69
+ verbose(`[${tag}] initial prompt sent (listening for broadcasts)`);
70
+ }
71
+ return { sessionId };
72
+ }
73
+ /**
74
+ * Send the wrap-up prompt after all workers have completed.
75
+ *
76
+ * Asks the arbiter to issue any remaining DECISION messages and then
77
+ * output its final `{ "status": "complete" }` response.
78
+ *
79
+ * This is intentionally **non-fatal** — if the arbiter fails, workers
80
+ * have already made their edits and the pipeline can continue.
81
+ */
82
+ async wrapUp(sessionId, verbose) {
83
+ const tag = "xref-arbiter";
84
+ if (verbose) {
85
+ verbose(`[${tag}] all workers done, sending wrap-up prompt`);
86
+ }
87
+ try {
88
+ const wrapUpResponse = await this.provider.prompt(sessionId, "All workers have completed their tasks. If you have any remaining " +
89
+ "DECISION messages to issue, broadcast them now. Otherwise, output your " +
90
+ 'final `{ "status": "complete" }` response.', { agent: "hem-xref" });
91
+ if (verbose) {
92
+ verbose(`[${tag}] wrap-up response (${(wrapUpResponse ?? "").length} chars)`);
93
+ }
94
+ }
95
+ catch (err) {
96
+ // Arbiter failure is not fatal — workers already made their edits
97
+ if (verbose) {
98
+ verbose(`[${tag}] ⚠ wrap-up failed: ${err instanceof Error ? err.message : String(err)}`);
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Builds the cross-ref arbiter prompt.
104
+ *
105
+ * The arbiter is a lightweight coordinator that:
106
+ * - Receives all broadcasts from cross-ref workers
107
+ * - Evaluates SUGGESTION messages and issues DECISION directives
108
+ * - Directs specific workers to add/update reciprocal links
109
+ * - Does NOT edit files directly
110
+ */
111
+ static buildPrompt(params) {
112
+ const { projectName, destinationPath, allDocFiles, workerAssignments, } = params;
113
+ const totalWorkers = workerAssignments.length;
114
+ const workerLabels = workerAssignments.map((a) => a.label);
115
+ const parts = [];
116
+ // ── Role ──────────────────────────────────────────────────────────
117
+ parts.push(`Coordinate a parallel cross-reference pass on`, `**${projectName}** as the **arbiter**. There are ${totalWorkers} workers running in parallel,`, `each responsible for adding cross-reference links to a subset of`, `documentation files. Coordinate them for link consistency.`, "", `Coordinate only — do NOT edit files. Workers have the edit tool`, `and make all file changes themselves. Issue DECISION messages to tell`, `workers what to do.`, "", `**IMPORTANT: Workers are terminated immediately after they finish.**`, `If a worker has already completed and you need further changes`, `to their files, use the RECALL mechanism (see below).`, "");
118
+ // ── Workers ───────────────────────────────────────────────────────
119
+ parts.push("## Workers", "", `There are ${totalWorkers} workers: ${workerLabels.join(", ")}`, "");
120
+ // ── Destination directory ─────────────────────────────────────────
121
+ parts.push("## Destination directory", "", `All documentation files are in: \`${destinationPath}\``, "");
122
+ // ── Worker file assignments ─────────────────────────────────────
123
+ parts.push("## Worker file assignments", "", "Each worker owns a specific subset of files. When issuing DECISION", "directives, always target the worker that owns the affected file(s).", "");
124
+ for (const { label, files } of workerAssignments) {
125
+ parts.push(`### ${label}`, "");
126
+ for (const file of files) {
127
+ parts.push(`- \`${destinationPath}/${file}\``);
128
+ }
129
+ parts.push("");
130
+ }
131
+ // ── All files (flat list for cross-reference) ─────────────────────
132
+ parts.push("## All documentation files", "");
133
+ for (const file of allDocFiles) {
134
+ parts.push(`- \`${destinationPath}/${file}\``);
135
+ }
136
+ parts.push("");
137
+ // ── How it works ──────────────────────────────────────────────────
138
+ parts.push("## How this works", "", "Workers broadcast messages as they work. You will receive these messages", "as user messages prefixed with \"Message from xref-worker-N:\". Message types:", "", "- **LINK-ADDED**: A worker added a cross-reference link to one of their files.", "- **SUGGESTION**: A worker identified a cross-worker link consistency issue", " they cannot resolve themselves (e.g., a reciprocal link is needed in", " another worker's file).", "- **ACK**: A worker completed an action (added/updated links as requested).", "");
139
+ // ── Your job ──────────────────────────────────────────────────────
140
+ parts.push("## Your job", "", "1. **Monitor all broadcasts.** You will passively receive LINK-ADDED", " and ACK messages — you do not need to respond to these unless", " you see a link consistency issue.", "", "2. **Act on SUGGESTION messages.** When a worker broadcasts a SUGGESTION", " (e.g., \"file X links to file Y but file Y does not link back\"),", " evaluate it and issue a DECISION telling the appropriate worker(s)", " what to do.", "", "3. **Issue DECISION messages.** Use the broadcast tool with one of these", " structured formats, always addressed to a specific worker:", "", " **ADD-LINK** — tell a worker to add a reciprocal cross-reference link:", "", ' `broadcast({ message: "DECISION: @xref-worker-2 ADD-LINK auth/overview.md → api/endpoints.md\\nAdd a link to api/endpoints.md in the Related Documentation section." })`', "", " **UPDATE-LINK** — tell a worker to update a cross-reference link:", "", ' `broadcast({ message: "DECISION: @xref-worker-2 UPDATE-LINK auth/overview.md\\nUpdate the link text for api/endpoints.md to be more descriptive." })`', "", " **Free-form** — for anything that doesn't fit the above patterns:", "", ' `broadcast({ message: "DECISION: @xref-worker-1 Review the Related Documentation section in auth/overview.md for completeness." })`', "", " **Rules for DECISION messages:**", " - **ALWAYS** address decisions to a specific worker using `@xref-worker-N`.", " - Messages without an `@` tag are broadcast to ALL workers, which wastes", " resources. Only use `@all-workers` when every worker needs to act.", " - If multiple workers need to act, issue multiple decisions each tagged", " to the specific worker.", "", "4. **Resolve conflicts.** If two workers propose contradictory link structures", " or make conflicting changes, issue a DECISION picking one approach.", "", "5. **Never edit files.** This is a coordination task, not editing. Workers have", " the edit tool and make all file changes themselves.", "");
141
+ // ── RECALL mechanism ──────────────────────────────────────────────
142
+ parts.push("## RECALL mechanism", "", "Workers are terminated immediately after they finish. If", "you discover during wrap-up that a worker's files need further changes", "(e.g., you spot link consistency issues after all workers have completed),", "you can recall a worker by broadcasting a message with this exact prefix:", "", ' `broadcast({ message: "RECALL: @xref-worker-2 Add a reciprocal link in auth/overview.md pointing back to api/endpoints.md" })`', "", "The orchestrator will intercept this message and spawn a **new** session", "with that worker's original file assignment plus your fix instructions.", "The recalled worker will make the edits directly.", "", "Rules for RECALL:", "- Use `RECALL:` prefix followed by `@xref-worker-N` and clear instructions.", "- Only recall workers whose files actually need changes.", "- Be specific about what needs to be fixed — the recalled worker gets", " your instructions verbatim.", "");
143
+ // ── Output format ─────────────────────────────────────────────────
144
+ parts.push("## Output format", "", "Wait for worker broadcasts. Process them as described above. When you are", "finished coordinating (no pending suggestions to resolve), respond with", "ONLY:", "", "```json", '{ "status": "complete" }', "```");
145
+ return parts.join("\n");
146
+ }
147
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * LLM-assisted documentation generation agent for Hem.
3
+ *
4
+ * Uses an OpenCode session to generate documentation for a single file
5
+ * group. The agent writes documentation files directly via the edit tool.
6
+ * The pipeline discovers what was written by scanning disk afterward.
7
+ *
8
+ * Architecture (v2):
9
+ * - The agent has full autonomy over file naming, structure, and how
10
+ * many files to create per group.
11
+ * - The agent writes files directly — Hem does NOT write files.
12
+ * - If existing docs are provided, the agent merges inline.
13
+ */
14
+ import type { Provider } from "../providers/types.js";
15
+ import type { FileGroup, GenerationContext, ExplorationFindings } from "../types.js";
16
+ import { BaseAgent } from "./base-agent.js";
17
+ /**
18
+ * An agent that uses an LLM to generate documentation for a single
19
+ * file group. The agent writes files directly via the edit tool.
20
+ */
21
+ export declare class DocumentationAgent extends BaseAgent {
22
+ constructor(provider: Provider);
23
+ /**
24
+ * Run the documentation generation pipeline for a single group.
25
+ *
26
+ * @param group - The file group to document.
27
+ * @param context - Shared generation context (includes exploration findings,
28
+ * existing docs for merge, destination path).
29
+ * @param verbose - Optional logging callback (writes to stderr).
30
+ * @throws If session creation or prompting fails.
31
+ */
32
+ run(group: FileGroup, context: GenerationContext, verbose?: (msg: string) => void): Promise<void>;
33
+ /**
34
+ * Returns the exploration findings text for inclusion in the prompt.
35
+ */
36
+ static formatFindings(findings: ExplorationFindings): string;
37
+ /**
38
+ * Summarizes all findings for cross-group context (excludes the current group).
39
+ */
40
+ static summarizeCrossGroupFindings(allFindings: ExplorationFindings[], currentGroupId: string): string;
41
+ /**
42
+ * Builds the documentation generation prompt.
43
+ *
44
+ * The agent writes files directly using the edit tool and returns
45
+ * a JSON manifest of files written. The prompt includes:
46
+ * - File group details
47
+ * - Quality standards
48
+ * - Exploration findings
49
+ * - Existing docs for merge context
50
+ * - Tool-usage instructions
51
+ * - Cross-group context
52
+ * - Output manifest format
53
+ */
54
+ static buildPrompt(group: FileGroup, context: GenerationContext): string;
55
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * LLM-assisted documentation generation agent for Hem.
3
+ *
4
+ * Uses an OpenCode session to generate documentation for a single file
5
+ * group. The agent writes documentation files directly via the edit tool.
6
+ * The pipeline discovers what was written by scanning disk afterward.
7
+ *
8
+ * Architecture (v2):
9
+ * - The agent has full autonomy over file naming, structure, and how
10
+ * many files to create per group.
11
+ * - The agent writes files directly — Hem does NOT write files.
12
+ * - If existing docs are provided, the agent merges inline.
13
+ */
14
+ import { BaseAgent } from "./base-agent.js";
15
+ // ── Agent ───────────────────────────────────────────────────────────────
16
+ /**
17
+ * An agent that uses an LLM to generate documentation for a single
18
+ * file group. The agent writes files directly via the edit tool.
19
+ */
20
+ export class DocumentationAgent extends BaseAgent {
21
+ constructor(provider) {
22
+ super(provider);
23
+ }
24
+ /**
25
+ * Run the documentation generation pipeline for a single group.
26
+ *
27
+ * @param group - The file group to document.
28
+ * @param context - Shared generation context (includes exploration findings,
29
+ * existing docs for merge, destination path).
30
+ * @param verbose - Optional logging callback (writes to stderr).
31
+ * @throws If session creation or prompting fails.
32
+ */
33
+ async run(group, context, verbose) {
34
+ const tag = `doc-agent:${group.id}`;
35
+ // 1. Build prompt (file contents are NOT embedded — agent reads via tools)
36
+ const prompt = DocumentationAgent.buildPrompt(group, context);
37
+ if (verbose) {
38
+ verbose(`[${tag}] Prompt: ${prompt.length.toLocaleString()} chars`);
39
+ }
40
+ // 2. Create a new session
41
+ const sessionId = await this.createSession(`Hem: doc — ${group.label}`);
42
+ if (verbose) {
43
+ verbose(`[${tag}] Session created: ${sessionId}`);
44
+ }
45
+ // 3. Send prompt and wait for response — use hem-doc agent
46
+ await this.provider.prompt(sessionId, prompt, { agent: "hem-doc" });
47
+ if (verbose) {
48
+ verbose(`[${tag}] Agent completed`);
49
+ }
50
+ }
51
+ // ── Static helpers (pure functions, easy to unit test) ───────────────
52
+ /**
53
+ * Returns the exploration findings text for inclusion in the prompt.
54
+ */
55
+ static formatFindings(findings) {
56
+ return findings.text;
57
+ }
58
+ /**
59
+ * Summarizes all findings for cross-group context (excludes the current group).
60
+ */
61
+ static summarizeCrossGroupFindings(allFindings, currentGroupId) {
62
+ const others = allFindings.filter((f) => f.groupId !== currentGroupId);
63
+ if (others.length === 0)
64
+ return "No other groups explored.";
65
+ return others.map((f) => `### ${f.groupId}\n${f.text}`).join("\n\n");
66
+ }
67
+ /**
68
+ * Builds the documentation generation prompt.
69
+ *
70
+ * The agent writes files directly using the edit tool and returns
71
+ * a JSON manifest of files written. The prompt includes:
72
+ * - File group details
73
+ * - Quality standards
74
+ * - Exploration findings
75
+ * - Existing docs for merge context
76
+ * - Tool-usage instructions
77
+ * - Cross-group context
78
+ * - Output manifest format
79
+ */
80
+ static buildPrompt(group, context) {
81
+ const parts = [];
82
+ const groupFindings = context.groupFindings;
83
+ const allFindings = context.explorationFindings;
84
+ // 1. System-level instructions
85
+ parts.push(`Generate documentation files that answer the questions the code asks — not`, `merely describe what the code does.`, "", `**You write files directly using the edit tool.** Do NOT return Markdown content`, `in your response text. Instead, use the edit tool to create and write files in`, `the destination directory. When you are done writing all files, stop.`, "");
86
+ // 2. Where to write files
87
+ parts.push("## Destination directory", "", `Write all documentation files under: \`${context.destinationPath}\``, "", `You have full autonomy over:`, `- **File naming**: Choose descriptive kebab-case filenames (e.g., \`user-authentication.md\`)`, `- **Directory structure**: Create subdirectories that make sense for this codebase`, `- **Number of files**: Create as many files as needed to properly document the group`, `- **File structure**: Design each document's heading hierarchy and sections`, "", `Guidelines for file organization:`, `- Choose a directory layout that reflects how the codebase is actually organized`, `- You may use flat files, subdirectories, or nested structures — whatever fits best`, `- Use \`.md\` extension for all files`, "");
88
+ // 3. Quality standards
89
+ parts.push("## Quality Standard: Answer Every Question", "", "Your primary quality standard is: **every question from the exploration findings", "MUST be answered in the generated documentation.** Do NOT leave any question", "unanswered. If you cannot find a definitive answer, state what is known and what", "requires further investigation — but NEVER silently skip a question.", "", "For each integration discovered in the exploration findings:", "", "1. **Use `webfetch` to research answers**: When an integration has an", " `officialDocsUrl`, fetch that URL to find authoritative answers.", "2. **Address HOW, not just WHAT**: Explain HOW to access, query, monitor,", " and troubleshoot each integration.", "3. **Answer operational questions**: Every `operationalQuestions` entry for each", " integration MUST be answered in the documentation.", "");
90
+ parts.push("## Documentation quality standards", "");
91
+ parts.push("You MUST follow these principles:", "");
92
+ parts.push(`1. **Answer the questions the code asks**: For every service, integration, or`, ` dependency, address the operational and conceptual questions a reader would`, ` naturally ask.`, "", `2. **WHAT/WHY/HOW structure**:`, ` - Overview pages: convey WHAT the application is and WHY it exists`, ` - Module/component pages: convey WHAT it does, WHY it exists, and HOW it works`, "", `3. **No verbatim source code (FR-006 / FR-007)**: Reference source files by`, ` path link only.`, "", `4. **Google Markdown style guide**: ATX-style headings, single H1 per page,`, ` informative link titles, 4-space indent for nested lists, Markdown over HTML.`, "", `5. **Mermaid diagrams**: Include only when complexity warrants them (check`, ` exploration findings for \`ComplexitySignal\` entries).`, "", `6. **Readable structure**: Heading hierarchy, section ordering, and navigation`, ` flow are first-class concerns.`, "", `7. **Cross-references**: Link to related documentation pages using relative paths.`, "");
93
+ // 4. Task description
94
+ parts.push("## Your task", "");
95
+ parts.push("Generate documentation for the following file group.", "");
96
+ parts.push(`**Group**: ${group.label} (ID: ${group.id})`, `**Group type**: ${group.type} (${group.type === "vertical" ? "feature slice" : "architectural layer"})`, `**Files to document**:`);
97
+ for (const file of group.files) {
98
+ parts.push(`- \`${file.path}\``);
99
+ }
100
+ parts.push("");
101
+ parts.push(`**Source root**: ${context.sourceRoot}`, `**Project name**: ${context.projectName}`, "");
102
+ // 5. Exploration findings for this group
103
+ parts.push("## Exploration findings for this group", "");
104
+ if (groupFindings) {
105
+ parts.push("The exploration phase discovered these findings for your group:", "");
106
+ parts.push(DocumentationAgent.formatFindings(groupFindings));
107
+ }
108
+ else {
109
+ parts.push("No exploration findings available for this group. Use tools to read and analyze the source files directly.", "");
110
+ }
111
+ // 6. Cross-group context
112
+ parts.push("## Cross-group context", "");
113
+ parts.push("Other groups discovered these integrations and dependencies that may relate to", "your group:", "");
114
+ parts.push(DocumentationAgent.summarizeCrossGroupFindings(allFindings, group.id));
115
+ parts.push("");
116
+ // 7. Existing docs — search-before-write with skip/update/create decisions
117
+ if (context.existingDocs.length > 0 || (context.mentionedDocPaths && context.mentionedDocPaths.length > 0)) {
118
+ parts.push("## Existing documentation in destination", "");
119
+ parts.push("The destination directory already contains documentation files.", "**Before writing ANY file, you MUST search for related existing docs.**", "", "### Decision criteria for each topic you plan to document:", "", "1. **SKIP** — if an existing doc already covers the topic accurately and", " completely. Do NOT rewrite content that is already correct.", "2. **UPDATE** — if an existing doc covers the topic but is stale, incomplete,", " or missing sections. Update it **in place** using the edit tool. Preserve", " accurate content; fix or expand what is stale or missing.", "3. **CREATE** — if no existing doc covers the topic. Write a new file.", "", "**Content-only changes**: Do NOT rename, move, or delete existing files.", "Only modify file content.", "");
120
+ // Full content for the most relevant docs
121
+ if (context.existingDocs.length > 0) {
122
+ parts.push(`### Most relevant existing docs (${context.existingDocs.length} file${context.existingDocs.length === 1 ? "" : "s"}, full content)`, "");
123
+ for (const doc of context.existingDocs) {
124
+ parts.push(`#### \`${doc.path}\``);
125
+ parts.push("```markdown");
126
+ parts.push(doc.content);
127
+ parts.push("```");
128
+ parts.push("");
129
+ }
130
+ }
131
+ // Path-only mentions for the rest
132
+ if (context.mentionedDocPaths && context.mentionedDocPaths.length > 0) {
133
+ parts.push(`### Other existing docs (${context.mentionedDocPaths.length} file${context.mentionedDocPaths.length === 1 ? "" : "s"}, paths only)`, "", "These files also exist in the destination directory. Use `cat` or `grep`", "to read them if they might overlap with your topic:", "");
134
+ for (const p of context.mentionedDocPaths) {
135
+ parts.push(`- \`${p}\``);
136
+ }
137
+ parts.push("");
138
+ }
139
+ parts.push("### How to search before writing", "", "Before creating or updating documentation for a topic, search the", "destination directory for existing coverage:", "", `1. Run \`find ${context.destinationPath} -name '*.md' | xargs grep -li '<keyword>'\``, " where `<keyword>` is a key term from the topic you're about to write.", "2. Read any matching files with `cat` to assess their current content.", "3. Apply the SKIP / UPDATE / CREATE decision above.", "", "You may delegate this search to a subagent if you prefer — spawn a", "subagent to search and summarize existing coverage, then use its report", "to decide what to write or update.", "");
140
+ }
141
+ // 8. How to work
142
+ parts.push("## How to work", "");
143
+ parts.push(`0. **Search before writing**: Before creating or updating any documentation`, ` file, search the destination directory for existing docs that cover the`, ` same topic. Apply the SKIP / UPDATE / CREATE decision criteria above.`, `1. **Read files using tools**: Use the file read tool, grep, or bash commands to`, ` read the source files. Do NOT ask me to provide file contents.`, `2. **Answer EVERY question**: Go through the exploration findings systematically.`, ` Use \`webfetch\` to fetch official documentation URLs.`, `3. **Write files directly**: Use the edit tool to create documentation files in`, ` the destination directory. Do NOT return Markdown in your response text.`, `4. **Generate diagrams**: If exploration findings include complexity signals,`, ` generate Mermaid diagrams.`, `5. **Link to related pages**: Use relative paths to link to other documentation`, ` pages listed in the group summaries below.`, "");
144
+ // 9. Other documentation groups
145
+ if (context.allGroups.length > 1) {
146
+ parts.push("## Other documentation groups being generated", "");
147
+ for (const g of context.allGroups) {
148
+ if (g.id !== group.id) {
149
+ parts.push(`- **${g.label}** (${g.id}): ${g.files.join(", ")}`);
150
+ }
151
+ }
152
+ parts.push("");
153
+ }
154
+ // 10. Done
155
+ parts.push("## When you are done", "");
156
+ parts.push(`After writing all documentation files 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.`);
157
+ return parts.join("\n");
158
+ }
159
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * LLM-assisted code exploration agent for Hem.
3
+ *
4
+ * Uses an OpenCode session to discover what questions the code asks.
5
+ * Explores a file group using tools (read, glob, grep, bash) and
6
+ * webfetch, and outputs a structured natural-language report that
7
+ * informs the DocumentationAgent.
8
+ *
9
+ * The agent:
10
+ * 1. Builds a structured prompt with file paths (NOT contents).
11
+ * 2. Sends the prompt to an OpenCode session with `agent: "hem-explore"`.
12
+ * 3. Returns the response text directly — no JSON parsing needed.
13
+ *
14
+ * This agent implements FR-002 (answer the questions code asks),
15
+ * FR-013 (tool-based exploration), and FR-014 (read-only plan mode).
16
+ */
17
+ import type { Provider } from "../providers/types.js";
18
+ import type { FileGroup, ExplorationFindings } from "../types.js";
19
+ import { BaseAgent } from "./base-agent.js";
20
+ /**
21
+ * An agent that uses an LLM to explore a file group and produce a
22
+ * natural-language exploration report covering questions, integrations,
23
+ * complexity signals, style guides, and cross-group dependencies.
24
+ */
25
+ export declare class ExplorationAgent extends BaseAgent {
26
+ constructor(provider: Provider);
27
+ /**
28
+ * Run the exploration pipeline: prompt → send → return text.
29
+ *
30
+ * @param group - The file group to explore.
31
+ * @param sourceRoot - Absolute path to the source root directory.
32
+ * @param allGroups - Summaries of all groups for cross-group awareness.
33
+ * @param verbose - Optional logging callback (writes to stderr).
34
+ * @returns `ExplorationFindings` containing the natural-language report.
35
+ * @throws If session creation or prompting fails.
36
+ */
37
+ run(group: FileGroup, sourceRoot: string, allGroups: Array<{
38
+ id: string;
39
+ label: string;
40
+ files: string[];
41
+ }>, verbose?: (msg: string) => void): Promise<ExplorationFindings>;
42
+ /**
43
+ * Builds the exploration prompt for a file group.
44
+ *
45
+ * Contains file paths (NOT file contents), instructions for tool-based
46
+ * exploration, and the expected natural-language report format.
47
+ *
48
+ * @param group - The file group to explore.
49
+ * @param sourceRoot - Absolute path to the source root directory.
50
+ * @param allGroups - Summaries of all groups for cross-group awareness.
51
+ * @returns The prompt string.
52
+ */
53
+ static buildPrompt(group: FileGroup, sourceRoot: string, allGroups: Array<{
54
+ id: string;
55
+ label: string;
56
+ files: string[];
57
+ }>): string;
58
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * LLM-assisted code exploration agent for Hem.
3
+ *
4
+ * Uses an OpenCode session to discover what questions the code asks.
5
+ * Explores a file group using tools (read, glob, grep, bash) and
6
+ * webfetch, and outputs a structured natural-language report that
7
+ * informs the DocumentationAgent.
8
+ *
9
+ * The agent:
10
+ * 1. Builds a structured prompt with file paths (NOT contents).
11
+ * 2. Sends the prompt to an OpenCode session with `agent: "hem-explore"`.
12
+ * 3. Returns the response text directly — no JSON parsing needed.
13
+ *
14
+ * This agent implements FR-002 (answer the questions code asks),
15
+ * FR-013 (tool-based exploration), and FR-014 (read-only plan mode).
16
+ */
17
+ import { BaseAgent } from "./base-agent.js";
18
+ // ── Agent ───────────────────────────────────────────────────────────────
19
+ /**
20
+ * An agent that uses an LLM to explore a file group and produce a
21
+ * natural-language exploration report covering questions, integrations,
22
+ * complexity signals, style guides, and cross-group dependencies.
23
+ */
24
+ export class ExplorationAgent extends BaseAgent {
25
+ constructor(provider) {
26
+ super(provider);
27
+ }
28
+ /**
29
+ * Run the exploration pipeline: prompt → send → return text.
30
+ *
31
+ * @param group - The file group to explore.
32
+ * @param sourceRoot - Absolute path to the source root directory.
33
+ * @param allGroups - Summaries of all groups for cross-group awareness.
34
+ * @param verbose - Optional logging callback (writes to stderr).
35
+ * @returns `ExplorationFindings` containing the natural-language report.
36
+ * @throws If session creation or prompting fails.
37
+ */
38
+ async run(group, sourceRoot, allGroups, verbose) {
39
+ const tag = `explore-agent:${group.id}`;
40
+ // 1. Build prompt
41
+ const prompt = ExplorationAgent.buildPrompt(group, sourceRoot, allGroups);
42
+ if (verbose) {
43
+ verbose(`[${tag}] Prompt: ${prompt.length.toLocaleString()} chars`);
44
+ }
45
+ // 2. Create a new session
46
+ const sessionId = await this.createSession(`Hem: explore — ${group.label}`);
47
+ if (verbose) {
48
+ verbose(`[${tag}] Session created: ${sessionId}`);
49
+ }
50
+ // 3. Send prompt and wait for response
51
+ const response = await this.provider.prompt(sessionId, prompt, { agent: "hem-explore" }) ?? "";
52
+ if (verbose) {
53
+ verbose(`[${tag}] Response: ${response.length.toLocaleString()} chars`);
54
+ }
55
+ return { groupId: group.id, text: response };
56
+ }
57
+ // ── Static helpers (pure functions, easy to unit test) ───────────────
58
+ /**
59
+ * Builds the exploration prompt for a file group.
60
+ *
61
+ * Contains file paths (NOT file contents), instructions for tool-based
62
+ * exploration, and the expected natural-language report format.
63
+ *
64
+ * @param group - The file group to explore.
65
+ * @param sourceRoot - Absolute path to the source root directory.
66
+ * @param allGroups - Summaries of all groups for cross-group awareness.
67
+ * @returns The prompt string.
68
+ */
69
+ static buildPrompt(group, sourceRoot, allGroups) {
70
+ const parts = [];
71
+ // System-level instructions
72
+ parts.push(`Discover what questions the code asks — the operational, conceptual, and`, `architectural questions that a reader would naturally have after reading`, `this code.`, "", "## Quality Standard: Answer the Questions Code Asks", "", "Your primary quality standard is: **documentation must go beyond surface-level", "descriptions to answer the operational, conceptual, and architectural", "questions that integrations, services, and dependencies naturally", "raise.** Simply noting that an integration exists (e.g., \"Application Insights is", "configured\") is NOT sufficient. You must discover the NEXT questions a reader", "would ask:", "", "- **Operational**: How do I access, query, monitor, rotate credentials for, or", " troubleshoot this integration? Where is data stored? What dashboard do I use?", "- **Conceptual**: Why was this technology chosen? What problem does it solve?", " What are its limitations?", "- **Architectural**: How does this integration fit into the larger system? What", " depends on it? What happens if it fails?", "", "For every integration you discover, you MUST:", "1. Identify at least 2 operational questions (e.g., \"Where is telemetry data", " stored?\", \"How do I access the dashboard?\", \"How do I rotate secrets?\")", "2. Categorize each question as operational, conceptual, or architectural", "3. Suggest sources where answers might be found (e.g., \"Azure Application", " Insights docs\", \"AWS Secrets Manager console\", \"Redis CLI documentation\")", "");
73
+ // FR-006: No verbatim source code in findings
74
+ parts.push("## No verbatim source code (FR-006)", "", "Your exploration report MUST NOT contain verbatim project source code.", "Follow these rules strictly:", "", "1. Reference code by file path and line number, NOT by embedding code snippets.", " - CORRECT: \"Call to AddApplicationInsights() in startup.ts:42\"", " - WRONG: \"const client = new Redis({ host: 'localhost' })\"", "", "2. If you need to describe what code does, use prose. For example, instead", " of copying a function body, write: \"The retry logic in `src/client.ts:85`", " uses exponential backoff with a maximum of 3 attempts.\"", "", "3. You MAY mention function names, class names, and identifiers by name (e.g.,", " \"the `connectToDatabase()` function in `src/db.ts:22`\"), but you MUST NOT", " reproduce their implementation code.", "");
75
+ // Task description
76
+ parts.push("## Your task", "", "Explore the following file group and produce a structured exploration report.", "", `**Group**: ${group.label} (ID: ${group.id})`, "**Files to explore**:");
77
+ for (const file of group.files) {
78
+ parts.push(`- ${file.path}`);
79
+ }
80
+ parts.push("");
81
+ parts.push(`**Source root**: ${sourceRoot}`);
82
+ parts.push("");
83
+ // How to explore
84
+ parts.push("## How to explore", "", "Use your tools to read and analyze the source files. Do NOT ask me to provide", "file contents — read them yourself using the file read tool, grep, or bash", "commands.", "", "For each file in the group:", "", "1. **Read the file** using the file read tool", "2. **Identify integrations**: Look for imports, API calls, database connections,", " cloud service SDKs, message queue producers/consumers, HTTP clients, and any", " external dependency", "3. **Ask \"what questions does this raise?\"**: For each integration or complex", " pattern, think about what a reader would need to know:", " - Where is data stored? How do I access it?", " - How do I query/monitor/troubleshoot this?", " - What happens when this fails?", " - How do I rotate credentials for this?", " - What are the performance characteristics?", "4. **Generate operational questions for every integration**: At least 2 per", " integration. Examples:", " - Database: \"How do I access the database?\", \"How do backups work?\"", " - Monitoring: \"How do I access the dashboard?\", \"What is the data retention?\"", " - Secrets: \"How do I rotate secrets?\", \"What happens if a secret expires?\"", "5. **Detect complexity signals**: Look for patterns that warrant diagrams:", " - 3+ services interacting in a flow", " - 5+ database tables with relationships", " - Complex state machines or workflows", " - Cloud infrastructure configuration", "6. **Find style guides**: Systematically search for style guides, brand", " guidelines, and design system artifacts (`.editorconfig`, ESLint/Prettier", " configs, brand asset directories, design token files).", "7. **Note cross-group dependencies**: If this code depends on or shares entities", " with files in other groups, note which groups", "");
85
+ // Webfetch policy
86
+ parts.push("## Webfetch policy", "", "You MAY use webfetch to look up official documentation for integrations you", "discover. Rules:", "- Only fetch from official/authoritative sources (e.g., official docs sites,", " GitHub repos of the integration)", "- You may first discover what the official docs URL is, then fetch it", "- Do NOT fetch random blog posts, Stack Overflow, or unofficial sources", "- If you cannot determine the official source, note the integration without a URL", "");
87
+ // Other groups
88
+ const otherGroups = allGroups.filter((g) => g.id !== group.id);
89
+ if (otherGroups.length > 0) {
90
+ parts.push("## Other groups in this project (for cross-reference awareness)", "");
91
+ for (const g of otherGroups) {
92
+ parts.push(`- **${g.label}** (${g.id}): ${g.files.join(", ")}`);
93
+ }
94
+ parts.push("");
95
+ }
96
+ // Style Guide Reference (Google Markdown Style Guide — FR-010)
97
+ parts.push("## Style Guide Reference", "", "All text you produce MUST follow these formatting rules:", "", "1. **ATX-style headings only** — use `#` syntax, never Setext style.", "2. **Informative link titles** — use descriptive text, never `[click here](…)`.", "3. **4-space indent for nested lists**.", "4. **Prefer Markdown over HTML**.", "");
98
+ // Output format
99
+ parts.push("## Output format", "", "Write a structured natural-language exploration report with these sections:", "", "### Summary", "One paragraph explaining what this group does and why it exists.", "", "### Questions to Answer", "Key questions a reader of this code would have. For each question:", "- State the question", "- Classify it: **operational**, **conceptual**, or **architectural**", "- Note what in the source raised it (file:line reference)", "- Suggest where the answer might be found", "", "### Integrations Discovered", "External services, libraries, databases, APIs, or cloud services found.", "For each integration:", "- Name and type (e.g., database, cloud-service, messaging, auth, monitoring)", "- Source file(s) where configured or used (file:line references)", "- Official docs URL if known", "- At least 2 operational questions about how to access/monitor/troubleshoot it", "", "### Complexity Signals", "Patterns that warrant a diagram or deeper architectural explanation.", "For each signal:", "- What makes it complex", "- Which files are involved", "- What diagram type would help (flowchart, sequence, er, c4, or state)", "", "### Style Guides Found", "Any linting, formatting, brand, or design system artifacts found.", "If none found, write: \"None found.\"", "", "### Cross-Group Dependencies", "Other group IDs this group depends on or shares entities with.", "Use the group IDs from the list above (e.g., `agent-system`, `orchestrator`).", "If none, write: \"None.\"", "", "Write clear, concise prose. Do not use JSON. Do not include verbatim source code.");
100
+ return parts.join("\n");
101
+ }
102
+ }