byterover-cli 3.11.0 → 3.13.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 (102) hide show
  1. package/.env.production +2 -1
  2. package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
  3. package/dist/oclif/commands/curate/index.js +6 -0
  4. package/dist/oclif/commands/providers/connect.d.ts +26 -1
  5. package/dist/oclif/commands/providers/connect.js +95 -17
  6. package/dist/oclif/commands/providers/list.d.ts +10 -1
  7. package/dist/oclif/commands/providers/list.js +35 -3
  8. package/dist/oclif/commands/query.js +6 -0
  9. package/dist/oclif/commands/status.js +4 -0
  10. package/dist/oclif/lib/billing-line.d.ts +8 -0
  11. package/dist/oclif/lib/billing-line.js +45 -0
  12. package/dist/oclif/lib/format-billing-line.d.ts +2 -0
  13. package/dist/oclif/lib/format-billing-line.js +19 -0
  14. package/dist/oclif/lib/insufficient-credits.d.ts +11 -0
  15. package/dist/oclif/lib/insufficient-credits.js +36 -0
  16. package/dist/server/config/environment.d.ts +1 -0
  17. package/dist/server/config/environment.js +3 -0
  18. package/dist/server/constants.d.ts +6 -0
  19. package/dist/server/constants.js +11 -0
  20. package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
  21. package/dist/server/core/domain/entities/task-history-entry.js +88 -0
  22. package/dist/server/core/domain/transport/schemas.d.ts +1420 -11
  23. package/dist/server/core/domain/transport/schemas.js +160 -6
  24. package/dist/server/core/domain/transport/task-info.d.ts +18 -0
  25. package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
  26. package/dist/server/core/interfaces/services/i-billing-service.d.ts +26 -0
  27. package/dist/server/core/interfaces/services/i-billing-service.js +1 -0
  28. package/dist/server/core/interfaces/storage/i-billing-config-store.d.ts +4 -0
  29. package/dist/server/core/interfaces/storage/i-billing-config-store.js +1 -0
  30. package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
  31. package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
  32. package/dist/server/infra/billing/billing-state-endpoint.d.ts +4 -0
  33. package/dist/server/infra/billing/billing-state-endpoint.js +7 -0
  34. package/dist/server/infra/billing/build-status-billing.d.ts +9 -0
  35. package/dist/server/infra/billing/build-status-billing.js +36 -0
  36. package/dist/server/infra/billing/http-billing-service.d.ts +19 -0
  37. package/dist/server/infra/billing/http-billing-service.js +57 -0
  38. package/dist/server/infra/billing/paid-organizations-endpoint.d.ts +8 -0
  39. package/dist/server/infra/billing/paid-organizations-endpoint.js +18 -0
  40. package/dist/server/infra/billing/resolve-billing-source.d.ts +13 -0
  41. package/dist/server/infra/billing/resolve-billing-source.js +36 -0
  42. package/dist/server/infra/billing/resolve-billing-team.d.ts +5 -0
  43. package/dist/server/infra/billing/resolve-billing-team.js +8 -0
  44. package/dist/server/infra/connectors/rules/rules-connector.js +7 -2
  45. package/dist/server/infra/connectors/shared/constants.d.ts +9 -0
  46. package/dist/server/infra/connectors/shared/constants.js +31 -5
  47. package/dist/server/infra/daemon/agent-process.js +10 -8
  48. package/dist/server/infra/daemon/brv-server.js +48 -18
  49. package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
  50. package/dist/server/infra/dream/dream-response-schemas.js +7 -0
  51. package/dist/server/infra/dream/operations/consolidate.js +21 -8
  52. package/dist/server/infra/dream/operations/synthesize.js +35 -8
  53. package/dist/server/infra/http/provider-model-fetchers.js +10 -4
  54. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  55. package/dist/server/infra/process/feature-handlers.js +26 -2
  56. package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
  57. package/dist/server/infra/process/task-history-entry-builder.js +101 -0
  58. package/dist/server/infra/process/task-history-hook.d.ts +37 -0
  59. package/dist/server/infra/process/task-history-hook.js +70 -0
  60. package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
  61. package/dist/server/infra/process/task-history-store-cache.js +106 -0
  62. package/dist/server/infra/process/task-router.d.ts +72 -0
  63. package/dist/server/infra/process/task-router.js +690 -15
  64. package/dist/server/infra/process/transport-handlers.d.ts +8 -0
  65. package/dist/server/infra/process/transport-handlers.js +2 -0
  66. package/dist/server/infra/storage/file-billing-config-store.d.ts +13 -0
  67. package/dist/server/infra/storage/file-billing-config-store.js +55 -0
  68. package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
  69. package/dist/server/infra/storage/file-task-history-store.js +912 -0
  70. package/dist/server/infra/transport/handlers/auth-handler.d.ts +4 -0
  71. package/dist/server/infra/transport/handlers/auth-handler.js +20 -2
  72. package/dist/server/infra/transport/handlers/billing-handler.d.ts +30 -0
  73. package/dist/server/infra/transport/handlers/billing-handler.js +132 -0
  74. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  75. package/dist/server/infra/transport/handlers/index.js +2 -0
  76. package/dist/server/infra/transport/handlers/init-handler.js +2 -0
  77. package/dist/server/infra/transport/handlers/status-handler.d.ts +14 -0
  78. package/dist/server/infra/transport/handlers/status-handler.js +16 -0
  79. package/dist/server/infra/transport/handlers/team-handler.d.ts +19 -0
  80. package/dist/server/infra/transport/handlers/team-handler.js +40 -0
  81. package/dist/shared/transport/events/auth-events.d.ts +3 -0
  82. package/dist/shared/transport/events/billing-events.d.ts +48 -0
  83. package/dist/shared/transport/events/billing-events.js +8 -0
  84. package/dist/shared/transport/events/index.d.ts +16 -0
  85. package/dist/shared/transport/events/index.js +6 -0
  86. package/dist/shared/transport/events/task-events.d.ts +204 -1
  87. package/dist/shared/transport/events/task-events.js +11 -0
  88. package/dist/shared/transport/events/team-events.d.ts +8 -0
  89. package/dist/shared/transport/events/team-events.js +3 -0
  90. package/dist/shared/transport/types/dto.d.ts +80 -0
  91. package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
  92. package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
  93. package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
  94. package/dist/tui/types/messages.d.ts +2 -9
  95. package/dist/webui/assets/index-B9JmEFOK.js +130 -0
  96. package/dist/webui/assets/index-CMIKsBMr.css +1 -0
  97. package/dist/webui/index.html +2 -2
  98. package/dist/webui/sw.js +1 -1
  99. package/oclif.manifest.json +653 -645
  100. package/package.json +1 -1
  101. package/dist/webui/assets/index--sXE__bc.css +0 -1
  102. package/dist/webui/assets/index-Bkkx961b.js +0 -130
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { AGENT_CONNECTOR_CONFIG } from '../../../core/domain/entities/agent.js';
3
- import { hasMcpToolsInBrvSection } from '../shared/constants.js';
3
+ import { extractInstalledAgentFromBrvSection, hasMcpToolsInBrvSection } from '../shared/constants.js';
4
4
  import { RuleFileManager } from '../shared/rule-file-manager.js';
