minovative-mind-cli 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +51 -45
  2. package/dist/commands/chat.js +7 -2
  3. package/dist/services/agent/slashCommands.js +156 -30
  4. package/dist/services/agent/toolLoop.d.ts +1 -1
  5. package/dist/services/agent/toolLoop.js +7 -2
  6. package/dist/services/agent/types.d.ts +2 -0
  7. package/dist/services/agent-tools.d.ts +9 -4
  8. package/dist/services/agent-tools.js +145 -21
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +285 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +167 -35
  13. package/dist/services/changeLogger.d.ts +142 -0
  14. package/dist/services/changeLogger.js +132 -3
  15. package/dist/services/contextAgent.d.ts +6 -1
  16. package/dist/services/contextAgent.js +95 -14
  17. package/dist/services/embeddingIndex.d.ts +82 -0
  18. package/dist/services/embeddingIndex.js +613 -0
  19. package/dist/services/investigationComplexity.d.ts +45 -0
  20. package/dist/services/investigationComplexity.js +91 -0
  21. package/dist/services/metrics.d.ts +18 -0
  22. package/dist/services/metrics.js +7 -0
  23. package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
  24. package/dist/services/orchestration/fileLockRegistry.js +276 -0
  25. package/dist/services/orchestration/investigationAgent.d.ts +85 -0
  26. package/dist/services/orchestration/investigationAgent.js +359 -0
  27. package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
  28. package/dist/services/orchestration/investigationOrchestrator.js +180 -0
  29. package/dist/services/orchestration/messageBus.d.ts +162 -0
  30. package/dist/services/orchestration/messageBus.js +225 -0
  31. package/dist/services/orchestration/orchestrator.d.ts +45 -0
  32. package/dist/services/orchestration/orchestrator.js +214 -0
  33. package/dist/services/orchestration/readCache.d.ts +79 -0
  34. package/dist/services/orchestration/readCache.js +108 -0
  35. package/dist/services/orchestration/scopedTools.d.ts +57 -0
  36. package/dist/services/orchestration/scopedTools.js +172 -0
  37. package/dist/services/orchestration/subAgent.d.ts +58 -0
  38. package/dist/services/orchestration/subAgent.js +187 -0
  39. package/dist/services/orchestration/taskGraph.d.ts +129 -0
  40. package/dist/services/orchestration/taskGraph.js +254 -0
  41. package/dist/services/proxyClient.d.ts +25 -0
  42. package/dist/services/proxyClient.js +60 -0
  43. package/dist/utils/asyncContext.d.ts +16 -0
  44. package/dist/utils/asyncContext.js +25 -0
  45. package/dist/utils/config.d.ts +3 -1
  46. package/dist/utils/config.js +3 -1
  47. package/dist/utils/contextPrompts.js +3 -2
  48. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  49. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  50. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  52. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  53. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  54. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  56. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  57. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  58. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  59. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  60. package/dist/utils/dependencyTracer.js +31 -17
  61. package/dist/utils/excludedExtensions.js +0 -1
  62. package/dist/utils/historyPrompt.d.ts +9 -0
  63. package/dist/utils/historyPrompt.js +87 -0
  64. package/dist/utils/logo.js +7 -7
  65. package/dist/utils/paste.d.ts +21 -0
  66. package/dist/utils/paste.js +22 -1
  67. package/dist/utils/profiles.d.ts +2 -0
  68. package/dist/utils/profiles.js +44 -0
  69. package/dist/utils/projectStorage.js +10 -7
  70. package/dist/utils/systemPrompts.d.ts +6 -3
  71. package/dist/utils/systemPrompts.js +106 -5
  72. package/dist/utils/types.d.ts +33 -0
  73. package/dist/utils/types.js +1 -0
  74. package/oclif.manifest.json +2 -2
  75. package/package.json +4 -3
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @fileoverview Investigation Complexity Evaluator.
3
+ *
4
+ * Lightweight LLM-powered gatekeeper that determines whether a user prompt
5
+ * warrants parallel investigation or should use the existing single Context
6
+ * Agent path. This prevents wasteful multi-agent spawns on simple prompts
7
+ * like "fix the typo in README" while enabling parallelism for broad
8
+ * architectural prompts like "refactor auth to OAuth2."
9
+ *
10
+ * Only invoked when the `/sub-agents` toggle is ON and the Intent Router
11
+ * has already determined that context gathering is needed (SEARCH).
12
+ */
13
+ import { createInvestigationComplexitySession } from './ai.js';
14
+ import { debugLog } from '../utils/logger.js';
15
+ // ─── Evaluator ───────────────────────────────────────────────────────
16
+ /**
17
+ * Evaluates whether the investigation phase should be parallelized.
18
+ *
19
+ * Uses `gemini-3.1-flash-lite` at temperature 0 to classify the prompt's
20
+ * investigation complexity. The PM dynamically identifies domains and groups
21
+ * them into agent assignments. Agent count = `agentAssignments.length`, which
22
+ * may be fewer than `domains.length` when related domains are batched together.
23
+ *
24
+ * @param userRequest - The user's prompt.
25
+ * @param projectType - Detected project type (e.g., "Node.js / TypeScript / React").
26
+ * @param approximateFileCount - Rough file count from the project tree.
27
+ * @param chatHistory - Recent conversation history for context.
28
+ * @returns The complexity classification with domain decomposition.
29
+ */
30
+ export async function evaluateInvestigationComplexity(userRequest, projectType, approximateFileCount, chatHistory = '') {
31
+ try {
32
+ const session = createInvestigationComplexitySession();
33
+ let prompt = `User Request: "${userRequest}"
34
+ Project Type: ${projectType}
35
+ Approximate File Count: ${approximateFileCount}`;
36
+ if (chatHistory) {
37
+ prompt = `Previous Conversation Context:\n${chatHistory}\n\n${prompt}`;
38
+ }
39
+ const result = await session.sendMessage(prompt);
40
+ const text = result.response.text()?.trim() || '{}';
41
+ const parsed = JSON.parse(text);
42
+ debugLog(`Investigation Complexity Parsed: ${JSON.stringify(parsed)}`);
43
+ // Validate the response shape
44
+ const strategy = parsed.strategy === 'PARALLEL' ? 'PARALLEL' : 'SINGLE';
45
+ if (strategy === 'SINGLE') {
46
+ return {
47
+ strategy: 'SINGLE',
48
+ domains: [],
49
+ agentAssignments: [],
50
+ reasoning: parsed.reasoning || 'Simple prompt — single agent sufficient.',
51
+ };
52
+ }
53
+ // Validate agentAssignments array
54
+ const agentAssignments = [];
55
+ if (Array.isArray(parsed.agentAssignments)) {
56
+ for (const assignment of parsed.agentAssignments) {
57
+ if (assignment.agentLabel && Array.isArray(assignment.domains) && assignment.domains.length > 0) {
58
+ agentAssignments.push({
59
+ agentLabel: String(assignment.agentLabel),
60
+ domains: assignment.domains.map(String),
61
+ });
62
+ }
63
+ }
64
+ }
65
+ // If the PM said PARALLEL but produced no valid assignments, fall back to SINGLE
66
+ if (agentAssignments.length < 2) {
67
+ debugLog('Investigation Complexity: PM said PARALLEL but produced <2 valid assignments. Falling back to SINGLE.');
68
+ return {
69
+ strategy: 'SINGLE',
70
+ domains: [],
71
+ agentAssignments: [],
72
+ reasoning: 'PM produced insufficient agent assignments — falling back to single agent.',
73
+ };
74
+ }
75
+ return {
76
+ strategy: 'PARALLEL',
77
+ domains: Array.isArray(parsed.domains) ? parsed.domains.map(String) : [],
78
+ agentAssignments,
79
+ reasoning: parsed.reasoning || '',
80
+ };
81
+ }
82
+ catch (e) {
83
+ debugLog(`Investigation Complexity Router failed, falling back to SINGLE. Error: ${String(e)}`);
84
+ return {
85
+ strategy: 'SINGLE',
86
+ domains: [],
87
+ agentAssignments: [],
88
+ reasoning: 'Evaluator failed — falling back to single agent.',
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,18 @@
1
+ export interface MetricCollector {
2
+ startTimer(name: string): void;
3
+ stopTimer(name: string): number;
4
+ recordTokenUsage(input: number, output: number, cached?: number): void;
5
+ recordCompressedContextSize(chars: number): void;
6
+ recordContextSelectedFiles(files: string[]): void;
7
+ recordFileModified(filePath: string): void;
8
+ recordToolTurn(): void;
9
+ recordSelfCorrection(): void;
10
+ recordVerificationResult(passed: boolean): void;
11
+ recordMatchTier(tier: 'exact' | 'normalized' | 'levenshtein' | 'none'): void;
12
+ recordInvestigationFailure(): void;
13
+ recordToolFailure(toolName: string): void;
14
+ recordModifyFailure(): void;
15
+ recordWriteFailure(): void;
16
+ }
17
+ export declare function setMetricCollector(collector: MetricCollector | null): void;
18
+ export declare function getMetricCollector(): MetricCollector | null;
@@ -0,0 +1,7 @@
1
+ let globalCollector = null;
2
+ export function setMetricCollector(collector) {
3
+ globalCollector = collector;
4
+ }
5
+ export function getMetricCollector() {
6
+ return globalCollector;
7
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @fileoverview File Lock Registry for Sub-Agent Orchestration.
3
+ *
4
+ * Implements a per-file locking mechanism that enables multiple sub-agents to safely
5
+ * edit the same file without race conditions. Key design properties:
6
+ *
7
+ * - **Sequential Handoff**: When Agent A releases a file, Agent B (queued) receives
8
+ * the diff of what A changed so it has full context before making its own edits.
9
+ *
10
+ * - **Timeout-Based Deadlock Recovery**: Every `acquire()` has a configurable timeout
11
+ * (default 120s). If the holding agent stalls, the lock is forcibly released and
12
+ * the waiting agent receives a warning about partial changes.
13
+ *
14
+ * - **Dead Agent Cleanup**: `forceReleaseAll(agentId)` releases all locks held by a
15
+ * crashed/stalled agent in one call, used by the orchestrator's health monitor.
16
+ *
17
+ * - **Pre-Allocation Ordering**: The orchestrator uses `detectFileConflicts()` from
18
+ * `taskGraph.ts` at dispatch time to serialize tasks that share target files,
19
+ * preventing circular wait deadlocks before they can form.
20
+ */
21
+ /** Result returned when a lock is successfully acquired. */
22
+ export interface LockAcquisitionResult {
23
+ /** Whether the lock was granted */
24
+ granted: true;
25
+ /** If true, the lock was taken by force from a stalled agent */
26
+ forceReleased: boolean;
27
+ /** The diff/context from the previous holder (if any), for sequential handoff */
28
+ previousDiff: string | null;
29
+ /** ID of the agent that held the lock before (if any) */
30
+ previousAgentId: string | null;
31
+ }
32
+ /** Error thrown when a lock acquisition times out. */
33
+ export declare class LockTimeoutError extends Error {
34
+ readonly filePath: string;
35
+ readonly holdingAgentId: string;
36
+ readonly waitingAgentId: string;
37
+ readonly timeoutMs: number;
38
+ constructor(filePath: string, holdingAgentId: string, waitingAgentId: string, timeoutMs: number);
39
+ }
40
+ /**
41
+ * Per-file locking system with sequential handoff, timeout recovery, and
42
+ * dead agent cleanup.
43
+ *
44
+ * Usage pattern (inside scoped tool wrapper):
45
+ * ```
46
+ * const result = await registry.acquire('src/config.ts', 'agent-a')
47
+ * // ... agent performs read/write operations ...
48
+ * registry.release('src/config.ts', 'agent-a', diffString)
49
+ * ```
50
+ */
51
+ export declare class FileLockRegistry {
52
+ private locks;
53
+ /** Default timeout for lock acquisition (120 seconds) */
54
+ static readonly DEFAULT_TIMEOUT_MS = 120000;
55
+ /**
56
+ * Acquires a lock on a file path for a given agent.
57
+ *
58
+ * - If the file is unlocked: immediately grants the lock.
59
+ * - If the file is locked by the SAME agent: immediately returns (re-entrant).
60
+ * - If the file is locked by ANOTHER agent: queues this agent and waits
61
+ * until the holder releases, or the timeout fires.
62
+ *
63
+ * When the timeout fires, the system forcibly releases the stalled agent's lock
64
+ * and grants it to the waiting agent with a warning about partial file state.
65
+ *
66
+ * @param filePath - Relative file path being locked.
67
+ * @param agentId - The requesting agent's identifier.
68
+ * @param timeoutMs - Max wait time before force-releasing (default 120s).
69
+ */
70
+ acquire(filePath: string, agentId: string, timeoutMs?: number): Promise<LockAcquisitionResult>;
71
+ /**
72
+ * Releases a lock held by the specified agent and propagates context to the
73
+ * next queued waiter (if any).
74
+ *
75
+ * @param filePath - The file to unlock.
76
+ * @param agentId - The releasing agent's identifier.
77
+ * @param diff - A human-readable summary of what the agent changed in the file.
78
+ */
79
+ release(filePath: string, agentId: string, diff?: string | null): void;
80
+ /**
81
+ * Forcibly releases a lock from a stalled agent without waiting for the
82
+ * holder to call `release()`. Used by timeout handlers and the orchestrator's
83
+ * dead agent recovery.
84
+ *
85
+ * @param filePath - The file to force-unlock.
86
+ * @param stalledAgentId - The agent being evicted from the lock.
87
+ */
88
+ forceRelease(filePath: string, stalledAgentId: string): void;
89
+ /**
90
+ * Releases ALL locks held by a specific agent. Used when the orchestrator
91
+ * detects that an agent has crashed or stalled and needs full cleanup.
92
+ *
93
+ * @param agentId - The dead/stalled agent whose locks should be released.
94
+ * @returns Array of file paths that were force-released.
95
+ */
96
+ forceReleaseAll(agentId: string): string[];
97
+ /**
98
+ * Returns the list of file paths currently locked by a specific agent.
99
+ * Used for diagnostics, terminal display, and dead agent recovery context.
100
+ */
101
+ getHeldLocks(agentId: string): string[];
102
+ /**
103
+ * Returns whether a specific file is currently locked.
104
+ */
105
+ isLocked(filePath: string): boolean;
106
+ /**
107
+ * Returns the agent currently holding the lock on a file, or null if unlocked.
108
+ */
109
+ getHolder(filePath: string): string | null;
110
+ /**
111
+ * Returns a diagnostic snapshot of all active locks.
112
+ * Used for terminal output and debugging.
113
+ */
114
+ getSnapshot(): Array<{
115
+ filePath: string;
116
+ holder: string;
117
+ waiters: string[];
118
+ heldForMs: number;
119
+ }>;
120
+ /**
121
+ * Rejects all waiting agents and clears all locks.
122
+ * Called during orchestration teardown.
123
+ */
124
+ shutdown(): void;
125
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @fileoverview File Lock Registry for Sub-Agent Orchestration.
3
+ *
4
+ * Implements a per-file locking mechanism that enables multiple sub-agents to safely
5
+ * edit the same file without race conditions. Key design properties:
6
+ *
7
+ * - **Sequential Handoff**: When Agent A releases a file, Agent B (queued) receives
8
+ * the diff of what A changed so it has full context before making its own edits.
9
+ *
10
+ * - **Timeout-Based Deadlock Recovery**: Every `acquire()` has a configurable timeout
11
+ * (default 120s). If the holding agent stalls, the lock is forcibly released and
12
+ * the waiting agent receives a warning about partial changes.
13
+ *
14
+ * - **Dead Agent Cleanup**: `forceReleaseAll(agentId)` releases all locks held by a
15
+ * crashed/stalled agent in one call, used by the orchestrator's health monitor.
16
+ *
17
+ * - **Pre-Allocation Ordering**: The orchestrator uses `detectFileConflicts()` from
18
+ * `taskGraph.ts` at dispatch time to serialize tasks that share target files,
19
+ * preventing circular wait deadlocks before they can form.
20
+ */
21
+ import { debugLog } from '../../utils/logger.js';
22
+ /** Error thrown when a lock acquisition times out. */
23
+ export class LockTimeoutError extends Error {
24
+ filePath;
25
+ holdingAgentId;
26
+ waitingAgentId;
27
+ timeoutMs;
28
+ constructor(filePath, holdingAgentId, waitingAgentId, timeoutMs) {
29
+ super(`Lock timeout: Agent "${waitingAgentId}" waited ${timeoutMs}ms for ` +
30
+ `"${filePath}" held by "${holdingAgentId}".`);
31
+ this.filePath = filePath;
32
+ this.holdingAgentId = holdingAgentId;
33
+ this.waitingAgentId = waitingAgentId;
34
+ this.timeoutMs = timeoutMs;
35
+ this.name = 'LockTimeoutError';
36
+ }
37
+ }
38
+ // ─── File Lock Registry ──────────────────────────────────────────────
39
+ /**
40
+ * Per-file locking system with sequential handoff, timeout recovery, and
41
+ * dead agent cleanup.
42
+ *
43
+ * Usage pattern (inside scoped tool wrapper):
44
+ * ```
45
+ * const result = await registry.acquire('src/config.ts', 'agent-a')
46
+ * // ... agent performs read/write operations ...
47
+ * registry.release('src/config.ts', 'agent-a', diffString)
48
+ * ```
49
+ */
50
+ export class FileLockRegistry {
51
+ locks = new Map();
52
+ /** Default timeout for lock acquisition (120 seconds) */
53
+ static DEFAULT_TIMEOUT_MS = 120_000;
54
+ /**
55
+ * Acquires a lock on a file path for a given agent.
56
+ *
57
+ * - If the file is unlocked: immediately grants the lock.
58
+ * - If the file is locked by the SAME agent: immediately returns (re-entrant).
59
+ * - If the file is locked by ANOTHER agent: queues this agent and waits
60
+ * until the holder releases, or the timeout fires.
61
+ *
62
+ * When the timeout fires, the system forcibly releases the stalled agent's lock
63
+ * and grants it to the waiting agent with a warning about partial file state.
64
+ *
65
+ * @param filePath - Relative file path being locked.
66
+ * @param agentId - The requesting agent's identifier.
67
+ * @param timeoutMs - Max wait time before force-releasing (default 120s).
68
+ */
69
+ async acquire(filePath, agentId, timeoutMs = FileLockRegistry.DEFAULT_TIMEOUT_MS) {
70
+ const existing = this.locks.get(filePath);
71
+ // Case 1: File is unlocked — grant immediately
72
+ if (!existing) {
73
+ this.locks.set(filePath, {
74
+ holderId: agentId,
75
+ acquiredAt: Date.now(),
76
+ waiters: [],
77
+ accumulatedDiff: null,
78
+ });
79
+ debugLog(`FileLock: Agent "${agentId}" acquired "${filePath}" (no contention).`);
80
+ return {
81
+ granted: true,
82
+ forceReleased: false,
83
+ previousDiff: null,
84
+ previousAgentId: null,
85
+ };
86
+ }
87
+ // Case 2: Re-entrant — same agent already holds this lock
88
+ if (existing.holderId === agentId) {
89
+ debugLog(`FileLock: Agent "${agentId}" re-entered lock on "${filePath}".`);
90
+ return {
91
+ granted: true,
92
+ forceReleased: false,
93
+ previousDiff: null,
94
+ previousAgentId: null,
95
+ };
96
+ }
97
+ // Case 3: Another agent holds the lock — queue and wait
98
+ debugLog(`FileLock: Agent "${agentId}" queued for "${filePath}" (held by "${existing.holderId}").`);
99
+ return new Promise((resolve, reject) => {
100
+ const timeoutHandle = setTimeout(() => {
101
+ // Timeout fired — force-release the stalled holder
102
+ debugLog(`FileLock: Timeout! Force-releasing "${filePath}" from stalled ` +
103
+ `agent "${existing.holderId}" for waiting agent "${agentId}".`);
104
+ this.forceRelease(filePath, existing.holderId);
105
+ // Grant to the waiting agent with a warning
106
+ this.locks.set(filePath, {
107
+ holderId: agentId,
108
+ acquiredAt: Date.now(),
109
+ waiters: [],
110
+ accumulatedDiff: null,
111
+ });
112
+ resolve({
113
+ granted: true,
114
+ forceReleased: true,
115
+ previousDiff: '[Lock forcibly released — previous agent stalled. File state may be partial.]',
116
+ previousAgentId: existing.holderId,
117
+ });
118
+ }, timeoutMs);
119
+ existing.waiters.push({ agentId, resolve, reject, timeoutHandle });
120
+ });
121
+ }
122
+ /**
123
+ * Releases a lock held by the specified agent and propagates context to the
124
+ * next queued waiter (if any).
125
+ *
126
+ * @param filePath - The file to unlock.
127
+ * @param agentId - The releasing agent's identifier.
128
+ * @param diff - A human-readable summary of what the agent changed in the file.
129
+ */
130
+ release(filePath, agentId, diff = null) {
131
+ const entry = this.locks.get(filePath);
132
+ if (!entry) {
133
+ debugLog(`FileLock: Attempted to release unlocked file "${filePath}" by "${agentId}".`);
134
+ return;
135
+ }
136
+ if (entry.holderId !== agentId) {
137
+ debugLog(`FileLock: Agent "${agentId}" attempted to release "${filePath}" ` +
138
+ `but it is held by "${entry.holderId}". Ignoring.`);
139
+ return;
140
+ }
141
+ const nextWaiter = entry.waiters.shift();
142
+ if (nextWaiter) {
143
+ // Clear the waiter's timeout since we're granting naturally
144
+ clearTimeout(nextWaiter.timeoutHandle);
145
+ // Transfer lock to the next waiter with context propagation
146
+ entry.holderId = nextWaiter.agentId;
147
+ entry.acquiredAt = Date.now();
148
+ entry.accumulatedDiff = null;
149
+ debugLog(`FileLock: Agent "${agentId}" released "${filePath}". ` +
150
+ `Granted to queued agent "${nextWaiter.agentId}" with diff context.`);
151
+ nextWaiter.resolve({
152
+ granted: true,
153
+ forceReleased: false,
154
+ previousDiff: diff,
155
+ previousAgentId: agentId,
156
+ });
157
+ }
158
+ else {
159
+ // No waiters — fully release the lock
160
+ this.locks.delete(filePath);
161
+ debugLog(`FileLock: Agent "${agentId}" released "${filePath}" (no waiters).`);
162
+ }
163
+ }
164
+ /**
165
+ * Forcibly releases a lock from a stalled agent without waiting for the
166
+ * holder to call `release()`. Used by timeout handlers and the orchestrator's
167
+ * dead agent recovery.
168
+ *
169
+ * @param filePath - The file to force-unlock.
170
+ * @param stalledAgentId - The agent being evicted from the lock.
171
+ */
172
+ forceRelease(filePath, stalledAgentId) {
173
+ const entry = this.locks.get(filePath);
174
+ if (!entry || entry.holderId !== stalledAgentId)
175
+ return;
176
+ debugLog(`FileLock: Force-releasing "${filePath}" from stalled agent "${stalledAgentId}".`);
177
+ // Clear all remaining waiter timeouts (they will be re-dispatched)
178
+ for (const waiter of entry.waiters) {
179
+ clearTimeout(waiter.timeoutHandle);
180
+ }
181
+ // If there's a next waiter, grant to them with a force-release warning
182
+ const nextWaiter = entry.waiters.shift();
183
+ if (nextWaiter) {
184
+ entry.holderId = nextWaiter.agentId;
185
+ entry.acquiredAt = Date.now();
186
+ entry.waiters = entry.waiters; // remaining waiters stay queued
187
+ entry.accumulatedDiff = null;
188
+ nextWaiter.resolve({
189
+ granted: true,
190
+ forceReleased: true,
191
+ previousDiff: '[Lock forcibly released — previous agent stalled. File state may be partial.]',
192
+ previousAgentId: stalledAgentId,
193
+ });
194
+ }
195
+ else {
196
+ this.locks.delete(filePath);
197
+ }
198
+ }
199
+ /**
200
+ * Releases ALL locks held by a specific agent. Used when the orchestrator
201
+ * detects that an agent has crashed or stalled and needs full cleanup.
202
+ *
203
+ * @param agentId - The dead/stalled agent whose locks should be released.
204
+ * @returns Array of file paths that were force-released.
205
+ */
206
+ forceReleaseAll(agentId) {
207
+ const releasedFiles = [];
208
+ for (const [filePath, entry] of this.locks) {
209
+ if (entry.holderId === agentId) {
210
+ this.forceRelease(filePath, agentId);
211
+ releasedFiles.push(filePath);
212
+ }
213
+ }
214
+ if (releasedFiles.length > 0) {
215
+ debugLog(`FileLock: Force-released ${releasedFiles.length} locks from dead agent "${agentId}": ` +
216
+ `${releasedFiles.join(', ')}`);
217
+ }
218
+ return releasedFiles;
219
+ }
220
+ /**
221
+ * Returns the list of file paths currently locked by a specific agent.
222
+ * Used for diagnostics, terminal display, and dead agent recovery context.
223
+ */
224
+ getHeldLocks(agentId) {
225
+ const held = [];
226
+ for (const [filePath, entry] of this.locks) {
227
+ if (entry.holderId === agentId) {
228
+ held.push(filePath);
229
+ }
230
+ }
231
+ return held;
232
+ }
233
+ /**
234
+ * Returns whether a specific file is currently locked.
235
+ */
236
+ isLocked(filePath) {
237
+ return this.locks.has(filePath);
238
+ }
239
+ /**
240
+ * Returns the agent currently holding the lock on a file, or null if unlocked.
241
+ */
242
+ getHolder(filePath) {
243
+ return this.locks.get(filePath)?.holderId ?? null;
244
+ }
245
+ /**
246
+ * Returns a diagnostic snapshot of all active locks.
247
+ * Used for terminal output and debugging.
248
+ */
249
+ getSnapshot() {
250
+ const now = Date.now();
251
+ const snapshot = [];
252
+ for (const [filePath, entry] of this.locks) {
253
+ snapshot.push({
254
+ filePath,
255
+ holder: entry.holderId,
256
+ waiters: entry.waiters.map(w => w.agentId),
257
+ heldForMs: now - entry.acquiredAt,
258
+ });
259
+ }
260
+ return snapshot;
261
+ }
262
+ /**
263
+ * Rejects all waiting agents and clears all locks.
264
+ * Called during orchestration teardown.
265
+ */
266
+ shutdown() {
267
+ for (const [, entry] of this.locks) {
268
+ for (const waiter of entry.waiters) {
269
+ clearTimeout(waiter.timeoutHandle);
270
+ waiter.reject(new Error('FileLockRegistry is shutting down.'));
271
+ }
272
+ }
273
+ this.locks.clear();
274
+ debugLog('FileLock: Registry shut down — all locks cleared.');
275
+ }
276
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @fileoverview Read-Only Investigation Sub-Agent Runner.
3
+ *
4
+ * Implements the lifecycle, health monitoring, and tool-loop execution for a single
5
+ * parallelized investigation agent. Unlike execution sub-agents in `subAgent.ts`,
6
+ * investigation agents are **strictly read-only** — they have no filesystem write
7
+ * tools, no file locks, and no message bus. They share a `ReadCache` to avoid
8
+ * redundant file reads across sibling agents.
9
+ *
10
+ * Each investigation agent is scoped to a set of domains (e.g., "Frontend auth
11
+ * components" + "Auth UI styling") and produces an `InvestigationResult` containing
12
+ * the files it found and a summary of its findings.
13
+ */
14
+ import { ReadCache } from './readCache.js';
15
+ /**
16
+ * Result returned when an investigation agent completes its work.
17
+ * The `InvestigationOrchestrator` merges these across all agents.
18
+ */
19
+ export interface InvestigationResult {
20
+ /** The domain(s) this agent investigated */
21
+ domains: string[];
22
+ /** Agent label (e.g., "Frontend", "Backend") */
23
+ agentLabel: string;
24
+ /** Files discovered as relevant, with their content */
25
+ relevantFiles: Map<string, {
26
+ text: string;
27
+ inlineData?: any;
28
+ }>;
29
+ /** Summary of findings for this agent's scope */
30
+ summary: string;
31
+ /** Web search findings (if any) */
32
+ webSearchSummary: string;
33
+ /** Total tokens consumed by this agent */
34
+ creditsUsed: number;
35
+ /** Whether the agent completed successfully */
36
+ success: boolean;
37
+ }
38
+ /**
39
+ * Executes a read-only investigation within a scoped set of domains.
40
+ *
41
+ * Key differences from `SubAgentRunner` (execution):
42
+ * - **No write tools**: Cannot modify, create, or delete files
43
+ * - **No file locks**: Read-only access means zero contention
44
+ * - **No message bus**: Agents don't need to coordinate — they work independently
45
+ * - **Shared ReadCache**: Checks cache before hitting disk to avoid redundant reads
46
+ * - **Shorter stall timeout**: 30s vs 60s — investigation should be faster
47
+ */
48
+ export declare class InvestigationAgentRunner {
49
+ private readonly agentLabel;
50
+ private readonly domains;
51
+ private readonly workspaceRoot;
52
+ private readonly readCache;
53
+ private readonly projectTree;
54
+ private readonly projectType;
55
+ private chat;
56
+ private lastHeartbeat;
57
+ private creditsUsed;
58
+ /** Shorter stall timeout for investigation agents (120s to allow for heavy vision/web tasks) */
59
+ static readonly STALL_TIMEOUT_MS = 120000;
60
+ constructor(agentLabel: string, domains: string[], workspaceRoot: string, readCache: ReadCache, projectTree: string, projectType: string);
61
+ /**
62
+ * Builds a domain-scoped system instruction. Extends the base Context Agent
63
+ * instruction with a domain focus preamble.
64
+ */
65
+ private buildSystemInstruction;
66
+ /**
67
+ * Updates the heartbeat timestamp. Prevents the stall detector from
68
+ * killing the agent during long-running tool calls.
69
+ */
70
+ private pingHeartbeat;
71
+ /**
72
+ * Executes the investigation agent's tool loop.
73
+ *
74
+ * @param userRequest - The original user prompt.
75
+ * @param chatHistory - Recent conversation history for context.
76
+ * @param abortSignal - Signal to cancel execution.
77
+ * @param onProgress - Callback for spinner/terminal updates.
78
+ * @returns The investigation result with discovered files and summary.
79
+ */
80
+ execute(userRequest: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<InvestigationResult>;
81
+ /**
82
+ * Accumulates token usage from the chat session.
83
+ */
84
+ private updateUsage;
85
+ }