micode 0.8.3 → 0.8.5

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 (64) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +21020 -0
  3. package/package.json +10 -6
  4. package/src/agents/artifact-searcher.ts +0 -1
  5. package/src/agents/bootstrapper.ts +164 -0
  6. package/src/agents/brainstormer.ts +140 -33
  7. package/src/agents/codebase-analyzer.ts +0 -1
  8. package/src/agents/codebase-locator.ts +0 -1
  9. package/src/agents/commander.ts +99 -10
  10. package/src/agents/executor.ts +18 -1
  11. package/src/agents/implementer.ts +83 -6
  12. package/src/agents/index.ts +29 -19
  13. package/src/agents/ledger-creator.ts +0 -1
  14. package/src/agents/octto.ts +132 -0
  15. package/src/agents/pattern-finder.ts +0 -1
  16. package/src/agents/planner.ts +139 -49
  17. package/src/agents/probe.ts +152 -0
  18. package/src/agents/project-initializer.ts +0 -1
  19. package/src/agents/reviewer.ts +75 -5
  20. package/src/config-loader.test.ts +226 -0
  21. package/src/config-loader.ts +132 -6
  22. package/src/hooks/artifact-auto-index.ts +2 -1
  23. package/src/hooks/auto-compact.ts +14 -21
  24. package/src/hooks/context-injector.ts +6 -13
  25. package/src/hooks/context-window-monitor.ts +8 -13
  26. package/src/hooks/ledger-loader.ts +4 -6
  27. package/src/hooks/token-aware-truncation.ts +11 -17
  28. package/src/index.ts +54 -22
  29. package/src/indexing/milestone-artifact-classifier.ts +26 -0
  30. package/src/indexing/milestone-artifact-ingest.ts +42 -0
  31. package/src/octto/constants.ts +20 -0
  32. package/src/octto/session/browser.ts +32 -0
  33. package/src/octto/session/index.ts +25 -0
  34. package/src/octto/session/server.ts +89 -0
  35. package/src/octto/session/sessions.ts +383 -0
  36. package/src/octto/session/types.ts +305 -0
  37. package/src/octto/session/utils.ts +25 -0
  38. package/src/octto/session/waiter.ts +139 -0
  39. package/src/octto/state/index.ts +5 -0
  40. package/src/octto/state/persistence.ts +65 -0
  41. package/src/octto/state/store.ts +161 -0
  42. package/src/octto/state/types.ts +51 -0
  43. package/src/octto/types.ts +376 -0
  44. package/src/octto/ui/bundle.ts +1650 -0
  45. package/src/octto/ui/index.ts +2 -0
  46. package/src/tools/artifact-index/index.ts +152 -3
  47. package/src/tools/artifact-index/schema.sql +21 -0
  48. package/src/tools/milestone-artifact-search.ts +48 -0
  49. package/src/tools/octto/brainstorm.ts +332 -0
  50. package/src/tools/octto/extractor.ts +95 -0
  51. package/src/tools/octto/factory.ts +89 -0
  52. package/src/tools/octto/formatters.ts +63 -0
  53. package/src/tools/octto/index.ts +27 -0
  54. package/src/tools/octto/processor.ts +165 -0
  55. package/src/tools/octto/questions.ts +508 -0
  56. package/src/tools/octto/responses.ts +135 -0
  57. package/src/tools/octto/session.ts +114 -0
  58. package/src/tools/octto/types.ts +21 -0
  59. package/src/tools/octto/utils.ts +4 -0
  60. package/src/tools/pty/manager.ts +13 -7
  61. package/src/tools/spawn-agent.ts +1 -3
  62. package/src/utils/config.ts +123 -0
  63. package/src/utils/errors.ts +57 -0
  64. package/src/utils/logger.ts +50 -0
@@ -0,0 +1,2 @@
1
+ // src/octto/ui/index.ts
2
+ export { getHtmlBundle } from "./bundle";
@@ -1,9 +1,8 @@
1
1
  // src/tools/artifact-index/index.ts