5
5
  import { RULES_CONNECTOR_CONFIGS } from './rules-connector-config.js';
6
6
  /**
@@ -99,7 +99,12 @@ export class RulesConnector {
99
99
  }
100
100
  const content = await this.fileService.read(fullPath);
101
101
  const hasMcpTools = hasMcpToolsInBrvSection(content);
102
- const installed = hasMarkers && !hasMcpTools;
102
+ const footerAgent = extractInstalledAgentFromBrvSection(content);
103
+ // Footer present: only the agent named in the footer owns this rule file.
104
+ // Footer absent (legacy file pre-footer): fall back to marker presence so
105
+ // existing installs keep reporting installed until the next reinstall.
106
+ const matchesFooter = footerAgent === undefined ? true : footerAgent === agent;
107
+ const installed = hasMarkers && !hasMcpTools && matchesFooter;
103
108
  return {
104
109
  configExists: true,
105
110
  configPath: config.filePath,
@@ -15,3 +15,12 @@ export declare const BRV_RULE_MARKERS: {
15
15
  * Only checks within the markers section to avoid false positives from user content.
16
16
  */
17
17
  export declare const hasMcpToolsInBrvSection: (content: string) => boolean;
18
+ /**
19
+ * Extracts the agent name from a `Generated by ByteRover CLI for X` footer
20
+ * inside the BRV markers section. Used to disambiguate which agent owns a
21
+ * shared rule file (Amp / Codex / OpenCode all map to AGENTS.md).
22
+ *
23
+ * Returns undefined when markers are missing, when the footer is absent
24
+ * (legacy pre-footer installs), or when the footer line is empty.
25
+ */
26
+ export declare const extractInstalledAgentFromBrvSection: (content: string) => string | undefined;
@@ -10,16 +10,42 @@ export const BRV_RULE_MARKERS = {
10
10
  END: '<!-- END BYTEROVER RULES -->',
11
11
  START: '<!-- BEGIN BYTEROVER RULES -->',
12
12
  };
13
+ const sliceBrvSection = (content) => {
14
+ const startIdx = content.indexOf(BRV_RULE_MARKERS.START);
15
+ const endIdx = content.indexOf(BRV_RULE_MARKERS.END);
16
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx)
17
+ return undefined;
18
+ return content.slice(startIdx, endIdx);
19
+ };
13
20
  /**
14
21
  * Checks if the BRV markers section contains MCP tool references (brv-query/brv-curate).
15
22
  * Only checks within the markers section to avoid false positives from user content.
16
23
  */
17
24
  export const hasMcpToolsInBrvSection = (content) => {
18
- const startIdx = content.indexOf(BRV_RULE_MARKERS.START);
19
- const endIdx = content.indexOf(BRV_RULE_MARKERS.END);
20
- if (startIdx === -1 || endIdx === -1)
25
+ const brvSection = sliceBrvSection(content);
26
+ if (brvSection === undefined)
21
27
  return false;
22
- // eslint-disable-next-line unicorn/prefer-set-has
23
- const brvSection = content.slice(startIdx, endIdx);
24
28
  return brvSection.includes('brv-query') || brvSection.includes('brv-curate');
25
29
  };
