byterover-cli 3.5.0 → 3.6.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 (91) hide show
  1. package/.env.production +4 -6
  2. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  3. package/dist/agent/infra/agent/cipher-agent.d.ts +1 -0
  4. package/dist/agent/infra/agent/cipher-agent.js +1 -0
  5. package/dist/oclif/commands/curate/view.js +5 -25
  6. package/dist/oclif/commands/dream.d.ts +18 -0
  7. package/dist/oclif/commands/dream.js +230 -0
  8. package/dist/oclif/commands/query-log/summary.d.ts +18 -0
  9. package/dist/oclif/commands/query-log/summary.js +75 -0
  10. package/dist/oclif/commands/query-log/view.d.ts +23 -0
  11. package/dist/oclif/commands/query-log/view.js +95 -0
  12. package/dist/oclif/lib/time-filter.d.ts +10 -0
  13. package/dist/oclif/lib/time-filter.js +21 -0
  14. package/dist/server/config/environment.d.ts +10 -3
  15. package/dist/server/config/environment.js +34 -15
  16. package/dist/server/constants.d.ts +5 -0
  17. package/dist/server/constants.js +7 -0
  18. package/dist/server/core/domain/entities/query-log-entry.d.ts +61 -0
  19. package/dist/server/core/domain/entities/query-log-entry.js +40 -0
  20. package/dist/server/core/domain/transport/schemas.d.ts +108 -7
  21. package/dist/server/core/domain/transport/schemas.js +34 -2
  22. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +23 -2
  23. package/dist/server/core/interfaces/i-terminal.d.ts +3 -0
  24. package/dist/server/core/interfaces/i-terminal.js +1 -0
  25. package/dist/server/core/interfaces/storage/i-query-log-store.d.ts +23 -0
  26. package/dist/server/core/interfaces/storage/i-query-log-store.js +2 -0
  27. package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.d.ts +44 -0
  28. package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.js +1 -0
  29. package/dist/server/core/interfaces/usecase/i-query-log-use-case.d.ts +13 -0
  30. package/dist/server/core/interfaces/usecase/i-query-log-use-case.js +3 -0
  31. package/dist/server/infra/daemon/agent-process.js +79 -9
  32. package/dist/server/infra/daemon/brv-server.js +74 -5
  33. package/dist/server/infra/dream/dream-lock-service.d.ts +37 -0
  34. package/dist/server/infra/dream/dream-lock-service.js +88 -0
  35. package/dist/server/infra/dream/dream-log-schema.d.ts +966 -0
  36. package/dist/server/infra/dream/dream-log-schema.js +57 -0
  37. package/dist/server/infra/dream/dream-log-store.d.ts +55 -0
  38. package/dist/server/infra/dream/dream-log-store.js +141 -0
  39. package/dist/server/infra/dream/dream-response-schemas.d.ts +219 -0
  40. package/dist/server/infra/dream/dream-response-schemas.js +38 -0
  41. package/dist/server/infra/dream/dream-state-schema.d.ts +67 -0
  42. package/dist/server/infra/dream/dream-state-schema.js +23 -0
  43. package/dist/server/infra/dream/dream-state-service.d.ts +38 -0
  44. package/dist/server/infra/dream/dream-state-service.js +91 -0
  45. package/dist/server/infra/dream/dream-trigger.d.ts +46 -0
  46. package/dist/server/infra/dream/dream-trigger.js +65 -0
  47. package/dist/server/infra/dream/dream-undo.d.ts +38 -0
  48. package/dist/server/infra/dream/dream-undo.js +293 -0
  49. package/dist/server/infra/dream/operations/consolidate.d.ts +52 -0
  50. package/dist/server/infra/dream/operations/consolidate.js +514 -0
  51. package/dist/server/infra/dream/operations/prune.d.ts +45 -0
  52. package/dist/server/infra/dream/operations/prune.js +362 -0
  53. package/dist/server/infra/dream/operations/synthesize.d.ts +37 -0
  54. package/dist/server/infra/dream/operations/synthesize.js +278 -0
  55. package/dist/server/infra/dream/parse-dream-response.d.ts +11 -0
  56. package/dist/server/infra/dream/parse-dream-response.js +35 -0
  57. package/dist/server/infra/executor/curate-executor.js +10 -0
  58. package/dist/server/infra/executor/dream-executor.d.ts +97 -0
  59. package/dist/server/infra/executor/dream-executor.js +431 -0
  60. package/dist/server/infra/executor/query-executor.d.ts +2 -2
  61. package/dist/server/infra/executor/query-executor.js +92 -22
  62. package/dist/server/infra/mcp/mcp-server.js +3 -0
  63. package/dist/server/infra/mcp/tools/brv-curate-tool.js +3 -7
  64. package/dist/server/infra/mcp/tools/brv-query-tool.js +3 -7
  65. package/dist/server/infra/mcp/tools/index.d.ts +1 -0
  66. package/dist/server/infra/mcp/tools/index.js +1 -0
  67. package/dist/server/infra/mcp/tools/shared-schema.d.ts +3 -0
  68. package/dist/server/infra/mcp/tools/shared-schema.js +17 -0
  69. package/dist/server/infra/process/feature-handlers.js +10 -6
  70. package/dist/server/infra/process/query-log-handler.d.ts +42 -0
  71. package/dist/server/infra/process/query-log-handler.js +150 -0
  72. package/dist/server/infra/process/task-router.d.ts +40 -0
  73. package/dist/server/infra/process/task-router.js +67 -9
  74. package/dist/server/infra/process/transport-handlers.d.ts +4 -0
  75. package/dist/server/infra/process/transport-handlers.js +1 -0
  76. package/dist/server/infra/storage/file-curate-log-store.js +1 -1
  77. package/dist/server/infra/storage/file-query-log-store.d.ts +81 -0
  78. package/dist/server/infra/storage/file-query-log-store.js +249 -0
  79. package/dist/server/infra/transport/handlers/config-handler.js +1 -1
  80. package/dist/server/infra/usecase/curate-log-use-case.js +7 -3
  81. package/dist/server/infra/usecase/query-log-summary-narrative-formatter.d.ts +15 -0
  82. package/dist/server/infra/usecase/query-log-summary-narrative-formatter.js +79 -0
  83. package/dist/server/infra/usecase/query-log-summary-use-case.d.ts +13 -0
  84. package/dist/server/infra/usecase/query-log-summary-use-case.js +217 -0
  85. package/dist/server/infra/usecase/query-log-use-case.d.ts +31 -0
  86. package/dist/server/infra/usecase/query-log-use-case.js +128 -0
  87. package/dist/server/utils/log-format-utils.d.ts +5 -0
  88. package/dist/server/utils/log-format-utils.js +23 -0
  89. package/dist/shared/transport/events/config-events.d.ts +1 -1
  90. package/oclif.manifest.json +439 -184
  91. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { join, relative } from 'node:path';
