micode 0.8.4 → 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 (63) hide show
  1. package/dist/index.js +21020 -0
  2. package/package.json +6 -5
  3. package/src/agents/artifact-searcher.ts +0 -1
  4. package/src/agents/bootstrapper.ts +164 -0
  5. package/src/agents/brainstormer.ts +140 -33
  6. package/src/agents/codebase-analyzer.ts +0 -1
  7. package/src/agents/codebase-locator.ts +0 -1
  8. package/src/agents/commander.ts +99 -10
  9. package/src/agents/executor.ts +18 -1
  10. package/src/agents/implementer.ts +83 -6
  11. package/src/agents/index.ts +29 -19
  12. package/src/agents/ledger-creator.ts +0 -1
  13. package/src/agents/octto.ts +132 -0
  14. package/src/agents/pattern-finder.ts +0 -1
  15. package/src/agents/planner.ts +139 -49
  16. package/src/agents/probe.ts +152 -0
  17. package/src/agents/project-initializer.ts +0 -1
  18. package/src/agents/reviewer.ts +75 -5
  19. package/src/config-loader.test.ts +226 -0
  20. package/src/config-loader.ts +132 -6
  21. package/src/hooks/artifact-auto-index.ts +2 -1
  22. package/src/hooks/auto-compact.ts +14 -21
  23. package/src/hooks/context-injector.ts +6 -13
  24. package/src/hooks/context-window-monitor.ts +8 -13
  25. package/src/hooks/ledger-loader.ts +4 -6
  26. package/src/hooks/token-aware-truncation.ts +11 -17
  27. package/src/index.ts +54 -22
  28. package/src/indexing/milestone-artifact-classifier.ts +26 -0
  29. package/src/indexing/milestone-artifact-ingest.ts +42 -0
  30. package/src/octto/constants.ts +20 -0
  31. package/src/octto/session/browser.ts +32 -0
  32. package/src/octto/session/index.ts +25 -0
  33. package/src/octto/session/server.ts +89 -0
  34. package/src/octto/session/sessions.ts +383 -0
  35. package/src/octto/session/types.ts +305 -0
  36. package/src/octto/session/utils.ts +25 -0
  37. package/src/octto/session/waiter.ts +139 -0
  38. package/src/octto/state/index.ts +5 -0
  39. package/src/octto/state/persistence.ts +65 -0
  40. package/src/octto/state/store.ts +161 -0
  41. package/src/octto/state/types.ts +51 -0
  42. package/src/octto/types.ts +376 -0
  43. package/src/octto/ui/bundle.ts +1650 -0
  44. package/src/octto/ui/index.ts +2 -0
  45. package/src/tools/artifact-index/index.ts +152 -3
  46. package/src/tools/artifact-index/schema.sql +21 -0
  47. package/src/tools/milestone-artifact-search.ts +48 -0
  48. package/src/tools/octto/brainstorm.ts +332 -0
  49. package/src/tools/octto/extractor.ts +95 -0
  50. package/src/tools/octto/factory.ts +89 -0
  51. package/src/tools/octto/formatters.ts +63 -0
  52. package/src/tools/octto/index.ts +27 -0
  53. package/src/tools/octto/processor.ts +165 -0
  54. package/src/tools/octto/questions.ts +508 -0
  55. package/src/tools/octto/responses.ts +135 -0
  56. package/src/tools/octto/session.ts +114 -0
  57. package/src/tools/octto/types.ts +21 -0
  58. package/src/tools/octto/utils.ts +4 -0
  59. package/src/tools/pty/manager.ts +13 -7
  60. package/src/tools/spawn-agent.ts +1 -3
  61. package/src/utils/config.ts +123 -0
  62. package/src/utils/errors.ts +57 -0
  63. package/src/utils/logger.ts +50 -0
@@ -1,15 +1,11 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
- import { getContextLimit } from "../utils/model-limits";
3
1
  import { mkdir, writeFile } from "node:fs/promises";
4
2
  import { join } from "node:path";
5
3
 
6
- // Compact when this percentage of context is used
7
- const COMPACT_THRESHOLD = 0.5;
8
-
9
- const LEDGER_DIR = "thoughts/ledgers";
4
+ import type { PluginInput } from "@opencode-ai/plugin";
10
5
 