30
+ /**
31
+ * Extracts the agent name from a `Generated by ByteRover CLI for X` footer
32
+ * inside the BRV markers section. Used to disambiguate which agent owns a
33
+ * shared rule file (Amp / Codex / OpenCode all map to AGENTS.md).
34
+ *
35
+ * Returns undefined when markers are missing, when the footer is absent
36
+ * (legacy pre-footer installs), or when the footer line is empty.
37
+ */
38
+ export const extractInstalledAgentFromBrvSection = (content) => {
39
+ const brvSection = sliceBrvSection(content);
40
+ if (brvSection === undefined)
41
+ return undefined;
42
+ const tagWithDelimiter = `${BRV_RULE_TAG} `;
43
+ const tagIdx = brvSection.indexOf(tagWithDelimiter);
44
+ if (tagIdx === -1)
45
+ return undefined;
46
+ const afterTag = brvSection.slice(tagIdx + tagWithDelimiter.length);
47
+ const newlineIdx = afterTag.indexOf('\n');
48
+ const agentLine = newlineIdx === -1 ? afterTag : afterTag.slice(0, newlineIdx);
49
+ const agent = agentLine.trim();
50
+ return agent.length === 0 ? undefined : agent;
51
+ };
@@ -133,7 +133,7 @@ async function activateExistingSession(sessionId, providerId) {
133
133
  */
134
134
  let cachedSessionKey = '';
135
135
  let cachedBrvConfig;
136
- let cachedTeamId = '';
136
+ let cachedPinnedOrgId;
137
137
  let cachedSpaceId = '';
138
138
  let cachedActiveProvider = '';
139
139
  let cachedActiveModel = '';
@@ -177,15 +177,16 @@ async function start() {
177
177
  transport.on('connect_error', (err) => {
178
178
  agentLog(`Transport connect_error: ${err?.message ?? 'unknown'}`);
179
179
  });
180
- const [configResult, authResult, providerResult] = await Promise.all([
180
+ const [configResult, authResult, providerResult, billingResult] = await Promise.all([
181
181
  transport.requestWithAck(TransportStateEventNames.GET_PROJECT_CONFIG, { projectPath }),
182
182
  transport.requestWithAck(TransportStateEventNames.GET_AUTH),
183
183
  transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG),
184
+ transport.requestWithAck(TransportStateEventNames.GET_BILLING_CONFIG, { projectPath }),
184
185
  ]);
185
186
  cachedBrvConfig = configResult.brvConfig;
186
- cachedTeamId = configResult.teamId ?? '';
187
187
  cachedSpaceId = configResult.spaceId ?? '';
188
188
  cachedSessionKey = authResult.sessionKey ?? '';
189
+ cachedPinnedOrgId = billingResult.pinnedTeamId;
189
190
  agentLog('Initial config loaded from state server');
190
191
  // 3. Listen for config/auth/provider updates from daemon
191
192
  transport.on('config:updated', (data) => {
@@ -193,8 +194,6 @@ async function start() {
193
194
  return;
194
195
  if (data.brvConfig)
195
196
  cachedBrvConfig = data.brvConfig;
196
- if (data.teamId !== undefined)
197
- cachedTeamId = data.teamId;
198
197
  if (data.spaceId !== undefined)
199
198
  cachedSpaceId = data.spaceId;
200
199
  });
@@ -206,6 +205,11 @@ async function start() {
206
205
  providerConfigDirty = true;
207
206
  providerFetchRetries = 0;
208
207
  });
208
+ transport.on(TransportDaemonEventNames.BILLING_PIN_CHANGED, (data) => {
209
+ if (data.projectPath !== projectPath)
210
+ return;
211
+ cachedPinnedOrgId = data.teamId;
212
+ });
209
213
  // 4. Provider config resolved by daemon (API key, base URL, headers, etc.)
210
214
  const { activeModel, activeProvider } = providerResult;
211
215
  cachedActiveProvider = activeProvider;
@@ -244,7 +248,7 @@ async function start() {
244
248
  projectIdProvider: () => PROJECT,
245
249
  sessionKeyProvider: () => cachedSessionKey,
246
250
  spaceIdProvider: () => cachedSpaceId,
247
- teamIdProvider: () => cachedTeamId,
251
+ teamIdProvider: () => cachedPinnedOrgId ?? '',
248
252
  transportClient: transport,
249
253
  });
250
254
  await agent.start();
@@ -393,8 +397,6 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
393
397
  const configResult = await transport.requestWithAck(TransportStateEventNames.GET_PROJECT_CONFIG, { projectPath });
394
398
  if (configResult.brvConfig)
395
399
  cachedBrvConfig = configResult.brvConfig;
396
- if (configResult.teamId !== undefined)
397
- cachedTeamId = configResult.teamId;
398
400
  if (configResult.spaceId !== undefined)
399
401
  cachedSpaceId = configResult.spaceId;
400
402
  }
@@ -34,6 +34,7 @@ import { TransportStateEventNames, TransportTaskEventNames, } from '../../core/d
34
34
  import { getGlobalDataDir } from '../../utils/global-data-path.js';
35
35
  import { getProjectDataDir } from '../../utils/path-utils.js';
36
36
  import { crashLog, processLog } from '../../utils/process-logger.js';
37
+ import { createBillingStateHandler } from '../billing/billing-state-endpoint.js';
37
38
  import { ClientManager } from '../client/client-manager.js';
38
39
  import { ProjectConfigStore } from '../config/file-config-store.js';
39
40
  import { readContextTreeRemoteUrl } from '../context-tree/read-context-tree-remote.js';
@@ -44,6 +45,8 @@ import { broadcastToProjectRoom } from '../process/broadcast-utils.js';
44
45
  import { CurateLogHandler } from '../process/curate-log-handler.js';
45
46
  import { setupFeatureHandlers } from '../process/feature-handlers.js';
46
47
  import { QueryLogHandler } from '../process/query-log-handler.js';
48
+ import { TaskHistoryHook } from '../process/task-history-hook.js';
49
+ import { getStore as getTaskHistoryStore } from '../process/task-history-store-cache.js';
47
50
  import { TransportHandlers } from '../process/transport-handlers.js';
48
51
  import { ProjectRegistry } from '../project/project-registry.js';
49
52
  import { createProviderOAuthTokenStore } from '../provider-oauth/provider-oauth-token-store.js';
@@ -52,6 +55,7 @@ import { clearStaleProviderConfig, resolveProviderConfig } from '../provider/pro
52
55
  import { ProjectRouter } from '../routing/project-router.js';
53
56
  import { AuthStateStore } from '../state/auth-state-store.js';
54
57
  import { ProjectStateLoader } from '../state/project-state-loader.js';
58
+ import { FileBillingConfigStore } from '../storage/file-billing-config-store.js';
55
59
  import { FileCurateLogStore } from '../storage/file-curate-log-store.js';
56
60
  import { FileProviderConfigStore } from '../storage/file-provider-config-store.js';
57
61
  import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
@@ -334,6 +338,32 @@ async function main() {
334
338
  broadcastToProjectRoom(projectRegistry, projectRouter, info.projectPath, ReviewEvents.NOTIFY, payload, info.clientId);
335
339
  });