2
2
  import { ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
3
+ import { TIER_DIRECT_SEARCH, TIER_EXACT_CACHE, TIER_FULL_AGENTIC, TIER_FUZZY_CACHE, TIER_OPTIMIZED_LLM, } from '../../core/domain/entities/query-log-entry.js';
3
4
  import { loadSources } from '../../core/domain/source/source-schema.js';
4
5
  import { isDerivedArtifact } from '../context-tree/derived-artifact.js';
5
6
  import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
@@ -7,6 +8,10 @@ import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } fro
7
8
  import { QueryResultCache } from './query-result-cache.js';
8
9
  /** Attribution footer appended to all query responses */
9
10
  const ATTRIBUTION_FOOTER = '\n\n---\nSource: ByteRover Knowledge Base';
11
+ /** Map search results to the matchedDocs shape for QueryExecutorResult. */
12
+ function buildMatchedDocs(sr) {
13
+ return (sr?.results ?? []).map((r) => ({ path: r.path, score: r.score, title: r.title }));
14
+ }
10
15
  /** Minimum normalized score to consider a result high-confidence for pre-fetching */
11
16
  const SMART_ROUTING_SCORE_THRESHOLD = 0.7;
12
17
  /** Maximum number of documents to pre-fetch and inject into the prompt */
@@ -46,6 +51,7 @@ export class QueryExecutor {
46
51
  }
47
52
  }
