@a-t-h-i/bot-lobby 0.1.0

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 (61) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +406 -0
  3. package/package.json +46 -0
  4. package/prompts/backend.md +28 -0
  5. package/prompts/designer.md +33 -0
  6. package/prompts/global.md +42 -0
  7. package/prompts/master.md +107 -0
  8. package/prompts/qa.md +26 -0
  9. package/prompts/researcher.md +32 -0
  10. package/prompts/reviewer.md +43 -0
  11. package/prompts/scout.md +36 -0
  12. package/prompts/worker.md +49 -0
  13. package/src/agents/backend.ts +10 -0
  14. package/src/agents/designer.ts +10 -0
  15. package/src/agents/qa.ts +10 -0
  16. package/src/agents/registry.ts +14 -0
  17. package/src/execution/agent-runner.ts +150 -0
  18. package/src/execution/git.ts +42 -0
  19. package/src/execution/pi-runner.ts +292 -0
  20. package/src/index.ts +13 -0
  21. package/src/knowledge/compactor.ts +135 -0
  22. package/src/knowledge/paths.ts +43 -0
  23. package/src/knowledge/selector.ts +82 -0
  24. package/src/knowledge/store.ts +111 -0
  25. package/src/master/decisions.ts +53 -0
  26. package/src/master/master.ts +298 -0
  27. package/src/master/research.ts +98 -0
  28. package/src/master/synthesis.ts +57 -0
  29. package/src/pi/activity.ts +60 -0
  30. package/src/pi/commands.ts +268 -0
  31. package/src/pi/events.ts +76 -0
  32. package/src/pi/expressions.ts +101 -0
  33. package/src/pi/mascot-art.ts +252 -0
  34. package/src/pi/notify.ts +42 -0
  35. package/src/pi/quiet.ts +46 -0
  36. package/src/pi/settings-ui.ts +258 -0
  37. package/src/pi/tool-renderers.ts +121 -0
  38. package/src/pi/tools.ts +158 -0
  39. package/src/pi/ui.ts +227 -0
  40. package/src/pi/zen-large.ts +483 -0
  41. package/src/pi/zen-metrics.ts +80 -0
  42. package/src/pi/zen.ts +460 -0
  43. package/src/prompts/compiler.ts +50 -0
  44. package/src/prompts/loader.ts +20 -0
  45. package/src/roles/markdown.ts +64 -0
  46. package/src/roles/registry.ts +16 -0
  47. package/src/roles/researcher.ts +83 -0
  48. package/src/roles/reviewer.ts +65 -0
  49. package/src/roles/scout.ts +61 -0
  50. package/src/roles/worker.ts +94 -0
  51. package/src/schemas/agent.ts +39 -0
  52. package/src/schemas/configuration.ts +107 -0
  53. package/src/schemas/findings.ts +110 -0
  54. package/src/schemas/task.ts +113 -0
  55. package/src/state/persistence.ts +232 -0
  56. package/src/state/project.ts +99 -0
  57. package/src/state/task-state.ts +22 -0
  58. package/src/text.ts +51 -0
  59. package/src/workflow/approvals.ts +45 -0
  60. package/src/workflow/transitions.ts +41 -0
  61. package/src/workflow/workflow.ts +771 -0