336
340
  const queryLogHandler = new QueryLogHandler();
341
+ // Task-history hook — persists every lifecycle transition + accumulated
342
+ // llmservice events to a per-project FileTaskHistoryStore. The store
343
+ // factory is module-scoped so M2.09 wire handlers can read from the
344
+ // same instances this hook writes to.
345
+ const taskHistoryHook = new TaskHistoryHook({ getStore: getTaskHistoryStore });
346
+ // Provider config/keychain stores — shared between feature handlers and state endpoint.
347
+ // Hoisted ahead of `new TransportHandlers` so the resolveActiveProvider callback below
348
+ // can close over them and call resolveProviderConfig synchronously at task-create time.
349
+ const providerConfigStore = new FileProviderConfigStore();
350
+ const providerKeychainStore = createProviderKeychainStore();
351
+ const providerOAuthTokenStore = createProviderOAuthTokenStore();
352
+ // Token refresh manager — transparently refreshes OAuth tokens before they expire
353
+ const tokenRefreshManager = new TokenRefreshManager({
354
+ providerConfigStore,
355
+ providerKeychainStore,
356
+ providerOAuthTokenStore,
357
+ transport: transportServer,
358
+ });
359
+ // Clear stale provider config on startup (e.g. migration from v1 system keychain to v2 file keystore).
360
+ // If a provider is configured but its API key is no longer accessible, disconnect it so the user
361
+ // is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
362
+ await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
363
+ // State endpoint: provider config — agents request this on startup and after provider:updated
364
+ transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
365
+ const billingConfigStoreFactory = (projectPath) => new FileBillingConfigStore({ baseDir: join(projectPath, BRV_DIR) });
366
+ transportServer.onRequest(TransportStateEventNames.GET_BILLING_CONFIG, createBillingStateHandler(billingConfigStoreFactory));
337
367
  const transportHandlers = new TransportHandlers({
338
368
  agentPool,
339
369
  clientManager,
@@ -341,6 +371,7 @@ async function main() {
341
371
  // so peer clients (TUI / MCP) can render drift indicators without an
342
372
  // extra round-trip.
343
373
  daemonVersion: version,
374
+ getTaskHistoryStore,
344
375
  // Resolves the project's review-disabled flag once at task-create. The result
345
376
  // is stamped onto TaskInfo + TaskExecute so daemon hooks (CurateLogHandler) and
346
377
  // the agent process (curate-tool backups, dream review entries) all observe a
@@ -348,7 +379,7 @@ async function main() {
348
379
  // idle-dream dispatch above so review semantics are identical regardless of
349
380
  // dispatch source (CLI task:create vs agent-idle trigger).
350
381
  isReviewDisabled: resolveReviewDisabled,
351
- lifecycleHooks: [curateLogHandler, queryLogHandler],
382
+ lifecycleHooks: [curateLogHandler, queryLogHandler, taskHistoryHook],
352
383
  // Daemon-side gate for dream task:create — mirrors the idle-trigger pre-check
353
384
  // in this file so the CLI path (brv dream without --force) actually honors
354
385
  // gate 3 (queue). The agent-side check kept gate 3 hardcoded to skip,
@@ -369,6 +400,21 @@ async function main() {
369
400
  },
370
401
  projectRegistry,
371
402
  projectRouter,
403
+ // Stamp the active provider/model snapshot onto every created task so the
404
+ // Web UI can display which provider handled which task. Failures are
405
+ // swallowed by TaskRouter's safeResolveActiveProvider — never blocks dispatch.
406
+ async resolveActiveProvider() {
407
+ const config = await resolveProviderConfig({
408
+ authStateStore,
409
+ providerConfigStore,
410
+ providerKeychainStore,
411
+ tokenRefreshManager,
412
+ });
413
+ return {
414
+ ...(config.activeModel ? { model: config.activeModel } : {}),
415
+ ...(config.activeProvider ? { provider: config.activeProvider } : {}),
416
+ };
417
+ },
372
418
  transport: transportServer,
373
419
  });
374
420
  transportHandlers.setup();
@@ -526,28 +572,12 @@ async function main() {
526
572
  running: transportServer.isRunning(),
527
573
  },
528
574
  }));
