@magic-ingredients/tiny-brain-local 0.18.1 → 0.19.1

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.
@@ -58,6 +58,11 @@ export declare class RepoService {
58
58
  * - Operational Tracking Directory (when SDD enabled)
59
59
  */
60
60
  writeWorkflowSections(options?: WriteWorkflowSectionsOptions): Promise<void>;
61
+ /**
62
+ * Format the Repository Context section
63
+ * When agentic coding is enabled, includes a mandatory agent delegation section
64
+ */
65
+ private formatRepoContextSection;
61
66
  /**
62
67
  * Format the Commit Message Format section
63
68
  */
@@ -1 +1 @@
1
- {"version":3,"file":"repo-service.d.ts","sourceRoot":"","sources":["../../src/services/repo-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIlE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,WAAW;IAMV,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA2B;IACnE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAyB;IAE/D,OAAO,CAAC,aAAa,CAAgB;gBAEjB,OAAO,EAAE,cAAc;IAS3C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACG,4BAA4B,CAChC,eAAe,GAAE,MAAoB,GACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmCzB;;;;OAIG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,cAAc,IAAI,OAAO;IAUzB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAcjD;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAa5B;;;;;;;;OAQG;IACG,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwFtF;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA4GlC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAoChC;;OAEG;IACH,OAAO,CAAC,6BAA6B;IAwErC;;OAEG;IACH,OAAO,CAAC,gCAAgC;CAqCzC"}
1
+ {"version":3,"file":"repo-service.d.ts","sourceRoot":"","sources":["../../src/services/repo-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIlE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,WAAW;IAMV,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA2B;IACnE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAyB;IAE/D,OAAO,CAAC,aAAa,CAAgB;gBAEjB,OAAO,EAAE,cAAc;IAS3C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACG,4BAA4B,CAChC,eAAe,GAAE,MAAoB,GACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmCzB;;;;OAIG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,cAAc,IAAI,OAAO;IAUzB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAcjD;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAa5B;;;;;;;;OAQG;IACG,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IA6FtF;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAyBhC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA4GlC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAoChC;;OAEG;IACH,OAAO,CAAC,6BAA6B;IAwErC;;OAEG;IACH,OAAO,CAAC,gCAAgC;CAyCzC"}
@@ -147,12 +147,15 @@ export class RepoService {
147
147
  // Check config flags
148
148
  const enableSDD = await this.configService.isSDDEnabled();
149
149
  const enableTDD = await this.configService.isTDDEnabled();
150
+ const enableAgentic = await this.configService.isAgenticCodingEnabled();
150
151
  // If nothing is enabled, don't write anything
151
152
  if (!enableSDD && !enableTDD) {
152
153
  return;
153
154
  }
154
155
  // Build the workflow sections content
155
156
  let workflowContent = '';
157
+ // Repository Context (always when workflow sections are enabled)
158
+ workflowContent += this.formatRepoContextSection(enableAgentic);
156
159
  // Commit Message Format (when SDD enabled)
157
160
  if (enableSDD) {
158
161
  workflowContent += this.formatCommitMessageSection();
@@ -169,8 +172,9 @@ export class RepoService {
169
172
  if (enableSDD) {
170
173
  workflowContent += this.formatOperationalTrackingSection();
171
174
  }
172
- // Build the complete tiny-brain block
173
- const repoBlock = `${RepoService.REPO_BLOCK_START}\n\n${workflowContent}${RepoService.REPO_BLOCK_END}\n`;
175
+ // Build the complete tiny-brain block with version frontmatter
176
+ const version = this.getAnalysisVersion();
177
+ const repoBlock = `${RepoService.REPO_BLOCK_START}\n---\nversion: ${version}\n---\n\n${workflowContent}${RepoService.REPO_BLOCK_END}\n`;
174
178
  // Get full path
175
179
  const repoRoot = getRepoRoot();
176
180
  const fullPath = path.join(repoRoot, contextPath);
@@ -218,6 +222,32 @@ export class RepoService {
218
222
  await fs.writeFile(fullPath, existingContent, 'utf-8');
219
223
  this.context.logger.info(`Updated workflow sections in ${contextPath}`);
220
224
  }
225
+ /**
226
+ * Format the Repository Context section
227
+ * When agentic coding is enabled, includes a mandatory agent delegation section
228
+ */
229
+ formatRepoContextSection(enableAgenticCoding) {
230
+ let content = `## Repository Context
231
+
232
+ Before starting work, read \`.tiny-brain/analysis.json\` for the detected tech stack, test patterns, and documentation locations.
233
+
234
+ `;
235
+ if (enableAgenticCoding) {
236
+ content += `## Agent Delegation (MANDATORY)
237
+
238
+ IMPORTANT: You MUST delegate implementation work to specialized agents using the Task tool. Do NOT write production code or tests directly.
239
+
240
+ When the user asks you to implement a feature, fix a bug, or refactor code:
241
+ 1. Use the Task tool with \`subagent_type="tiny-brain:developer"\`
242
+ 2. Provide the full task context in the prompt
243
+ 3. Let the agent handle the TDD workflow (red/green/refactor)
244
+
245
+ Only skip delegation for trivial tasks (single-line fixes, config changes).
246
+
247
+ `;
248
+ }
249
+ return content;
250
+ }
221
251
  /**
222
252
  * Format the Commit Message Format section
223
253
  */
@@ -450,6 +480,10 @@ The \`.tiny-brain/\` directory stores operational tracking data separate from do
450
480
 
451
481
  \`\`\`
452
482
  .tiny-brain/
483
+ ├── analysis.json # Detected tech stack and repo analysis
484
+ ├── tech/ # Technology-specific context files
485
+ │ ├── config.json # Tech context mode configuration
486
+ │ └── {name}.md # One file per detected technology
453
487
  ├── progress/ # PRD progress tracking
454
488
  │ └── {prd-id}.json # One file per PRD
455
489
  └── fixes/ # Fix progress tracking
@@ -37,6 +37,10 @@ export declare class AsTool {
37
37
  * Check if agents are installed by checking the platform-specific agents directory
38
38
  */
39
39
  private static checkHasAgents;
40
+ /**
41
+ * Resolve feature flags from ConfigService
42
+ */
43
+ private static resolveFeatureFlags;
40
44
  /**
41
45
  * Build persona response data structure
42
46
  */
@@ -1 +1 @@
1
- {"version":3,"file":"as.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/persona/as.tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAWrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAqCrE;;;;;;;;;GASG;AACH,qBAAa,MAAM;IACjB,MAAM,CAAC,iBAAiB,IAAI,OAAO;IAqDnC;;OAEG;WACU,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;IA4BvF;;OAEG;mBACkB,wBAAwB;IAmE7C;;OAEG;mBACkB,mBAAmB;IA2ExC;;OAEG;mBACkB,oBAAoB;IAmGzC;;OAEG;mBACkB,eAAe;IAsBpC;;OAEG;mBACkB,cAAc;IAWnC;;OAEG;mBACkB,aAAa;IA6DlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IA8H7B;;OAEG;mBACkB,mBAAmB;mBAyDnB,qBAAqB;CAsC3C"}
1
+ {"version":3,"file":"as.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/persona/as.tool.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAYrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAyCrE;;;;;;;;;GASG;AACH,qBAAa,MAAM;IACjB,MAAM,CAAC,iBAAiB,IAAI,OAAO;IAqDnC;;OAEG;WACU,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC;IA4BvF;;OAEG;mBACkB,wBAAwB;IAsE7C;;OAEG;mBACkB,mBAAmB;IA8ExC;;OAEG;mBACkB,oBAAoB;IAsGzC;;OAEG;mBACkB,eAAe;IAsBpC;;OAEG;mBACkB,cAAc;IAWnC;;OAEG;mBACkB,mBAAmB;IAexC;;OAEG;mBACkB,aAAa;IA6DlC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IA8H7B;;OAEG;mBACkB,mBAAmB;mBA4DnB,qBAAqB;CAsC3C"}
@@ -1,12 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createErrorResult } from '../index.js';
3
- import { createDefaultProfile, PlanningService, RulesService, LibraryClient, parsePersonaMarkdown } from '@magic-ingredients/tiny-brain-core';
3
+ import { createDefaultProfile, PlanningService, RulesService, LibraryClient, ConfigService, parsePersonaMarkdown } from '@magic-ingredients/tiny-brain-core';
4
4
  import { PersonaService } from '../../services/persona-service.js';
5
5
  import { RepoService } from '../../services/repo-service.js';
6
6
  import { AgentService } from '../../services/agent-service.js';
7
- import { existsSync } from 'fs';
8
- import { join } from 'path';
9
- import { getRepoRoot } from '../../utils/repo-utils.js';
10
7
  const AsArgsSchema = z.object({
11
8
  personaName: z.string().optional(),
12
9
  confirmCreate: z.boolean().optional(),
@@ -147,11 +144,14 @@ export class AsTool {
147
144
  const formatter = agentService.getDefaultFormatter();
148
145
  const contextFilePath = formatter.getRepoContextFilePath();
149
146
  const response = await AsTool.buildResponse(context, parsedPersona, contextFilePath);
150
- const hasAgents = await AsTool.checkHasAgents(context, contextFilePath);
147
+ const [hasAgents, featureFlags] = await Promise.all([
148
+ AsTool.checkHasAgents(context, contextFilePath),
149
+ AsTool.resolveFeatureFlags(context),
150
+ ]);
151
151
  return {
152
152
  content: [{
153
153
  type: 'text',
154
- text: AsTool.formatResponse(response, contextFilePath, hasAgents),
154
+ text: AsTool.formatResponse(response, contextFilePath, hasAgents, featureFlags, context.libraryAuth),
155
155
  }],
156
156
  isError: false,
157
157
  };
@@ -201,11 +201,14 @@ export class AsTool {
201
201
  // Activate persona and return response with updated context
202
202
  const updatedContext = await AsTool.activatePersona(personaName, profile, context);
203
203
  const response = await AsTool.buildResponse(updatedContext, parsedPersona, contextFilePath);
204
- const hasAgents = await AsTool.checkHasAgents(updatedContext, contextFilePath);
204
+ const [hasAgents, featureFlags] = await Promise.all([
205
+ AsTool.checkHasAgents(updatedContext, contextFilePath),
206
+ AsTool.resolveFeatureFlags(updatedContext),
207
+ ]);
205
208
  return {
206
209
  content: [{
207
210
  type: 'text',
208
- text: AsTool.formatResponse(response, contextFilePath, hasAgents),
211
+ text: AsTool.formatResponse(response, contextFilePath, hasAgents, featureFlags, updatedContext.libraryAuth),
209
212
  }],
210
213
  isError: false,
211
214
  };
@@ -280,11 +283,14 @@ export class AsTool {
280
283
  const contextFilePath = formatter?.getRepoContextFilePath();
281
284
  // Step 8: Build and return response
282
285
  const response = await AsTool.buildResponse(updatedContext, parsedPersona, contextFilePath);
283
- const hasAgents = await AsTool.checkHasAgents(updatedContext, contextFilePath);
286
+ const [hasAgents, featureFlags] = await Promise.all([
287
+ AsTool.checkHasAgents(updatedContext, contextFilePath),
288
+ AsTool.resolveFeatureFlags(updatedContext),
289
+ ]);
284
290
  return {
285
291
  content: [{
286
292
  type: 'text',
287
- text: AsTool.formatResponse(response, contextFilePath, hasAgents) + needsInitMessage,
293
+ text: AsTool.formatResponse(response, contextFilePath, hasAgents, featureFlags, updatedContext.libraryAuth) + needsInitMessage,
288
294
  }],
289
295
  isError: false,
290
296
  };
@@ -327,6 +333,24 @@ export class AsTool {
327
333
  return false;
328
334
  }
329
335
  }
336
+ /**
337
+ * Resolve feature flags from ConfigService
338
+ */
339
+ static async resolveFeatureFlags(context) {
340
+ try {
341
+ const configService = new ConfigService(context);
342
+ const [sddEnabled, tddEnabled, adrEnabled, agenticCodingEnabled] = await Promise.all([
343
+ configService.isSDDEnabled(),
344
+ configService.isTDDEnabled(),
345
+ configService.isADREnabled(),
346
+ configService.isAgenticCodingEnabled(),
347
+ ]);
348
+ return { sddEnabled, tddEnabled, adrEnabled, agenticCodingEnabled };
349
+ }
350
+ catch {
351
+ return { sddEnabled: false, tddEnabled: false, adrEnabled: false, agenticCodingEnabled: false };
352
+ }
353
+ }
330
354
  /**
331
355
  * Build persona response data structure
332
356
  */
@@ -385,7 +409,7 @@ export class AsTool {
385
409
  /**
386
410
  * Format the persona response as human-readable markdown with enforced workflow
387
411
  */
388
- static formatResponse(response, contextFilePath, hasAgents) {
412
+ static formatResponse(response, contextFilePath, hasAgents, featureFlags, libraryAuth) {
389
413
  let formatted = `# THIS IS YOUR CONTEXT FOR THIS SESSION - YOU MUST USE IT\n\n`;
390
414
  formatted += `IMPORTANT: USER RULES and USER DETAILS ALWAYS take precedence over SYSTEM RULES and SYSTEM DETAILS. When there are conflicts, follow the user's preferences.\n\n`;
391
415
  formatted += `## PERSONA: ${response.PERSONA.name}\n\n`;
@@ -461,33 +485,32 @@ export class AsTool {
461
485
  formatted += `**EXCEPTION**: Only skip agents if you can explicitly justify why none apply\n\n`;
462
486
  formatted += `**MANDATORY CONFIRMATION:** When ready to proceed, you MUST respond with exactly this format (copy it exactly, do not modify):\n\n`;
463
487
  formatted += `\`\`\`\n`;
488
+ formatted += ` 🧠 tiny brain\n\n`;
464
489
  formatted += ` ✅ Received context for persona\n`;
465
490
  formatted += ` ✅ Re-read ${contextFilePath}\n`;
466
- // Check for PRD and ADR directories and add checklist items (indented by 2 spaces)
467
- try {
468
- const repoRoot = getRepoRoot();
469
- const hasPRD = existsSync(join(repoRoot, 'docs', 'prd'));
470
- const hasADR = existsSync(join(repoRoot, 'docs', 'adr'));
471
- if (hasPRD) {
472
- formatted += ` ✅ Enabled Spec Driven Design workflow with Test Driven Development\n`;
473
- }
474
- if (hasADR) {
475
- formatted += ` ✅ Enabled Architecture Decision Records\n`;
476
- }
491
+ if (featureFlags?.sddEnabled) {
492
+ formatted += ` ✅ Enabled Spec Driven Design workflow with Test Driven Development\n`;
493
+ }
494
+ if (featureFlags?.adrEnabled) {
495
+ formatted += ` ✅ Enabled Architecture Decision Records\n`;
496
+ }
497
+ if (featureFlags?.agenticCodingEnabled) {
498
+ formatted += ` ✅ Enabled agentic coding\n`;
477
499
  }
478
- catch {
479
- // Not in a repo, skip PRD/ADR checklist items
500
+ if (libraryAuth?.token) {
501
+ formatted += ` 🔑 Connected to Tiny Brain Remote\n`;
502
+ }
503
+ if (libraryAuth?.hasLlmApiKey) {
504
+ formatted += ` 🔑 LLM API key configured\n`;
480
505
  }
481
506
  // Only show pre-flight and agent-first workflow if agents are installed
482
507
  if (hasAgents) {
483
508
  formatted += ` ✅ Completed pre-flight checklist\n`;
484
509
  formatted += ` ✅ Ready for agent-first workflow\n`;
485
510
  }
486
- formatted += ` \n\n`;
487
- formatted += ` I've switched to your **${response.PERSONA.name}** persona.\n`;
488
- formatted += ` \n`;
489
- formatted += ` 🧠 Dashboard available at: [http://localhost:8765](http://localhost:8765)\n`;
490
511
  formatted += `\`\`\`\n\n`;
512
+ formatted += `I've switched to your **${response.PERSONA.name}** persona.\n\n`;
513
+ formatted += `🧠 Dashboard available at: [http://localhost:8765](http://localhost:8765)\n`;
491
514
  }
492
515
  else {
493
516
  // Add persona switch confirmation message when no context file
@@ -524,11 +547,14 @@ export class AsTool {
524
547
  context.logger.info(`Renamed persona '${oldName}' to '${newName}'`);
525
548
  // Return response showing the renamed persona is now active with updated context
526
549
  const response = await AsTool.buildResponse(updatedContext, parsedPersona, contextFilePath);
527
- const hasAgents = await AsTool.checkHasAgents(updatedContext, contextFilePath);
550
+ const [hasAgents, featureFlags] = await Promise.all([
551
+ AsTool.checkHasAgents(updatedContext, contextFilePath),
552
+ AsTool.resolveFeatureFlags(updatedContext),
553
+ ]);
528
554
  return {
529
555
  content: [{
530
556
  type: 'text',
531
- text: AsTool.formatResponse(response, contextFilePath, hasAgents),
557
+ text: AsTool.formatResponse(response, contextFilePath, hasAgents, featureFlags, updatedContext.libraryAuth),
532
558
  }],
533
559
  isError: false,
534
560
  };
@@ -16,5 +16,12 @@ export declare class QualityTool {
16
16
  private static handleSave;
17
17
  private static handleHistory;
18
18
  private static handleDetails;
19
+ private static handleCompare;
20
+ private static handlePlan;
21
+ private static handlePlanDetails;
22
+ private static handleDetectAnalyzers;
23
+ private static handleRunAnalyzers;
24
+ private static handleAssembleRun;
25
+ private static handleMergeResults;
19
26
  }
20
27
  //# sourceMappingURL=quality.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quality.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/quality/quality.tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAiCrE;;GAEG;AACH,qBAAa,WAAW;IACtB,MAAM,CAAC,iBAAiB,IAAI,OAAO;WA4EtB,OAAO,CAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC;mBAgCD,UAAU;mBAqCV,aAAa;mBA2Bb,aAAa;CA0EnC"}
1
+ {"version":3,"file":"quality.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/quality/quality.tool.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAyDrE;;GAEG;AACH,qBAAa,WAAW;IACtB,MAAM,CAAC,iBAAiB,IAAI,OAAO;WA+KtB,OAAO,CAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC;mBA8CD,UAAU;mBAqCV,aAAa;mBA2Bb,aAAa;mBA2Eb,aAAa;mBAsEb,UAAU;mBA0CV,iBAAiB;mBAgEjB,qBAAqB;mBA6BrB,kBAAkB;mBA+ClB,iBAAiB;IA8CtC,OAAO,CAAC,MAAM,CAAC,kBAAkB;CAoBlC"}
@@ -5,13 +5,18 @@
5
5
  * Analysis is performed by agents; this tool only handles persistence.
6
6
  */
7
7
  import { z } from 'zod';
8
+ import { promises as fs } from 'fs';
9
+ import path from 'path';
8
10
  import { createSuccessResult, createErrorResult } from '../index.js';
9
- import { QualityService, QualityIssueSchema, SaveQualityRunInputSchema, } from '@magic-ingredients/tiny-brain-core';
11
+ import { QualityService, AnalyzerDetectionService, AnalyzerExecutorService, AssemblyService, mergeResults, QualityIssueSchema, SaveQualityRunInputSchema, generateRunId, runIdToPath, } from '@magic-ingredients/tiny-brain-core';
10
12
  /**
11
13
  * Zod schema for QualityTool arguments
12
14
  */
13
15
  const QualityArgsSchema = z.object({
14
- operation: z.enum(['save', 'history', 'details']),
16
+ operation: z.enum([
17
+ 'save', 'history', 'details', 'compare', 'plan', 'plan-details',
18
+ 'detect-analyzers', 'run-analyzers', 'merge-results', 'assemble-run',
19
+ ]),
15
20
  // For 'save' operation
16
21
  score: z.number().min(0).max(100).optional(),
17
22
  grade: z.enum(['A', 'B', 'C', 'D', 'F']).optional(),
@@ -28,6 +33,17 @@ const QualityArgsSchema = z.object({
28
33
  limit: z.number().min(1).optional(),
29
34
  // For 'details' operation
30
35
  runId: z.string().optional(),
36
+ // For 'compare' operation
37
+ baseRunId: z.string().optional(),
38
+ targetRunId: z.string().optional(),
39
+ // For 'plan' operation
40
+ sourceRunId: z.string().optional(),
41
+ targetGrade: z.string().optional(),
42
+ // For 'plan-details' operation
43
+ planId: z.string().optional(),
44
+ // For 'merge-results' operation
45
+ analyzerIssues: z.array(QualityIssueSchema).optional(),
46
+ llmIssues: z.array(QualityIssueSchema).optional(),
31
47
  });
32
48
  /**
33
49
  * MCP Tool for quality analysis persistence
@@ -44,18 +60,29 @@ Persists and retrieves quality analysis results. Analysis is performed by agents
44
60
  • save: Persist a quality run to docs/quality/runs/
45
61
  • history: List previous quality runs with summary data
46
62
  • details: Get full details for a specific run by ID
63
+ • compare: Compare two runs to see new, resolved, and persistent issues
64
+ • plan: Generate a Quality Improvement Plan from a saved run
65
+ • plan-details: Retrieve a saved Quality Improvement Plan by ID
66
+ • detect-analyzers: Scan repo for configured static analyzers
67
+ • run-analyzers: Execute detected analyzers and return normalized issues
68
+ • merge-results: Merge analyzer + LLM issues with deduplication
69
+ • assemble-run: Read all intermediate files, merge, score, and save final report
47
70
 
48
71
  💡 WORKFLOW:
49
72
  1. Quality-coordinator agent performs analysis
50
73
  2. Agent calls quality save with score, grade, issues, recommendations
51
74
  3. Results stored as markdown in docs/quality/runs/YYYY-MM-DD-quality.md
52
- 4. Use history/details to retrieve past runs`,
75
+ 4. Use history/details to retrieve past runs
76
+ 5. Use plan to generate an improvement roadmap from a run`,
53
77
  inputSchema: {
54
78
  type: 'object',
55
79
  properties: {
56
80
  operation: {
57
81
  type: 'string',
58
- enum: ['save', 'history', 'details'],
82
+ enum: [
83
+ 'save', 'history', 'details', 'compare', 'plan', 'plan-details',
84
+ 'detect-analyzers', 'run-analyzers', 'merge-results', 'assemble-run',
85
+ ],
59
86
  description: 'The quality operation to perform',
60
87
  },
61
88
  // For 'save' operation
@@ -80,6 +107,42 @@ Persists and retrieves quality analysis results. Analysis is performed by agents
80
107
  line: { type: 'number' },
81
108
  message: { type: 'string' },
82
109
  suggestion: { type: 'string' },
110
+ effort: {
111
+ type: 'string',
112
+ enum: ['trivial', 'small', 'medium', 'large', 'epic'],
113
+ description: 'Estimated effort to remediate',
114
+ },
115
+ impact: {
116
+ type: 'string',
117
+ enum: ['critical', 'high', 'medium', 'low'],
118
+ description: 'Impact level if this issue is not addressed',
119
+ },
120
+ theme: {
121
+ type: 'string',
122
+ description: 'Thematic grouping tag (e.g., "type-safety", "auth-hardening")',
123
+ },
124
+ evidence: {
125
+ type: 'string',
126
+ description: 'Code snippet or evidence showing the problem',
127
+ },
128
+ relatedIssues: {
129
+ type: 'array',
130
+ items: { type: 'number' },
131
+ description: 'Indices of related issues in the same run',
132
+ },
133
+ references: {
134
+ type: 'array',
135
+ items: { type: 'string' },
136
+ description: 'External references such as CWE or OWASP identifiers',
137
+ },
138
+ scoreImpact: {
139
+ type: 'number',
140
+ description: 'Points that would be gained if this issue were fixed',
141
+ },
142
+ effortHours: {
143
+ type: 'number',
144
+ description: 'Estimated hours to remediate',
145
+ },
83
146
  },
84
147
  required: ['category', 'severity', 'file', 'message'],
85
148
  },
@@ -103,6 +166,58 @@ Persists and retrieves quality analysis results. Analysis is performed by agents
103
166
  type: 'string',
104
167
  description: 'Run ID to get details for (required for details)',
105
168
  },
169
+ // For 'compare' operation
170
+ baseRunId: {
171
+ type: 'string',
172
+ description: 'Base run ID for comparison (required for compare)',
173
+ },
174
+ targetRunId: {
175
+ type: 'string',
176
+ description: 'Target run ID for comparison (required for compare)',
177
+ },
178
+ // For 'plan' operation
179
+ sourceRunId: {
180
+ type: 'string',
181
+ description: 'Source run ID to generate improvement plan from (required for plan)',
182
+ },
183
+ targetGrade: {
184
+ type: 'string',
185
+ description: 'Target grade to aim for in the improvement plan (optional for plan)',
186
+ },
187
+ // For 'plan-details' operation
188
+ planId: {
189
+ type: 'string',
190
+ description: 'Plan ID to retrieve details for (required for plan-details)',
191
+ },
192
+ // For 'merge-results' operation
193
+ analyzerIssues: {
194
+ type: 'array',
195
+ description: 'Array of analyzer issues (required for merge-results)',
196
+ items: {
197
+ type: 'object',
198
+ properties: {
199
+ category: { type: 'string' },
200
+ severity: { type: 'string' },
201
+ file: { type: 'string' },
202
+ message: { type: 'string' },
203
+ },
204
+ required: ['category', 'severity', 'file', 'message'],
205
+ },
206
+ },
207
+ llmIssues: {
208
+ type: 'array',
209
+ description: 'Array of LLM investigation issues (required for merge-results)',
210
+ items: {
211
+ type: 'object',
212
+ properties: {
213
+ category: { type: 'string' },
214
+ severity: { type: 'string' },
215
+ file: { type: 'string' },
216
+ message: { type: 'string' },
217
+ },
218
+ required: ['category', 'severity', 'file', 'message'],
219
+ },
220
+ },
106
221
  },
107
222
  required: ['operation'],
108
223
  },
@@ -123,6 +238,20 @@ Persists and retrieves quality analysis results. Analysis is performed by agents
123
238
  return await QualityTool.handleHistory(validatedArgs, service);
124
239
  case 'details':
125
240
  return await QualityTool.handleDetails(validatedArgs, service);
241
+ case 'compare':
242
+ return await QualityTool.handleCompare(validatedArgs, service);
243
+ case 'plan':
244
+ return await QualityTool.handlePlan(validatedArgs, service);
245
+ case 'plan-details':
246
+ return await QualityTool.handlePlanDetails(validatedArgs, context.repositoryRoot);
247
+ case 'detect-analyzers':
248
+ return await QualityTool.handleDetectAnalyzers(context.repositoryRoot);
249
+ case 'run-analyzers':
250
+ return await QualityTool.handleRunAnalyzers(context.repositoryRoot);
251
+ case 'merge-results':
252
+ return QualityTool.handleMergeResults(validatedArgs);
253
+ case 'assemble-run':
254
+ return await QualityTool.handleAssembleRun(validatedArgs, context.repositoryRoot);
126
255
  default:
127
256
  return createErrorResult(`Unknown operation: ${validatedArgs.operation}`);
128
257
  }
@@ -243,4 +372,245 @@ Persists and retrieves quality analysis results. Analysis is performed by agents
243
372
  }
244
373
  return createSuccessResult(lines.join('\n'));
245
374
  }
375
+ static async handleCompare(args, service) {
376
+ if (!args.baseRunId) {
377
+ return createErrorResult('baseRunId is required for compare operation');
378
+ }
379
+ if (!args.targetRunId) {
380
+ return createErrorResult('targetRunId is required for compare operation');
381
+ }
382
+ const comparison = await service.compareRuns(args.baseRunId, args.targetRunId);
383
+ if (!comparison) {
384
+ return createErrorResult(`One or both runs not found: ${args.baseRunId}, ${args.targetRunId}`);
385
+ }
386
+ const deltaSign = comparison.scoreDelta >= 0 ? '+' : '';
387
+ const lines = [
388
+ `📊 Run Comparison: ${comparison.baseRunId} → ${comparison.targetRunId}`,
389
+ '',
390
+ '## Summary',
391
+ `**Score Delta:** ${deltaSign}${comparison.scoreDelta}`,
392
+ `**Grade Change:** ${comparison.gradeChange.from} → ${comparison.gradeChange.to}`,
393
+ `**New Issues:** ${comparison.newIssues.length}`,
394
+ `**Resolved Issues:** ${comparison.resolvedIssues.length}`,
395
+ `**Persistent Issues:** ${comparison.persistentIssues.length}`,
396
+ '',
397
+ ];
398
+ // Category changes
399
+ const categoryEntries = Object.entries(comparison.categoryChanges);
400
+ if (categoryEntries.length > 0) {
401
+ lines.push('## Category Changes');
402
+ lines.push('');
403
+ lines.push('| Category | Count Delta | Deduction Delta |');
404
+ lines.push('|----------|-------------|-----------------|');
405
+ for (const [category, changes] of categoryEntries) {
406
+ const countSign = changes.countDelta >= 0 ? '+' : '';
407
+ const deductionSign = changes.deductionDelta >= 0 ? '+' : '';
408
+ lines.push(`| ${category} | ${countSign}${changes.countDelta} | ${deductionSign}${changes.deductionDelta.toFixed(1)} |`);
409
+ }
410
+ lines.push('');
411
+ }
412
+ // New issues
413
+ if (comparison.newIssues.length > 0) {
414
+ lines.push('## New Issues');
415
+ for (const issue of comparison.newIssues) {
416
+ lines.push(`- **[${issue.severity}]** ${issue.message} (${issue.file})`);
417
+ }
418
+ lines.push('');
419
+ }
420
+ // Resolved issues
421
+ if (comparison.resolvedIssues.length > 0) {
422
+ lines.push('## Resolved Issues');
423
+ for (const issue of comparison.resolvedIssues) {
424
+ lines.push(`- **[${issue.severity}]** ${issue.message} (${issue.file})`);
425
+ }
426
+ lines.push('');
427
+ }
428
+ return createSuccessResult(lines.join('\n'));
429
+ }
430
+ static async handlePlan(args, service) {
431
+ if (!args.sourceRunId) {
432
+ return createErrorResult('sourceRunId is required for plan operation');
433
+ }
434
+ const run = await service.getRunDetails(args.sourceRunId);
435
+ if (!run) {
436
+ return createErrorResult(`Quality run not found: ${args.sourceRunId}`);
437
+ }
438
+ const plan = QualityService.generateImprovementPlan(run, args.targetGrade);
439
+ const result = await service.saveImprovementPlan(plan);
440
+ const totalInitiatives = plan.phases.reduce((sum, phase) => sum + phase.initiatives.length, 0);
441
+ const response = [
442
+ '✅ Quality Improvement Plan generated successfully!',
443
+ '',
444
+ `📋 Plan ID: ${result.planId}`,
445
+ `📁 File: ${result.filePath}`,
446
+ `📊 Current Score: ${plan.currentScore}/100 (Grade ${plan.currentGrade})`,
447
+ `🎯 Phases: ${plan.phases.length}`,
448
+ `📦 Initiatives: ${totalInitiatives}`,
449
+ `⏱️ Total Effort: ${plan.totalEffortHours} hours`,
450
+ '',
451
+ '## Executive Summary',
452
+ '',
453
+ plan.executiveSummary,
454
+ '',
455
+ 'Use "quality plan-details planId=<id>" to see full plan details.',
456
+ ].join('\n');
457
+ return createSuccessResult(response);
458
+ }
459
+ static async handlePlanDetails(args, repositoryRoot) {
460
+ if (!args.planId) {
461
+ return createErrorResult('planId is required for plan-details operation');
462
+ }
463
+ try {
464
+ const plansDir = path.join(repositoryRoot, 'docs', 'quality', 'plans');
465
+ const filePath = path.join(plansDir, `${args.planId}.md`);
466
+ const content = await fs.readFile(filePath, 'utf-8');
467
+ // Extract JSON from the Raw Data code block
468
+ const jsonMatch = content.match(/```json\n([\s\S]*?)\n```/);
469
+ if (!jsonMatch) {
470
+ return createErrorResult(`Could not parse plan data from: ${args.planId}`);
471
+ }
472
+ const planData = JSON.parse(jsonMatch[1]);
473
+ const lines = [
474
+ `📋 Quality Improvement Plan: ${planData.planId}`,
475
+ '',
476
+ '## Summary',
477
+ `**Date:** ${planData.date}`,
478
+ `**Source Run:** ${planData.sourceRunId}`,
479
+ `**Current Score:** ${planData.currentScore}/100 (Grade ${planData.currentGrade})`,
480
+ `**Total Effort:** ${planData.totalEffortHours} hours`,
481
+ '',
482
+ '## Executive Summary',
483
+ '',
484
+ planData.executiveSummary,
485
+ '',
486
+ ];
487
+ // Phase summaries
488
+ if (planData.phases && planData.phases.length > 0) {
489
+ lines.push('## Phases');
490
+ lines.push('');
491
+ for (const phase of planData.phases) {
492
+ lines.push(`### Phase ${phase.phase}: ${phase.name}`);
493
+ lines.push(`**Projected Score:** ${phase.projectedScore}/100 (Grade ${phase.projectedGrade})`);
494
+ lines.push(`**Effort:** ${phase.totalEffortHours} hours`);
495
+ lines.push(`**Initiatives:** ${phase.initiatives.length}`);
496
+ lines.push('');
497
+ }
498
+ }
499
+ // ROI
500
+ if (planData.roiAnalysis) {
501
+ lines.push('## ROI Analysis');
502
+ lines.push(`**Estimated Cost:** ${planData.roiAnalysis.currency} ${planData.roiAnalysis.estimatedCostToFix}`);
503
+ lines.push('');
504
+ }
505
+ return createSuccessResult(lines.join('\n'));
506
+ }
507
+ catch {
508
+ return createErrorResult(`Improvement plan not found: ${args.planId}`);
509
+ }
510
+ }
511
+ static async handleDetectAnalyzers(repositoryRoot) {
512
+ const service = new AnalyzerDetectionService(repositoryRoot);
513
+ const analyzers = await service.detectAnalyzers();
514
+ if (analyzers.length === 0) {
515
+ return createSuccessResult('No static analyzers detected in this repository.\n\nSupported analyzers: ESLint, TypeScript, npm audit, RuboCop, ruff.');
516
+ }
517
+ const lines = [
518
+ `🔍 Detected ${analyzers.length} analyzer(s):`,
519
+ '',
520
+ ];
521
+ for (const analyzer of analyzers) {
522
+ lines.push(`- **${analyzer.name}** (${analyzer.analyzerId})`);
523
+ lines.push(` Configs: ${analyzer.configPaths.join(', ')}`);
524
+ lines.push(` Categories: ${analyzer.categories.join(', ')}`);
525
+ }
526
+ lines.push('');
527
+ lines.push(JSON.stringify(analyzers, null, 2));
528
+ return createSuccessResult(lines.join('\n'));
529
+ }
530
+ static async handleRunAnalyzers(repositoryRoot) {
531
+ const runId = generateRunId();
532
+ const runDir = path.join(repositoryRoot, 'docs', 'quality', 'runs', runIdToPath(runId));
533
+ const outputPath = path.join(runDir, 'analysis.json');
534
+ const outputDir = path.join(runDir, 'analysers');
535
+ const detectionService = new AnalyzerDetectionService(repositoryRoot);
536
+ const analyzers = await detectionService.detectAnalyzers();
537
+ const emptyResults = {
538
+ issues: [],
539
+ executions: [],
540
+ totalDurationMs: 0,
541
+ summary: { total: 0, succeeded: 0, failed: 0, timedOut: 0, skipped: 0 },
542
+ };
543
+ if (analyzers.length === 0) {
544
+ await fs.mkdir(runDir, { recursive: true });
545
+ await fs.writeFile(outputPath, JSON.stringify(emptyResults, null, 2), 'utf-8');
546
+ return createSuccessResult(`🔧 No analyzers detected. Empty results written to file.\n Run ID: ${runId}\n Output: ${outputPath}`);
547
+ }
548
+ const executorService = new AnalyzerExecutorService(repositoryRoot);
549
+ const results = await executorService.executeAnalyzers(analyzers, outputDir);
550
+ // Write merged/normalized result to the auto-generated run directory
551
+ await fs.mkdir(runDir, { recursive: true });
552
+ await fs.writeFile(outputPath, JSON.stringify(results, null, 2), 'utf-8');
553
+ const lines = [
554
+ `🔧 Executed ${results.summary.total} analyzer(s):`,
555
+ ` Succeeded: ${results.summary.succeeded}`,
556
+ ` Failed: ${results.summary.failed}`,
557
+ ` Total issues: ${results.issues.length}`,
558
+ ` Duration: ${results.totalDurationMs}ms`,
559
+ ` Run ID: ${runId}`,
560
+ ` Output: ${outputPath}`,
561
+ ` Raw files: ${outputDir}/`,
562
+ ];
563
+ return createSuccessResult(lines.join('\n'));
564
+ }
565
+ static async handleAssembleRun(args, repositoryRoot) {
566
+ if (!args.runId) {
567
+ return createErrorResult('runId is required for assemble-run operation (YYYY-MM-DD date prefix)');
568
+ }
569
+ const assemblyService = new AssemblyService(repositoryRoot);
570
+ const result = await assemblyService.assembleRun(args.runId, args.baseRunId);
571
+ const lines = [
572
+ '✅ Quality run assembled successfully!',
573
+ '',
574
+ `📊 Run ID: ${result.runId}`,
575
+ `📈 Score: ${result.score}/100 (Grade: ${result.grade})`,
576
+ `🔍 Issues: ${result.issueCount}`,
577
+ `📁 File: ${result.filePath}`,
578
+ ];
579
+ if (result.incremental) {
580
+ lines.push('');
581
+ lines.push('### Incremental Analysis');
582
+ lines.push(` Base run: ${result.baseRunId}`);
583
+ lines.push(` Files analyzed: ${result.filesAnalyzed}`);
584
+ lines.push(` Files carried forward: ${result.filesCarriedForward}`);
585
+ }
586
+ lines.push('');
587
+ lines.push('### Source Breakdown');
588
+ lines.push(` Analyzer: ${result.sourceBreakdown.analyzer}`);
589
+ lines.push(` Specialist: ${result.sourceBreakdown.llm}`);
590
+ lines.push(` Total: ${result.sourceBreakdown.total}`);
591
+ const categoryEntries = Object.entries(result.categoryBreakdown);
592
+ if (categoryEntries.length > 0) {
593
+ lines.push('');
594
+ lines.push('### Category Breakdown');
595
+ for (const [category, count] of categoryEntries) {
596
+ lines.push(` ${category}: ${count}`);
597
+ }
598
+ }
599
+ return createSuccessResult(lines.join('\n'));
600
+ }
601
+ static handleMergeResults(args) {
602
+ const analyzerIssues = args.analyzerIssues ?? [];
603
+ const llmIssues = args.llmIssues ?? [];
604
+ const result = mergeResults(analyzerIssues, llmIssues);
605
+ const lines = [
606
+ `🔀 Merged results:`,
607
+ ` Analyzer issues: ${result.sourceBreakdown.analyzer}`,
608
+ ` LLM issues: ${result.sourceBreakdown.llm}`,
609
+ ` Total (deduplicated): ${result.sourceBreakdown.total}`,
610
+ ` Duplicates removed: ${result.duplicatesRemoved}`,
611
+ '',
612
+ JSON.stringify(result, null, 2),
613
+ ];
614
+ return createSuccessResult(lines.join('\n'));
615
+ }
246
616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-ingredients/tiny-brain-local",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
4
4
  "description": "MCP server for Tiny Brain AI assistant",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "dxt:init": "cd dxt && dxt init"
32
32
  },
33
33
  "dependencies": {
34
- "@magic-ingredients/tiny-brain-core": "^0.18.1",
34
+ "@magic-ingredients/tiny-brain-core": "^0.19.1",
35
35
  "@magic-ingredients/tiny-brain-dashboard": "file:../tiny-brain-dashboard",
36
36
  "@modelcontextprotocol/sdk": "^1.0.6",
37
37
  "chalk": "^5.3.0",