@pruddiman/hem 0.0.1-beta-6f925fe → 0.0.1-beta-1aff12a

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 (36) hide show
  1. package/dist/agents/arbiter-agent.d.ts +10 -39
  2. package/dist/agents/arbiter-agent.js +10 -78
  3. package/dist/agents/architecture-agent.d.ts +1 -1
  4. package/dist/agents/architecture-agent.js +14 -3
  5. package/dist/agents/base-arbiter.d.ts +82 -0
  6. package/dist/agents/base-arbiter.js +101 -0
  7. package/dist/agents/crossref-agent.d.ts +0 -5
  8. package/dist/agents/crossref-agent.js +19 -48
  9. package/dist/agents/crossref-arbiter-agent.d.ts +10 -39
  10. package/dist/agents/crossref-arbiter-agent.js +10 -78
  11. package/dist/agents/documentation-agent.d.ts +1 -9
  12. package/dist/agents/documentation-agent.js +13 -19
  13. package/dist/agents/index-agent.d.ts +1 -1
  14. package/dist/agents/index-agent.js +20 -4
  15. package/dist/agents/organization-agent.d.ts +0 -5
  16. package/dist/agents/organization-agent.js +20 -49
  17. package/dist/agents/parallel-coordinator.d.ts +47 -0
  18. package/dist/agents/parallel-coordinator.js +77 -0
  19. package/dist/auth.d.ts +13 -4
  20. package/dist/auth.js +59 -17
  21. package/dist/grouping.d.ts +54 -1
  22. package/dist/grouping.js +114 -13
  23. package/dist/index.d.ts +18 -6
  24. package/dist/index.js +13 -51
  25. package/dist/orchestrator.d.ts +40 -41
  26. package/dist/orchestrator.js +93 -126
  27. package/dist/output.js +5 -13
  28. package/dist/providers/copilot.d.ts +7 -2
  29. package/dist/providers/copilot.js +48 -18
  30. package/dist/providers/index.d.ts +31 -0
  31. package/dist/providers/index.js +34 -0
  32. package/dist/providers/opencode.d.ts +41 -3
  33. package/dist/providers/opencode.js +27 -6
  34. package/dist/session.js +8 -14
  35. package/dist/types.d.ts +35 -11
  36. package/package.json +2 -2
@@ -12,53 +12,24 @@
12
12
  * 2. `wrapUp()` — called after all workers complete. Sends a final
13
13
  * prompt so the arbiter can issue any remaining
14
14
  * DECISION messages. Non-fatal on failure.
15
+ *
16
+ * Lifecycle methods are inherited from {@link BaseArbiter}; this class
17
+ * supplies only the configuration values and the prompt body.
15
18
  */
16
- import type { Provider } from "../providers/types.js";
17
- import { BaseAgent } from "./base-agent.js";
18
- import type { WorkerAssignment } from "./organization-agent.js";
19
+ import { BaseArbiter, type BaseArbiterPromptParams } from "./base-arbiter.js";
19
20
  /** Parameters for the arbiter prompt. */
20
- export interface ArbiterPromptParams {
21
- /** Project name. */
22
- projectName: string;
23
- /** Absolute path to the destination directory. */
24
- destinationPath: string;
25
- /** ALL documentation files (for full awareness). */
26
- allDocFiles: string[];
27
- /** Worker assignments mapping each worker label to its owned files. */
28
- workerAssignments: WorkerAssignment[];
29
- }
21
+ export type ArbiterPromptParams = BaseArbiterPromptParams;
30
22
  /**
31
23
  * A coordinator agent that monitors worker broadcasts and issues
32
24
  * DECISION directives to resolve cross-subset issues.
33
25
  *
34
26
  * Does NOT edit files — directs workers to make all changes.
35
27
  */