@@ -0,0 +1,53 @@
1
+ import type { Decision, Task, Verdict } from "../schemas/task.ts";
2
+ import type { Domain } from "../schemas/agent.ts";
3
+ import type { ScoutOutcome } from "./master.ts";
4
+ import { detectGaps, domainsInvolved } from "./synthesis.ts";
5
+
6
+ /** §19: everything the engine requires before completion may be declared. */
7
+ export function completionBlockers(task: Task, pendingCount: number): string[] {
8
+ const blockers: string[] = [];
9
+ if (!task.plan) blockers.push("no approved plan is recorded");
10
+ if (task.qaVerdict !== "pass") blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
11
+ if (pendingCount > 0) blockers.push(`${pendingCount} unresolved approval request(s)`);
12
+ if (task.blockers.length > 0) blockers.push(`${task.blockers.length} unresolved blocker(s)`);
13
+ return blockers;
14
+ }
15
+
16
+ /** Record a Master decision on the task so it survives into knowledge. */
17
+ export function recordDecision(
18
+ task: Task,
19
+ text: string,
20
+ domain: Decision["domain"] = "master",
21
+ now = new Date().toISOString(),
22
+ ): void {
23
+ task.decisions.push({ domain, text, createdAt: now });
24
+ }
25
+
26
+ /** §17: a changes-required verdict iterates until the configured limit. */
27
+ export function decideReviewLoop(
28
+ verdict: Verdict,
29
+ iterations: number,
30
+ maxIterations: number,
31
+ ): "accept" | "iterate" | "blocked" {
32
+ if (verdict === "pass") return "accept";
33
+ if (verdict === "blocked") return "blocked";
34
+ return iterations < maxIterations ? "iterate" : "blocked";
35
+ }
36
+
37
+ export interface ReconAssessment {
38
+ usableDomains: Domain[];
39
+ warnings: string[];
40
+ /** False when no scout produced usable evidence at all. */
41
+ hasEvidence: boolean;
42
+ }
43
+
44
+ /**
45
+ * Advisory (not a gate): the Master decides whether to proceed, but the engine
46
+ * surfaces what reconnaissance is missing so the decision is informed.
47
+ */
48
+ export function assessReconnaissance(outcomes: ScoutOutcome[]): ReconAssessment {
49
+ const warnings = detectGaps(outcomes);
50
+ const usableDomains = domainsInvolved(outcomes);
51
+ if (outcomes.length === 0) warnings.push("no scouts were run");
52
+ return { usableDomains, warnings, hasEvidence: usableDomains.length > 0 };
53
+ }
@@ -0,0 +1,298 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { BotLobbyConfig } from "../schemas/configuration.ts";
4
+ import type { Domain } from "../schemas/agent.ts";
5
+ import type { AgentRun, ReviewResult, ScoutResult, WorkerResult } from "../schemas/findings.ts";
6
+ import { domainSpec } from "../agents/registry.ts";
7
+ import { runAgent, runParallel, type AgentRequest } from "../execution/agent-runner.ts";
8
+ import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
9
+ import { readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
10
+ import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
11
+ import { summarizeOutcomes } from "./synthesis.ts";
12
+ import { parseWorkerResult, validateWorkerResult } from "../roles/worker.ts";
13
+ import { parseReviewResult, validateReviewResult } from "../roles/reviewer.ts";
14
+ import { truncate } from "../text.ts";
15
+ import { isScoutResultUsable, parseScoutResult, validateScoutResult } from "../roles/scout.ts";
16
+
17
+ export interface ScoutOutcome {
18
+ result: ScoutResult;
19
+ run: AgentRun;
20
+ issues: string[];
21
+ usable: boolean;
22
+ }
23
+
24
+ export interface ScoutRequest {
25
+ taskId: string;
26
+ /** Requirements/objective text used for context selection. */
27
+ taskText: string;
28
+ /** What the Master wants investigated. */
29
+ instruction: string;
30
+ domains: Domain[];
31
+ cwd: string;
32
+ dataRoots: readonly string[];
33
+ taskDir: string;
34
+ config: BotLobbyConfig;
35
+ signal?: AbortSignal;
36
+ onUpdate?: (run: AgentRun) => void;
37
+ }
38
+
39
+ /** Models are inherited from the session unless the config pins one. */
40
+ export function resolveModel(config: BotLobbyConfig, domain: Domain): string | undefined {
41
+ const model = config.agents[domain].model;
42
+ return model === "inherit" ? undefined : model;
43
+ }
44
+
45
+ function scoutInstruction(request: ScoutRequest, domain: Domain): string {
46
+ return [
47
+ request.instruction,
48
+ "",
49
+ `Domain focus: ${domainSpec(domain).scoutFocus}.`,
50
+ "Investigate the repository and report structured findings. Do not modify any file.",
51
+ ].join("\n");
52
+ }
53
+
54
+ function scoutContext(request: ScoutRequest, domain: Domain): AgentRequest["context"] {
55
+ const slices = readAgentKnowledge(request.dataRoots, domain);
56
+ return {
57
+ task: request.taskText,
58
+ ...selectKnowledge(`${request.taskText} ${domainSpec(domain).scoutFocus}`, slices),
59
+ instructions: request.config.agents[domain].instructions,
60
+ workflowContext: `Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`,
61
+ };
62
+ }
63
+
64
+ function toOutcome(run: AgentRun, domain: Domain): ScoutOutcome {
65
+ if (run.status !== "success") {
66
+ return {
67
+ result: parseScoutResult(domain, run.output),
68
+ run,
69
+ issues: [`scout ${run.status}: ${run.error ?? "no detail"}`],
70
+ usable: false,
71
+ };
72
+ }
73
+ const result = parseScoutResult(domain, run.output);
74
+ return { result, run, issues: validateScoutResult(result), usable: isScoutResultUsable(result) };
75
+ }
76
+
77
+ /** Run the selected domain scouts concurrently and persist their findings. */
78
+ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spawnPiProcess): Promise<ScoutOutcome[]> {
79
+ const requests: AgentRequest[] = request.domains.map((domain) => ({
80
+ taskId: request.taskId,
81
+ domain,
82
+ role: "scout",
83
+ instruction: scoutInstruction(request, domain),
84
+ context: scoutContext(request, domain),
85
+ model: resolveModel(request.config, domain),
86
+ thinking: request.config.agents[domain].thinking,
87
+ timeoutMs: request.config.workflow.agentTimeoutMs,
88
+ cwd: request.cwd,
89
+ signal: request.signal,
90
+ onUpdate: request.onUpdate,
91
+ retries: request.config.workflow.maxAgentRetries,
92
+ }));
93
+ const runs = await runParallel(requests, request.config.workflow.maxParallelScouts, run);
94
+ const outcomes = runs.map((agentRun) => toOutcome(agentRun, agentRun.domain));
95
+ saveScoutResults(request.taskDir, outcomes);
96
+ return outcomes;
97
+ }
98
+ export function scoutResultPath(taskDir: string, domain: Domain): string {
99
+ return join(taskDir, `scout-${domain}.json`);
100
+ }
101
+
102
+ export function saveScoutResults(taskDir: string, outcomes: ScoutOutcome[]): void {
103
+ for (const outcome of outcomes) {
104
+ writeFileEnsured(scoutResultPath(taskDir, outcome.result.domain), JSON.stringify(outcome, null, 2));
105
+ }
106
+ }
107
+
108
+ /** Re-read persisted scout findings for the given domains; the first dir with a file wins. */
109
+ export function loadScoutResults(taskDirs: string | readonly string[], domains: Domain[]): ScoutOutcome[] {
110
+ const dirs = typeof taskDirs === "string" ? [taskDirs] : taskDirs;
111
+ const outcomes: ScoutOutcome[] = [];
112
+ for (const domain of domains) {
113
+ const found = readScoutArtifact(dirs, domain);
114
+ if (found) outcomes.push(found);
115
+ }
116
+ return outcomes;
117
+ }
118
+
119
+ function readScoutArtifact(dirs: readonly string[], domain: Domain): ScoutOutcome | undefined {
120
+ for (const dir of dirs) {
121
+ const path = scoutResultPath(dir, domain);
122
+ if (!existsSync(path)) continue;
123
+ try {
124
+ return JSON.parse(readFileSync(path, "utf8")) as ScoutOutcome;
125
+ } catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ export interface WorkerOutcome {
133
+ result: WorkerResult;
134
+ run: AgentRun;
135
+ issues: string[];
136
+ }
137
+
138
+ export interface WorkerRequest {
139
+ taskId: string;
140
+ domain: Domain;
141
+ /** What the Master wants implemented. */
142
+ instruction: string;
143
+ /** Requirements + approved objective + approved plan. */
144
+ taskText: string;
145
+ scoutOutcomes: ScoutOutcome[];
146
+ cwd: string;
147
+ dataRoots: readonly string[];
148
+ config: BotLobbyConfig;
149
+ signal?: AbortSignal;
150
+ onUpdate?: (run: AgentRun) => void;
151
+ }
152
+
153
+ function workerWorkflowContext(request: WorkerRequest): string {
154
+ const spec = domainSpec(request.domain);
155
+ const own = request.scoutOutcomes.filter(
156
+ (outcome) => outcome.result.domain === request.domain && outcome.usable,
157
+ );
158
+ return [
159
+ `Task state: implementing. Domain: ${request.domain}.`,
160
+ `Domain boundary: ${spec.boundary}`,
161
+ "Dependency policy: never add a dependency or make a significant architectural change yourself; list them under the matching output section and stop that part of the work.",
162
+ own.length > 0
163
+ ? `Scout findings for your domain:\n${summarizeOutcomes(own, 1500)}`
164
+ : "No scout findings were collected for your domain; verify the repository yourself.",
165
+ ].join("\n\n");
166
+ }
167
+
168
+ /** Delegate one implementation step to a domain worker. */
169
+ export async function runWorker(
170
+ request: WorkerRequest,
171
+ run: ProcessRunner = spawnPiProcess,
172
+ ): Promise<WorkerOutcome> {
173
+ const slices = readAgentKnowledge(request.dataRoots, request.domain);
174
+ const selected = selectKnowledge(`${request.taskText} ${request.instruction}`, slices);
175
+ const agentRun = await runAgent(
176
+ {
177
+ taskId: request.taskId,
178
+ domain: request.domain,
179
+ role: "worker",
180
+ instruction: request.instruction,
181
+ context: {
182
+ task: request.taskText,
183
+ ...selected,
184
+ instructions: request.config.agents[request.domain].instructions,
185
+ workflowContext: workerWorkflowContext(request),
186
+ },
187
+ model: resolveModel(request.config, request.domain),
188
+ thinking: request.config.agents[request.domain].thinking,
189
+ timeoutMs: request.config.workflow.agentTimeoutMs,
190
+ cwd: request.cwd,
191
+ signal: request.signal,
192
+ onUpdate: request.onUpdate,
193
+ retries: request.config.workflow.maxAgentRetries,
194
+ },
195
+ run,
196
+ );
197
+ const result = parseWorkerResult(request.domain, agentRun.output);
198
+ const issues =
199
+ agentRun.status === "success"
200
+ ? validateWorkerResult(result)
201
+ : [`worker ${agentRun.status}: ${agentRun.error ?? "no detail"}`];
202
+ return { result, run: agentRun, issues };
203
+ }
204
+
205
+ export interface ReviewerOutcome {
206
+ result: ReviewResult;
207
+ run: AgentRun;
208
+ issues: string[];
209
+ }
210
+
211
+ export interface ReviewerRequest {
212
+ taskId: string;
213
+ domain: Domain;
214
+ taskText: string;
215
+ workerSummary: string;
216
+ scoutOutcomes: ScoutOutcome[];
217
+ diff: string;
218
+ instruction?: string;
219
+ cwd: string;
220
+ dataRoots: readonly string[];
221
+ config: BotLobbyConfig;
222
+ signal?: AbortSignal;
223
+ onUpdate?: (run: AgentRun) => void;
224
+ }
225
+
226
+ function reviewerContext(request: ReviewerRequest): string {
227
+ const owns = request.scoutOutcomes.filter(
228
+ (outcome) => outcome.result.domain === request.domain && outcome.usable,
229
+ );
230
+ return [
231
+ `Task state: reviewing. Domain: ${request.domain}.`,
232
+ "You may not modify implementation. Report required changes instead.",
233
+ request.workerSummary ? `Worker summary:\n${truncate(request.workerSummary, 2000)}` : "No worker summary available.",
234
+ owns.length > 0 ? `Scout findings:\n${summarizeOutcomes(owns, 1200)}` : "",
235
+ `Repository changes:\n${request.diff}`,
236
+ ]
237
+ .filter((line) => line.length > 0)
238
+ .join("\n\n");
239
+ }
240
+
241
+ /** True when the output carries the reviewer contract's required heading. */
242
+ export function hasReviewVerdict(text: string): boolean {
243
+ return /##\s*verdict/i.test(text);
244
+ }
245
+
246
+ /** A success without the Verdict section is off-contract and worth resampling. */
247
+ function isRetryableReviewRun(run: AgentRun): boolean {
248
+ return run.status !== "cancelled" && (run.status !== "success" || !hasReviewVerdict(run.output));
249
+ }
250
+
251
+ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelection): AgentRequest {
252
+ return {
253
+ taskId: request.taskId,
254
+ domain: request.domain,
255
+ role: "reviewer",
256
+ instruction:
257
+ request.instruction?.trim() ||
258
+ "Review the current repository changes against the approved requirements and plan.",
259
+ context: {
260
+ task: request.taskText,
261
+ ...selected,
262
+ instructions: request.config.agents[request.domain].instructions,
263
+ workflowContext: reviewerContext(request),
264
+ },
265
+ model: resolveModel(request.config, request.domain),
266
+ thinking: request.config.agents[request.domain].thinking,
267
+ timeoutMs: request.config.workflow.agentTimeoutMs,
268
+ cwd: request.cwd,
269
+ signal: request.signal,
270
+ onUpdate: request.onUpdate,
271
+ retries: 0,
272
+ };
273
+ }
274
+
275
+ function reviewerIssues(run: AgentRun, result: ReviewResult): string[] {
276
+ return run.status === "success"
277
+ ? validateReviewResult(result)
278
+ : [`reviewer ${run.status}: ${run.error ?? "no detail"}`];
279
+ }
280
+
281
+ /** Independently review the current repository state for one domain. */
282
+ export async function runReviewer(
283
+ request: ReviewerRequest,
284
+ run: ProcessRunner = spawnPiProcess,
285
+ ): Promise<ReviewerOutcome> {
286
+ const selected = selectKnowledge(
287
+ `${request.taskText} ${request.workerSummary}`,
288
+ readAgentKnowledge(request.dataRoots, request.domain),
289
+ );
290
+ const agentRequest = reviewerAgentRequest(request, selected);
291
+ const attempts = Math.max(1, request.config.workflow.maxAgentRetries + 1);
292
+ let agentRun = await runAgent(agentRequest, run);
293
+ for (let attempt = 2; attempt <= attempts && isRetryableReviewRun(agentRun); attempt++) {
294
+ agentRun = await runAgent(agentRequest, run);
295
+ }
296
+ const result = parseReviewResult(request.domain, agentRun.output);
297
+ return { result, run: agentRun, issues: reviewerIssues(agentRun, result) };
298
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Domain } from "../schemas/agent.ts";
4
+ import type { BotLobbyConfig } from "../schemas/configuration.ts";
5
+ import type { AgentRun, ResearchResult } from "../schemas/findings.ts";
6
+ import { runAgent, type AgentRequest } from "../execution/agent-runner.ts";
7
+ import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
8
+ import { writeFileEnsured } from "../knowledge/store.ts";
9
+ import { isResearchResultUsable, parseResearchResult, validateResearchResult } from "../roles/researcher.ts";
10
+ import { resolveModel } from "./master.ts";
11
+
12
+ export interface ResearchOutcome {
13
+ result: ResearchResult;
14
+ run: AgentRun;
15
+ issues: string[];
16
+ usable: boolean;
17
+ }
18
+
19
+ export interface ResearchRequest {
20
+ taskId: string;
21
+ /** Domain whose model, thinking level, and prompt layers the researcher reuses. */
22
+ domain: Domain;
23
+ /** The question or topic the Master wants researched. */
24
+ instruction: string;
25
+ cwd: string;
26
+ taskDir: string;
27
+ config: BotLobbyConfig;
28
+ signal?: AbortSignal;
29
+ onUpdate?: (run: AgentRun) => void;
30
+ }
31
+
32
+ export function researchResultPath(taskDir: string, domain: Domain): string {
33
+ return join(taskDir, `research-${domain}.json`);
34
+ }
35
+
36
+ export function saveResearchResult(taskDir: string, outcome: ResearchOutcome): void {
37
+ writeFileEnsured(researchResultPath(taskDir, outcome.result.domain), JSON.stringify(outcome, null, 2));
38
+ }
39
+
40
+ /** Re-read persisted research reports for the given domains. */
41
+ export function loadResearchResults(taskDir: string, domains: Domain[]): ResearchOutcome[] {
42
+ const outcomes: ResearchOutcome[] = [];
43
+ for (const domain of domains) {
44
+ const path = researchResultPath(taskDir, domain);
45
+ if (!existsSync(path)) continue;
46
+ try {
47
+ outcomes.push(JSON.parse(readFileSync(path, "utf8")) as ResearchOutcome);
48
+ } catch {
49
+ // Corrupted research artifact: skip it rather than failing the workflow.
50
+ }
51
+ }
52
+ return outcomes;
53
+ }
54
+
55
+ function researchContext(request: ResearchRequest): AgentRequest["context"] {
56
+ return {
57
+ task: request.instruction,
58
+ instructions: request.config.agents[request.domain].instructions,
59
+ workflowContext: `Task state: research requested by the Master. Domain: ${request.domain}. Read-only; report cited findings only.`,
60
+ };
61
+ }
62
+
63
+ function toOutcome(run: AgentRun, domain: Domain): ResearchOutcome {
64
+ if (run.status !== "success") {
65
+ return {
66
+ result: parseResearchResult(domain, run.output),
67
+ run,
68
+ issues: [`research ${run.status}: ${run.error ?? "no detail"}`],
69
+ usable: false,
70
+ };
71
+ }
72
+ const result = parseResearchResult(domain, run.output);
73
+ return { result, run, issues: validateResearchResult(result), usable: isResearchResultUsable(result) };
74
+ }
75
+
76
+ /** Run the researcher for one domain and persist its report for audit. */
77
+ export async function runResearch(request: ResearchRequest, run: ProcessRunner = spawnPiProcess): Promise<ResearchOutcome> {
78
+ const agentRun = await runAgent(
79
+ {
80
+ taskId: request.taskId,
81
+ domain: request.domain,
82
+ role: "researcher",
83
+ instruction: request.instruction,
84
+ context: researchContext(request),
85
+ model: resolveModel(request.config, request.domain),
86
+ thinking: request.config.agents[request.domain].thinking,
87
+ timeoutMs: request.config.workflow.agentTimeoutMs,
88
+ cwd: request.cwd,
89
+ signal: request.signal,
90
+ onUpdate: request.onUpdate,
91
+ retries: request.config.workflow.maxAgentRetries,
92
+ },
93
+ run,
94
+ );
95
+ const outcome = toOutcome(agentRun, agentRun.domain);
96
+ saveResearchResult(request.taskDir, outcome);
97
+ return outcome;
98
+ }
@@ -0,0 +1,57 @@
1
+ import type { Domain } from "../schemas/agent.ts";
2
+ import { truncate } from "../text.ts";
3
+ import type { ScoutOutcome } from "./master.ts";
4
+
5
+ /** Domains whose reconnaissance produced usable evidence. */
6
+ export function domainsInvolved(outcomes: ScoutOutcome[]): Domain[] {
7
+ return [...new Set(outcomes.filter((outcome) => outcome.usable).map((outcome) => outcome.result.domain))];
8
+ }
9
+
10
+ /** Missing, failed, or low-confidence recon the Master should treat carefully. */
11
+ export function detectGaps(outcomes: ScoutOutcome[]): string[] {
12
+ const gaps: string[] = [];
13
+ for (const outcome of outcomes) {
14
+ const { domain, confidence } = outcome.result;
15
+ if (!outcome.usable) gaps.push(`${domain}: no usable reconnaissance`);
16
+ if (outcome.usable && confidence === "low") gaps.push(`${domain}: low confidence, verify claims`);
17
+ for (const issue of outcome.issues) gaps.push(`${domain}: ${issue}`);
18
+ }
19
+ return [...new Set(gaps)];
20
+ }
21
+
22
+ /** Files reported by more than one domain: likely cross-domain touchpoints. */
23
+ export function detectSharedFiles(outcomes: ScoutOutcome[]): { path: string; domains: Domain[] }[] {
24
+ const byPath = new Map<string, Set<Domain>>();
25
+ for (const outcome of outcomes) {
26
+ for (const file of outcome.result.relevantFiles) {
27
+ const domains = byPath.get(file.path) ?? new Set<Domain>();
28
+ domains.add(outcome.result.domain);
29
+ byPath.set(file.path, domains);
30
+ }
31
+ }
32
+ return [...byPath.entries()]
33
+ .filter(([, domains]) => domains.size > 1)
34
+ .map(([path, domains]) => ({ path, domains: [...domains] }));
35
+ }
36
+
37
+ function describeOutcome(outcome: ScoutOutcome, rawLimit: number): string {
38
+ const { result } = outcome;
39
+ const lines = [
40
+ `### ${result.domain} (${outcome.run.status}${outcome.usable ? "" : ", unusable"}) — confidence: ${result.confidence}`,
41
+ result.scope ? `Scope: ${result.scope}` : "",
42
+ ...result.findings.slice(0, 12).map((finding) => `- ${finding}`),
43
+ ...result.risks.slice(0, 5).map((risk) => `- RISK: ${risk}`),
44
+ ...result.recommendations.slice(0, 5).map((rec) => `- REC: ${rec}`),
45
+ result.relevantFiles.length > 0
46
+ ? `Files: ${result.relevantFiles.slice(0, 10).map((file) => file.path).join(", ")}`
47
+ : "",
48
+ outcome.issues.length > 0 ? `Issues: ${outcome.issues.join("; ")}` : "",
49
+ result.raw.trim() ? `Raw (truncated): ${truncate(result.raw.trim(), rawLimit)}` : "",
50
+ ];
51
+ return lines.filter((line) => line.length > 0).join("\n");
52
+ }
53
+
54
+ /** Compact, bounded view of all scout findings for the Master's context (§36). */
55
+ export function summarizeOutcomes(outcomes: ScoutOutcome[], rawLimit = 2000): string {
56
+ return outcomes.map((outcome) => describeOutcome(outcome, rawLimit)).join("\n\n");
57
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One-word descriptions of the tool an agent is running: subagents show it as
3
+ * `[spinner] word - elapsed`, and the oracle names the master's `orchestrate`
4
+ * action. Pure lookup:
5
+ * no clock, environment or I/O reads.
6
+ */
7
+ const ACTIVITY_WORDS: Record<string, string> = {
8
+ read: "reading",
9
+ edit: "editing",
10
+ write: "editing",
11
+ grep: "searching",
12
+ find: "searching",
13
+ bash: "running",
14
+ orchestrate: "orchestrating",
15
+ web_search: "researching",
16
+ web_fetch: "researching",
17
+ fetch: "researching",
18
+ fetch_content: "researching",
19
+ get_search_content: "researching",
20
+ source_check: "researching",
21
+ };
22
+
23
+ const FALLBACK_WORD = "working";
24
+
25
+ export function activityWord(toolName: string): string {
26
+ return ACTIVITY_WORDS[toolName.trim().toLowerCase()] ?? FALLBACK_WORD;
27
+ }
28
+
29
+ /** Master `orchestrate` actions -> the word the oracle shows instead of the generic "orchestrating". */
30
+ const ORACLE_ACTION_WORDS: Record<string, string> = {
31
+ clarify: "asking",
32
+ scout: "scouting",
33
+ research: "researching",
34
+ propose: "proposing",
35
+ plan: "planning",
36
+ implement: "delegating",
37
+ qa: "reviewing",
38
+ knowledge: "recording",
39
+ compact: "recording",
40
+ decide: "deciding",
41
+ resolve_approval: "deciding",
42
+ complete: "wrapping up",
43
+ status: "checking",
44
+ block: "blocking",
45
+ resume: "resuming",
46
+ cancel: "cancelling",
47
+ };
48
+
49
+ function orchestrateAction(args: unknown): string | undefined {
50
+ if (!args || typeof args !== "object" || !("action" in args)) return undefined;
51
+ const action = (args as { action?: unknown }).action;
52
+ return typeof action === "string" ? action.trim().toLowerCase() : undefined;
53
+ }
54
+
55
+ /** One word for what the oracle (master) is doing; `orchestrate` maps by its action. */
56
+ export function oracleActivityWord(toolName: string, args?: unknown): string {
57
+ if (toolName.trim().toLowerCase() !== "orchestrate") return activityWord(toolName);
58
+ const action = orchestrateAction(args);
59
+ return (action && ORACLE_ACTION_WORDS[action]) || FALLBACK_WORD;
60
+ }