48
53
  async executeWithAgent(agent, options) {
54
+ const startTime = Date.now();
49
55
  const { query, taskId, worktreeRoot } = options;
50
56
  const workspaceScope = this.deriveWorkspaceScope(worktreeRoot);
51
57
  // Start search early — runs in parallel with fingerprint computation (independent operations)
@@ -58,14 +64,24 @@ export class QueryExecutor {
58
64
  fingerprint = await this.computeContextTreeFingerprint(worktreeRoot);
59
65
  const cached = this.cache.get(query, fingerprint);
60
66
  if (cached) {
61
- return cached + ATTRIBUTION_FOOTER;
67
+ return {
68
+ matchedDocs: [],
69
+ response: cached + ATTRIBUTION_FOOTER,
70
+ tier: TIER_EXACT_CACHE,
71
+ timing: { durationMs: Date.now() - startTime },
72
+ };
62
73
  }
63
74
  }
64
75
  // === Tier 1: Fuzzy cache match (~50ms) ===
65
76
  if (this.cache && fingerprint) {
66
77
  const fuzzyHit = this.cache.findSimilar(query, fingerprint);
67
78
  if (fuzzyHit) {
68
- return fuzzyHit + ATTRIBUTION_FOOTER;
79
+ return {
80
+ matchedDocs: [],
81
+ response: fuzzyHit + ATTRIBUTION_FOOTER,
82
+ tier: TIER_FUZZY_CACHE,
83
+ timing: { durationMs: Date.now() - startTime },
84
+ };
69
85
  }
70
86
  }
71
87
  // Await search result (already started in parallel with fingerprint computation)
@@ -86,7 +102,13 @@ export class QueryExecutor {
86
102
  if (this.cache && fingerprint) {
87
103
  this.cache.set(query, response, fingerprint);
88
104
  }
89
- return response + ATTRIBUTION_FOOTER;
105
+ return {
106
+ matchedDocs: [],
107
+ response: response + ATTRIBUTION_FOOTER,
108
+ searchMetadata: { resultCount: 0, topScore: 0, totalFound: 0 },
109
+ tier: TIER_DIRECT_SEARCH,
110
+ timing: { durationMs: Date.now() - startTime },
111
+ };
90
112
  }
91
113
  // === Tier 2: Direct search response (~100-200ms) ===
92
114
  if (searchResult && this.fileSystem) {
@@ -95,7 +117,18 @@ export class QueryExecutor {
95
117
  if (this.cache && fingerprint) {
96
118
  this.cache.set(query, directResult, fingerprint);
97
119
  }
98
- return directResult + ATTRIBUTION_FOOTER;
120
+ return {
121
+ matchedDocs: buildMatchedDocs(searchResult),
122
+ response: directResult + ATTRIBUTION_FOOTER,
123
+ searchMetadata: {
124
+ cacheFingerprint: fingerprint,
125
+ resultCount: searchResult.results.length,
126
+ topScore: searchResult.results[0]?.score ?? 0,
127
+ totalFound: searchResult.totalFound,
128
+ },
129
+ tier: TIER_DIRECT_SEARCH,
130
+ timing: { durationMs: Date.now() - startTime },
131
+ };
99
132
  }
100
133
  }
101
134
  // === Tier 3/4: LLM call with RLM pattern (variable-based search results) ===
@@ -115,9 +148,7 @@ export class QueryExecutor {
115
148
  if (manifest) {
116
149
  const resolved = await manifestService.resolveForInjection(manifest, query, this.baseDirectory);
117
150
  if (resolved.length > 0) {
118
- manifestContext = resolved
119
- .map((e) => `[${e.type} ${e.path}]\n${e.content}`)
120
- .join('\n\n---\n\n');
151
+ manifestContext = resolved.map((e) => `[${e.type} ${e.path}]\n${e.content}`).join('\n\n---\n\n');
121
152
  }
122
153
  }
123
154
  }
@@ -169,7 +200,19 @@ export class QueryExecutor {
169
200
  if (this.cache && fingerprint) {
170
201
  this.cache.set(query, response, fingerprint);
171
202
  }
172
- return response + ATTRIBUTION_FOOTER;
203
+ const tier = prefetchedContext ? TIER_OPTIMIZED_LLM : TIER_FULL_AGENTIC;
204
+ return {
205
+ matchedDocs: buildMatchedDocs(searchResult),
206
+ response: response + ATTRIBUTION_FOOTER,
207
+ searchMetadata: {
208
+ cacheFingerprint: fingerprint,
209
+ resultCount: searchResult?.results.length ?? 0,
210
+ topScore: searchResult?.results[0]?.score ?? 0,
211
+ totalFound: searchResult?.totalFound ?? 0,
212
+ },
213
+ tier,
214
+ timing: { durationMs: Date.now() - startTime },
215
+ };
173
216
  }
174
217
  finally {
175
218
  // Clean up entire task session (sandbox + history) in one call
@@ -188,9 +231,7 @@ export class QueryExecutor {
188
231
  if (highConfidenceResults.length === 0)
189
232
  return undefined;
190
233
  const sections = highConfidenceResults.map((r) => {
191
- const source = r.origin === 'shared' && r.originAlias
192
- ? `[${r.originAlias}]:${r.path}`
193
- : `.brv/context-tree/${r.path}`;
234
+ const source = r.origin === 'shared' && r.originAlias ? `[${r.originAlias}]:${r.path}` : `.brv/context-tree/${r.path}`;
194
235
  return `### ${r.title}\n**Source**: ${source}\n\n${r.excerpt}`;
195
236
  });
196
237
  return sections.join('\n\n---\n\n');
@@ -274,9 +315,10 @@ ${responseFormat}`;
274
315
  async computeContextTreeFingerprint(worktreeRoot) {
275
316
  // Fast path: return cached fingerprint if still valid (avoids globFiles I/O)
276
317
  // Invalidate if worktreeRoot changed or knowledge source validity changed
277
- if (this.cachedFingerprint && Date.now() < this.cachedFingerprint.expiresAt
278
- && this.cachedFingerprint.worktreeRoot === worktreeRoot
279
- && this.cachedFingerprint.sourceValidityHash === this.computeSourceValidityHash()) {
318
+ if (this.cachedFingerprint &&
319
+ Date.now() < this.cachedFingerprint.expiresAt &&
320
+ this.cachedFingerprint.worktreeRoot === worktreeRoot &&
321
+ this.cachedFingerprint.sourceValidityHash === this.computeSourceValidityHash()) {
280
322
  return this.cachedFingerprint.value;
281
323
  }
282
324
  try {
@@ -298,7 +340,7 @@ ${responseFormat}`;
298
340
  }));
