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.
- package/README.md +2 -2
- package/dist/index.js +21020 -0
- package/package.json +10 -6
- package/src/agents/artifact-searcher.ts +0 -1
- package/src/agents/bootstrapper.ts +164 -0
- package/src/agents/brainstormer.ts +140 -33
- package/src/agents/codebase-analyzer.ts +0 -1
- package/src/agents/codebase-locator.ts +0 -1
- package/src/agents/commander.ts +99 -10
- package/src/agents/executor.ts +18 -1
- package/src/agents/implementer.ts +83 -6
- package/src/agents/index.ts +29 -19
- package/src/agents/ledger-creator.ts +0 -1
- package/src/agents/octto.ts +132 -0
- package/src/agents/pattern-finder.ts +0 -1
- package/src/agents/planner.ts +139 -49
- package/src/agents/probe.ts +152 -0
- package/src/agents/project-initializer.ts +0 -1
- package/src/agents/reviewer.ts +75 -5
- package/src/config-loader.test.ts +226 -0
- package/src/config-loader.ts +132 -6
- package/src/hooks/artifact-auto-index.ts +2 -1
- package/src/hooks/auto-compact.ts +14 -21
- package/src/hooks/context-injector.ts +6 -13
- package/src/hooks/context-window-monitor.ts +8 -13
- package/src/hooks/ledger-loader.ts +4 -6
- package/src/hooks/token-aware-truncation.ts +11 -17
- package/src/index.ts +54 -22
- package/src/indexing/milestone-artifact-classifier.ts +26 -0
- package/src/indexing/milestone-artifact-ingest.ts +42 -0
- package/src/octto/constants.ts +20 -0
- package/src/octto/session/browser.ts +32 -0
- package/src/octto/session/index.ts +25 -0
- package/src/octto/session/server.ts +89 -0
- package/src/octto/session/sessions.ts +383 -0
- package/src/octto/session/types.ts +305 -0
- package/src/octto/session/utils.ts +25 -0
- package/src/octto/session/waiter.ts +139 -0
- package/src/octto/state/index.ts +5 -0
- package/src/octto/state/persistence.ts +65 -0
- package/src/octto/state/store.ts +161 -0
- package/src/octto/state/types.ts +51 -0
- package/src/octto/types.ts +376 -0
- package/src/octto/ui/bundle.ts +1650 -0
- package/src/octto/ui/index.ts +2 -0
- package/src/tools/artifact-index/index.ts +152 -3
- package/src/tools/artifact-index/schema.sql +21 -0
- package/src/tools/milestone-artifact-search.ts +48 -0
- package/src/tools/octto/brainstorm.ts +332 -0
- package/src/tools/octto/extractor.ts +95 -0
- package/src/tools/octto/factory.ts +89 -0
- package/src/tools/octto/formatters.ts +63 -0
- package/src/tools/octto/index.ts +27 -0
- package/src/tools/octto/processor.ts +165 -0
- package/src/tools/octto/questions.ts +508 -0
- package/src/tools/octto/responses.ts +135 -0
- package/src/tools/octto/session.ts +114 -0
- package/src/tools/octto/types.ts +21 -0
- package/src/tools/octto/utils.ts +4 -0
- package/src/tools/pty/manager.ts +13 -7
- package/src/tools/spawn-agent.ts +1 -3
- package/src/utils/config.ts +123 -0
- package/src/utils/errors.ts +57 -0
- package/src/utils/logger.ts +50 -0
|
@@ -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
|
+
}
|