529
- // Provider config/keychain stores — shared between feature handlers and state endpoint
530
- const providerConfigStore = new FileProviderConfigStore();
531
- const providerKeychainStore = createProviderKeychainStore();
532
- const providerOAuthTokenStore = createProviderOAuthTokenStore();
533
- // Token refresh manager — transparently refreshes OAuth tokens before they expire
534
- const tokenRefreshManager = new TokenRefreshManager({
535
- providerConfigStore,
536
- providerKeychainStore,
537
- providerOAuthTokenStore,
538
- transport: transportServer,
539
- });
540
- // Clear stale provider config on startup (e.g. migration from v1 system keychain to v2 file keystore).
541
- // If a provider is configured but its API key is no longer accessible, disconnect it so the user
542
- // is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
543
- await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
544
- // State endpoint: provider config — agents request this on startup and after provider:updated
545
- transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
546
575
  // Feature handlers (auth, init, status, push, pull, etc.) require async OIDC discovery.
547
576
  // Placed after daemon:getState so the debug endpoint is available immediately,
548
577
  // without waiting for OIDC discovery (~400ms).
549
578
  await setupFeatureHandlers({
550
579
  authStateStore,
580
+ billingConfigStoreFactory,
551
581
  broadcastToProject(projectPath, event, data) {
552
582
  broadcastToProjectRoom(projectRegistry, projectRouter, projectPath, event, data);
553
583
  },
@@ -86,10 +86,16 @@ export declare const SynthesisCandidateSchema: z.ZodObject<{
86
86
  domain: string;
87
87
  fact: string;
88
88
  }>, "many">;
89
+ keywords: z.ZodArray<z.ZodString, "many">;
89
90
  placement: z.ZodString;
91
+ summary: z.ZodString;
92
+ tags: z.ZodArray<z.ZodString, "many">;
90
93
  title: z.ZodString;
91
94
  }, "strip", z.ZodTypeAny, {
95
+ summary: string;
96
+ tags: string[];
92
97
  title: string;
98
+ keywords: string[];
93
99
  confidence: number;
94
100
  claim: string;
95
101
  evidence: {
@@ -98,7 +104,10 @@ export declare const SynthesisCandidateSchema: z.ZodObject<{
98
104
  }[];
99
105
  placement: string;
100
106
  }, {
107
+ summary: string;
108
+ tags: string[];
101
109
  title: string;
110
+ keywords: string[];
102
111
  confidence: number;
103
112
  claim: string;
104
113
  evidence: {
@@ -121,10 +130,16 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
121
130
  domain: string;
122
131
  fact: string;
123
132
  }>, "many">;
133
+ keywords: z.ZodArray<z.ZodString, "many">;
124
134
  placement: z.ZodString;
135
+ summary: z.ZodString;
136
+ tags: z.ZodArray<z.ZodString, "many">;
125
137
  title: z.ZodString;
126
138
  }, "strip", z.ZodTypeAny, {
139
+ summary: string;
140
+ tags: string[];
127
141
  title: string;
142
+ keywords: string[];
128
143
  confidence: number;
129
144
  claim: string;
130
145
  evidence: {
@@ -133,7 +148,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
133
148
  }[];
134
149
  placement: string;
135
150
  }, {
151
+ summary: string;
152
+ tags: string[];
136
153
  title: string;
154
+ keywords: string[];
137
155
  confidence: number;
138
156
  claim: string;
139
157
  evidence: {
@@ -144,7 +162,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
144
162
  }>, "many">;
145
163
  }, "strip", z.ZodTypeAny, {
146
164
  syntheses: {
165
+ summary: string;
166
+ tags: string[];
147
167
  title: string;
168
+ keywords: string[];
148
169
  confidence: number;
149
170
  claim: string;
150
171
  evidence: {
@@ -155,7 +176,10 @@ export declare const SynthesizeResponseSchema: z.ZodObject<{
155
176
  }[];
156
177
  }, {
157
178
  syntheses: {
179
+ summary: string;
180
+ tags: string[];
158
181
  title: string;
182
+ keywords: string[];
159
183
  confidence: number;
160
184
  claim: string;
161
185
  evidence: {
@@ -13,6 +13,10 @@ export const ConsolidateResponseSchema = z.object({
13
13
  actions: z.array(ConsolidationActionSchema),
14
14
  });
15
15
  // ── Synthesize ───────────────────────────────────────────────────────────────
16
+ // Bounds are slightly above the prompt's soft targets (200 chars / 3-5 tags /
17
+ // 5-10 keywords) so a model that goes a little over still produces a usable
18
+ // synthesis instead of being rejected outright; the caps still prevent a
19
+ // runaway model from landing oversized text directly in card-mode YAML.
16
20
  export const SynthesisCandidateSchema = z.object({
17
21
  claim: z.string(),
18
22
  confidence: z.number().min(0).max(1),
@@ -20,7 +24,10 @@ export const SynthesisCandidateSchema = z.object({
20
24
  domain: z.string(),
21
25
  fact: z.string(),
22
26
  })),
27
+ keywords: z.array(z.string()).max(15),
23
28
  placement: z.string(),
29
+ summary: z.string().max(500),
30
+ tags: z.array(z.string()).max(8),
24
31
  title: z.string(),
25
32
  });
26
33
  export const SynthesizeResponseSchema = z.object({
@@ -15,6 +15,7 @@ import { randomUUID } from 'node:crypto';
15
15
  import { access, mkdir, readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
16
16
  import { dirname, join } from 'node:path';
17
17
  import { warnSidecarFailure } from '../../../core/domain/knowledge/sidecar-logging.js';
18
+ import { isExcludedFromSync } from '../../context-tree/derived-artifact.js';
18
19
  import { ConsolidateResponseSchema } from '../dream-response-schemas.js';
19
20
  import { parseDreamResponse } from '../parse-dream-response.js';
20
21
  /**
@@ -226,7 +227,7 @@ function addFrontmatterFields(content, fields) {
226
227
  if (parsed && typeof parsed === 'object') {
227
228
  // Spread preserves existing key order; new fields are appended at end.
228
229
  const merged = { ...parsed, ...fields };
229
- const newYaml = yamlDump(merged, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
230
+ const newYaml = yamlDump(merged, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
230
231
  return `---\n${newYaml}\n---\n${body}`;
231
232
  }
232
233
  }
@@ -236,7 +237,7 @@ function addFrontmatterFields(content, fields) {
236
237
  }
237
238
  }
238
239
  // No valid frontmatter — prepend
239
- const yaml = yamlDump(fields, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
240
+ const yaml = yamlDump(fields, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
240
241
  return `---\n${yaml}\n---\n${content}`;
241
242
  }
242
243
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -458,8 +459,10 @@ async function executeCrossReference(action, ctx) {
458
459
  await Promise.all(Object.entries(previousTexts).map(([file, content]) => reviewBackupStore.save(file, content).catch(() => { })));
459
460
  }
460
461
  // For each file, add the other files to its related frontmatter
461
- await Promise.all(action.files.map((file) => {
462
- const otherFiles = action.files.filter((f) => f !== file);
462
+ // Skip derived-artifact targets so we never write related: onto them.
463
+ const eligibleFiles = action.files.filter((f) => !isExcludedFromSync(f));
464
+ await Promise.all(eligibleFiles.map((file) => {
465
+ const otherFiles = eligibleFiles.filter((f) => f !== file);
463
466
  return addRelatedLinks(join(contextTreeDir, file), otherFiles);
464
467
  }));
465
468
  return {
@@ -472,6 +475,8 @@ async function executeCrossReference(action, ctx) {
472
475
  };
473
476
  }
474
477
  async function addRelatedLinks(filePath, relatedPaths) {
478
+ // Skip paths that won't be pushed — they'd be dangling refs on remote.
479
+ const incoming = relatedPaths.filter((p) => !isExcludedFromSync(p));
475
480
  let content;
476
481
  try {
477
482
  content = await readFile(filePath, 'utf8');
@@ -491,8 +496,14 @@ async function addRelatedLinks(filePath, relatedPaths) {
491
496
  try {
492
497
  const parsed = yamlLoad(yamlBlock);
493
498
  if (parsed && typeof parsed === 'object') {
494
- const existing = Array.isArray(parsed.related) ? parsed.related : [];
495
- parsed.related = [...new Set([...existing, ...relatedPaths])];
499
+ const hadRelated = Array.isArray(parsed.related);
500
+ const existing = (Array.isArray(parsed.related) ? parsed.related : [])
501
+ .filter((p) => !isExcludedFromSync(p));
502
+ const merged = [...new Set([...existing, ...incoming])];
503
+ // Don't introduce a related: [] key into a file that didn't have one.
504
+ if (!hadRelated && merged.length === 0)
505
+ return;
506
+ parsed.related = merged;
496
507
  const newYaml = yamlDump(parsed, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
497
508
  await atomicWrite(filePath, `---\n${newYaml}\n---\n${body}`);
498
509
  return;
@@ -503,8 +514,10 @@ async function addRelatedLinks(filePath, relatedPaths) {
503
514
  }
504
515
  }
505
516
  }
506
- // No existing frontmatter — add one with related field
507
- const yaml = yamlDump({ related: relatedPaths }, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
517
+ // No existing frontmatter — add one with related field, unless filter left nothing to add.
518
+ if (incoming.length === 0)
519
+ return;
520
+ const yaml = yamlDump({ related: incoming }, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
508
521
  await atomicWrite(filePath, `---\n${yaml}\n---\n${content}`);
509
522
  }
510
523
  async function determineNeedsReview(actionType, files, opts) {
@@ -200,15 +200,36 @@ async function writeSynthesisFile(candidate, contextTreeDir, runtimeSignalStore,
200
200
  // ENOENT — good, proceed
201
201
  }
202
202
  const sources = candidate.evidence.map((e) => `${e.domain}/_index.md`);
203
+ // Normalize tags to lowercase kebab-case so card chips and BM25 search see
204
+ // a consistent label regardless of whether the model honored the prompt's
205
+ // formatting rule. Empty entries (post-trim) are dropped.
206
+ const normalizedTags = candidate.tags
207
+ .map((t) => t.toLowerCase().trim().replaceAll(/\s+/g, '-'))
208
+ .filter((t) => t.length > 0);
209
+ const now = new Date().toISOString();
210
+ // Field order is enforced by insertion order (yamlDump uses sortKeys:false).
211
+ // Synthesis markers (confidence, sources, synthesized_at, type) come first
212
+ // in the order pre-existing synthesized files use on disk, so re-generating
213
+ // an old file does not produce a mechanical reorder diff. The seven
214
+ // semantic fields below mirror the order in markdown-writer.ts's
215
+ // generateFrontmatter so the on-disk shape matches regular `brv save`
216
+ // files; cogit then exposes them in DtoV3MemoryCardResource for card-mode
217
+ // display in the web UI.
203
218
  /* eslint-disable camelcase */
204
- const frontmatter = {
205
- confidence: candidate.confidence,
206
- sources,
207
- synthesized_at: new Date().toISOString(),
208
- type: 'synthesis',
209
- };
219
+ const frontmatter = {};
220
+ frontmatter.confidence = candidate.confidence;
221
+ frontmatter.sources = sources;
222
+ frontmatter.synthesized_at = now;
223
+ frontmatter.type = 'synthesis';
224
+ frontmatter.title = candidate.title;
225
+ frontmatter.summary = candidate.summary;
226
+ frontmatter.tags = normalizedTags;
227
+ frontmatter.related = [];
228
+ frontmatter.keywords = candidate.keywords;
229
+ frontmatter.createdAt = now;
230
+ frontmatter.updatedAt = now;
210
231
  /* eslint-enable camelcase */
211
- const yaml = yamlDump(frontmatter, { lineWidth: -1, sortKeys: false }).trimEnd();
232
+ const yaml = yamlDump(frontmatter, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
212
233
  const body = [
213
234
  `# ${candidate.title}`,
214
235
  '',
@@ -280,11 +301,17 @@ function buildPrompt(domains, existingSyntheses) {
280
301
  '- Do NOT report trivial or obvious connections (e.g., "both domains use TypeScript").',
281
302
  '- Each synthesis must reference at least 2 domains with specific evidence.',
282
303
  '- For "placement", choose the domain where this insight is MOST actionable.',
304
+ '- "summary" is one sentence (≤ 200 chars) describing the insight; this is what the UI shows as a card preview.',
305
+ '- "tags" are 3-5 short topical labels drawn from the source domains (e.g., "auth", "caching"). Lowercase, kebab-case.',
306
+ '- "keywords" are 5-10 single words a developer would search for to surface this synthesis.',
283
307
  '- If nothing meaningful is found, return an empty array. That is fine — but missing a clear cross-domain pattern is a failure.',
284
308
  '',
309
+ // Keep the JSON shape below in sync with SynthesisCandidateSchema in
310
+ // dream-response-schemas.ts; the schema rejects responses that omit any
311
+ // listed field, so adding a field there requires updating this example.
285
312
  'Respond with JSON:',
286
313
  '```',
287
- '{ "syntheses": [{ "title": "...", "claim": "...", "evidence": [{"domain": "...", "fact": "..."}], "confidence": 0.0-1.0, "placement": "..." }] }',
314
+ '{ "syntheses": [{ "title": "...", "summary": "...", "claim": "...", "evidence": [{"domain": "...", "fact": "..."}], "tags": ["..."], "keywords": ["..."], "confidence": 0.0-1.0, "placement": "..." }] }',
288
315
  '```',
289
316
  ].join('\n');
290
317
  }
@@ -361,17 +361,23 @@ export class OpenAICompatibleModelFetcher {
361
361
  const modelList = Array.isArray(responseData)
362
362
  ? responseData
363
363
  : (responseData.data ?? responseData.models ?? []);
364
- const models = modelList.map((model) => {
364
+ const uniqueModels = new Map();
365
+ for (const model of modelList) {
365
366
  const id = String(model.id ?? model.name ?? '');
366
- return {
367
+ if (!id)
368
+ continue;
369
+ if (uniqueModels.has(id))
370
+ continue;
371
+ uniqueModels.set(id, {
367
372
  contextLength: typeof model.context_length === 'number' ? model.context_length : 128_000,
368
373
  id,
369
374
  isFree: false,
370
375
  name: id,
371
376
  pricing: { inputPerM: 0, outputPerM: 0 },
372
377
  provider: this.providerName,
373
- };
374
- });
378
+ });
379
+ }
380
+ const models = [...uniqueModels.values()];
375
381
  // Sort by ID
376
382
  models.sort((a, b) => a.id.localeCompare(b.id));
377
383
  this.cache = { models, timestamp: Date.now() };
@@ -9,10 +9,12 @@ import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-ke
9
9
  import type { IProviderOAuthTokenStore } from '../../core/interfaces/i-provider-oauth-token-store.js';
10
10
  import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
11
11
  import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
12
+ import type { IBillingConfigStore } from '../../core/interfaces/storage/i-billing-config-store.js';
12
13
  import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
13
14
  import type { ProjectBroadcaster, ProjectPathResolver } from '../transport/handlers/handler-types.js';
14
15
  export interface FeatureHandlersOptions {
15
16
  authStateStore: IAuthStateStore;
17
+ billingConfigStoreFactory: (projectPath: string) => IBillingConfigStore;
16
18
  broadcastToProject: ProjectBroadcaster;
17
19
  getActiveProjectPaths: () => string[];
18
20
  log: (msg: string) => void;
@@ -28,4 +30,4 @@ export interface FeatureHandlersOptions {
28
30
  * Setup all feature handlers on the transport server.
29
31
  * These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
30
32
  */
31
- export declare function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }: FeatureHandlersOptions): Promise<void>;
33
+ export declare function setupFeatureHandlers({ authStateStore, billingConfigStoreFactory, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }: FeatureHandlersOptions): Promise<void>;
@@ -10,9 +10,12 @@ 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
12
  import { API_V1_PATH, BRV_DIR } from '../../constants.js';
13
+ import { TransportStateEventNames } from '../../core/domain/transport/schemas.js';
13
14
  import { getProjectDataDir } from '../../utils/path-utils.js';
14
15
  import { OAuthService } from '../auth/oauth-service.js';
15
16
  import { OidcDiscoveryService } from '../auth/oidc-discovery-service.js';
17
+ import { HttpBillingService } from '../billing/http-billing-service.js';
18
+ import { createPaidOrganizationsHandler } from '../billing/paid-organizations-endpoint.js';
16
19
  import { SystemBrowserLauncher } from '../browser/system-browser-launcher.js';
17
20
  import { HttpCogitPullService } from '../cogit/http-cogit-pull-service.js';
18
21
  import { HttpCogitPushService } from '../cogit/http-cogit-push-service.js';
@@ -37,23 +40,25 @@ import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
37
40
  import { createTokenStore } from '../storage/token-store.js';
38
41
  import { HttpTeamService } from '../team/http-team-service.js';
39
42
  import { FsTemplateLoader } from '../template/fs-template-loader.js';
40
- import { AuthHandler, ConfigHandler, ConnectorsHandler, ContextTreeHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
43
+ import { AuthHandler, BillingHandler, ConfigHandler, ConnectorsHandler, ContextTreeHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, TeamHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
41
44
  import { HttpUserService } from '../user/http-user-service.js';
42
45
  import { FileVcGitConfigStore } from '../vc/file-vc-git-config-store.js';
43
46
  /**
44
47
  * Setup all feature handlers on the transport server.
45
48
  * These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
46
49
  */
47
- export async function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }) {
50
+ export async function setupFeatureHandlers({ authStateStore, billingConfigStoreFactory, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }) {
48
51
  const envConfig = getCurrentConfig();
49
52
  const tokenStore = createTokenStore();
50
53
  const projectConfigStore = new ProjectConfigStore();
51
54
  // API version paths appended at point of use.
52
55
  // Note: IAM and Cogit currently share this version path, but may version independently in the future.
53
56
  const iamApiV1 = `${envConfig.iamBaseUrl}${API_V1_PATH}`;
57
+ const billingApiV1 = `${envConfig.billingBaseUrl}${API_V1_PATH}`;
54
58
  const userService = new HttpUserService({ apiBaseUrl: iamApiV1 });
55
59
  const teamService = new HttpTeamService({ apiBaseUrl: iamApiV1 });
56
60
  const spaceService = new HttpSpaceService({ apiBaseUrl: iamApiV1 });
61
+ const billingService = new HttpBillingService({ apiBaseUrl: billingApiV1 });
57
62
  // Auth handler requires async OIDC discovery
58
63
  const discoveryService = new OidcDiscoveryService();
59
64
  const authConfig = await getAuthConfig(discoveryService);
@@ -65,6 +70,7 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
65
70
  browserLauncher: new SystemBrowserLauncher(),
66
71
  callbackHandler: new CallbackHandler(),
67
72
  projectConfigStore,
73
+ providerConfigStore,
68
74
  resolveProjectPath,
69
75
  tokenStore,
70
76
  transport,
@@ -78,6 +84,20 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
78
84
  providerOAuthTokenStore,
79
85
  transport,
80
86
  }).setup();
87
+ new BillingHandler({
88
+ authStateStore,
89
+ billingConfigStoreFactory,
90
+ billingService,
91
+ providerConfigStore,
92
+ resolveProjectPath,
93
+ transport,
94
+ }).setup();
95
+ transport.onRequest(TransportStateEventNames.GET_PAID_ORGANIZATIONS, createPaidOrganizationsHandler({ authStateStore, billingService }));
96
+ new TeamHandler({
97
+ authStateStore,
98
+ teamService,
99
+ transport,
100
+ }).setup();
81
101
  new ModelHandler({
82
102
  providerConfigStore,
83
103
  providerKeychainStore,
@@ -100,10 +120,14 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
100
120
  // Project-scoped handlers (receive resolveProjectPath for client → project resolution)
101
121
  const gitService = new IsomorphicGitService(authStateStore);
102
122
  new StatusHandler({
123
+ authStateStore,
124
+ billingConfigStoreFactory,
125
+ billingService,
103
126
  contextTreeService,
104
127
  contextTreeSnapshotService,
105
128
  curateLogStoreFactory: (projectPath) => new FileCurateLogStore({ baseDir: getProjectDataDir(projectPath) }),
106
129
  projectConfigStore,
130
+ providerConfigStore,
107
131
  resolveProjectPath,
108
132
  tokenStore,
109
133
  transport,