2
2
  import { Database } from "bun:sqlite";
3
- import { readFileSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { mkdirSync, existsSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
6
4
  import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
7
6
 
8
7
  const DEFAULT_DB_DIR = join(homedir(), ".config", "opencode", "artifact-index");
9
8
  const DB_NAME = "context.db";
@@ -27,6 +26,16 @@ export interface LedgerRecord {
27
26
  filesModified?: string;
28
27
  }
29
28
 
29
+ export interface MilestoneArtifactRecord {
30
+ id: string;
31
+ milestoneId: string;
32
+ artifactType: string;
33
+ sourceSessionId?: string;
34
+ createdAt?: string;
35
+ tags?: string[];
36
+ payload: string;
37
+ }
38
+
30
39
  export interface SearchResult {
31
40
  type: "plan" | "ledger";
32
41
  id: string;
@@ -36,6 +45,18 @@ export interface SearchResult {
36
45
  score: number;
37
46
  }
38
47
 
48
+ export interface MilestoneArtifactSearchResult {
49
+ type: "milestone";
50
+ id: string;
51
+ milestoneId: string;
52
+ artifactType: string;
53
+ sourceSessionId?: string;
54
+ createdAt?: string;
55
+ tags: string[];
56
+ payload: string;
57
+ score: number;
58
+ }
59
+
39
60
  export class ArtifactIndex {
40
61
  private db: Database | null = null;
41
62
  private dbPath: string;
@@ -91,8 +112,26 @@ export class ArtifactIndex {
91
112
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
113
  indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
93
114
  );
115
+ CREATE TABLE IF NOT EXISTS milestone_artifacts (
116
+ id TEXT PRIMARY KEY,
117
+ milestone_id TEXT NOT NULL,
118
+ artifact_type TEXT NOT NULL,
119
+ source_session_id TEXT,
120
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
121
+ tags TEXT,
122
+ payload TEXT NOT NULL,
123
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
124
+ );
94
125
  CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(id, title, overview, approach);
95
126
  CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(id, session_name, goal, state_now, key_decisions);
127
+ CREATE VIRTUAL TABLE IF NOT EXISTS milestone_artifacts_fts USING fts5(
128
+ id,
129
+ milestone_id,
130
+ artifact_type,
131
+ payload,
132
+ tags,
133
+ source_session_id
134
+ );
96
135
  `;
97
136
  }
98
137
 
@@ -239,6 +278,116 @@ export class ArtifactIndex {
239
278
  return results.slice(0, limit);
240
279
  }
241
280
 
281
+ async indexMilestoneArtifact(record: MilestoneArtifactRecord): Promise<void> {
282
+ if (!this.db) throw new Error("Database not initialized");
283
+
284
+ const tags = JSON.stringify(record.tags ?? []);
285
+ const createdAt = record.createdAt ?? new Date().toISOString();
286
+ const existing = this.db.query("SELECT id FROM milestone_artifacts WHERE id = ?").get(record.id) as
287
+ | { id: string }
288
+ | undefined;
289
+
290
+ if (existing) {
291
+ this.db.run("DELETE FROM milestone_artifacts_fts WHERE id = ?", [existing.id]);
292
+ }
293
+
294
+ this.db.run(
295
+ `INSERT INTO milestone_artifacts (
296
+ id,
297
+ milestone_id,
298
+ artifact_type,
299
+ source_session_id,
300
+ created_at,
301
+ tags,
302
+ payload,
303
+ indexed_at
304
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
305
+ ON CONFLICT(id) DO UPDATE SET
306
+ milestone_id = excluded.milestone_id,
307
+ artifact_type = excluded.artifact_type,
308
+ source_session_id = excluded.source_session_id,
309
+ created_at = excluded.created_at,
310
+ tags = excluded.tags,
311
+ payload = excluded.payload,
312
+ indexed_at = CURRENT_TIMESTAMP`,
313
+ [
314
+ record.id,
315
+ record.milestoneId,
316
+ record.artifactType,
317
+ record.sourceSessionId ?? null,
318
+ createdAt,
319
+ tags,
320
+ record.payload,
321
+ ],
322
+ );
323
+
324
+ this.db.run(
325
+ `INSERT INTO milestone_artifacts_fts (
326
+ id,
327
+ milestone_id,
328
+ artifact_type,
329
+ payload,
330
+ tags,
331
+ source_session_id
332
+ ) VALUES (?, ?, ?, ?, ?, ?)`,
333
+ [record.id, record.milestoneId, record.artifactType, record.payload, tags, record.sourceSessionId ?? ""],
334
+ );
335
+ }
336
+
337
+ async searchMilestoneArtifacts(
338
+ query: string,
339
+ options: { milestoneId?: string; artifactType?: string; limit?: number } = {},
340
+ ): Promise<MilestoneArtifactSearchResult[]> {
341
+ if (!this.db) throw new Error("Database not initialized");
342
+
343
+ const escapedQuery = this.escapeFtsQuery(query);
344
+ const milestoneId = options.milestoneId ?? null;
345
+ const artifactType = options.artifactType ?? null;
346
+ const limit = options.limit ?? 10;
347
+
348
+ const rows = this.db
349
+ .query(
350
+ `SELECT
351
+ milestone_artifacts.id,
352
+ milestone_artifacts.milestone_id,
353
+ milestone_artifacts.artifact_type,
354
+ milestone_artifacts.source_session_id,
355
+ milestone_artifacts.created_at,
356
+ milestone_artifacts.tags,
357
+ milestone_artifacts.payload,
358
+ milestone_artifacts_fts.rank
359
+ FROM milestone_artifacts_fts
360
+ JOIN milestone_artifacts ON milestone_artifacts.id = milestone_artifacts_fts.id
361
+ WHERE milestone_artifacts_fts MATCH ?
362
+ AND (? IS NULL OR milestone_artifacts.milestone_id = ?)
363
+ AND (? IS NULL OR milestone_artifacts.artifact_type = ?)
364
+ ORDER BY milestone_artifacts_fts.rank
365
+ LIMIT ?`,
366
+ )
367
+ .all(escapedQuery, milestoneId, milestoneId, artifactType, artifactType, limit) as Array<{
368
+ id: string;
369
+ milestone_id: string;
370
+ artifact_type: string;
371
+ source_session_id: string | null;
372
+ created_at: string | null;
373
+ tags: string | null;
374
+ payload: string;
375
+ rank: number;
376
+ }>;
377
+
378
+ return rows.map((row) => ({
379
+ type: "milestone",
380
+ id: row.id,
381
+ milestoneId: row.milestone_id,
382
+ artifactType: row.artifact_type,
383
+ sourceSessionId: row.source_session_id ?? undefined,
384
+ createdAt: row.created_at ?? undefined,
385
+ tags: row.tags ? JSON.parse(row.tags) : [],
386
+ payload: row.payload,
387
+ score: -row.rank,
388
+ }));
389
+ }
390
+
242
391
  private escapeFtsQuery(query: string): string {
243
392
  // Escape special FTS5 characters and wrap terms in quotes
244
393
  return query
@@ -27,6 +27,18 @@ CREATE TABLE IF NOT EXISTS ledgers (
27
27
  indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28
28
  );
29
29
 
30
+ -- Milestone artifacts table
31
+ CREATE TABLE IF NOT EXISTS milestone_artifacts (
32
+ id TEXT PRIMARY KEY,
33
+ milestone_id TEXT NOT NULL,
34
+ artifact_type TEXT NOT NULL,
35
+ source_session_id TEXT,
36
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
37
+ tags TEXT,
38
+ payload TEXT NOT NULL,
39
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
40
+ );
41
+
30
42
  -- FTS5 virtual tables for full-text search (standalone, manually synced)
31
43
  CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(
32
44
  id,
@@ -42,3 +54,12 @@ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(
42
54
  state_now,
43
55
  key_decisions
44
56
  );
57
+
58
+ CREATE VIRTUAL TABLE IF NOT EXISTS milestone_artifacts_fts USING fts5(
59
+ id,
60
+ milestone_id,
61
+ artifact_type,
62
+ payload,
63
+ tags,
64
+ source_session_id
65
+ );
@@ -0,0 +1,48 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+
3
+ import { getArtifactIndex } from "./artifact-index";
4
+
5
+ const ARTIFACT_TYPES = ["feature", "decision", "session"] as const;
6
+
7
+ export const milestone_artifact_search = tool({
8
+ description: `Search milestone-driven artifacts stored in SQLite.
9
+ Use this to find feature, decision, or session artifacts for a specific milestone.
10
+ Returns ranked results filtered by milestone metadata.`,
11
+ args: {
12
+ query: tool.schema.string().describe("Search query for milestone artifacts"),
13
+ milestone_id: tool.schema.string().optional().describe("Optional milestone identifier to filter results"),
14
+ artifact_type: tool.schema.enum(ARTIFACT_TYPES).optional().describe("Optional artifact type to filter results"),
15
+ limit: tool.schema.number().optional().describe("Max results to return (default: 10)"),
16
+ },
17
+ execute: async (args) => {
18
+ try {
19
+ const index = await getArtifactIndex();
20
+ const results = await index.searchMilestoneArtifacts(args.query, {
21
+ milestoneId: args.milestone_id,
22
+ artifactType: args.artifact_type,
23
+ limit: args.limit,
24
+ });
25
+
26
+ if (results.length === 0) {
27
+ return "No milestone artifact results found for that query.";
28
+ }
29
+
30
+ let output = `## Milestone Artifact Search Results\n\nFound ${results.length} result(s).\n\n`;
31
+
32
+ for (const result of results) {
33
+ const tags = result.tags.length ? result.tags.join(", ") : "none";
34
+ output += `### ${result.milestoneId} · ${result.artifactType}\n`;
35
+ output += `- ID: ${result.id}\n`;
36
+ output += `- Source Session: ${result.sourceSessionId ?? "unknown"}\n`;
37
+ output += `- Created: ${result.createdAt ?? "unknown"}\n`;
38
+ output += `- Tags: ${tags}\n`;
39
+ output += `- Payload: ${result.payload}\n`;
40
+ output += `- Score: ${result.score.toFixed(2)}\n\n`;
41
+ }
42
+
43
+ return output;
44
+ } catch (error) {
45
+ return `Error searching milestone artifacts: ${error instanceof Error ? error.message : String(error)}`;
46
+ }
47
+ },
48
+ });
@@ -0,0 +1,332 @@
1
+ // src/tools/octto/brainstorm.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+
4
+ import type { ReviewAnswer, SessionStore } from "../../octto/session";
5
+ import { QUESTION_TYPES, QUESTIONS, STATUSES } from "../../octto/session";
6
+ import { BRANCH_STATUSES, type BrainstormState, createStateStore, type StateStore } from "../../octto/state";
7
+ import { config } from "../../utils/config";
8
+ import { log } from "../../utils/logger";
9
+ import { formatBranchStatus, formatFindings, formatFindingsList, formatQASummary } from "./formatters";
10
+ import { processAnswer } from "./processor";
11
+ import type { OcttoSessionTracker, OcttoTools, OpencodeClient } from "./types";
12
+ import { generateSessionId } from "./utils";
13
+
14
+ // --- Extracted helper functions ---
15
+
16
+ interface CollectionResult {
17
+ state: BrainstormState | null;
18
+ allComplete: boolean;
19
+ }
20
+
21
+ async function collectAnswers(
22
+ stateStore: StateStore,
23
+ sessions: SessionStore,
24
+ sessionId: string,
25
+ browserSessionId: string,
26
+ client: OpencodeClient,
27
+ ): Promise<CollectionResult> {
28
+ const pendingProcessing: Promise<void>[] = [];
29
+
30
+ for (let i = 0; i < config.octto.maxIterations; i++) {
31
+ if (await stateStore.isSessionComplete(sessionId)) break;
32
+
33
+ const answer = await sessions.getNextAnswer({
34
+ session_id: browserSessionId,
35
+ block: true,
36
+ timeout: config.octto.answerTimeoutMs,
37
+ });
38
+
39
+ if (!answer.completed) {
40
+ if (answer.status === STATUSES.NONE_PENDING) {
41
+ await Promise.all(pendingProcessing);
42
+ pendingProcessing.length = 0;
43
+ continue;
44
+ }
45
+ if (answer.status === STATUSES.TIMEOUT) break;
46
+ continue;
47
+ }
48
+
49
+ const { question_id, response } = answer;
50
+ if (!question_id || response === undefined) continue;
51
+
52
+ const processing = processAnswer(
53
+ stateStore,
54
+ sessions,
55
+ sessionId,
56
+ browserSessionId,
57
+ question_id,
58
+ response,
59
+ client,
60
+ ).catch((error) => {
61
+ log.error("octto", `Error processing answer ${question_id}`, error);
62
+ });
63
+ pendingProcessing.push(processing);
64
+ }
65
+
66
+ await Promise.all(pendingProcessing);
67
+
68
+ const [state, allComplete] = await Promise.all([
69
+ stateStore.getSession(sessionId),
70
+ stateStore.isSessionComplete(sessionId),
71
+ ]);
72
+
73
+ return { state, allComplete };
74
+ }
75
+
76
+ interface ReviewSection {
77
+ id: string;
78
+ title: string;
79
+ content: string;
80
+ }
81
+
82
+ function buildReviewSections(state: BrainstormState): ReviewSection[] {
83
+ return [
84
+ {
85
+ id: "summary",
86
+ title: "Original Request",
87
+ content: state.request,
88
+ },
89
+ ...state.branch_order.map((id) => {
90
+ const b = state.branches[id];
91
+ const qaSummary = formatQASummary(b);
92
+ return {
93
+ id,
94
+ title: b.scope,
95
+ content: `**Finding:** ${b.finding || "No finding"}\n\n**Discussion:**\n${qaSummary || "(no questions answered)"}`,
96
+ };
97
+ }),
98
+ ];
99
+ }
100
+
101
+ interface ReviewResult {
102
+ approved: boolean;
103
+ feedback: string;
104
+ }
105
+
106
+ async function waitForReviewApproval(sessions: SessionStore, browserSessionId: string): Promise<ReviewResult> {
107
+ const result = await sessions.getNextAnswer({
108
+ session_id: browserSessionId,
109
+ block: true,
110
+ timeout: config.octto.reviewTimeoutMs,
111
+ });
112
+
113
+ if (!result.completed || !result.response) {
114
+ return { approved: false, feedback: "" };
115
+ }
116
+
117
+ const response = result.response as ReviewAnswer;
118
+ return {
119
+ approved: response.decision === "approve",
120
+ feedback: response.feedback ?? "",
121
+ };
122
+ }
123
+
124
+ // --- Format functions ---
125
+
126
+ function formatInProgressResult(state: BrainstormState): string {
127
+ const branches = state.branch_order.map((id) => formatBranchStatus(state.branches[id])).join("\n");
128
+ return `<brainstorm_in_progress>
129
+ <request>${state.request}</request>
130
+ <branches>
131
+ ${branches}
132
+ </branches>
133
+ <next_action>Call await_brainstorm_complete again to continue</next_action>
134
+ </brainstorm_in_progress>`;
135
+ }
136
+
137
+ function formatSkippedReviewResult(state: BrainstormState): string {
138
+ return `<brainstorm_complete status="review_skipped">
139
+ <request>${state.request}</request>
140
+ <branch_count>${state.branch_order.length}</branch_count>
141
+ <note>Browser session ended before review</note>
142
+ ${formatFindings(state)}
143
+ <next_action>Write the design document to thoughts/shared/designs/</next_action>
144
+ </brainstorm_complete>`;
145
+ }
146
+
147
+ function formatCompletionResult(state: BrainstormState, approved: boolean, feedback: string): string {
148
+ const feedbackXml = feedback ? `\n <feedback>${feedback}</feedback>` : "";
149
+ const nextAction = approved
150
+ ? "Write the design document to thoughts/shared/designs/"
151
+ : "Review feedback and discuss with user before proceeding";
152
+ return `<brainstorm_complete status="${approved ? "approved" : "changes_requested"}">
153
+ <request>${state.request}</request>
154
+ <branch_count>${state.branch_order.length}</branch_count>${feedbackXml}
155
+ ${formatFindings(state)}
156
+ <next_action>${nextAction}</next_action>
157
+ </brainstorm_complete>`;
158
+ }
159
+
160
+ // --- Tool definitions ---
161
+
162
+ export function createBrainstormTools(
163
+ sessions: SessionStore,
164
+ client: OpencodeClient,
165
+ tracker?: OcttoSessionTracker,
166
+ ): OcttoTools {
167
+ const store = createStateStore();
168
+
169
+ const create_brainstorm = tool({
170
+ description: "Create a new brainstorm session with exploration branches",
171
+ args: {
172
+ request: tool.schema.string().describe("The original user request"),
173
+ branches: tool.schema
174
+ .array(
175
+ tool.schema.object({
176
+ id: tool.schema.string(),
177
+ scope: tool.schema.string(),
178
+ initial_question: tool.schema.object({
179
+ type: tool.schema.enum(QUESTION_TYPES),
180
+ config: tool.schema.looseObject({
181
+ question: tool.schema.string().optional(),
182
+ context: tool.schema.string().optional(),
183
+ }),
184
+ }),
185
+ }),
186
+ )
187
+ .describe("Branches to explore"),
188
+ },
189
+ execute: async (args, context) => {
190
+ const sessionId = generateSessionId();
191
+
192
+ await store.createSession(
193
+ sessionId,
194
+ args.request,
195
+ args.branches.map((b) => ({ id: b.id, scope: b.scope })),
196
+ );
197
+
198
+ const initialQuestions = args.branches.map((b) => {
199
+ const { type, config } = b.initial_question;
200
+ const context = `[${b.scope}] ${config.context ?? ""}`.trim();
201
+ return {
202
+ type,
203
+ config: { ...config, context },
204
+ };
205
+ });
206
+
207
+ const browserSession = await sessions.startSession({
208
+ title: "Brainstorming Session",
209
+ questions: initialQuestions,
210
+ });
211
+
212
+ tracker?.onCreated?.(context.sessionID, browserSession.session_id);
213
+ await store.setBrowserSessionId(sessionId, browserSession.session_id);
214
+
215
+ for (const [i, branch] of args.branches.entries()) {
216
+ const questionId = browserSession.question_ids?.[i];
217
+ if (!questionId) continue;
218
+
219
+ const { type, config } = branch.initial_question;
220
+ await store.addQuestionToBranch(sessionId, branch.id, {
221
+ id: questionId,
222
+ type,
223
+ text: config.question ?? "Question",
224
+ config,
225
+ });
226
+ }
227
+
228
+ const branchesXml = args.branches.map((b) => ` <branch id="${b.id}">${b.scope}</branch>`).join("\n");
229
+ return `<brainstorm_created>
230
+ <session_id>${sessionId}</session_id>
231
+ <browser_session>${browserSession.session_id}</browser_session>
232
+ <url>${browserSession.url}</url>
233
+ <branches>
234
+ ${branchesXml}
235
+ </branches>
236
+ <next_action>Call get_next_answer(session_id="${browserSession.session_id}", block=true)</next_action>
237
+ </brainstorm_created>`;
238
+ },
239
+ });
240
+
241
+ const get_session_summary = tool({
242
+ description: "Get summary of all branches and their findings",
243
+ args: {
244
+ session_id: tool.schema.string().describe("Brainstorm session ID"),
245
+ },
246
+ execute: async (args) => {
247
+ const state = await store.getSession(args.session_id);
248
+ if (!state) return `<error>Session not found: ${args.session_id}</error>`;
249
+
250
+ const branches = state.branch_order.map((id) => formatBranchStatus(state.branches[id])).join("\n");
251
+ const allDone = Object.values(state.branches).every((b) => b.status === BRANCH_STATUSES.DONE);
252
+
253
+ return `<session_summary>
254
+ <request>${state.request}</request>
255
+ <status>${allDone ? "complete" : "in_progress"}</status>
256
+ <branches>
257
+ ${branches}
258
+ </branches>
259
+ </session_summary>`;
260
+ },
261
+ });
262
+
263
+ const end_brainstorm = tool({
264
+ description: "End a brainstorm session and get final summary",
265
+ args: {
266
+ session_id: tool.schema.string().describe("Brainstorm session ID"),
267
+ },
268
+ execute: async (args, context) => {
269
+ const state = await store.getSession(args.session_id);
270
+ if (!state) return `<error>Session not found: ${args.session_id}</error>`;
271
+
272
+ if (state.browser_session_id) {
273
+ const result = await sessions.endSession(state.browser_session_id);
274
+ if (result.ok) {
275
+ tracker?.onEnded?.(context.sessionID, state.browser_session_id);
276
+ }
277
+ }
278
+
279
+ const findings = formatFindingsList(state);
280
+ await store.deleteSession(args.session_id);
281
+
282
+ return `<brainstorm_ended>
283
+ <request>${state.request}</request>
284
+ ${findings}
285
+ <next_action>Write the design document based on these findings to thoughts/shared/designs/</next_action>
286
+ </brainstorm_ended>`;
287
+ },
288
+ });
289
+
290
+ const await_brainstorm_complete = tool({
291
+ description: `Wait for brainstorm session to complete. Processes answers asynchronously as they arrive.
292
+ Returns when all branches are done with their findings.
293
+ This is the recommended way to run a brainstorm - just create_brainstorm then await_brainstorm_complete.`,
294
+ args: {
295
+ session_id: tool.schema.string().describe("Brainstorm session ID (state session)"),
296
+ browser_session_id: tool.schema.string().describe("Browser session ID (for collecting answers)"),
297
+ },
298
+ execute: async (args) => {
299
+ const { state, allComplete } = await collectAnswers(
300
+ store,
301
+ sessions,
302
+ args.session_id,
303
+ args.browser_session_id,
304
+ client,
305
+ );
306
+
307
+ if (!state) return "<error>Session lost</error>";
308
+ if (!allComplete) return formatInProgressResult(state);
309
+
310
+ const sections = buildReviewSections(state);
311
+
312
+ try {
313
+ sessions.pushQuestion(args.browser_session_id, QUESTIONS.SHOW_PLAN, {
314
+ question: "Review Design Plan",
315
+ sections,
316
+ });
317
+ } catch {
318
+ return formatSkippedReviewResult(state);
319
+ }
320
+
321
+ const { approved, feedback } = await waitForReviewApproval(sessions, args.browser_session_id);
322
+ return formatCompletionResult(state, approved, feedback);
323
+ },
324
+ });
325
+
326
+ return {
327
+ create_brainstorm,
328
+ get_session_summary,
329
+ end_brainstorm,
330
+ await_brainstorm_complete,
331
+ };
332
+ }
@@ -0,0 +1,95 @@
1
+ // src/tools/octto/extractor.ts
2
+ // Utility functions for extracting answer summaries
3
+
4
+ import type {
5
+ Answer,
6
+ AskCodeAnswer,
7
+ AskTextAnswer,
8
+ ConfirmAnswer,
9
+ EmojiReactAnswer,
10
+ PickManyAnswer,
11
+ PickOneAnswer,
12
+ QuestionType,
13
+ RankAnswer,
14
+ RateAnswer,
15
+ ReviewAnswer,
16
+ ShowOptionsAnswer,
17
+ SliderAnswer,
18
+ ThumbsAnswer,
19
+ } from "../../octto/session";
20
+ import { QUESTIONS } from "../../octto/session";
21
+
22
+ const MAX_TEXT_LENGTH = 100;
23
+
24
+ function truncateText(text: string): string {
25
+ return text.length > MAX_TEXT_LENGTH ? `${text.substring(0, MAX_TEXT_LENGTH)}...` : text;
26
+ }
27
+
28
+ export function extractAnswerSummary(type: QuestionType, answer: Answer): string {
29
+ switch (type) {
30
+ case QUESTIONS.PICK_ONE:
31
+ return (answer as PickOneAnswer).selected;
32
+
33
+ case QUESTIONS.PICK_MANY:
34
+ return (answer as PickManyAnswer).selected.join(", ");
35
+
36
+ case QUESTIONS.CONFIRM:
37
+ return (answer as ConfirmAnswer).choice;
38
+
39
+ case QUESTIONS.THUMBS:
40
+ return (answer as ThumbsAnswer).choice;
41
+
42
+ case QUESTIONS.EMOJI_REACT:
43
+ return (answer as EmojiReactAnswer).emoji;
44
+
45
+ case QUESTIONS.ASK_TEXT:
46
+ return truncateText((answer as AskTextAnswer).text);
47
+
48
+ case QUESTIONS.SLIDER:
49
+ return String((answer as SliderAnswer).value);
50
+
51
+ case QUESTIONS.RANK: {
52
+ const rankAnswer = answer as RankAnswer;
53
+ const sorted = [...rankAnswer.ranking].sort((a, b) => a.rank - b.rank);
54
+ return sorted.map((r) => r.id).join(" → ");
55
+ }
56
+
57
+ case QUESTIONS.RATE: {
58
+ const rateAnswer = answer as RateAnswer;
59
+ const entries = Object.entries(rateAnswer.ratings);
60
+ if (entries.length === 0) return "no ratings";
61
+ const sorted = entries.sort((a, b) => b[1] - a[1]);
62
+ return sorted
63
+ .slice(0, 3)
64
+ .map(([k, v]) => `${k}: ${v}`)
65
+ .join(", ");
66
+ }
67
+
68
+ case QUESTIONS.ASK_CODE:
69
+ return truncateText((answer as AskCodeAnswer).code);
70
+
71
+ case QUESTIONS.ASK_IMAGE:
72
+ case QUESTIONS.ASK_FILE:
73
+ return "file(s) uploaded";
74
+
75
+ case QUESTIONS.SHOW_DIFF:
76
+ case QUESTIONS.SHOW_PLAN:
77
+ case QUESTIONS.REVIEW_SECTION: {
78
+ const reviewAnswer = answer as ReviewAnswer;
79
+ return reviewAnswer.feedback
80
+ ? `${reviewAnswer.decision}: ${truncateText(reviewAnswer.feedback)}`
81
+ : reviewAnswer.decision;
82
+ }
83
+
84
+ case QUESTIONS.SHOW_OPTIONS: {
85
+ const optAnswer = answer as ShowOptionsAnswer;
86
+ return optAnswer.feedback ? `${optAnswer.selected}: ${truncateText(optAnswer.feedback)}` : optAnswer.selected;
87
+ }
88
+
89
+ default: {
90
+ // Exhaustiveness check - if we get here, we missed a case
91
+ const _exhaustive: never = type;
92
+ return String(_exhaustive);
93
+ }
94
+ }
95
+ }