@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,265 @@
1
+ /**
2
+ * Decision queue for Hem's parallel organization pass.
3
+ *
4
+ * Solves the race condition where the arbiter issues a MERGE and a DELETE
5
+ * for the same source file simultaneously. The MERGE must complete (worker
6
+ * reads source → writes merged target) before the DELETE removes the source.
7
+ *
8
+ * ## How it works
9
+ *
10
+ * 1. **MERGE src INTO dst** — relayed immediately. The queue records that
11
+ * `src` has a pending read (the merge worker needs to read it).
12
+ * A filesystem watcher on `dst` waits for the merged file to appear.
13
+ *
14
+ * 2. **DELETE filepath** — if `filepath` has a pending MERGE read, the
15
+ * DELETE is queued. Otherwise it's relayed immediately.
16
+ *
17
+ * 3. **UPDATE-LINKS / free-form** — always relayed immediately (graceful
18
+ * degradation for unparsed decisions).
19
+ *
20
+ * ## Release triggers
21
+ *
22
+ * - **Filesystem watcher** (`fs.watch`) detects when the merge target file
23
+ * is written, releasing queued DELETEs for the corresponding source.
24
+ * - **Worker completion** — when a worker's `promptAndWait` resolves, any
25
+ * decisions still blocked on that worker's actions are released.
26
+ * - **Timeout** — queued decisions are released after a configurable
27
+ * deadline (default 60 s) to prevent permanent blocking.
28
+ *
29
+ * The watcher uses `node:fs` callback-based `watch()` (not the async
30
+ * iterator from `node:fs/promises`) so we can `.close()` it cleanly.
31
+ */
32
+ import { watch } from "node:fs";
33
+ import { resolve } from "node:path";
34
+ // ── Constants ───────────────────────────────────────────────────────────
35
+ /** Default timeout (ms) after which a queued decision is released regardless. */
36
+ export const QUEUE_TIMEOUT_MS = 60_000;
37
+ /** Regex patterns for structured DECISION parsing. */
38
+ const RE_MERGE = /^DECISION:\s*@(org-worker-\d+)\s+MERGE\s+(\S+)\s+INTO\s+(\S+)/i;
39
+ const RE_DELETE = /^DECISION:\s*@(org-worker-\d+)\s+DELETE\s+(\S+)/i;
40
+ const RE_UPDATE_LINKS = /^DECISION:\s*@(org-worker-\d+)\s+UPDATE-LINKS\s+(\S+)/i;
41
+ const RE_FREEFORM = /^DECISION:\s*@(org-worker-\d+)/i;
42
+ /**
43
+ * Parse a DECISION message into a structured action.
44
+ * Returns `undefined` if the message is not a DECISION.
45
+ *
46
+ * NOTE: `@all-workers` decisions intentionally return `undefined` here
47
+ * because the regex only matches `@org-worker-\d+`. This is correct —
48
+ * `@all-workers` decisions are broadcast to all workers by the SSE relay
49
+ * routing logic and do not need file-based sequencing via DecisionQueue.
50
+ */
51
+ export function parseDecision(message) {
52
+ let m;
53
+ m = message.match(RE_MERGE);
54
+ if (m)
55
+ return { kind: "merge", worker: m[1], src: m[2], dst: m[3] };
56
+ m = message.match(RE_DELETE);
57
+ if (m)
58
+ return { kind: "delete", worker: m[1], filePath: m[2] };
59
+ m = message.match(RE_UPDATE_LINKS);
60
+ if (m)
61
+ return { kind: "update-links", worker: m[1], filePath: m[2] };
62
+ m = message.match(RE_FREEFORM);
63
+ if (m)
64
+ return { kind: "freeform", worker: m[1] };
65
+ return undefined;
66
+ }
67
+ // ── DecisionQueue ───────────────────────────────────────────────────────
68
+ /**
69
+ * Filesystem-watched decision queue.
70
+ *
71
+ * Instantiated once per `runParallel()` invocation. Call `start()` before
72
+ * the SSE relay loop and `stop()` in the `finally` block.
73
+ */
74
+ export class DecisionQueue {
75
+ /** Files that have a pending MERGE read (source → merge-target). */
76
+ pendingReads = new Map();
77
+ /** Queued DELETE decisions blocked on a pending MERGE. */
78
+ queue = [];
79
+ /** The filesystem watcher (recursive), or null if not started. */
80
+ watcher = null;
81
+ /** Absolute path to the documentation destination directory. */
82
+ destPath;
83
+ /** Timeout in ms for queued decisions. */
84
+ timeoutMs;
85
+ /** Relay callback provided by the caller. */
86
+ relay;
87
+ /** Optional verbose logger. */
88
+ verbose;
89
+ constructor(opts) {
90
+ this.destPath = resolve(opts.destPath);
91
+ this.relay = opts.relay;
92
+ this.timeoutMs = opts.timeoutMs ?? QUEUE_TIMEOUT_MS;
93
+ this.verbose = opts.verbose;
94
+ }
95
+ // ── Lifecycle ───────────────────────────────────────────────────────
96
+ /**
97
+ * Start the filesystem watcher on the destination directory.
98
+ * Must be called before processing any decisions.
99
+ */
100
+ start() {
101
+ if (this.watcher)
102
+ return; // already running
103
+ try {
104
+ this.watcher = watch(this.destPath, { recursive: true }, (_eventType, filename) => {
105
+ if (!filename)
106
+ return;
107
+ // Normalize: fs.watch may return OS-native separators
108
+ const relPath = filename.replace(/\\/g, "/");
109
+ this.onFileChange(relPath);
110
+ });
111
+ // Suppress watcher errors (e.g., ENOENT if dir is deleted)
112
+ this.watcher.on("error", () => { });
113
+ if (this.verbose) {
114
+ this.verbose(`[decision-queue] Watcher started on ${this.destPath}`);
115
+ }
116
+ }
117
+ catch {
118
+ // If fs.watch fails (e.g., dir doesn't exist yet), proceed without
119
+ // it — timeouts and worker completion will still release queued items.
120
+ if (this.verbose) {
121
+ this.verbose(`[decision-queue] Could not start watcher — using timeout fallback`);
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Stop the filesystem watcher and release all queued decisions.
127
+ * Call in the `finally` block of `runParallel()`.
128
+ */
129
+ stop() {
130
+ if (this.watcher) {
131
+ this.watcher.close();
132
+ this.watcher = null;
133
+ }
134
+ // Release all remaining queued decisions
135
+ this.releaseAll("stop");
136
+ }
137
+ // ── Public API ──────────────────────────────────────────────────────
138
+ /**
139
+ * Process a parsed DECISION and either relay it immediately or queue it.
140
+ *
141
+ * @param action - Parsed decision action.
142
+ * @param target - The worker session to relay to.
143
+ * @param relayText - Full relay text string.
144
+ */
145
+ async handleDecision(action, target, relayText) {
146
+ switch (action.kind) {
147
+ case "merge":
148
+ // Record that the source file has a pending read
149
+ this.pendingReads.set(action.src, action.dst);
150
+ if (this.verbose) {
151
+ this.verbose(`[decision-queue] MERGE ${action.src} INTO ${action.dst} — pending read recorded`);
152
+ }
153
+ // Relay the MERGE immediately
154
+ await this.relay(target, relayText);
155
+ break;
156
+ case "delete":
157
+ if (this.pendingReads.has(action.filePath)) {
158
+ // Block this DELETE until the MERGE completes
159
+ const blockedBy = this.pendingReads.get(action.filePath);
160
+ this.enqueue(target, relayText, action.filePath, blockedBy);
161
+ }
162
+ else {
163
+ // No pending MERGE — relay immediately
164
+ await this.relay(target, relayText);
165
+ }
166
+ break;
167
+ case "update-links":
168
+ case "freeform":
169
+ // Always relay immediately
170
+ await this.relay(target, relayText);
171
+ break;
172
+ }
173
+ }
174
+ /**
175
+ * Called when a worker completes. Releases any queued decisions that
176
+ * were blocked on actions by that worker (identified by label match
177
+ * in the target).
178
+ */
179
+ releaseForWorker(workerLabel) {
180
+ const toRelease = this.queue.filter((q) => {
181
+ // Release decisions targeting this worker OR blocked by files
182
+ // this worker was responsible for merging
183
+ return q.target.label === workerLabel;
184
+ });
185
+ for (const item of toRelease) {
186
+ this.release(item, "worker-complete");
187
+ }
188
+ }
189
+ /**
190
+ * Returns the number of currently queued (blocked) decisions.
191
+ */
192
+ get pendingCount() {
193
+ return this.queue.length;
194
+ }
195
+ /**
196
+ * Returns a snapshot of pending reads (source → merge-target).
197
+ * Useful for testing.
198
+ */
199
+ get pendingReadSnapshot() {
200
+ return new Map(this.pendingReads);
201
+ }
202
+ // ── Internal ────────────────────────────────────────────────────────
203
+ /** Enqueue a blocked DELETE decision. */
204
+ enqueue(target, relayText, filePath, blockedBy) {
205
+ const timer = setTimeout(() => {
206
+ this.release(item, "timeout");
207
+ }, this.timeoutMs);
208
+ const item = {
209
+ target,
210
+ relayText,
211
+ action: "DELETE",
212
+ filePath,
213
+ blockedBy,
214
+ queuedAt: Date.now(),
215
+ timer,
216
+ };
217
+ this.queue.push(item);
218
+ if (this.verbose) {
219
+ this.verbose(`[decision-queue] DELETE ${filePath} queued — waiting for MERGE target ${blockedBy}`);
220
+ }
221
+ }
222
+ /**
223
+ * Release a single queued decision — relay it and remove from queue.
224
+ */
225
+ release(item, reason) {
226
+ const idx = this.queue.indexOf(item);
227
+ if (idx === -1)
228
+ return; // already released
229
+ this.queue.splice(idx, 1);
230
+ clearTimeout(item.timer);
231
+ // Remove the pending read since the MERGE is considered done
232
+ this.pendingReads.delete(item.filePath);
233
+ if (this.verbose) {
234
+ this.verbose(`[decision-queue] Releasing DELETE ${item.filePath} (${reason})`);
235
+ }
236
+ // Fire-and-forget — don't block the watcher/timer callback
237
+ void this.relay(item.target, item.relayText).catch((err) => {
238
+ if (this.verbose) {
239
+ this.verbose(`[decision-queue] Relay failed for DELETE ${item.filePath}: ${err instanceof Error ? err.message : String(err)}`);
240
+ }
241
+ });
242
+ }
243
+ /** Release all queued decisions (used during stop/cleanup). */
244
+ releaseAll(reason) {
245
+ // Copy to avoid mutation during iteration
246
+ const items = [...this.queue];
247
+ for (const item of items) {
248
+ this.release(item, reason);
249
+ }
250
+ }
251
+ /**
252
+ * Filesystem watcher callback. When a file in the destination directory
253
+ * changes, check if it matches a merge target and release blocked DELETEs.
254
+ */
255
+ onFileChange(relPath) {
256
+ // Check if any queued DELETE is blocked by this file being written
257
+ const toRelease = this.queue.filter((q) => q.blockedBy === relPath);
258
+ if (toRelease.length > 0 && this.verbose) {
259
+ this.verbose(`[decision-queue] File change detected: ${relPath} — releasing ${toRelease.length} queued decision(s)`);
260
+ }
261
+ for (const item of toRelease) {
262
+ this.release(item, "fs-watch");
263
+ }
264
+ }
265
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Diff-based scoping for incremental documentation generation.
3
+ *
4
+ * When a previous Hem run's commit SHA is available in `changelog.md`,
5
+ * this module filters the discovered source files down to only those
6
+ * that changed since the last run. This enables incremental generation
7
+ * where only modified/added source files are re-documented.
8
+ *
9
+ * Reference: specs/004-changelog-diff-scope.
10
+ */
11
+ import type { FileInfo } from "./types.js";
12
+ /**
13
+ * Filters discovered source files to only those that changed since the
14
+ * last Hem run.
15
+ *
16
+ * Matches are performed by comparing `FileInfo.path` (relative to source
17
+ * root) against the paths returned by `git diff --name-only`. The
18
+ * comparison normalizes forward slashes for cross-platform consistency.
19
+ *
20
+ * @param allFiles - All discovered source files from the discovery phase.
21
+ * @param changedPaths - Relative paths of changed files from `computeChangedFiles()`.
22
+ * @returns The subset of `allFiles` whose paths appear in `changedPaths`.
23
+ */
24
+ export declare function scopeToChangedFiles(allFiles: FileInfo[], changedPaths: string[]): FileInfo[];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Diff-based scoping for incremental documentation generation.
3
+ *
4
+ * When a previous Hem run's commit SHA is available in `changelog.md`,
5
+ * this module filters the discovered source files down to only those
6
+ * that changed since the last run. This enables incremental generation
7
+ * where only modified/added source files are re-documented.
8
+ *
9
+ * Reference: specs/004-changelog-diff-scope.
10
+ */
11
+ // ── Scope to Changed Files ────────────────────────────────────────────
12
+ /**
13
+ * Filters discovered source files to only those that changed since the
14
+ * last Hem run.
15
+ *
16
+ * Matches are performed by comparing `FileInfo.path` (relative to source
17
+ * root) against the paths returned by `git diff --name-only`. The
18
+ * comparison normalizes forward slashes for cross-platform consistency.
19
+ *
20
+ * @param allFiles - All discovered source files from the discovery phase.
21
+ * @param changedPaths - Relative paths of changed files from `computeChangedFiles()`.
22
+ * @returns The subset of `allFiles` whose paths appear in `changedPaths`.
23
+ */
24
+ export function scopeToChangedFiles(allFiles, changedPaths) {
25
+ // Build a set for O(1) lookup, normalizing path separators
26
+ const changedSet = new Set(changedPaths.map((p) => p.replace(/\\/g, "/")));
27
+ return allFiles.filter((f) => changedSet.has(f.path.replace(/\\/g, "/")));
28
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * File discovery module for Hem.
3
+ *
4
+ * Scans a source directory for files matching a glob pattern, classifies
5
+ * each file as text or binary, and returns a `FileInfo[]` array.
6
+ *
7
+ * Also provides `detectProjectName()` for inferring the project name
8
+ * from manifest files (package.json, Cargo.toml, etc.) in ancestor
9
+ * directories of the source path.
10
+ *
11
+ * Reference: FR-002 (file scanning), FR-014 (binary detection), FR-015 (destination exclusion).
12
+ */
13
+ import type { FileInfo } from "./types.js";
14
+ /**
15
+ * Discovers source files within `sourcePath` that match the given glob
16
+ * pattern. Each file is classified as binary or text and returned as a
17
+ * `FileInfo` object.
18
+ *
19
+ * Key behaviours:
20
+ * - `sourcePath` and `destinationPath` are resolved to absolute paths.
21
+ * - Files within `destinationPath` are excluded when the destination
22
+ * overlaps with the source (FR-015).
23
+ * - Binary files are included in the returned array with
24
+ * `isBinary: true` so callers can count/warn about them.
25
+ *
26
+ * @param sourcePath - Root directory to scan.
27
+ * @param globPattern - Glob pattern relative to `sourcePath`.
28
+ * @param destinationPath - Destination directory for generated docs
29
+ * (excluded from results when overlapping with source).
30
+ * @returns Array of `FileInfo` objects for every matched file.
31
+ */
32
+ export declare function discoverFiles(sourcePath: string, globPattern: string, destinationPath: string): Promise<FileInfo[]>;
33
+ /**
34
+ * Detect the project name by walking up from the source directory
35
+ * and inspecting manifest files.
36
+ *
37
+ * Detection priority (first match wins):
38
+ * 1. `package.json` → `name` field (scoped names stripped)
39
+ * 2. `Cargo.toml` → `[package] name`
40
+ * 3. `pyproject.toml` → `[project] name` or `[tool.poetry] name`
41
+ * 4. `go.mod` → last segment of module path
42
+ *
43
+ * The walk starts at the parent of `sourcePath` (since `sourcePath`
44
+ * itself is typically `src/` or similar) and continues up to the
45
+ * git root or a maximum of {@link MAX_WALK_DEPTH} levels.
46
+ *
47
+ * If no manifest is found, falls back to the basename of the
48
+ * directory containing the first manifest-like file, the parent
49
+ * directory, or ultimately `basename(sourcePath)`.
50
+ *
51
+ * @param sourcePath - The absolute source directory path.
52
+ * @returns The detected project name.
53
+ */
54
+ export declare function detectProjectName(sourcePath: string): Promise<string>;