36
- export declare class ArbiterAgent extends BaseAgent {
37
- constructor(provider: Provider);
38
- /**
39
- * Create the arbiter session and send the initial prompt.
40
- *
41
- * The initial prompt is sent via `promptAsync` (fire-and-forget) because
42
- * the arbiter sits idle until it receives broadcasts from workers.
43
- *
44
- * @returns The session ID so the caller can register it in the SSE
45
- * relay map and later call `wrapUp()`.
46
- * @throws {AuthExpiredError} If session creation or the initial prompt
47
- * fails due to authentication expiry.
48
- */
49
- run(params: ArbiterPromptParams, verbose?: (msg: string) => void): Promise<{
50
- sessionId: string;
51
- }>;
52
- /**
53
- * Send the wrap-up prompt after all workers have completed.
54
- *
55
- * Asks the arbiter to issue any remaining DECISION messages and then
56
- * output its final `{ "status": "complete" }` response.
57
- *
58
- * This is intentionally **non-fatal** — if the arbiter fails, workers
59
- * have already produced their manifests and the pipeline can continue.
60
- */
61
- wrapUp(sessionId: string, verbose?: (msg: string) => void): Promise<void>;
28
+ export declare class ArbiterAgent extends BaseArbiter<ArbiterPromptParams> {
29
+ protected readonly tag = "arbiter";
30
+ protected readonly sessionTitle = "Hem: arbiter";
31
+ protected readonly agentField = "hem-org";
32
+ protected makePrompt(params: ArbiterPromptParams): string;
62
33
  /**
63
34
  * Builds the arbiter prompt.
64
35
  *
@@ -12,9 +12,11 @@
12
12
  * 2. `wrapUp()` — called after all workers complete. Sends a final
13
13
  * prompt so the arbiter can issue any remaining
14
14
  * DECISION messages. Non-fatal on failure.
15
+ *
16
+ * Lifecycle methods are inherited from {@link BaseArbiter}; this class
17
+ * supplies only the configuration values and the prompt body.
15
18
  */
16
- import { isAuthExpired, AuthExpiredError } from "../auth.js";
17
- import { BaseAgent } from "./base-agent.js";
19
+ import { BaseArbiter, } from "./base-arbiter.js";
18
20
  // ── Agent ───────────────────────────────────────────────────────────────
19
21
  /**
20
22
  * A coordinator agent that monitors worker broadcasts and issues
@@ -22,82 +24,12 @@ import { BaseAgent } from "./base-agent.js";
22
24
  *
23
25
  * Does NOT edit files — directs workers to make all changes.
24
26
  */
25
- export class ArbiterAgent 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 = "arbiter";
42
- // 1. Build prompt
43
- const prompt = ArbiterAgent.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: 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-org",
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 produced their manifests and the pipeline can continue.
81
- */
82
- async wrapUp(sessionId, verbose) {
83
- const tag = "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-org" });
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 produced manifests
97
- if (verbose) {
98
- verbose(`[${tag}] ⚠ wrap-up failed: ${err instanceof Error ? err.message : String(err)}`);
99
- }
100
- }
27
+ export class ArbiterAgent extends BaseArbiter {
28
+ tag = "arbiter";
29
+ sessionTitle = "Hem: arbiter";
30
+ agentField = "hem-org";
31
+ makePrompt(params) {
32
+ return ArbiterAgent.buildPrompt(params);
101
33
  }
102
34
  /**
103
35
  * Builds the arbiter prompt.
@@ -144,5 +144,5 @@ export declare class ArchitectureAgent extends BaseAgent {
144
144
  * normal-mode `buildPrompt()`, but substitutes chunk summaries for
145
145
  * the raw findings.
146
146
  */
147
- static buildSynthesisPrompt(params: ArchPromptParams, chunkSummaries: ArchChunkSummary[]): string;
147
+ static buildSynthesisPrompt(params: ArchPromptParams, chunkSummaries: ArchChunkSummary[], failedChunks?: number): string;
148
148
  }