299
341
  // Include shared source state in fingerprint so edits in shared
300
342
  // projects invalidate cached query answers.
301
- const loaded = this.baseDirectory ? loadSources(this.baseDirectory) : null;
343
+ const loaded = this.baseDirectory ? loadSources(this.baseDirectory) : undefined;
302
344
  if (loaded) {
303
345
  // sources-file mtime detects source additions/removals
304
346
  if (loaded.mtime) {
@@ -357,7 +399,10 @@ ${responseFormat}`;
357
399
  const loaded = loadSources(this.baseDirectory);
358
400
  if (!loaded)
359
401
  return 'no-sources';
360
- return loaded.origins.map((o) => o.originKey).sort().join(',');
402
+ return loaded.origins
403
+ .map((o) => o.originKey)
404
+ .sort()
405
+ .join(',');
361
406
  }
362
407
  /**
363
408
  * Derive a workspace scope for search from the worktreeRoot.
@@ -385,9 +430,36 @@ ${responseFormat}`;
385
430
  */
386
431
  extractQueryEntities(query) {
387
432
  const stopwords = new Set([
388
- 'a', 'about', 'an', 'and', 'by', 'did', 'do', 'does', 'for', 'from',
389
- 'how', 'in', 'is', 'my', 'of', 'or', 'our', 'that', 'the', 'their',
390
- 'this', 'to', 'was', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
433
+ 'a',
434
+ 'about',
435
+ 'an',
436
+ 'and',
437
+ 'by',
438
+ 'did',
439
+ 'do',
440
+ 'does',
441
+ 'for',
442
+ 'from',
443
+ 'how',
444
+ 'in',
445
+ 'is',
446
+ 'my',
447
+ 'of',
448
+ 'or',
449
+ 'our',
450
+ 'that',
451
+ 'the',
452
+ 'their',
453
+ 'this',
454
+ 'to',
455
+ 'was',
456
+ 'were',
457
+ 'what',
458
+ 'when',
459
+ 'where',
460
+ 'which',
461
+ 'who',
462
+ 'with',
391
463
  ]);
392
464
  const words = query.toLowerCase().split(/\s+/);
393
465
  return words.filter((w) => w.length >= 3 && !stopwords.has(w));
@@ -461,9 +533,7 @@ ${responseFormat}`;
461
533
  // Use excerpt if full read fails
462
534
  }
463
535
  // Include source attribution in path for shared results
464
- const displayPath = result.origin === 'shared' && result.originAlias
465
- ? `[${result.originAlias}]:${result.path}`
466
- : result.path;
536
+ const displayPath = result.origin === 'shared' && result.originAlias ? `[${result.originAlias}]:${result.path}` : result.path;
467
537
  return { content, path: displayPath, score: result.score, title: result.title };
468
538
  }));
469
539
  if (!canRespondDirectly(fullResults))
@@ -39,6 +39,9 @@ export class ByteRoverMcpServer {
39
39
  this.server = new McpServer({
40
40
  name: 'byterover',
41
41
  version: config.version,
42
+ }, {
43
+ instructions: 'ByteRover MCP — curate and query project context trees. ' +
44
+ 'See the `cwd` parameter description on each tool for how to provide the project path correctly.',
42
45
  });
43
46
  this.connectOptions = {
44
47
  clientType: 'mcp',
@@ -2,19 +2,15 @@ import { waitForConnectedClient } from '@campfirein/brv-transport-client';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { z } from 'zod';
4
4
  import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
- import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
5
+ import { associateProjectWithRetry, resolveMcpTaskContext } from './mcp-project-context.js';
6
6
  import { resolveClientCwd } from './resolve-client-cwd.js';
7
+ import { cwdField } from './shared-schema.js';
7
8
  export const BrvCurateInputSchema = z.object({
8
9
  context: z
9
10
  .string()
10
11
  .optional()
11
12
  .describe('Knowledge to store: patterns, decisions, errors, or insights about the codebase. Required unless files or folder are provided.'),
12
- cwd: z
13
- .string()
14
- .optional()
15
- .describe('Working directory of the project (absolute path). ' +
16
- 'Required when the MCP server runs in global mode (e.g., Windsurf). ' +
17
- 'Optional in project mode — defaults to the project directory.'),
13
+ cwd: cwdField,
18
14
  files: z
19
15
  .array(z.string())
20
16
  .max(5)
@@ -2,16 +2,12 @@ import { waitForConnectedClient } from '@campfirein/brv-transport-client';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { z } from 'zod';
4
4
  import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
5
- import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
5
+ import { associateProjectWithRetry, resolveMcpTaskContext } from './mcp-project-context.js';
6
6
  import { resolveClientCwd } from './resolve-client-cwd.js';
7
+ import { cwdField } from './shared-schema.js';
7
8
  import { waitForTaskResult } from './task-result-waiter.js';
8
9
  export const BrvQueryInputSchema = z.object({
9
- cwd: z
10
- .string()
11
- .optional()
12
- .describe('Working directory of the project (absolute path). ' +
13
- 'Required when the MCP server runs in global mode (e.g., Windsurf). ' +
14
- 'Optional in project mode — defaults to the project directory.'),
10
+ cwd: cwdField,
15
11
  query: z.string().describe('Natural language question about the codebase or project'),
16
12
  });
17
13
  /**
@@ -1,4 +1,5 @@
1
1
  export { registerBrvCurateTool } from './brv-curate-tool.js';
2
2
  export { registerBrvQueryTool } from './brv-query-tool.js';
3
3
  export { resolveClientCwd } from './resolve-client-cwd.js';
4
+ export { CWD_DESCRIPTION, cwdField } from './shared-schema.js';
4
5
  export { waitForTaskResult } from './task-result-waiter.js';
@@ -1,4 +1,5 @@
1
1
  export { registerBrvCurateTool } from './brv-curate-tool.js';
2
2
  export { registerBrvQueryTool } from './brv-query-tool.js';
3
3
  export { resolveClientCwd } from './resolve-client-cwd.js';
4
+ export { CWD_DESCRIPTION, cwdField } from './shared-schema.js';
4
5
  export { waitForTaskResult } from './task-result-waiter.js';
@@ -0,0 +1,3 @@
1
+ import { z } from 'zod';
2
+ export declare const CWD_DESCRIPTION: string;
3
+ export declare const cwdField: z.ZodOptional<z.ZodString>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const CWD_DESCRIPTION = 'Absolute path to the project root — selects which ByteRover context tree to use ' +
3
+ String.raw `(e.g., "/Users/me/code/myapp", "C:\\code\\myapp").` +
4
+ '\n\n' +
5
+ 'When to provide:\n' +
6
+ '- If your runtime does NOT expose any workspace/project context to you ' +
7
+ '(e.g., Claude Desktop, hosted MCP, global Windsurf): you MUST provide cwd. ' +
8
+ 'Use the path the user mentions, or ASK the user for the absolute path if unknown.\n' +
9
+ '- If your runtime DOES expose an open workspace/project root to you ' +
10
+ '(e.g., Cursor, Cline, Zed, Claude Code): you can OMIT this field — ' +
11
+ 'the MCP server was launched from that same project and already knows the cwd. ' +
12
+ 'Providing it is harmless but unnecessary.\n' +
13
+ '\n' +
14
+ 'If you omit cwd and the tool returns an error about the project not being resolved or cwd being required, retry with cwd explicitly set to the project root path.\n' +
15
+ '\n' +
16
+ 'Never guess, never invent paths, never use relative paths.';
17
+ export const cwdField = z.string().optional().describe(CWD_DESCRIPTION);
@@ -9,7 +9,7 @@ import { join } from 'node:path';
9
9
  import { ReviewEvents } from '../../../shared/transport/events/review-events.js';
10
10
  import { getAuthConfig } from '../../config/auth.config.js';
11
11
  import { getCurrentConfig } from '../../config/environment.js';
12
- import { BRV_DIR } from '../../constants.js';
12
+ import { API_V1_PATH, BRV_DIR } from '../../constants.js';
13
13
  import { getProjectDataDir } from '../../utils/path-utils.js';
14
14
  import { OAuthService } from '../auth/oauth-service.js';
15
15
  import { OidcDiscoveryService } from '../auth/oidc-discovery-service.js';
@@ -48,9 +48,12 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
48
48
  const envConfig = getCurrentConfig();
49
49
  const tokenStore = createTokenStore();
50
50
  const projectConfigStore = new ProjectConfigStore();
51
- const userService = new HttpUserService({ apiBaseUrl: envConfig.apiBaseUrl });
52
- const teamService = new HttpTeamService({ apiBaseUrl: envConfig.apiBaseUrl });
53
- const spaceService = new HttpSpaceService({ apiBaseUrl: envConfig.apiBaseUrl });
51
+ // API version paths appended at point of use.
52
+ // Note: IAM and Cogit currently share this version path, but may version independently in the future.
53
+ const iamApiV1 = `${envConfig.iamBaseUrl}${API_V1_PATH}`;
54
+ const userService = new HttpUserService({ apiBaseUrl: iamApiV1 });
55
+ const teamService = new HttpTeamService({ apiBaseUrl: iamApiV1 });
56
+ const spaceService = new HttpSpaceService({ apiBaseUrl: iamApiV1 });
54
57
  // Auth handler requires async OIDC discovery
55
58
  const discoveryService = new OidcDiscoveryService();
56
59
  const authConfig = await getAuthConfig(discoveryService);
@@ -86,8 +89,9 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
86
89
  const contextTreeWriterService = new FileContextTreeWriterService({ snapshotService: contextTreeSnapshotService });
87
90
  const contextTreeMerger = new FileContextTreeMerger({ snapshotService: contextTreeSnapshotService });
88
91
  const contextFileReader = new FileContextFileReader();
89
- const cogitPushService = new HttpCogitPushService({ apiBaseUrl: envConfig.cogitApiBaseUrl });
90
- const cogitPullService = new HttpCogitPullService({ apiBaseUrl: envConfig.cogitApiBaseUrl });
92
+ const cogitApiV1 = `${envConfig.cogitBaseUrl}${API_V1_PATH}`;
93
+ const cogitPushService = new HttpCogitPushService({ apiBaseUrl: cogitApiV1 });
94
+ const cogitPullService = new HttpCogitPullService({ apiBaseUrl: cogitApiV1 });
91
95
  // ConnectorManager factory — creates per-project instances since constructor binds to projectRoot
92
96
  const fileService = new FsFileService();
93
97
  const templateLoader = new FsTemplateLoader(fileService);
@@ -0,0 +1,42 @@
1
+ import type { TaskInfo } from '../../core/domain/transport/task-info.js';
2
+ import type { QueryExecutorResult } from '../../core/interfaces/executor/i-query-executor.js';
3
+ import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
4
+ import type { IQueryLogStore } from '../../core/interfaces/storage/i-query-log-store.js';
5
+ /** Query metadata without the response string (response arrives via task:completed). */
6
+ type QueryResultMetadata = Omit<QueryExecutorResult, 'response'>;
7
+ /**
8
+ * Lifecycle hook that transparently logs query task execution.
9
+ *
10
+ * Wired into TaskRouter via lifecycleHooks[]. Writes log entries to
11
+ * per-project FileQueryLogStore. All I/O errors are swallowed — logging
12
+ * must never block or affect query task execution.
13
+ *
14
+ * Key difference from CurateLogHandler: no onToolResult accumulation.
15
+ * Query metadata (tier, timing, matchedDocs, searchMetadata) arrives via
16
+ * setQueryResult() called after QueryExecutor.executeWithAgent() returns.
17
+ */
18
+ export declare class QueryLogHandler implements ITaskLifecycleHook {
19
+ private readonly createStore?;
20
+ /** Active task count per projectPath — used to evict idle stores. */
21
+ private readonly activeTaskCount;
22
+ /** Per-project store cache (one store per projectPath). Evicted when no active tasks remain. */
23
+ private readonly stores;
24
+ /** In-memory state per active task. Cleared on cleanup(). */
25
+ private readonly tasks;
26
+ constructor(createStore?: ((projectPath: string) => IQueryLogStore) | undefined);
27
+ cleanup(taskId: string): void;
28
+ onTaskCancelled(taskId: string, _task: TaskInfo): Promise<void>;
29
+ onTaskCompleted(taskId: string, result: string, _task: TaskInfo): Promise<void>;
30
+ onTaskCreate(task: TaskInfo): Promise<void | {
31
+ logId?: string;
32
+ }>;
33
+ onTaskError(taskId: string, errorMessage: string, _task: TaskInfo): Promise<void>;
34
+ /**
35
+ * Store query execution metadata for later finalization.
36
+ * Called by agent-process after QueryExecutor.executeWithAgent() returns.
37
+ * Synchronous — no I/O. Metadata is merged into the final entry on completion.
38
+ */
39
+ setQueryResult(taskId: string, result: QueryResultMetadata): void;
40
+ private getOrCreateStore;
41
+ }
42
+ export {};
@@ -0,0 +1,150 @@
1
+ import { getProjectDataDir } from '../../utils/path-utils.js';
2
+ import { transportLog } from '../../utils/process-logger.js';
3
+ import { FileQueryLogStore } from '../storage/file-query-log-store.js';
4
+ const QUERY_TASK_TYPES = new Set(['query']);
5
+ // ── QueryLogHandler ──────────────────────────────────────────────────────────
6
+ /**
7
+ * Lifecycle hook that transparently logs query task execution.
8
+ *
9
+ * Wired into TaskRouter via lifecycleHooks[]. Writes log entries to
10
+ * per-project FileQueryLogStore. All I/O errors are swallowed — logging
11
+ * must never block or affect query task execution.
12
+ *
13
+ * Key difference from CurateLogHandler: no onToolResult accumulation.
14
+ * Query metadata (tier, timing, matchedDocs, searchMetadata) arrives via
15
+ * setQueryResult() called after QueryExecutor.executeWithAgent() returns.
16
+ */
17
+ export class QueryLogHandler {
18
+ createStore;
19
+ /** Active task count per projectPath — used to evict idle stores. */
20
+ activeTaskCount = new Map();
21
+ /** Per-project store cache (one store per projectPath). Evicted when no active tasks remain. */
22
+ stores = new Map();
23
+ /** In-memory state per active task. Cleared on cleanup(). */
24
+ tasks = new Map();
25
+ constructor(createStore) {
26
+ this.createStore = createStore;
27
+ }
28
+ cleanup(taskId) {
29
+ const state = this.tasks.get(taskId);
30
+ this.tasks.delete(taskId);
31
+ if (state) {
32
+ const remaining = (this.activeTaskCount.get(state.projectPath) ?? 1) - 1;
33
+ if (remaining <= 0) {
34
+ this.activeTaskCount.delete(state.projectPath);
35
+ this.stores.delete(state.projectPath);
36
+ }
37
+ else {
38
+ this.activeTaskCount.set(state.projectPath, remaining);
39
+ }
40
+ }
41
+ }
42
+ async onTaskCancelled(taskId, _task) {
43
+ const state = this.tasks.get(taskId);
44
+ if (!state)
45
+ return;
46
+ const store = this.getOrCreateStore(state.projectPath);
47
+ const updated = {
48
+ ...state.entry,
49
+ completedAt: Date.now(),
50
+ matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
51
+ searchMetadata: state.queryResult?.searchMetadata,
52
+ status: 'cancelled',
53
+ tier: state.queryResult?.tier,
54
+ timing: state.queryResult?.timing,
55
+ };
56
+ await store.save(updated).catch((error) => {
57
+ transportLog(`QueryLogHandler: failed to save cancelled entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
58
+ });
59
+ }
60
+ async onTaskCompleted(taskId, result, _task) {
61
+ const state = this.tasks.get(taskId);
62
+ if (!state)
63
+ return;
64
+ const store = this.getOrCreateStore(state.projectPath);
65
+ const updated = {
66
+ ...state.entry,
67
+ completedAt: Date.now(),
68
+ matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
69
+ response: result.length > 0 ? result : undefined,
70
+ searchMetadata: state.queryResult?.searchMetadata,
71
+ status: 'completed',
72
+ tier: state.queryResult?.tier,
73
+ timing: state.queryResult?.timing,
74
+ };
75
+ await store.save(updated).catch((error) => {
76
+ transportLog(`QueryLogHandler: failed to save completed entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
77
+ });
78
+ }
79
+ async onTaskCreate(task) {
80
+ if (!QUERY_TASK_TYPES.has(task.type))
81
+ return;
82
+ if (!task.projectPath)
83
+ return;
84
+ const store = this.getOrCreateStore(task.projectPath);
85
+ const logId = await store.getNextId().catch((error) => {
86
+ transportLog(`QueryLogHandler: getNextId failed for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
87
+ });
88
+ if (!logId)
89
+ return;
90
+ const entry = {
91
+ id: logId,
92
+ matchedDocs: [],
93
+ query: task.content,
94
+ startedAt: task.createdAt,
95
+ status: 'processing',
96
+ taskId: task.taskId,
97
+ };
98
+ // MEMORY-FIRST: Set in-memory state BEFORE disk write so setQueryResult can access it immediately.
99
+ // Caching `entry` here lets onTaskCompleted/onTaskError rebuild the final entry
100
+ // without a getById round-trip — so completion is never lost even if this initial
101
+ // save fails.
102
+ this.tasks.set(task.taskId, { entry, projectPath: task.projectPath });
103
+ this.activeTaskCount.set(task.projectPath, (this.activeTaskCount.get(task.projectPath) ?? 0) + 1);
104
+ // Fire-and-forget disk I/O — logId is already known and returned.
105
+ store.save(entry).catch((error) => {
106
+ transportLog(`QueryLogHandler: failed to save processing entry for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
107
+ });
108
+ return { logId };
109
+ }
110
+ async onTaskError(taskId, errorMessage, _task) {
111
+ const state = this.tasks.get(taskId);
112
+ if (!state)
113
+ return;
114
+ const store = this.getOrCreateStore(state.projectPath);
115
+ const updated = {
116
+ ...state.entry,
117
+ completedAt: Date.now(),
118
+ error: errorMessage,
119
+ matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
120
+ searchMetadata: state.queryResult?.searchMetadata,
121
+ status: 'error',
122
+ tier: state.queryResult?.tier,
123
+ timing: state.queryResult?.timing,
124
+ };
125
+ await store.save(updated).catch((error) => {
126
+ transportLog(`QueryLogHandler: failed to save error entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
127
+ });
128
+ }
129
+ /**
130
+ * Store query execution metadata for later finalization.
131
+ * Called by agent-process after QueryExecutor.executeWithAgent() returns.
132
+ * Synchronous — no I/O. Metadata is merged into the final entry on completion.
133
+ */
134
+ setQueryResult(taskId, result) {
135
+ const state = this.tasks.get(taskId);
136
+ if (!state)
137
+ return;
138
+ state.queryResult = result;
139
+ }
140
+ getOrCreateStore(projectPath) {
141
+ const existing = this.stores.get(projectPath);
142
+ if (existing)
143
+ return existing;
144
+ const store = this.createStore
145
+ ? this.createStore(projectPath)
146
+ : new FileQueryLogStore({ baseDir: getProjectDataDir(projectPath) });
147
+ this.stores.set(projectPath, store);
148
+ return store;
149
+ }
150
+ }
@@ -14,18 +14,41 @@
14
14
  *
15
15
  * Consumed by TransportHandlers (orchestrator).
16
16
  */
17
+ import type { TaskCreateRequest } from '../../core/domain/transport/schemas.js';
17
18
  import type { IAgentPool } from '../../core/interfaces/agent/i-agent-pool.js';
18
19
  import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
19
20
  import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
20
21
  import type { IProjectRouter } from '../../core/interfaces/routing/i-project-router.js';
21
22
  import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
22
23
  import type { TaskInfo } from './types.js';
24
+ /**
25
+ * Outcome of the daemon-side pre-dispatch check.
26
+ *
27
+ * `skipResult` is the full string sent to the client as the task:completed `result`.
28
+ * The callback owns the message format so task-router stays task-type-agnostic
29
+ * (e.g. dream uses "Dream skipped: <reason>"; future task types can use their own).
30
+ */
31
+ export type PreDispatchCheckResult = {
32
+ eligible: false;
33
+ skipResult: string;
34
+ } | {
35
+ eligible: true;
36
+ };
37
+ export type PreDispatchCheck = (task: TaskCreateRequest, projectPath?: string) => Promise<PreDispatchCheckResult>;
23
38
  type TaskRouterOptions = {
24
39
  agentPool?: IAgentPool;
25
40
  /** Function to resolve agent clientId for a given project */
26
41
  getAgentForProject: (projectPath?: string) => string | undefined;
27
42
  /** Lifecycle hooks for task events (e.g. CurateLogHandler). */
28
43
  lifecycleHooks?: ITaskLifecycleHook[];
44
+ /**
45
+ * Optional daemon-side gate run before dispatching to the agent pool. If it
46
+ * resolves ineligible, task-router short-circuits with task:completed carrying
47
+ * the skip reason and never submits the task to an agent.
48
+ * Used for dream task type to enforce gates 1-3 (time, activity, queue) even
49
+ * on the CLI dispatch path — mirrors the idle-trigger pre-check pattern.
50
+ */
51
+ preDispatchCheck?: PreDispatchCheck;
29
52
  projectRegistry?: IProjectRegistry;
30
53
  projectRouter?: IProjectRouter;
31
54
  /** Resolves the projectPath a client registered with (from client:register). */
@@ -41,6 +64,7 @@ export declare class TaskRouter {
41
64
  private completedTasks;
42
65
  private readonly getAgentForProject;
43
66
  private readonly lifecycleHooks;
67
+ private readonly preDispatchCheck;
44
68
  private readonly projectRegistry;
45
69
  private readonly projectRouter;
46
70
  private readonly resolveClientProjectPath;
@@ -100,6 +124,22 @@ export declare class TaskRouter {
100
124
  */
101
125
  private handleTaskCreate;
102
126
  private handleTaskError;
127
+ /**
128
+ * Emit `task:completed` for a task that the daemon's pre-dispatch gate skipped
129
+ * before it ever reached `AgentPool.submitTask`.
130
+ *
131
+ * Distinct from {@link handleTaskCompleted}:
132
+ * - does NOT call `agentPool.notifyTaskCompleted` (the pool's `activeTasks`
133
+ * counter was never incremented, so decrementing here would undercount real
134
+ * load and let `drainQueue` dispatch an extra queued task)
135
+ * - does NOT fire `onTaskCompleted` lifecycle hooks (counters/metrics that
136
+ * act on completed tasks should not see pre-check skips as completions)
137
+ *
138
+ * Still emits the event to the client and the project room so REPL/TUI
139
+ * receive the skip result, and still calls `moveToCompleted` so the task is
140
+ * removed from the active set.
141
+ */
142
+ private handleTaskSkippedByPreCheck;
103
143
  private handleTaskStarted;
104
144
  /**
105
145
  * Move a task to the completed tasks map with grace period cleanup.