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,139 @@
1
+ // src/octto/session/waiter.ts
2
+ // Immutable waiter management for async response handling
3
+
4
+ export interface Waiters<K, T> {
5
+ register: (key: K, callback: (data: T) => void) => () => void;
6
+ notifyFirst: (key: K, data: T) => void;
7
+ notifyAll: (key: K, data: T) => void;
8
+ has: (key: K) => boolean;
9
+ count: (key: K) => number;
10
+ clear: (key: K) => void;
11
+ }
12
+
13
+ /**
14
+ * Create a waiter registry for async response handling.
15
+ * Each operation creates a new array rather than mutating in place.
16
+ *
17
+ * @typeParam K - Key type (e.g., string for question_id or session_id)
18
+ * @typeParam T - Data type passed to waiter callbacks
19
+ */
20
+ export function createWaiters<K, T>(): Waiters<K, T> {
21
+ const waiters = new Map<K, Array<(data: T) => void>>();
22
+
23
+ return {
24
+ /**
25
+ * Register a waiter callback for a key.
26
+ * Returns a cleanup function to remove this specific waiter.
27
+ */
28
+ register(key: K, callback: (data: T) => void): () => void {
29
+ // Create new array with callback appended (immutable)
30
+ const current = waiters.get(key) || [];
31
+ waiters.set(key, [...current, callback]);
32
+
33
+ // Return cleanup function that removes this specific callback
34
+ return () => {
35
+ const callbacks = waiters.get(key);
36
+ if (!callbacks) return;
37
+
38
+ const idx = callbacks.indexOf(callback);
39
+ if (idx >= 0) {
40
+ // Create new array without this callback (immutable)
41
+ const remaining = [...callbacks.slice(0, idx), ...callbacks.slice(idx + 1)];
42
+ if (remaining.length === 0) {
43
+ waiters.delete(key);
44
+ } else {
45
+ waiters.set(key, remaining);
46
+ }
47
+ }
48
+ };
49
+ },
50
+
51
+ /**
52
+ * Notify only the first waiter for a key and remove it.
53
+ * Other waiters remain registered for subsequent notifications.
54
+ */
55
+ notifyFirst(key: K, data: T): void {
56
+ const callbacks = waiters.get(key);
57
+ if (!callbacks || callbacks.length === 0) return;
58
+
59
+ const [first, ...rest] = callbacks;
60
+ first(data);
61
+
62
+ // Set new array without first element (immutable)
63
+ if (rest.length === 0) {
64
+ waiters.delete(key);
65
+ } else {
66
+ waiters.set(key, rest);
67
+ }
68
+ },
69
+
70
+ /**
71
+ * Notify all waiters for a key and remove them all.
72
+ */
73
+ notifyAll(key: K, data: T): void {
74
+ const callbacks = waiters.get(key);
75
+ if (!callbacks) return;
76
+
77
+ try {
78
+ for (const callback of callbacks) {
79
+ try {
80
+ callback(data);
81
+ } catch (error) {
82
+ console.error("Waiter notifyAll failed", error);
83
+ break;
84
+ }
85
+ }
86
+ } finally {
87
+ waiters.delete(key);
88
+ }
89
+ },
90
+
91
+ /**
92
+ * Check if there are any waiters for a key.
93
+ */
94
+ has(key: K): boolean {
95
+ const callbacks = waiters.get(key);
96
+ return callbacks !== undefined && callbacks.length > 0;
97
+ },
98
+
99
+ /**
100
+ * Get the number of waiters for a key.
101
+ */
102
+ count(key: K): number {
103
+ return waiters.get(key)?.length ?? 0;
104
+ },
105
+
106
+ /**
107
+ * Remove all waiters for a key without notifying them.
108
+ */
109
+ clear(key: K): void {
110
+ waiters.delete(key);
111
+ },
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Result of waiting for a response
117
+ */
118
+ export type WaitResult<T> = { ok: true; data: T } | { ok: false; reason: "timeout" };
119
+
120
+ /**
121
+ * Wait for a response with timeout.
122
+ * Registers a waiter and returns a promise that resolves when notified or times out.
123
+ */
124
+ export function waitForResponse<K, T>(waiters: Waiters<K, T>, key: K, timeoutMs: number): Promise<WaitResult<T>> {
125
+ return new Promise((resolve) => {
126
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
127
+ let cleanup: (() => void) | undefined;
128
+
129
+ cleanup = waiters.register(key, (data) => {
130
+ if (timeoutId) clearTimeout(timeoutId);
131
+ resolve({ ok: true, data });
132
+ });
133
+
134
+ timeoutId = setTimeout(() => {
135
+ if (cleanup) cleanup();
136
+ resolve({ ok: false, reason: "timeout" });
137
+ }, timeoutMs);
138
+ });
139
+ }
@@ -0,0 +1,5 @@
1
+ // src/octto/state/index.ts
2
+
3
+ export * from "./persistence";
4
+ export * from "./store";
5
+ export * from "./types";
@@ -0,0 +1,65 @@
1
+ // src/octto/state/persistence.ts
2
+ import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { STATE_DIR } from "../constants";
6
+ import type { BrainstormState } from "./types";
7
+
8
+ export interface StatePersistence {
9
+ save: (state: BrainstormState) => Promise<void>;
10
+ load: (sessionId: string) => Promise<BrainstormState | null>;
11
+ delete: (sessionId: string) => Promise<void>;
12
+ list: () => Promise<string[]>;
13
+ }
14
+
15
+ function validateSessionId(sessionId: string): void {
16
+ if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
17
+ throw new Error(`Invalid session ID: ${sessionId}`);
18
+ }
19
+ }
20
+
21
+ export function createStatePersistence(baseDir = STATE_DIR): StatePersistence {
22
+ function getFilePath(sessionId: string): string {
23
+ validateSessionId(sessionId);
24
+ return join(baseDir, `${sessionId}.json`);
25
+ }
26
+
27
+ function ensureDir(): void {
28
+ if (!existsSync(baseDir)) {
29
+ mkdirSync(baseDir, { recursive: true });
30
+ }
31
+ }
32
+
33
+ return {
34
+ async save(state: BrainstormState): Promise<void> {
35
+ ensureDir();
36
+ const filePath = getFilePath(state.session_id);
37
+ state.updated_at = Date.now();
38
+ await Bun.write(filePath, JSON.stringify(state, null, 2));
39
+ },
40
+
41
+ async load(sessionId: string): Promise<BrainstormState | null> {
42
+ const filePath = getFilePath(sessionId);
43
+ if (!existsSync(filePath)) {
44
+ return null;
45
+ }
46
+ const content = await Bun.file(filePath).text();
47
+ return JSON.parse(content) as BrainstormState;
48
+ },
49
+
50
+ async delete(sessionId: string): Promise<void> {
51
+ const filePath = getFilePath(sessionId);
52
+ if (existsSync(filePath)) {
53
+ rmSync(filePath);
54
+ }
55
+ },
56
+
57
+ async list(): Promise<string[]> {
58
+ if (!existsSync(baseDir)) {
59
+ return [];
60
+ }
61
+ const files = readdirSync(baseDir);
62
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,161 @@
1
+ // src/octto/state/store.ts
2
+
3
+ import { STATE_DIR } from "../constants";
4
+ import type { Answer } from "../session";
5
+ import { createStatePersistence } from "./persistence";
6
+ import {
7
+ BRANCH_STATUSES,
8
+ type BrainstormState,
9
+ type Branch,
10
+ type BranchQuestion,
11
+ type CreateBranchInput,
12
+ } from "./types";
13
+
14
+ export interface StateStore {
15
+ createSession: (sessionId: string, request: string, branches: CreateBranchInput[]) => Promise<BrainstormState>;
16
+ getSession: (sessionId: string) => Promise<BrainstormState | null>;
17
+ setBrowserSessionId: (sessionId: string, browserSessionId: string) => Promise<void>;
18
+ addQuestionToBranch: (sessionId: string, branchId: string, question: BranchQuestion) => Promise<BranchQuestion>;
19
+ recordAnswer: (sessionId: string, questionId: string, answer: Answer) => Promise<void>;
20
+ completeBranch: (sessionId: string, branchId: string, finding: string) => Promise<void>;
21
+ getNextExploringBranch: (sessionId: string) => Promise<Branch | null>;
22
+ isSessionComplete: (sessionId: string) => Promise<boolean>;
23
+ deleteSession: (sessionId: string) => Promise<void>;
24
+ }
25
+
26
+ export function createStateStore(baseDir = STATE_DIR): StateStore {
27
+ const persistence = createStatePersistence(baseDir);
28
+
29
+ // Operation queue per session to prevent concurrent read-modify-write races
30
+ const operationQueues = new Map<string, Promise<void>>();
31
+
32
+ // Serialize operations for a given session
33
+ function withSessionLock<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
34
+ const currentQueue = operationQueues.get(sessionId) ?? Promise.resolve();
35
+ const newOperation = currentQueue.then(operation, operation); // Run even if previous failed
36
+ operationQueues.set(
37
+ sessionId,
38
+ newOperation.then(
39
+ () => {},
40
+ () => {},
41
+ ),
42
+ ); // Ignore result for queue
43
+ return newOperation;
44
+ }
45
+
46
+ return {
47
+ async createSession(
48
+ sessionId: string,
49
+ request: string,
50
+ branchInputs: CreateBranchInput[],
51
+ ): Promise<BrainstormState> {
52
+ const branches: Record<string, Branch> = {};
53
+ const order: string[] = [];
54
+
55
+ for (const input of branchInputs) {
56
+ branches[input.id] = {
57
+ id: input.id,
58
+ scope: input.scope,
59
+ status: BRANCH_STATUSES.EXPLORING,
60
+ questions: [],
61
+ finding: null,
62
+ };
63
+ order.push(input.id);
64
+ }
65
+
66
+ const state: BrainstormState = {
67
+ session_id: sessionId,
68
+ browser_session_id: null,
69
+ request,
70
+ created_at: Date.now(),
71
+ updated_at: Date.now(),
72
+ branches,
73
+ branch_order: order,
74
+ };
75
+
76
+ await persistence.save(state);
77
+ return state;
78
+ },
79
+
80
+ async getSession(sessionId: string): Promise<BrainstormState | null> {
81
+ return persistence.load(sessionId);
82
+ },
83
+
84
+ async setBrowserSessionId(sessionId: string, browserSessionId: string): Promise<void> {
85
+ return withSessionLock(sessionId, async () => {
86
+ const state = await persistence.load(sessionId);
87
+ if (!state) throw new Error(`Session not found: ${sessionId}`);
88
+ state.browser_session_id = browserSessionId;
89
+ await persistence.save(state);
90
+ });
91
+ },
92
+
93
+ async addQuestionToBranch(sessionId: string, branchId: string, question: BranchQuestion): Promise<BranchQuestion> {
94
+ return withSessionLock(sessionId, async () => {
95
+ const state = await persistence.load(sessionId);
96
+ if (!state) throw new Error(`Session not found: ${sessionId}`);
97
+ if (!state.branches[branchId]) throw new Error(`Branch not found: ${branchId}`);
98
+
99
+ state.branches[branchId].questions.push(question);
100
+ await persistence.save(state);
101
+ return question;
102
+ });
103
+ },
104
+
105
+ async recordAnswer(sessionId: string, questionId: string, answer: Answer): Promise<void> {
106
+ return withSessionLock(sessionId, async () => {
107
+ const state = await persistence.load(sessionId);
108
+ if (!state) throw new Error(`Session not found: ${sessionId}`);
109
+
110
+ for (const branch of Object.values(state.branches)) {
111
+ const question = branch.questions.find((q) => q.id === questionId);
112
+ if (question) {
113
+ question.answer = answer;
114
+ question.answeredAt = Date.now();
115
+ await persistence.save(state);
116
+ return;
117
+ }
118
+ }
119
+ throw new Error(`Question not found: ${questionId}`);
120
+ });
121
+ },
122
+
123
+ async completeBranch(sessionId: string, branchId: string, finding: string): Promise<void> {
124
+ return withSessionLock(sessionId, async () => {
125
+ const state = await persistence.load(sessionId);
126
+ if (!state) throw new Error(`Session not found: ${sessionId}`);
127
+ if (!state.branches[branchId]) throw new Error(`Branch not found: ${branchId}`);
128
+
129
+ state.branches[branchId].status = BRANCH_STATUSES.DONE;
130
+ state.branches[branchId].finding = finding;
131
+ await persistence.save(state);
132
+ });
133
+ },
134
+
135
+ async getNextExploringBranch(sessionId: string): Promise<Branch | null> {
136
+ const state = await persistence.load(sessionId);
137
+ if (!state) return null;
138
+
139
+ for (const branchId of state.branch_order) {
140
+ const branch = state.branches[branchId];
141
+ if (branch.status === BRANCH_STATUSES.EXPLORING) {
142
+ return branch;
143
+ }
144
+ }
145
+ return null;
146
+ },
147
+
148
+ async isSessionComplete(sessionId: string): Promise<boolean> {
149
+ const state = await persistence.load(sessionId);
150
+ if (!state) return false;
151
+
152
+ return Object.values(state.branches).every((b) => b.status === BRANCH_STATUSES.DONE);
153
+ },
154
+
155
+ async deleteSession(sessionId: string): Promise<void> {
156
+ await withSessionLock(sessionId, async () => {
157
+ await persistence.delete(sessionId);
158
+ });
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,51 @@
1
+ // src/octto/state/types.ts
2
+ import type { Answer, BaseConfig, QuestionType } from "../session";
3
+
4
+ export const BRANCH_STATUSES = {
5
+ EXPLORING: "exploring",
6
+ DONE: "done",
7
+ } as const;
8
+
9
+ export type BranchStatus = (typeof BRANCH_STATUSES)[keyof typeof BRANCH_STATUSES];
10
+
11
+ export interface BranchQuestion {
12
+ id: string;
13
+ type: QuestionType;
14
+ text: string;
15
+ config: BaseConfig;
16
+ answer?: Answer;
17
+ answeredAt?: number;
18
+ }
19
+
20
+ export interface Branch {
21
+ id: string;
22
+ scope: string;
23
+ status: BranchStatus;
24
+ questions: BranchQuestion[];
25
+ finding: string | null;
26
+ }
27
+
28
+ export interface BrainstormState {
29
+ session_id: string;
30
+ browser_session_id: string | null;
31
+ request: string;
32
+ created_at: number;
33
+ updated_at: number;
34
+ branches: Record<string, Branch>;
35
+ branch_order: string[];
36
+ }
37
+
38
+ export interface CreateBranchInput {
39
+ id: string;
40
+ scope: string;
41
+ }
42
+
43
+ export interface BranchProbeResult {
44
+ done: boolean;
45
+ reason: string;
46
+ finding?: string;
47
+ question?: {
48
+ type: QuestionType;
49
+ config: BaseConfig;
50
+ };
51
+ }