11
- // Timeout for waiting for compaction to complete (2 minutes)
12
- const COMPACTION_TIMEOUT_MS = 120_000;
6
+ import { config } from "../utils/config";
7
+ import { extractErrorMessage } from "../utils/errors";
8
+ import { getContextLimit } from "../utils/model-limits";
13
9
 
14
10
  interface PendingCompaction {
15
11
  resolve: () => void;
@@ -23,9 +19,6 @@ interface AutoCompactState {
23
19
  pendingCompactions: Map<string, PendingCompaction>;
24
20
  }
25
21
 
26
- // Cooldown between compaction attempts (prevent rapid re-triggering)
27
- const COMPACT_COOLDOWN_MS = 30_000; // 30 seconds
28
-
29
22
  export function createAutoCompactHook(ctx: PluginInput) {
30
23
  const state: AutoCompactState = {
31
24
  inProgress: new Set(),
@@ -65,13 +58,13 @@ export function createAutoCompactHook(ctx: PluginInput) {
65
58
  if (!summaryText.trim()) return;
66
59
 
67
60
  // Create ledger directory if needed
68
- const ledgerDir = join(ctx.directory, LEDGER_DIR);
61
+ const ledgerDir = join(ctx.directory, config.paths.ledgerDir);
69
62
  await mkdir(ledgerDir, { recursive: true });
70
63
 
71
64
  // Write ledger file - summary is already structured (Factory.ai/pi-mono format)
72
65
  const timestamp = new Date().toISOString();
73
66
  const sessionName = sessionID.slice(0, 8); // Use first 8 chars of session ID
74
- const ledgerPath = join(ledgerDir, `CONTINUITY_${sessionName}.md`);
67
+ const ledgerPath = join(ledgerDir, `${config.paths.ledgerPrefix}${sessionName}.md`);
75
68
 
76
69
  // Add metadata header, then the structured summary as-is
77
70
  const ledgerContent = `---
@@ -94,7 +87,7 @@ ${summaryText}
94
87
  const timeoutId = setTimeout(() => {
95
88
  state.pendingCompactions.delete(sessionID);
96
89
  reject(new Error("Compaction timed out"));
97
- }, COMPACTION_TIMEOUT_MS);
90
+ }, config.compaction.timeoutMs);
98
91
 
99
92
  state.pendingCompactions.set(sessionID, { resolve, reject, timeoutId });
100
93
  });
@@ -112,7 +105,7 @@ ${summaryText}
112
105
 
113
106
  // Check cooldown
114
107
  const lastCompact = state.lastCompactTime.get(sessionID) || 0;
115
- if (Date.now() - lastCompact < COMPACT_COOLDOWN_MS) {
108
+ if (Date.now() - lastCompact < config.compaction.cooldownMs) {
116
109
  return;
117
110
  }
118
111
 
@@ -120,7 +113,7 @@ ${summaryText}
120
113
 
121
114
  try {
122
115
  const usedPercent = Math.round(usageRatio * 100);
123
- const thresholdPercent = Math.round(COMPACT_THRESHOLD * 100);
116
+ const thresholdPercent = Math.round(config.compaction.threshold * 100);
124
117
 
125
118
  await ctx.client.tui
126
119
  .showToast({
@@ -128,7 +121,7 @@ ${summaryText}
128
121
  title: "Auto Compacting",
129
122
  message: `Context at ${usedPercent}% (threshold: ${thresholdPercent}%). Summarizing...`,
130
123
  variant: "warning",
131
- duration: 3000,
124
+ duration: config.timeouts.toastWarningMs,
132
125
  },
133
126
  })
134
127
  .catch(() => {});
@@ -158,19 +151,19 @@ ${summaryText}
158
151
  title: "Compaction Complete",
159
152
  message: "Session summarized and ledger updated.",
160
153
  variant: "success",
161
- duration: 3000,
154
+ duration: config.timeouts.toastSuccessMs,
162
155
  },
163
156
  })
164
157
  .catch(() => {});
165
158
  } catch (e) {
166
- const errorMsg = e instanceof Error ? e.message : String(e);
159
+ const errorMsg = extractErrorMessage(e);
167
160
  await ctx.client.tui
168
161
  .showToast({
169
162
  body: {
170
163
  title: "Compaction Failed",
171
164
  message: errorMsg.slice(0, 100),
172
165
  variant: "error",
173
- duration: 5000,
166
+ duration: config.timeouts.toastErrorMs,
174
167
  },
175
168
  })
176
169
  .catch(() => {});
@@ -233,7 +226,7 @@ ${summaryText}
233
226
  const usageRatio = totalUsed / contextLimit;
234
227
 
235
228
  // Trigger compaction if over threshold
236
- if (usageRatio >= COMPACT_THRESHOLD) {
229
+ if (usageRatio >= config.compaction.threshold) {
237
230
  triggerCompaction(sessionID, providerID, modelID, usageRatio);
238
231
  }
239
232
  }
@@ -1,12 +1,7 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join, dirname, resolve } from "node:path";
4
-
5
- // Files to inject at project root level (AGENTS.md and CLAUDE.md handled by OpenCode natively)
6
- const ROOT_CONTEXT_FILES = ["ARCHITECTURE.md", "CODE_STYLE.md", "README.md"] as const;
7
-
8
- // Files to collect when walking up directories (AGENTS.md handled by OpenCode natively)
9
- const DIRECTORY_CONTEXT_FILES = ["README.md"] as const;
4
+ import { config } from "../utils/config";
10
5
 
11
6
  // Tools that trigger directory-aware context injection
12
7
  const FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
@@ -18,8 +13,6 @@ interface ContextCache {
18
13
  lastRootCheck: number;
19
14
  }
20
15
 
21
- const CACHE_TTL = 30_000; // 30 seconds
22
-
23
16
  export function createContextInjectorHook(ctx: PluginInput) {
24
17
  const cache: ContextCache = {
25
18
  rootContent: new Map(),
@@ -30,14 +23,14 @@ export function createContextInjectorHook(ctx: PluginInput) {
30
23
  async function loadRootContextFiles(): Promise<Map<string, string>> {
31
24
  const now = Date.now();
32
25
 
33
- if (now - cache.lastRootCheck < CACHE_TTL && cache.rootContent.size > 0) {
26
+ if (now - cache.lastRootCheck < config.limits.contextCacheTtlMs && cache.rootContent.size > 0) {
34
27
  return cache.rootContent;
35
28
  }
36
29
 
37
30
  cache.rootContent.clear();
38
31
  cache.lastRootCheck = now;
39
32
 
40
- for (const filename of ROOT_CONTEXT_FILES) {
33
+ for (const filename of config.paths.rootContextFiles) {
41
34
  try {
42
35
  const filepath = join(ctx.directory, filename);
43
36
  const content = await readFile(filepath, "utf-8");
@@ -67,7 +60,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
67
60
 
68
61
  // Walk up from file directory to project root
69
62
  while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
70
- for (const filename of DIRECTORY_CONTEXT_FILES) {
63
+ for (const filename of config.paths.dirContextFiles) {
71
64
  const contextPath = join(currentDir, filename);
72
65
  const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
73
66
  const key = `${relPath}/${filename}`;
@@ -98,7 +91,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
98
91
  cache.directoryContent.set(cacheKey, collected);
99
92
 
100
93
  // Limit cache size
101
- if (cache.directoryContent.size > 100) {
94
+ if (cache.directoryContent.size > config.limits.contextCacheMaxSize) {
102
95
  const firstKey = cache.directoryContent.keys().next().value;
103
96
  if (firstKey) cache.directoryContent.delete(firstKey);
104
97
  }
@@ -143,7 +136,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
143
136
  ) => {
144
137
  if (!FILE_ACCESS_TOOLS.includes(input.tool)) return;
145
138
 
146
- const filePath = input.args?.file_path as string | undefined;
139
+ const filePath = input.args?.filePath as string | undefined;
147
140
  if (!filePath) return;
148
141
 
149
142
  try {
@@ -1,17 +1,12 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
+ import { config } from "../utils/config";
2
3
  import { getContextLimit } from "../utils/model-limits";
3
4
 
4
- // Thresholds for context window warnings
5
- const WARNING_THRESHOLD = 0.7; // 70% - remind there's still room
6
- const CRITICAL_THRESHOLD = 0.85; // 85% - getting tight
7
-
8
5
  interface MonitorState {
9
6
  lastWarningTime: Map<string, number>;
10
7
  lastUsageRatio: Map<string, number>;
11
8
  }
12
9
 
13
- const WARNING_COOLDOWN_MS = 120_000; // 2 minutes between warnings
14
-
15
10
  export function createContextWindowMonitorHook(ctx: PluginInput) {
16
11
  const state: MonitorState = {
17
12
  lastWarningTime: new Map(),
@@ -21,11 +16,11 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
21
16
  function getEncouragementMessage(usageRatio: number): string {
22
17
  const remaining = Math.round((1 - usageRatio) * 100);
23
18
 
24
- if (usageRatio < WARNING_THRESHOLD) {
19
+ if (usageRatio < config.contextWindow.warningThreshold) {
25
20
  return ""; // No message needed
26
21
  }
27
22
 
28
- if (usageRatio < CRITICAL_THRESHOLD) {
23
+ if (usageRatio < config.contextWindow.criticalThreshold) {
29
24
  return `Context: ${remaining}% remaining. Plenty of room - don't rush.`;
30
25
  }
31
26
 
@@ -40,7 +35,7 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
40
35
  ) => {
41
36
  const usageRatio = state.lastUsageRatio.get(input.sessionID);
42
37
 
43
- if (usageRatio && usageRatio >= WARNING_THRESHOLD) {
38
+ if (usageRatio && usageRatio >= config.contextWindow.warningThreshold) {
44
39
  const message = getEncouragementMessage(usageRatio);
45
40
  if (message && output.system) {
46
41
  output.system = `${output.system}\n\n<context-status>${message}</context-status>`;
@@ -80,13 +75,13 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
80
75
  state.lastUsageRatio.set(sessionID, usageRatio);
81
76
 
82
77
  // Show toast warning if threshold crossed
83
- if (usageRatio >= WARNING_THRESHOLD) {
78
+ if (usageRatio >= config.contextWindow.warningThreshold) {
84
79
  const lastWarning = state.lastWarningTime.get(sessionID) || 0;
85
- if (Date.now() - lastWarning > WARNING_COOLDOWN_MS) {
80
+ if (Date.now() - lastWarning > config.contextWindow.warningCooldownMs) {
86
81
  state.lastWarningTime.set(sessionID, Date.now());
87
82
 
88
83
  const remaining = Math.round((1 - usageRatio) * 100);
89
- const variant = usageRatio >= CRITICAL_THRESHOLD ? "warning" : "info";
84
+ const variant = usageRatio >= config.contextWindow.criticalThreshold ? "warning" : "info";
90
85
 
91
86
  await ctx.client.tui
92
87
  .showToast({
@@ -94,7 +89,7 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
94
89
  title: "Context Window",
95
90
  message: `${remaining}% remaining (${Math.round(totalUsed / 1000)}K / ${Math.round(contextLimit / 1000)}K tokens)`,
96
91
  variant,
97
- duration: 4000,
92
+ duration: config.timeouts.toastWarningMs,
98
93
  },
99
94
  })
100
95
  .catch(() => {});
@@ -2,9 +2,7 @@
2
2
  import type { PluginInput } from "@opencode-ai/plugin";
3
3
  import { readFile, readdir } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
-
6
- const LEDGER_DIR = "thoughts/ledgers";
7
- const LEDGER_PREFIX = "CONTINUITY_";
5
+ import { config } from "../utils/config";
8
6
 
9
7
  export interface LedgerInfo {
10
8
  sessionName: string;
@@ -13,11 +11,11 @@ export interface LedgerInfo {
13
11
  }
14
12
 
15
13
  export async function findCurrentLedger(directory: string): Promise<LedgerInfo | null> {
16
- const ledgerDir = join(directory, LEDGER_DIR);
14
+ const ledgerDir = join(directory, config.paths.ledgerDir);
17
15
 
18
16
  try {
19
17
  const files = await readdir(ledgerDir);
20
- const ledgerFiles = files.filter((f) => f.startsWith(LEDGER_PREFIX) && f.endsWith(".md"));
18
+ const ledgerFiles = files.filter((f) => f.startsWith(config.paths.ledgerPrefix) && f.endsWith(".md"));
21
19
 
22
20
  if (ledgerFiles.length === 0) return null;
23
21
 
@@ -40,7 +38,7 @@ export async function findCurrentLedger(directory: string): Promise<LedgerInfo |
40
38
 
41
39
  const filePath = join(ledgerDir, latestFile);
42
40
  const content = await readFile(filePath, "utf-8");
43
- const sessionName = latestFile.replace(LEDGER_PREFIX, "").replace(".md", "");
41
+ const sessionName = latestFile.replace(config.paths.ledgerPrefix, "").replace(".md", "");
44
42
 
45
43
  return { sessionName, filePath, content };
46
44
  } catch {
@@ -1,23 +1,17 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
+ import { config } from "../utils/config";
2
3
 
3
4
  // Tools that benefit from truncation
4
5
  const TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
5
6
 
6
- // Token estimation (conservative: 4 chars = 1 token)
7
- const CHARS_PER_TOKEN = 4;
8
- const DEFAULT_CONTEXT_LIMIT = 200_000;
9
- const DEFAULT_MAX_OUTPUT_TOKENS = 50_000;
10
- const SAFETY_MARGIN = 0.5; // Keep 50% headroom
11
- const PRESERVE_HEADER_LINES = 3;
12
-
13
7
  function estimateTokens(text: string): number {
14
- return Math.ceil(text.length / CHARS_PER_TOKEN);
8
+ return Math.ceil(text.length / config.tokens.charsPerToken);
15
9
  }
16
10
 
17
11
  function truncateToTokenLimit(
18
12
  output: string,
19
13
  maxTokens: number,
20
- preserveLines: number = PRESERVE_HEADER_LINES,
14
+ preserveLines: number = config.tokens.preserveHeaderLines,
21
15
  ): string {
22
16
  const currentTokens = estimateTokens(output);
23
17
 
@@ -85,7 +79,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
85
79
 
86
80
  const messages = (resp as { data?: unknown[] }).data;
87
81
  if (!Array.isArray(messages) || messages.length === 0) {
88
- return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
82
+ return { used: 0, limit: config.tokens.defaultContextLimit };
89
83
  }
90
84
 
91
85
  // Find last assistant message with usage info
@@ -96,7 +90,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
96
90
  }) as Record<string, unknown> | undefined;
97
91
 
98
92
  if (!lastAssistant) {
99
- return { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
93
+ return { used: 0, limit: config.tokens.defaultContextLimit };
100
94
  }
101
95
 
102
96
  const info = lastAssistant.info as Record<string, unknown> | undefined;
@@ -107,25 +101,25 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
107
101
  const used = inputTokens + cacheRead;
108
102
 
109
103
  // Get model limit (simplified - use default for now)
110
- const limit = DEFAULT_CONTEXT_LIMIT;
104
+ const limit = config.tokens.defaultContextLimit;
111
105
 
112
106
  const result = { used, limit };
113
107
  state.sessionTokenUsage.set(sessionID, result);
114
108
  return result;
115
109
  } catch {
116
- return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: DEFAULT_CONTEXT_LIMIT };
110
+ return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: config.tokens.defaultContextLimit };
117
111
  }
118
112
  }
119
113
 
120
114
  function calculateMaxOutputTokens(used: number, limit: number): number {
121
115
  const remaining = limit - used;
122
- const available = Math.floor(remaining * SAFETY_MARGIN);
116
+ const available = Math.floor(remaining * config.tokens.safetyMargin);
123
117
 
124
118
  if (available <= 0) {
125
119
  return 0;
126
120
  }
127
121
 
128
- return Math.min(available, DEFAULT_MAX_OUTPUT_TOKENS);
122
+ return Math.min(available, config.tokens.defaultMaxOutputTokens);
129
123
  }
130
124
 
131
125
  return {
@@ -180,8 +174,8 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
180
174
  } catch {
181
175
  // On error, apply static truncation as fallback
182
176
  const currentTokens = estimateTokens(output.output);
183
- if (currentTokens > DEFAULT_MAX_OUTPUT_TOKENS) {
184
- output.output = truncateToTokenLimit(output.output, DEFAULT_MAX_OUTPUT_TOKENS);
177
+ if (currentTokens > config.tokens.defaultMaxOutputTokens) {
178
+ output.output = truncateToTokenLimit(output.output, config.tokens.defaultMaxOutputTokens);
185
179
  }
186
180
  }
187
181
  },
package/src/index.ts CHANGED
@@ -3,30 +3,28 @@ import type { McpLocalConfig } from "@opencode-ai/sdk";
3
3
 
4
4
  // Agents
5
5
  import { agents, PRIMARY_AGENT_NAME } from "./agents";
6
-
7
- // Tools
8
- import { ast_grep_search, ast_grep_replace, checkAstGrepAvailable } from "./tools/ast-grep";
9
- import { btca_ask, checkBtcaAvailable } from "./tools/btca";
10
- import { look_at } from "./tools/look-at";
11
- import { artifact_search } from "./tools/artifact-search";
12
- import { createSpawnAgentTool } from "./tools/spawn-agent";
13
-
6
+ // Config loader
7
+ import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
8
+ import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
14
9
  // Hooks
15
10
  import { createAutoCompactHook } from "./hooks/auto-compact";
11
+ import { createCommentCheckerHook } from "./hooks/comment-checker";
16
12
  import { createContextInjectorHook } from "./hooks/context-injector";
17
- import { createSessionRecoveryHook } from "./hooks/session-recovery";
18
- import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
19
13
  import { createContextWindowMonitorHook } from "./hooks/context-window-monitor";
20
- import { createCommentCheckerHook } from "./hooks/comment-checker";
21
- import { createLedgerLoaderHook } from "./hooks/ledger-loader";
22
- import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
23
14
  import { createFileOpsTrackerHook, getFileOps } from "./hooks/file-ops-tracker";
24
-
15
+ import { createLedgerLoaderHook } from "./hooks/ledger-loader";
16
+ import { createSessionRecoveryHook } from "./hooks/session-recovery";
17
+ import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
18
+ import { artifact_search } from "./tools/artifact-search";
19
+ // Tools
20
+ import { ast_grep_replace, ast_grep_search, checkAstGrepAvailable } from "./tools/ast-grep";
21
+ import { btca_ask, checkBtcaAvailable } from "./tools/btca";
22
+ import { look_at } from "./tools/look-at";
23
+ import { milestone_artifact_search } from "./tools/milestone-artifact-search";
24
+ import { createOcttoTools, createSessionStore } from "./tools/octto";
25
25
  // PTY System
26
- import { PTYManager, createPtyTools } from "./tools/pty";
27
-
28
- // Config loader
29
- import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
26
+ import { createPtyTools, PTYManager } from "./tools/pty";
27
+ import { createSpawnAgentTool } from "./tools/spawn-agent";
30
28
 
31
29
  // Think mode: detect keywords and enable extended thinking
32
30
  const THINK_KEYWORDS = [
@@ -75,7 +73,7 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
75
73
  console.warn(`[micode] ${btcaStatus.message}`);
76
74
  }
77
75
 
78
- // Load user config for model overrides
76
+ // Load user config for temperature/maxTokens overrides (model overrides not supported)
79
77
  const userConfig = await loadMicodeConfig();
80
78
 
81
79
  // Think mode state per session
@@ -99,6 +97,28 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
99
97
  // Spawn agent tool (for subagents to spawn other subagents)
100
98
  const spawn_agent = createSpawnAgentTool(ctx);
101
99
 
100
+ // Octto (browser-based brainstorming) tools
101
+ const octtoSessionStore = createSessionStore();
102
+
103
+ // Track octto sessions per opencode session for cleanup
104
+ const octtoSessionsMap = new Map<string, Set<string>>();
105
+
106
+ const octtoTools = createOcttoTools(octtoSessionStore, ctx.client, {
107
+ onCreated: (parentSessionId, octtoSessionId) => {
108
+ const sessions = octtoSessionsMap.get(parentSessionId) ?? new Set<string>();
109
+ sessions.add(octtoSessionId);
110
+ octtoSessionsMap.set(parentSessionId, sessions);
111
+ },
112
+ onEnded: (parentSessionId, octtoSessionId) => {
113
+ const sessions = octtoSessionsMap.get(parentSessionId);
114
+ if (!sessions) return;
115
+ sessions.delete(octtoSessionId);
116
+ if (sessions.size === 0) {
117
+ octtoSessionsMap.delete(parentSessionId);
118
+ }
119
+ },
120
+ });
121
+
102
122
  return {
103
123
  // Tools
104
124
  tool: {
@@ -107,8 +127,10 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
107
127
  btca_ask,
108
128
  look_at,
109
129
  artifact_search,
130
+ milestone_artifact_search,
110
131
  spawn_agent,
111
132
  ...ptyTools,
133
+ ...octtoTools,
112
134
  },
113
135
 
114
136
  config: async (config) => {
@@ -292,12 +314,22 @@ IMPORTANT:
292
314
  },
293
315
 
294
316
  event: async ({ event }) => {
295
- // Session cleanup (think mode + PTY)
317
+ // Session cleanup (think mode + PTY + octto)
296
318
  if (event.type === "session.deleted") {
297
319
  const props = event.properties as { info?: { id?: string } } | undefined;
298
320
  if (props?.info?.id) {
299
- thinkModeState.delete(props.info.id);
300
- ptyManager.cleanupBySession(props.info.id);
321
+ const sessionId = props.info.id;
322
+ thinkModeState.delete(sessionId);
323
+ ptyManager.cleanupBySession(sessionId);
324
+
325
+ // Cleanup octto sessions
326
+ const octtoSessions = octtoSessionsMap.get(sessionId);
327
+ if (octtoSessions) {
328
+ for (const octtoSessionId of octtoSessions) {
329
+ await octtoSessionStore.endSession(octtoSessionId).catch(() => {});
330
+ }
331
+ octtoSessionsMap.delete(sessionId);
332
+ }
301
333
  }
302
334
  }
303
335
 
@@ -0,0 +1,26 @@
1
+ export const MILESTONE_ARTIFACT_TYPES = {
2
+ FEATURE: "feature",
3
+ DECISION: "decision",
4
+ SESSION: "session",
5
+ } as const;
6
+
7
+ export type MilestoneArtifactType = (typeof MILESTONE_ARTIFACT_TYPES)[keyof typeof MILESTONE_ARTIFACT_TYPES];
8
+
9
+ const FEATURE_HINTS = ["requirement", "implementation", "capability", "scope", "spec"];
10
+ const DECISION_HINTS = ["decision", "decided", "trade-off", "rationale", "chosen"];
11
+ const SESSION_HINTS = ["meeting", "status", "discussion", "notes", "update"];
12
+
13
+ const matchesAny = (content: string, hints: string[]) => hints.some((hint) => content.includes(hint));
14
+
15
+ export function classifyMilestoneArtifact(content: string): MilestoneArtifactType {
16
+ const normalized = content.toLowerCase();
17
+ const isFeature = matchesAny(normalized, FEATURE_HINTS);
18
+ const isDecision = matchesAny(normalized, DECISION_HINTS);
19
+ const isSession = matchesAny(normalized, SESSION_HINTS);
20
+
21
+ if (isFeature) return MILESTONE_ARTIFACT_TYPES.FEATURE;
22
+ if (isDecision) return MILESTONE_ARTIFACT_TYPES.DECISION;
23
+ if (isSession) return MILESTONE_ARTIFACT_TYPES.SESSION;
24
+
25
+ return MILESTONE_ARTIFACT_TYPES.SESSION;
26
+ }
@@ -0,0 +1,42 @@
1
+ import { type ArtifactIndex, getArtifactIndex } from "../tools/artifact-index";
2
+ import { log } from "../utils/logger";
3
+ import {
4
+ classifyMilestoneArtifact,
5
+ MILESTONE_ARTIFACT_TYPES,
6
+ type MilestoneArtifactType,
7
+ } from "./milestone-artifact-classifier";
8
+
9
+ export interface MilestoneArtifactInput {
10
+ id: string;
11
+ milestoneId: string;
12
+ sourceSessionId?: string;
13
+ createdAt?: string;
14
+ tags?: string[];
15
+ payload: string;
16
+ }
17
+
18
+ export async function ingestMilestoneArtifact(
19
+ input: MilestoneArtifactInput,
20
+ index?: ArtifactIndex,
21
+ classifier: (content: string) => MilestoneArtifactType = classifyMilestoneArtifact,
22
+ ): Promise<void> {
23
+ const artifactIndex = index ?? (await getArtifactIndex());
24
+ let artifactType: MilestoneArtifactType;
25
+
26
+ try {
27
+ artifactType = classifier(input.payload);
28
+ } catch (error) {
29
+ log.error("milestone-ingest", "Failed to classify milestone artifact, defaulting to session", error);
30
+ artifactType = MILESTONE_ARTIFACT_TYPES.SESSION;
31
+ }
32
+
33
+ await artifactIndex.indexMilestoneArtifact({
34
+ id: input.id,
35
+ milestoneId: input.milestoneId,
36
+ artifactType,
37
+ sourceSessionId: input.sourceSessionId,
38
+ createdAt: input.createdAt,
39
+ tags: input.tags,
40
+ payload: input.payload,
41
+ });
42
+ }
@@ -0,0 +1,20 @@
1
+ // src/octto/constants.ts
2
+ // Re-exports from centralized config for backward compatibility
3
+ // Single source of truth is in src/utils/config.ts
4
+
5
+ import { config } from "../utils/config";
6
+
7
+ /** Default timeout for waiting for user answers (5 minutes) */
8
+ export const DEFAULT_ANSWER_TIMEOUT_MS = config.octto.answerTimeoutMs;
9
+
10
+ /** Default maximum number of follow-up questions per branch */
11
+ export const DEFAULT_MAX_QUESTIONS = config.octto.maxQuestions;
12
+
13
+ /** Default timeout for brainstorm review (10 minutes) */
14
+ export const DEFAULT_REVIEW_TIMEOUT_MS = config.octto.reviewTimeoutMs;
15
+
16
+ /** Maximum number of brainstorm iterations */
17
+ export const MAX_ITERATIONS = config.octto.maxIterations;
18
+
19
+ /** Directory for persisting brainstorm state files */
20
+ export const STATE_DIR = config.octto.stateDir;
@@ -0,0 +1,32 @@
1
+ // src/octto/session/browser.ts
2
+ // Cross-platform browser opener
3
+
4
+ /**
5
+ * Opens the default browser to the specified URL.
6
+ * Detects platform and uses appropriate command.
7
+ */
8
+ export async function openBrowser(url: string): Promise<void> {
9
+ const platform = process.platform;
10
+
11
+ let command: string[];
12
+
13
+ switch (platform) {
14
+ case "darwin":
15
+ command = ["open", url];
16
+ break;
17
+ case "win32":
18
+ command = ["cmd", "/c", "start", url];
19
+ break;
20
+ default:
21
+ // Linux and others
22
+ command = ["xdg-open", url];
23
+ break;
24
+ }
25
+
26
+ const proc = Bun.spawn(command, {
27
+ stdout: "ignore",
28
+ stderr: "ignore",
29
+ });
30
+
31
+ await proc.exited;
32
+ }
@@ -0,0 +1,25 @@
1
+ // src/octto/session/index.ts
2
+ export type { SessionStore, SessionStoreOptions } from "./sessions";
3
+ export { createSessionStore } from "./sessions";
4
+ export type {
5
+ Answer,
6
+ AskCodeAnswer,
7
+ AskFileAnswer,
8
+ AskImageAnswer,
9
+ AskTextAnswer,
10
+ BaseConfig,
11
+ ConfirmAnswer,
12
+ EmojiReactAnswer,
13
+ PickManyAnswer,
14
+ PickOneAnswer,
15
+ QuestionAnswers,
16
+ QuestionConfig,
17
+ QuestionType,
18
+ RankAnswer,
19
+ RateAnswer,
20
+ ReviewAnswer,
21
+ ShowOptionsAnswer,
22
+ SliderAnswer,
23
+ ThumbsAnswer,
24
+ } from "./types";
25
+ export { QUESTION_TYPES, QUESTIONS, STATUSES, WS_MESSAGES } from "./types";