@@ -120,6 +120,7 @@ export class ArchitectureAgent extends BaseAgent {
120
120
  // Collect results; use empty fallback for failed chunks so synthesis still runs.
121
121
  // Re-throw AuthExpiredError immediately.
122
122
  const chunkSummaries = [];
123
+ let failedChunks = 0;
123
124
  for (let i = 0; i < settled.length; i++) {
124
125
  const result = settled[i];
125
126
  if (result.status === "fulfilled") {
@@ -129,19 +130,26 @@ export class ArchitectureAgent extends BaseAgent {
129
130
  if (result.reason instanceof AuthExpiredError) {
130
131
  throw result.reason;
131
132
  }
133
+ failedChunks++;
134
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
132
135
  if (verbose) {
133
- const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
134
136
  verbose(`[${tag}] chunk-${i + 1}/${chunks.length} failed: ${msg}`);
135
137
  }
136
138
  // Push an empty fallback so the synthesis index is not skewed
137
139
  chunkSummaries.push(ArchitectureAgent.parseChunkSummary("", i));
138
140
  }
139
141
  }
142
+ // Always surface chunk failures to stderr so users running without --verbose
143
+ // know the architecture overview was synthesized from incomplete data.
144
+ if (failedChunks > 0) {
145
+ console.warn(`[${tag}] WARNING: ${failedChunks}/${chunks.length} chunk summarisation(s) failed; ` +
146
+ `architecture overview will be synthesized from incomplete data.`);
147
+ }
140
148
  // 3. Final synthesis — combine chunk summaries into architecture.md
141
149
  if (verbose) {
142
150
  verbose(`[${tag}] Running synthesis from ${chunkSummaries.length} chunk summaries`);
143
151
  }
144
- const synthesisPrompt = ArchitectureAgent.buildSynthesisPrompt(params, chunkSummaries);
152
+ const synthesisPrompt = ArchitectureAgent.buildSynthesisPrompt(params, chunkSummaries, failedChunks);
145
153
  if (verbose) {
146
154
  verbose(`[${tag}] Synthesis prompt: ${synthesisPrompt.length.toLocaleString()} chars`);
147
155
  }
@@ -378,11 +386,14 @@ export class ArchitectureAgent extends BaseAgent {
378
386
  * normal-mode `buildPrompt()`, but substitutes chunk summaries for
379
387
  * the raw findings.
380
388
  */
381
- static buildSynthesisPrompt(params, chunkSummaries) {
389
+ static buildSynthesisPrompt(params, chunkSummaries, failedChunks = 0) {
382
390
  const { projectName, sourceRoot, destinationPath, allDocFiles } = params;
383
391
  const parts = [];
384
392
  // 1. System-level instructions
385
393
  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.`, "");
394
+ if (failedChunks > 0) {
395
+ parts.push(`**DATA QUALITY CAVEAT**: ${failedChunks} of ${chunkSummaries.length} chunk`, `summarisation pass(es) failed and were replaced with empty placeholders.`, `Some areas of the codebase therefore have no findings in this synthesis.`, `Acknowledge this gap explicitly in the architecture overview rather than`, `inventing details about the missing chunks.`, "");
396
+ }
386
397
  // 2. Where to write
387
398
  parts.push("## Destination", "", `Write the architecture overview to: \`${destinationPath}/architecture.md\``, "");
388
399
  // 3. Quality standards (same as buildPrompt)
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared base class for Hem's parallel-pass coordinator agents.
3
+ *
4
+ * Both {@link ArbiterAgent} (organization pass) and
5
+ * {@link CrossRefArbiterAgent} (cross-reference pass) follow the same
6
+ * two-phase lifecycle:
7
+ *
8
+ * 1. `run()` — create the session and fire the initial prompt via
9
+ * `promptAsync` (fire-and-forget). Returns `{ sessionId }`
10
+ * so the caller can register it in the SSE relay map.
11
+ * 2. `wrapUp()` — send a final prompt asking the arbiter to issue any
12
+ * remaining DECISION messages. Intentionally non-fatal:
13
+ * workers have already produced their edits.
14
+ *
15
+ * The only differences between the two arbiters are:
16
+ * - log tag (`arbiter` vs `xref-arbiter`)
17
+ * - session title (`Hem: arbiter` vs `Hem: xref-arbiter`)
18
+ * - OpenCode agent field (`hem-org` vs `hem-xref`)
19
+ * - the body of the initial prompt (provided by subclass `makePrompt`)
20
+ *
21
+ * `BaseArbiter` factors out the lifecycle skeleton; subclasses provide
22
+ * the four configuration values above.
23
+ */
24
+ import type { Provider } from "../providers/types.js";
25
+ import { BaseAgent } from "./base-agent.js";
26
+ import type { WorkerAssignment } from "./organization-agent.js";
27
+ /**
28
+ * Shared shape for arbiter prompt parameters. Both
29
+ * {@link ArbiterPromptParams} and {@link CrossRefArbiterPromptParams}
30
+ * are structural aliases for this type.
31
+ */
32
+ export interface BaseArbiterPromptParams {
33
+ /** Project name. */
34
+ projectName: string;
35
+ /** Absolute path to the destination directory. */
36
+ destinationPath: string;
37
+ /** ALL documentation files (for full awareness). */
38
+ allDocFiles: string[];
39
+ /** Worker assignments mapping each worker label to its owned files. */
40
+ workerAssignments: WorkerAssignment[];
41
+ }
42
+ /**
43
+ * Abstract base for arbiter coordinator agents. Concrete subclasses
44
+ * override the four `protected` configuration fields and `makePrompt`.
45
+ */
46
+ export declare abstract class BaseArbiter<P extends BaseArbiterPromptParams = BaseArbiterPromptParams> extends BaseAgent {
47
+ /** Short tag used in verbose log lines. */
48
+ protected abstract readonly tag: string;
49
+ /** Human-readable session title shown in the OpenCode session UI. */
50
+ protected abstract readonly sessionTitle: string;
51
+ /** OpenCode agent name routed to in `body.agent`. */
52
+ protected abstract readonly agentField: string;
53
+ constructor(provider: Provider);
54
+ /**
55
+ * Subclass hook: build the initial prompt body. Called once from {@link run}.
56
+ */
57
+ protected abstract makePrompt(params: P): string;
58
+ /**
59
+ * Create the arbiter session and fire its initial prompt.
60
+ *
61
+ * The initial prompt uses `promptAsync` (fire-and-forget) because the
62
+ * arbiter sits idle until it receives broadcasts from workers.
63
+ *
64
+ * @returns `{ sessionId }` so the caller can register the session for
65
+ * SSE relaying and later call {@link wrapUp}.
66
+ * @throws {AuthExpiredError} If session creation or the initial prompt
67
+ * fails due to authentication expiry.
68
+ */
69
+ run(params: P, verbose?: (msg: string) => void): Promise<{
70
+ sessionId: string;
71
+ }>;
72
+ /**
73
+ * Send the wrap-up prompt after all workers have completed.
74
+ *
75
+ * Asks the arbiter to issue any remaining DECISION messages and emit
76
+ * its final `{ "status": "complete" }` response.
77
+ *
78
+ * Intentionally **non-fatal** — workers have already made their edits;
79
+ * a wrap-up failure does not block the pipeline.
80
+ */
81
+ wrapUp(sessionId: string, verbose?: (msg: string) => void): Promise<void>;
82
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared base class for Hem's parallel-pass coordinator agents.
3
+ *
4
+ * Both {@link ArbiterAgent} (organization pass) and
5
+ * {@link CrossRefArbiterAgent} (cross-reference pass) follow the same
6
+ * two-phase lifecycle:
7
+ *
8
+ * 1. `run()` — create the session and fire the initial prompt via
9
+ * `promptAsync` (fire-and-forget). Returns `{ sessionId }`
10
+ * so the caller can register it in the SSE relay map.
11
+ * 2. `wrapUp()` — send a final prompt asking the arbiter to issue any
12
+ * remaining DECISION messages. Intentionally non-fatal:
13
+ * workers have already produced their edits.
14
+ *
15
+ * The only differences between the two arbiters are:
16
+ * - log tag (`arbiter` vs `xref-arbiter`)
17
+ * - session title (`Hem: arbiter` vs `Hem: xref-arbiter`)
18
+ * - OpenCode agent field (`hem-org` vs `hem-xref`)
19
+ * - the body of the initial prompt (provided by subclass `makePrompt`)
20
+ *
21
+ * `BaseArbiter` factors out the lifecycle skeleton; subclasses provide
22
+ * the four configuration values above.
23
+ */
24
+ import { isAuthExpired, AuthExpiredError } from "../auth.js";
25
+ import { BaseAgent } from "./base-agent.js";
26
+ /**
27
+ * Abstract base for arbiter coordinator agents. Concrete subclasses
28
+ * override the four `protected` configuration fields and `makePrompt`.
29
+ */
30
+ export class BaseArbiter extends BaseAgent {
31
+ constructor(provider) {
32
+ super(provider);
33
+ }
34
+ /**
35
+ * Create the arbiter session and fire its initial prompt.
36
+ *
37
+ * The initial prompt uses `promptAsync` (fire-and-forget) because the
38
+ * arbiter sits idle until it receives broadcasts from workers.
39
+ *
40
+ * @returns `{ sessionId }` so the caller can register the session for
41
+ * SSE relaying and later call {@link wrapUp}.
42
+ * @throws {AuthExpiredError} If session creation or the initial prompt
43
+ * fails due to authentication expiry.
44
+ */
45
+ async run(params, verbose) {
46
+ const prompt = this.makePrompt(params);
47
+ if (verbose) {
48
+ verbose(`[${this.tag}] prompt ${prompt.length.toLocaleString()} chars`);
49
+ }
50
+ const sessionId = await this.createSession(this.sessionTitle);
51
+ if (verbose) {
52
+ verbose(`[${this.tag}] session ${sessionId}`);
53
+ }
54
+ try {
55
+ await this.provider.session.promptAsync({
56
+ path: { id: sessionId },
57
+ body: {
58
+ parts: [{ type: "text", text: prompt }],
59
+ agent: this.agentField,
60
+ },
61
+ });
62
+ }
63
+ catch (err) {
64
+ if (isAuthExpired(err)) {
65
+ throw new AuthExpiredError("the configured provider", err);
66
+ }
67
+ throw err;
68
+ }
69
+ if (verbose) {
70
+ verbose(`[${this.tag}] initial prompt sent (listening for broadcasts)`);
71
+ }
72
+ return { sessionId };
73
+ }
74
+ /**
75
+ * Send the wrap-up prompt after all workers have completed.
76
+ *
77
+ * Asks the arbiter to issue any remaining DECISION messages and emit
78
+ * its final `{ "status": "complete" }` response.
79
+ *
80
+ * Intentionally **non-fatal** — workers have already made their edits;
81
+ * a wrap-up failure does not block the pipeline.
82
+ */
83
+ async wrapUp(sessionId, verbose) {
84
+ if (verbose) {
85
+ verbose(`[${this.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: this.agentField });
91
+ if (verbose) {
92
+ verbose(`[${this.tag}] wrap-up response (${(wrapUpResponse ?? "").length} chars)`);
93
+ }
94
+ }
95
+ catch (err) {
96
+ if (verbose) {
97
+ verbose(`[${this.tag}] ⚠ wrap-up failed: ${err instanceof Error ? err.message : String(err)}`);
98
+ }
99
+ }
100
+ }
101
+ }
@@ -95,11 +95,6 @@ export declare class CrossRefAgent extends BaseAgent {
95
95
  * 10. Kills the arbiter session.
96
96
  */
97
97
  runParallel(params: CrossRefPromptParams, verbose?: (msg: string) => void): Promise<void>;
98
- /**
99
- * Kill a session: abort any running work, then delete the session.
100
- * Best-effort — failures are logged but not thrown.
101
- */
102
- private killSession;
103
98
  /**
104
99
  * Spawn a recalled worker session to apply specific fixes.
105
100
  *
@@ -20,6 +20,7 @@ import { BaseAgent } from "./base-agent.js";
20
20
  import { computeMaxConcurrency } from "../resources.js";
21
21
  import { CrossRefArbiterAgent } from "./crossref-arbiter-agent.js";
22
22
  import { BROADCAST_TOOL_NAME } from "./organization-agent.js";
23
+ import { buildRecallPrompt, killSession } from "./parallel-coordinator.js";
23
24
  // ── Constants ───────────────────────────────────────────────────────────
24
25
  /** File count threshold: use parallel workers above this, single agent below. */
25
26
  export const XREF_PARALLEL_THRESHOLD = 8;
@@ -344,7 +345,7 @@ export class CrossRefAgent extends BaseAgent {
344
345
  // and delete the session.
345
346
  completedSessions.add(sessionId);
346
347
  allSessions.delete(sessionId);
347
- await this.killSession(sessionId, assignment.label, verbose);
348
+ await killSession(this.provider, sessionId, assignment.label, "xref-parallel", verbose);
348
349
  });
349
350
  // 6. Wait for all workers to complete
350
351
  const results = await Promise.allSettled(workerPromises);
@@ -367,7 +368,7 @@ export class CrossRefAgent extends BaseAgent {
367
368
  // 10. Kill the arbiter session
368
369
  completedSessions.add(arbiterSessionId);
369
370
  allSessions.delete(arbiterSessionId);
370
- await this.killSession(arbiterSessionId, "xref-arbiter", verbose);
371
+ await killSession(this.provider, arbiterSessionId, "xref-arbiter", "xref-parallel", verbose);
371
372
  // 11. Log any worker failures (the pipeline discovers files via disk scan)
372
373
  for (let i = 0; i < results.length; i++) {
373
374
  const result = results[i];
@@ -396,29 +397,6 @@ export class CrossRefAgent extends BaseAgent {
396
397
  ]);
397
398
  }
398
399
  }
399
- /**
400
- * Kill a session: abort any running work, then delete the session.
401
- * Best-effort — failures are logged but not thrown.
402
- */
403
- async killSession(sessionId, label, verbose) {
404
- try {
405
- await this.provider.session.abort({ path: { id: sessionId } });
406
- }
407
- catch {
408
- // Session may already be idle — abort failing is fine
409
- }
410
- try {
411
- await this.provider.session.delete({ path: { id: sessionId } });
412
- if (verbose) {
413
- verbose(`[xref-parallel] 🗑 Killed session ${label} (${sessionId.slice(0, 8)}…)`);
414
- }
415
- }
416
- catch (err) {
417
- if (verbose) {
418
- verbose(`[xref-parallel] ⚠ Failed to delete session ${label}: ${err instanceof Error ? err.message : String(err)}`);
419
- }
420
- }
421
- }
422
400
  /**
423
401
  * Spawn a recalled worker session to apply specific fixes.
424
402
  *
@@ -429,29 +407,22 @@ export class CrossRefAgent extends BaseAgent {
429
407
  */
430
408
  async runRecalledWorker(projectName, destinationPath, assignment, _allDocFiles, _totalWorkers, instructions, verbose) {
431
409
  const tag = `recall-${assignment.label}`;
432
- const prompt = [
433
- `Worker **${assignment.label}** (recalled). Previously added cross-reference`,
434
- `links for **${projectName}** and the arbiter found link consistency issues.`,
435
- "",
436
- "## Destination directory",
437
- "",
438
- `All documentation files are in: \`${destinationPath}\``,
439
- "",
440
- "## Your assigned files",
441
- "",
442
- ...assignment.files.map((f) => `- \`${destinationPath}/${f}\``),
443
- "",
444
- "## Fix instructions from the arbiter",
445
- "",
410
+ const prompt = buildRecallPrompt({
411
+ destinationPath,
412
+ assignmentLabel: assignment.label,
413
+ assignmentFiles: assignment.files,
446
414
  instructions,
447
- "",
448
- "## Rules",
449
- "",
450
- "- Use the edit tool to make the requested fixes directly.",
451
- "- Only modify files in your assigned list unless the arbiter specifically",
452
- " instructed otherwise.",
453
- "- When you have completed all fixes, stop.",
454
- ].join("\n");
415
+ intro: [
416
+ `Worker **${assignment.label}** (recalled). Previously added cross-reference`,
417
+ `links for **${projectName}** and the arbiter found link consistency issues.`,
418
+ ],
419
+ rules: [
420
+ "- Use the edit tool to make the requested fixes directly.",
421
+ "- Only modify files in your assigned list unless the arbiter specifically",
422
+ " instructed otherwise.",
423
+ "- When you have completed all fixes, stop.",
424
+ ],
425
+ });
455
426
  if (verbose) {
456
427
  verbose(`[${tag}] Recall prompt: ${prompt.length.toLocaleString()} chars`);
457
428
  }
@@ -467,7 +438,7 @@ export class CrossRefAgent extends BaseAgent {
467
438
  }
468
439
  finally {
469
440
  // Always kill the recall session
470
- await this.killSession(sessionId, tag, verbose);
441
+ await killSession(this.provider, sessionId, tag, "xref-parallel", verbose);
471
442
  }
472
443
  }
473
444
  /**
@@ -12,53 +12,24 @@
12
12
  * 2. `wrapUp()` — called after all workers complete. Sends a final
13
13
  * prompt so the arbiter can issue any remaining
14
14
  * DECISION messages. Non-fatal on failure.
15
+ *
16
+ * Lifecycle methods are inherited from {@link BaseArbiter}; this class
17
+ * supplies only the configuration values and the prompt body.
15
18
  */
16
- import type { Provider } from "../providers/types.js";
17
- import { BaseAgent } from "./base-agent.js";
18
- import type { WorkerAssignment } from "./organization-agent.js";
19
+ import { BaseArbiter, type BaseArbiterPromptParams } from "./base-arbiter.js";
19
20
  /** Parameters for the cross-ref arbiter prompt. */
20
- export interface CrossRefArbiterPromptParams {
21
- /** Project name. */
22
- projectName: string;
23
- /** Absolute path to the destination directory. */
24
- destinationPath: string;
25
- /** ALL documentation files (for full awareness). */
26
- allDocFiles: string[];
27
- /** Worker assignments mapping each worker label to its owned files. */
28
- workerAssignments: WorkerAssignment[];
29
- }
21
+ export type CrossRefArbiterPromptParams = BaseArbiterPromptParams;
30
22
  /**
31
23
  * A coordinator agent that monitors cross-ref worker broadcasts and issues
32
24
  * DECISION directives to resolve cross-worker link consistency issues.
33
25
  *
34
26
  * Does NOT edit files — directs workers to make all changes.
35
27
  */
36
- export declare class CrossRefArbiterAgent extends BaseAgent {
37
- constructor(provider: Provider);
38
- /**
39
- * Create the arbiter session and send the initial prompt.
40
- *
41
- * The initial prompt is sent via `promptAsync` (fire-and-forget) because
42
- * the arbiter sits idle until it receives broadcasts from workers.
43
- *
44
- * @returns The session ID so the caller can register it in the SSE
45
- * relay map and later call `wrapUp()`.
46
- * @throws {AuthExpiredError} If session creation or the initial prompt
47
- * fails due to authentication expiry.
48
- */
49
- run(params: CrossRefArbiterPromptParams, verbose?: (msg: string) => void): Promise<{
50
- sessionId: string;
51
- }>;
52
- /**
53
- * Send the wrap-up prompt after all workers have completed.
54
- *
55
- * Asks the arbiter to issue any remaining DECISION messages and then
56
- * output its final `{ "status": "complete" }` response.
57
- *
58
- * This is intentionally **non-fatal** — if the arbiter fails, workers
59
- * have already made their edits and the pipeline can continue.
60
- */
61
- wrapUp(sessionId: string, verbose?: (msg: string) => void): Promise<void>;
28
+ export declare class CrossRefArbiterAgent extends BaseArbiter<CrossRefArbiterPromptParams> {
29
+ protected readonly tag = "xref-arbiter";
30
+ protected readonly sessionTitle = "Hem: xref-arbiter";
31
+ protected readonly agentField = "hem-xref";
32
+ protected makePrompt(params: CrossRefArbiterPromptParams): string;
62
33
  /**
63
34
  * Builds the cross-ref arbiter prompt.
64
35
  *