@happyvertical/smrt-dev-mcp 0.40.62 → 0.40.64

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.
@@ -4,6 +4,680 @@ import { execFileSync } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
5
  import { MODULE_DOC_HASH_PREFIX, readAgentModuleDocs } from "@happyvertical/smrt-core/knowledge";
6
6
  import { ManifestAdapter, OxcScanner } from "@happyvertical/smrt-scanner";
7
+ //#region src/tool-catalog.ts
8
+ var REVIEW_SKILL_NAME = "smrt-code-review";
9
+ var JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
10
+ function compareToolNames(left, right) {
11
+ return left < right ? -1 : left > right ? 1 : 0;
12
+ }
13
+ /**
14
+ * Stable success/error envelope for development tools. `data` preserves each
15
+ * tool's existing payload exactly; coverage/diagnostics are promoted only when
16
+ * the underlying result actually reports them.
17
+ */
18
+ var DEV_MCP_OUTPUT_SCHEMA = {
19
+ $schema: JSON_SCHEMA_2020_12,
20
+ type: "object",
21
+ additionalProperties: false,
22
+ required: [
23
+ "ok",
24
+ "coverage",
25
+ "diagnostics",
26
+ "data"
27
+ ],
28
+ properties: {
29
+ ok: { type: "boolean" },
30
+ coverage: {
31
+ type: ["object", "null"],
32
+ additionalProperties: true
33
+ },
34
+ diagnostics: {
35
+ type: "array",
36
+ items: {
37
+ type: "object",
38
+ additionalProperties: true
39
+ }
40
+ },
41
+ data: {}
42
+ }
43
+ };
44
+ var TOOLS = [
45
+ {
46
+ name: "generate-smrt-class",
47
+ description: "Generate a complete SMRT class with @smrt() decorator",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ className: {
52
+ type: "string",
53
+ description: "Name of the class (PascalCase)"
54
+ },
55
+ properties: {
56
+ type: "array",
57
+ description: "Array of property definitions",
58
+ items: {
59
+ type: "object",
60
+ properties: {
61
+ name: { type: "string" },
62
+ type: {
63
+ type: "string",
64
+ enum: [
65
+ "text",
66
+ "integer",
67
+ "decimal",
68
+ "boolean",
69
+ "datetime",
70
+ "json"
71
+ ]
72
+ },
73
+ required: { type: "boolean" },
74
+ nullable: { type: "boolean" },
75
+ description: { type: "string" },
76
+ defaultValue: { oneOf: [
77
+ { type: "string" },
78
+ { type: "number" },
79
+ { type: "boolean" },
80
+ { type: "object" },
81
+ { type: "null" }
82
+ ] }
83
+ },
84
+ required: ["name", "type"]
85
+ }
86
+ },
87
+ baseClass: {
88
+ type: "string",
89
+ enum: ["SmrtObject", "SmrtCollection"],
90
+ default: "SmrtObject"
91
+ },
92
+ template: {
93
+ type: "string",
94
+ enum: [
95
+ "basic",
96
+ "global-catalog",
97
+ "optional-catalog",
98
+ "tenant-project-object",
99
+ "tenant-event-log-object",
100
+ "cross-package-reference"
101
+ ],
102
+ default: "basic"
103
+ },
104
+ tableName: { type: "string" },
105
+ conflictColumns: {
106
+ type: "array",
107
+ items: { type: "string" }
108
+ },
109
+ tenantScoped: { oneOf: [{ type: "boolean" }, {
110
+ type: "object",
111
+ properties: {
112
+ mode: {
113
+ type: "string",
114
+ enum: ["required", "optional"]
115
+ },
116
+ field: { type: "string" },
117
+ autoFilter: { type: "boolean" },
118
+ autoPopulate: { type: "boolean" },
119
+ allowSuperAdminBypass: { type: "boolean" }
120
+ }
121
+ }] },
122
+ includeTenantIdField: { type: "boolean" },
123
+ relationships: {
124
+ type: "array",
125
+ items: {
126
+ type: "object",
127
+ properties: {
128
+ name: { type: "string" },
129
+ type: {
130
+ type: "string",
131
+ enum: [
132
+ "foreignKey",
133
+ "crossPackageRef",
134
+ "oneToMany",
135
+ "manyToMany"
136
+ ]
137
+ },
138
+ related: { type: "string" },
139
+ required: { type: "boolean" },
140
+ nullable: { type: "boolean" },
141
+ description: { type: "string" },
142
+ validate: { type: "boolean" },
143
+ foreignKey: { type: "string" },
144
+ through: { type: "string" },
145
+ sourceKey: { type: "string" },
146
+ targetKey: { type: "string" }
147
+ },
148
+ required: [
149
+ "name",
150
+ "type",
151
+ "related"
152
+ ]
153
+ }
154
+ },
155
+ includeCompanionSnippets: {
156
+ type: "boolean",
157
+ default: false
158
+ },
159
+ includeApiConfig: {
160
+ type: "boolean",
161
+ default: true
162
+ },
163
+ includeMcpConfig: {
164
+ type: "boolean",
165
+ default: true
166
+ },
167
+ includeCliConfig: {
168
+ type: "boolean",
169
+ default: true
170
+ }
171
+ },
172
+ required: ["className", "properties"]
173
+ }
174
+ },
175
+ {
176
+ name: "introspect-project",
177
+ description: "Scan a directory for SMRT objects. Returns a compact summary by default; pass detail: \"full\" for field, schema, and method details.",
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {
181
+ directory: {
182
+ type: "string",
183
+ description: "Project directory (default: cwd)"
184
+ },
185
+ manifestPath: {
186
+ type: "string",
187
+ description: "Optional manifest path. Defaults to .smrt/manifest.json, dist/manifest.json, then source scanning."
188
+ },
189
+ detail: {
190
+ type: "string",
191
+ enum: ["summary", "full"],
192
+ default: "summary",
193
+ description: "summary: one compact record per object. full: complete field/schema/method detail (large)."
194
+ },
195
+ maxChars: {
196
+ type: "number",
197
+ description: "Character budget for the `objects` payload. Objects past the budget are omitted and reported under `truncated`. Project metadata and diagnostics are always returned in full, so the serialized response is somewhat larger than this budget."
198
+ },
199
+ includeFields: {
200
+ type: "boolean",
201
+ description: "Include field details (detail: \"full\" only)"
202
+ },
203
+ includeRelationships: {
204
+ type: "boolean",
205
+ description: "Analyze relationships (detail: \"full\" only)"
206
+ },
207
+ includeMethods: {
208
+ type: "boolean",
209
+ description: "Include public method details (detail: \"full\" only)"
210
+ }
211
+ }
212
+ }
213
+ },
214
+ {
215
+ name: "review-smrt-project",
216
+ description: "Advisory ecosystem alignment review for downstream SMRT projects",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ directory: {
221
+ type: "string",
222
+ description: "Project directory (default: cwd)"
223
+ },
224
+ rootDir: {
225
+ type: "string",
226
+ description: "Compatibility alias for directory"
227
+ },
228
+ includeSourceEvidence: {
229
+ type: "boolean",
230
+ description: "Include file and line evidence in findings",
231
+ default: true
232
+ },
233
+ maxFindings: {
234
+ type: "number",
235
+ description: "Optional maximum number of findings to return"
236
+ }
237
+ }
238
+ }
239
+ },
240
+ {
241
+ name: "reflect-knowledge",
242
+ description: "Report deterministic SMRT + HappyVertical SDK knowledge coverage and freshness",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: { rootDir: {
246
+ type: "string",
247
+ description: "Project root directory (default: cwd)"
248
+ } }
249
+ }
250
+ },
251
+ {
252
+ name: "reflect-domain-knowledge",
253
+ description: "Report domain-scoped SMRT knowledge artifacts, SDK packages, and freshness",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ rootDir: { type: "string" },
258
+ scope: {
259
+ type: "string",
260
+ enum: [
261
+ "project",
262
+ "local",
263
+ "package",
264
+ "sdk",
265
+ "installed"
266
+ ],
267
+ default: "project"
268
+ },
269
+ package: { type: "string" }
270
+ }
271
+ }
272
+ },
273
+ {
274
+ name: "check-knowledge-freshness",
275
+ description: "Run deterministic freshness checks for SMRT agent knowledge",
276
+ inputSchema: {
277
+ type: "object",
278
+ properties: {
279
+ rootDir: { type: "string" },
280
+ changed: {
281
+ type: "boolean",
282
+ description: "Limit stale-pattern checks to changed files"
283
+ },
284
+ strict: {
285
+ type: "boolean",
286
+ description: "Treat stale-pattern findings as errors"
287
+ }
288
+ }
289
+ }
290
+ },
291
+ {
292
+ name: "check-domain-knowledge",
293
+ description: "Run deterministic freshness checks for domain knowledge artifacts",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ rootDir: { type: "string" },
298
+ changed: { type: "boolean" },
299
+ strict: { type: "boolean" },
300
+ scope: {
301
+ type: "string",
302
+ enum: [
303
+ "project",
304
+ "local",
305
+ "package",
306
+ "sdk",
307
+ "installed"
308
+ ],
309
+ default: "project"
310
+ },
311
+ package: { type: "string" }
312
+ }
313
+ }
314
+ },
315
+ {
316
+ name: "build-review-context",
317
+ description: "Build model-ready SMRT review context from changed files and optional focus text",
318
+ inputSchema: {
319
+ type: "object",
320
+ properties: {
321
+ rootDir: { type: "string" },
322
+ changedFiles: {
323
+ type: "array",
324
+ items: { type: "string" }
325
+ },
326
+ focus: { type: "string" },
327
+ documentation: { type: "string" },
328
+ detail: {
329
+ type: "string",
330
+ enum: ["summary", "full"],
331
+ default: "summary",
332
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
333
+ }
334
+ }
335
+ }
336
+ },
337
+ {
338
+ name: "build-domain-review-context",
339
+ description: "Build domain-scoped model-ready SMRT review context and prompt bundle",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ rootDir: { type: "string" },
344
+ changedFiles: {
345
+ type: "array",
346
+ items: { type: "string" }
347
+ },
348
+ focus: { type: "string" },
349
+ documentation: { type: "string" },
350
+ scope: {
351
+ type: "string",
352
+ enum: [
353
+ "project",
354
+ "local",
355
+ "package",
356
+ "sdk",
357
+ "installed"
358
+ ],
359
+ default: "project"
360
+ },
361
+ package: { type: "string" },
362
+ detail: {
363
+ type: "string",
364
+ enum: ["summary", "full"],
365
+ default: "summary",
366
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
367
+ }
368
+ }
369
+ }
370
+ },
371
+ {
372
+ name: "smrt-review",
373
+ description: "Return deterministic review findings and/or a reusable model prompt bundle. For a formal downstream review, first call get-agent-skill with { \"name\": \"smrt-code-review\" } or load the smrt-code-review MCP prompt/resource.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ rootDir: { type: "string" },
378
+ changedFiles: {
379
+ type: "array",
380
+ items: { type: "string" }
381
+ },
382
+ focus: { type: "string" },
383
+ documentation: { type: "string" },
384
+ mode: {
385
+ type: "string",
386
+ enum: [
387
+ "findings",
388
+ "prompt-bundle",
389
+ "both"
390
+ ],
391
+ default: "both"
392
+ },
393
+ detail: {
394
+ type: "string",
395
+ enum: ["summary", "full"],
396
+ default: "summary",
397
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
398
+ }
399
+ }
400
+ }
401
+ },
402
+ {
403
+ name: "build-architecture-context",
404
+ description: "Build model-ready SMRT architecture context from an idea or documentation",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {
408
+ rootDir: { type: "string" },
409
+ idea: { type: "string" },
410
+ documentation: { type: "string" },
411
+ focus: { type: "string" },
412
+ detail: {
413
+ type: "string",
414
+ enum: ["summary", "full"],
415
+ default: "summary",
416
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
417
+ }
418
+ }
419
+ }
420
+ },
421
+ {
422
+ name: "build-domain-architecture-context",
423
+ description: "Build domain-scoped model-ready SMRT architecture context and prompt bundle",
424
+ inputSchema: {
425
+ type: "object",
426
+ properties: {
427
+ rootDir: { type: "string" },
428
+ idea: { type: "string" },
429
+ documentation: { type: "string" },
430
+ focus: { type: "string" },
431
+ scope: {
432
+ type: "string",
433
+ enum: [
434
+ "project",
435
+ "local",
436
+ "package",
437
+ "sdk",
438
+ "installed"
439
+ ],
440
+ default: "project"
441
+ },
442
+ package: { type: "string" },
443
+ detail: {
444
+ type: "string",
445
+ enum: ["summary", "full"],
446
+ default: "summary",
447
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
448
+ }
449
+ }
450
+ }
451
+ },
452
+ {
453
+ name: "smrt-architecture",
454
+ description: "Suggest SMRT and HappyVertical SDK packages and return an architecture prompt bundle",
455
+ inputSchema: {
456
+ type: "object",
457
+ properties: {
458
+ rootDir: { type: "string" },
459
+ idea: { type: "string" },
460
+ documentation: { type: "string" },
461
+ focus: { type: "string" },
462
+ detail: {
463
+ type: "string",
464
+ enum: ["summary", "full"],
465
+ default: "summary",
466
+ description: "summary: authored package docs are listed by path to stay inside tool-result budgets. full: embed them."
467
+ }
468
+ }
469
+ }
470
+ },
471
+ {
472
+ name: "list-agent-skills",
473
+ description: "List bundled harness-agnostic agent skills shipped with smrt-dev-mcp",
474
+ inputSchema: {
475
+ type: "object",
476
+ properties: {}
477
+ }
478
+ },
479
+ {
480
+ name: "get-agent-skill",
481
+ description: "Return a bundled harness-agnostic agent skill as Markdown plus optional references",
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ name: {
486
+ type: "string",
487
+ enum: [REVIEW_SKILL_NAME],
488
+ description: "Bundled agent skill name"
489
+ },
490
+ includeReferences: {
491
+ type: "boolean",
492
+ default: true,
493
+ description: "Include referenced files with the skill bundle"
494
+ }
495
+ },
496
+ required: ["name"]
497
+ }
498
+ }
499
+ ].map((tool) => ({
500
+ ...tool,
501
+ inputSchema: {
502
+ ...tool.inputSchema,
503
+ $schema: JSON_SCHEMA_2020_12
504
+ },
505
+ outputSchema: DEV_MCP_OUTPUT_SCHEMA
506
+ })).sort((left, right) => compareToolNames(left.name, right.name));
507
+ //#endregion
508
+ //#region src/knowledge/mcp-docs.ts
509
+ var PACKAGE_NAME = "@happyvertical/smrt-dev-mcp";
510
+ /**
511
+ * Compare the authored development-MCP catalog to the actual exported tools.
512
+ * The parser intentionally checks only stable structural Markdown: tool names
513
+ * plus top-level input-property names, types, enums, and requiredness.
514
+ * Descriptive prose and prose-embedded defaults remain authored.
515
+ */
516
+ function checkMcpToolDocumentation(rootDir, packageDir, tools) {
517
+ const readmePath = join(packageDir, "README.md");
518
+ const agentsPath = join(packageDir, "AGENTS.md");
519
+ const issues = [];
520
+ if (!existsSync(readmePath)) {
521
+ issues.push(issue(rootDir, readmePath, "mcp-readme-missing", "MCP package README.md is missing"));
522
+ return issues;
523
+ }
524
+ const expectedTools = [...tools].sort((left, right) => compareNames(left.name, right.name));
525
+ const { sections: readmeSections, names: documentedToolNames } = readToolSections(readFileSync(readmePath, "utf8"));
526
+ const expectedToolNames = expectedTools.map((tool) => tool.name);
527
+ const readmeCatalogDiff = setDifference(expectedToolNames, documentedToolNames);
528
+ if (readmeCatalogDiff) issues.push(issue(rootDir, readmePath, "mcp-readme-tool-drift", `README tool catalog differs from exported TOOLS (${readmeCatalogDiff})`));
529
+ for (const tool of expectedTools) {
530
+ const section = readmeSections.get(tool.name);
531
+ if (section === void 0) continue;
532
+ const { parameters: documentedParameters, names: documentedParameterNames } = readParameterTable(section);
533
+ const expectedProperties = Object.keys(tool.inputSchema.properties ?? {}).sort(compareNames);
534
+ const propertyDiff = setDifference(expectedProperties, documentedParameterNames);
535
+ const required = new Set(tool.inputSchema.required ?? []);
536
+ const requiredness = expectedProperties.filter((name) => documentedParameters.has(name) && documentedParameters.get(name)?.required !== required.has(name)).map((name) => `${name} expected ${required.has(name) ? "Yes" : "No"} but documented ${documentedParameters.get(name)?.required ? "Yes" : "No"}`);
537
+ const inputProperties = tool.inputSchema.properties ?? {};
538
+ const types = expectedProperties.filter((name) => documentedParameters.has(name)).flatMap((name) => {
539
+ const expected = formatSchemaType(inputProperties[name]);
540
+ const documented = documentedParameters.get(name)?.type;
541
+ return documented === expected ? [] : [`${name} expected ${expected} but documented ${documented}`];
542
+ });
543
+ const details = [
544
+ propertyDiff,
545
+ requiredness.length > 0 ? `requiredness: ${requiredness.join(", ")}` : void 0,
546
+ types.length > 0 ? `types: ${types.join(", ")}` : void 0
547
+ ].filter((value) => Boolean(value));
548
+ if (details.length > 0) issues.push(issue(rootDir, readmePath, "mcp-readme-schema-drift", `README parameters for "${tool.name}" differ from its exported input schema (${details.join("; ")})`));
549
+ }
550
+ if (!existsSync(agentsPath)) return issues;
551
+ const agentsCatalogDiff = setDifference(expectedToolNames, readAgentToolNames(readFileSync(agentsPath, "utf8")).sort(compareNames));
552
+ if (agentsCatalogDiff) issues.push(issue(rootDir, agentsPath, "mcp-agents-tool-drift", `AGENTS.md tool catalog differs from exported TOOLS (${agentsCatalogDiff})`));
553
+ return issues;
554
+ }
555
+ function issue(rootDir, path, code, message) {
556
+ return {
557
+ severity: "error",
558
+ code,
559
+ message,
560
+ file: relative(rootDir, path),
561
+ packageName: PACKAGE_NAME
562
+ };
563
+ }
564
+ function readToolSections(markdown) {
565
+ const availableTools = sectionAfterHeading(markdown, "Available Tools");
566
+ if (!availableTools) return {
567
+ sections: /* @__PURE__ */ new Map(),
568
+ names: []
569
+ };
570
+ const headings = [...availableTools.matchAll(/^### `([^`]+)`\s*$/gm)];
571
+ return {
572
+ sections: new Map(headings.map((heading, index) => {
573
+ const start = (heading.index ?? 0) + heading[0].length;
574
+ const end = headings[index + 1]?.index ?? availableTools.length;
575
+ return [heading[1], availableTools.slice(start, end)];
576
+ })),
577
+ names: headings.map((heading) => heading[1])
578
+ };
579
+ }
580
+ function readParameterTable(section) {
581
+ const parameters = /* @__PURE__ */ new Map();
582
+ const names = [];
583
+ for (const line of section.split("\n")) {
584
+ if (!line.trimStart().startsWith("|")) continue;
585
+ const cells = splitMarkdownRow(line);
586
+ const name = /^`([^`]+)`$/.exec(cells[0]?.trim() ?? "")?.[1];
587
+ const required = cells[2]?.trim();
588
+ if (!name || required !== "Yes" && required !== "No") continue;
589
+ names.push(name);
590
+ parameters.set(name, {
591
+ name,
592
+ required: required === "Yes",
593
+ type: normalizeDocumentedType(cells[1] ?? "")
594
+ });
595
+ }
596
+ return {
597
+ parameters,
598
+ names
599
+ };
600
+ }
601
+ function normalizeDocumentedType(value) {
602
+ const trimmed = value.trim();
603
+ return (trimmed.startsWith("`") && trimmed.endsWith("`") ? trimmed.slice(1, -1) : trimmed).replaceAll("\\|", "|").replace(/\s*\|\s*/g, " | ").trim();
604
+ }
605
+ function formatSchemaType(value) {
606
+ if (!isRecord(value)) return "unknown";
607
+ if (Array.isArray(value.oneOf)) return unique(value.oneOf.map(formatSchemaType)).join(" | ");
608
+ if (Array.isArray(value.enum) && value.enum.length > 0) return value.enum.map(formatEnumValue).join(" | ");
609
+ if (value.type === "array") {
610
+ const itemType = formatSchemaType(value.items);
611
+ return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
612
+ }
613
+ if (Array.isArray(value.type)) return unique(value.type.map(String)).join(" | ");
614
+ return typeof value.type === "string" ? value.type : "unknown";
615
+ }
616
+ function formatEnumValue(value) {
617
+ if (typeof value === "string") return `'${value.replaceAll("'", "\\'")}'`;
618
+ return String(value);
619
+ }
620
+ function isRecord(value) {
621
+ return value !== null && typeof value === "object" && !Array.isArray(value);
622
+ }
623
+ function unique(values) {
624
+ return [...new Set(values)];
625
+ }
626
+ function splitMarkdownRow(row) {
627
+ const content = row.trim().replace(/^\|/, "").replace(/\|$/, "");
628
+ const cells = [];
629
+ let cell = "";
630
+ for (let index = 0; index < content.length; index += 1) {
631
+ const character = content[index];
632
+ if (character === "|" && content[index - 1] !== "\\") {
633
+ cells.push(cell);
634
+ cell = "";
635
+ } else cell += character;
636
+ }
637
+ cells.push(cell);
638
+ return cells;
639
+ }
640
+ function readAgentToolNames(markdown) {
641
+ const toolsSection = sectionAfterHeading(markdown, "Tools");
642
+ if (!toolsSection) return [];
643
+ return [...toolsSection.matchAll(/^\|\s*`([^`]+)`\s*\|/gm)].map((match) => match[1]);
644
+ }
645
+ function sectionAfterHeading(markdown, heading) {
646
+ const marker = new RegExp(`^## ${heading}\\s*$`, "m").exec(markdown);
647
+ if (!marker) return "";
648
+ const start = (marker.index ?? 0) + marker[0].length;
649
+ const remainder = markdown.slice(start);
650
+ const nextHeading = /^##\s+/m.exec(remainder);
651
+ return remainder.slice(0, nextHeading?.index ?? remainder.length);
652
+ }
653
+ function setDifference(expected, actual) {
654
+ const expectedSet = new Set(expected);
655
+ const actualSet = new Set(actual);
656
+ const missing = expected.filter((value) => !actualSet.has(value)).sort(compareNames);
657
+ const extra = unique(actual.filter((value) => !expectedSet.has(value))).sort(compareNames);
658
+ const expectedDuplicates = duplicateNames(expected);
659
+ const actualDuplicates = duplicateNames(actual);
660
+ const parts = [
661
+ missing.length > 0 ? `missing: ${missing.join(", ")}` : void 0,
662
+ extra.length > 0 ? `extra: ${extra.join(", ")}` : void 0,
663
+ expectedDuplicates.length > 0 ? `exported duplicates: ${expectedDuplicates.join(", ")}` : void 0,
664
+ actualDuplicates.length > 0 ? `duplicates: ${actualDuplicates.join(", ")}` : void 0
665
+ ].filter((value) => Boolean(value));
666
+ return parts.length > 0 ? parts.join("; ") : void 0;
667
+ }
668
+ function duplicateNames(values) {
669
+ const seen = /* @__PURE__ */ new Set();
670
+ const duplicates = /* @__PURE__ */ new Set();
671
+ for (const value of values) {
672
+ if (seen.has(value)) duplicates.add(value);
673
+ seen.add(value);
674
+ }
675
+ return [...duplicates].sort(compareNames);
676
+ }
677
+ function compareNames(left, right) {
678
+ return left < right ? -1 : left > right ? 1 : 0;
679
+ }
680
+ //#endregion
7
681
  //#region src/knowledge/index.ts
8
682
  var SDK_PACKAGE_NAMES = /* @__PURE__ */ new Set([
9
683
  "@happyvertical/ai",
@@ -117,7 +791,7 @@ async function buildKnowledgeIndex(options = {}) {
117
791
  packages.push(readKnowledgePackage(rootDir, dir, includeDocs));
118
792
  }
119
793
  await applyScannerFallbacks(packages, packageDirs.filter((dir) => resolve(dir) !== resolve(rootDir)).map((dir) => `${relative(rootDir, dir).replaceAll("\\", "/")}/**`));
120
- packages.push(...discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs));
794
+ packages.push(...discoverInstalledPackages(rootDir, packageDirs, includeDocs));
121
795
  const uniquePackages = dedupePackages(packages);
122
796
  const scopedPackages = filterKnowledgePackages(uniquePackages, options);
123
797
  const smrtPackages = scopedPackages.filter((pkg) => pkg.kind === "smrt");
@@ -130,24 +804,26 @@ async function buildKnowledgeIndex(options = {}) {
130
804
  packages: uniquePackages
131
805
  });
132
806
  return {
133
- schemaVersion: 2,
807
+ schemaVersion: 3,
134
808
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
135
809
  rootDir,
136
810
  packages: scopedPackages,
137
811
  smrtPackages,
138
812
  sdkPackages,
813
+ installedPackages: scopedPackages.filter((pkg) => pkg.isInstalledDependency),
139
814
  relationshipsV2: summarizeRelationshipsV2(scopedPackages),
140
815
  coverage,
141
816
  diagnostics: buildIndexDiagnostics(rootDir, uniquePackages, coverage, discoveryDiagnostics)
142
817
  };
143
818
  }
144
819
  function buildCoverage(options) {
820
+ const authored = options.packages.filter((pkg) => !pkg.isInstalledDependency);
145
821
  return {
146
822
  workspaceGlobs: options.globs,
147
823
  workspaceGlobSource: options.globSource,
148
824
  packageDirs: options.packageDirs.map((dir) => relative(options.rootDir, dir) || "."),
149
- packagesWithObjects: options.packages.filter((pkg) => pkg.objects.length > 0).map((pkg) => `${pkg.name} (${pkg.objects.length}, ${pkg.objectSource})`),
150
- packagesWithoutObjects: options.packages.filter((pkg) => pkg.objects.length === 0).map((pkg) => ({
825
+ packagesWithObjects: authored.filter((pkg) => pkg.objects.length > 0).map((pkg) => `${pkg.name} (${pkg.objects.length}, ${pkg.objectSource})`),
826
+ packagesWithoutObjects: authored.filter((pkg) => pkg.objects.length === 0).map((pkg) => ({
151
827
  name: pkg.name,
152
828
  reason: pkg.objectSourceReason ?? pkg.objectSource,
153
829
  checkedPaths: pkg.checkedObjectPaths,
@@ -171,7 +847,25 @@ function remedyForReason(pkg) {
171
847
  */
172
848
  function buildIndexDiagnostics(rootDir, packages, coverage, discoveryDiagnostics = []) {
173
849
  const diagnostics = [...discoveryDiagnostics];
174
- if (packages.reduce((total, pkg) => total + pkg.objects.length, 0) === 0) diagnostics.push({
850
+ const authoredPackages = packages.filter((pkg) => !pkg.isInstalledDependency);
851
+ const authoredObjects = authoredPackages.reduce((total, pkg) => total + pkg.objects.length, 0);
852
+ const installedObjects = packages.reduce((total, pkg) => total + (pkg.isInstalledDependency ? pkg.objects.length : 0), 0);
853
+ if (authoredObjects === 0 && installedObjects > 0) diagnostics.push({
854
+ severity: "warning",
855
+ code: "no-authored-smrt-objects",
856
+ message: [
857
+ `No SMRT objects were discovered in the project's own packages under ${rootDir},`,
858
+ `though ${installedObjects} were read from installed dependencies.`,
859
+ "Expected for an application that only consumes the framework;",
860
+ "a discovery failure if this workspace is supposed to declare @smrt() classes."
861
+ ].join(" "),
862
+ remedy: [
863
+ "If this workspace authors SMRT objects, confirm rootDir is the workspace root,",
864
+ "confirm the workspace globs cover the directories that hold @smrt() classes,",
865
+ "and run `pnpm build` in the owning package to emit .smrt/manifest.json."
866
+ ].join(" ")
867
+ });
868
+ if (authoredObjects === 0 && installedObjects === 0) diagnostics.push({
175
869
  severity: "error",
176
870
  code: "no-smrt-objects-discovered",
177
871
  message: [
@@ -180,7 +874,7 @@ function buildIndexDiagnostics(rootDir, packages, coverage, discoveryDiagnostics
180
874
  `Package directories checked (${coverage.packageDirs.length}): ${coverage.packageDirs.join(", ") || "(none)"}.`,
181
875
  "Treat this as a discovery failure, not as evidence that the project has no SMRT model."
182
876
  ].join(" "),
183
- checkedPaths: [...new Set(packages.flatMap((pkg) => pkg.checkedObjectPaths))],
877
+ checkedPaths: [...new Set(authoredPackages.flatMap((pkg) => pkg.checkedObjectPaths))],
184
878
  remedy: [
185
879
  "Confirm rootDir is the workspace root;",
186
880
  "confirm pnpm-workspace.yaml `packages:` covers the directories that hold @smrt() classes (for example apps/*);",
@@ -250,10 +944,11 @@ async function checkKnowledgeFreshness(options = {}) {
250
944
  async function checkKnowledgeFreshnessFromIndex(index, options = {}) {
251
945
  const issues = [];
252
946
  const changedFiles = options.changed ? getChangedFiles(index.rootDir) : void 0;
253
- const hasMemberPackages = index.packages.some((item) => item.kind !== "sdk" && !item.isWorkspaceRoot);
254
- const memberDirectories = index.packages.filter((item) => item.kind !== "sdk" && !item.isWorkspaceRoot && item.relativeDirectory).map((item) => item.relativeDirectory);
947
+ const authoredPackages = index.packages.filter((item) => !item.isInstalledDependency);
948
+ const hasMemberPackages = authoredPackages.some((item) => item.kind !== "sdk" && !item.isWorkspaceRoot);
949
+ const memberDirectories = authoredPackages.filter((item) => item.kind !== "sdk" && !item.isWorkspaceRoot && item.relativeDirectory).map((item) => item.relativeDirectory);
255
950
  const isNestedMember = (pkg) => Boolean(pkg.relativeDirectory) && memberDirectories.some((directory) => directory !== pkg.relativeDirectory && pkg.relativeDirectory.startsWith(`${directory}/`));
256
- for (const pkg of index.packages.filter((item) => item.kind !== "sdk" && !(item.isWorkspaceRoot && hasMemberPackages))) {
951
+ for (const pkg of authoredPackages.filter((item) => item.kind !== "sdk" && !(item.isWorkspaceRoot && hasMemberPackages))) {
257
952
  const packageJsonPath = join(pkg.directory, "package.json");
258
953
  const nested = isNestedMember(pkg);
259
954
  if (!pkg.hasAgentsMd && !nested) issues.push({
@@ -310,7 +1005,9 @@ async function checkKnowledgeFreshnessFromIndex(index, options = {}) {
310
1005
  packageName: pkg.name
311
1006
  });
312
1007
  }
313
- for (const pkg of index.packages) issues.push(...checkDomainKnowledgeArtifact(index.rootDir, pkg));
1008
+ for (const pkg of authoredPackages) issues.push(...checkDomainKnowledgeArtifact(index.rootDir, pkg));
1009
+ const devMcpPackage = authoredPackages.find((pkg) => pkg.name === "@happyvertical/smrt-dev-mcp");
1010
+ if (devMcpPackage) issues.push(...checkMcpToolDocumentation(index.rootDir, devMcpPackage.directory, TOOLS));
314
1011
  issues.push(...findStalePatternIssues(index.rootDir, changedFiles));
315
1012
  const effectiveIssues = issues.map((issue) => issue.code.startsWith("stale-") ? {
316
1013
  ...issue,
@@ -517,6 +1214,7 @@ function renderKnowledgeIndexMarkdown(index) {
517
1214
  "",
518
1215
  `- SMRT packages: ${index.smrtPackages.length}`,
519
1216
  `- SDK packages: ${index.sdkPackages.length}`,
1217
+ `- Installed dependencies: ${index.installedPackages.length}`,
520
1218
  `- foreignKey fields: ${index.relationshipsV2.foreignKeyFields}`,
521
1219
  `- crossPackageRef fields: ${index.relationshipsV2.crossPackageRefFields}`,
522
1220
  `- junction collections: ${index.relationshipsV2.junctionCollections}`,
@@ -540,6 +1238,10 @@ function renderKnowledgeIndexMarkdown(index) {
540
1238
  lines.push(`- MCP tools: ${pkg.mcpTools.length}`);
541
1239
  lines.push(`- domain knowledge: ${pkg.domainKnowledgePath ?? "(manifest fallback)"}`);
542
1240
  lines.push(`- docs: ${pkg.docSource ?? "(none)"}${pkg.hasClaudeShim ? " + CLAUDE.md shim" : ""}`);
1241
+ if (pkg.isInstalledDependency) {
1242
+ lines.push("- source: installed dependency");
1243
+ lines.push(`- AGENTS.md sha256: ${pkg.agentDocSha256 ?? "(none)"}`);
1244
+ }
543
1245
  if (pkg.moduleDocs.length > 0) lines.push(`- module docs: ${pkg.moduleDocs.map((doc) => doc.path).join(", ")}`);
544
1246
  if (pkg.relationshipFeatures.length > 0) lines.push(`- relationships-v2: ${pkg.relationshipFeatures.join(", ")}`);
545
1247
  lines.push("");
@@ -817,34 +1519,85 @@ function discoverProjectPackageDirs(rootDir) {
817
1519
  diagnostics: expansion.diagnostics
818
1520
  };
819
1521
  }
820
- function discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs) {
821
- return [join(rootDir, "node_modules", "@happyvertical"), ...packageDirs.map((dir) => join(dir, "node_modules", "@happyvertical"))].filter((scopeDir, index, all) => existsSync(scopeDir) && all.indexOf(scopeDir) === index).flatMap((scopeDir) => readdirSync(scopeDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => {
822
- const entryPath = join(scopeDir, entry.name);
1522
+ /**
1523
+ * Installed `@happyvertical/*` packages, resolved once each (#2275).
1524
+ *
1525
+ * Reads the `@happyvertical` scope directory of the project and of every
1526
+ * workspace package — one `readdir` per scope directory, resolving each entry's
1527
+ * real path — instead of descending through `node_modules`. Under pnpm those
1528
+ * entries are symlinks into a store whose entries link back out to each other,
1529
+ * so a descent revisits the same package once per path that reaches it. The
1530
+ * realpath is the dedupe key only: the same store entry reached from three
1531
+ * scope directories is read once, but it is read through its `node_modules`
1532
+ * path, because a realpath is the wrong thing to report. On a host where any
1533
+ * ancestor of the root is a symlink (`/var` on macOS, a symlinked worktree),
1534
+ * `relative(rootDir, realpath)` escapes the root entirely.
1535
+ *
1536
+ * Workspace packages linked into a sibling's `node_modules` are skipped here.
1537
+ * They are authored source that happens to be reachable through a link, and
1538
+ * calling them installed would exempt them from the freshness gate.
1539
+ *
1540
+ * Both SMRT and SDK packages are returned. A consumer app authors neither, and
1541
+ * its `@happyvertical/smrt-*` dependencies are exactly the surface it needs to
1542
+ * audit; excluding them is why the index could see nothing in a consumer app.
1543
+ */
1544
+ function discoverInstalledPackages(rootDir, packageDirs, includeDocs) {
1545
+ const scopeDirs = [join(rootDir, "node_modules", "@happyvertical"), ...packageDirs.map((dir) => join(dir, "node_modules", "@happyvertical"))];
1546
+ const workspaceRealPaths = new Set([rootDir, ...packageDirs].flatMap((dir) => {
1547
+ try {
1548
+ return [realpathSync(dir)];
1549
+ } catch {
1550
+ return [];
1551
+ }
1552
+ }));
1553
+ const byRealPath = /* @__PURE__ */ new Map();
1554
+ for (const scopeDir of new Set(scopeDirs)) {
1555
+ if (!existsSync(scopeDir)) continue;
1556
+ let entries;
823
1557
  try {
824
- return lstatSync(entryPath).isSymbolicLink() ? realpathSync(entryPath) : entryPath;
1558
+ entries = readdirSync(scopeDir, { withFileTypes: true });
825
1559
  } catch {
826
- return entryPath;
1560
+ continue;
827
1561
  }
828
- })).filter((dir) => {
829
- const pkg = objectRecord(readJson(join(dir, "package.json")));
830
- return typeof pkg.name === "string" && SDK_PACKAGE_NAMES.has(pkg.name) && !pkg.name.startsWith("@happyvertical/smrt-");
831
- }).map((dir) => readKnowledgePackage(rootDir, dir, includeDocs));
1562
+ for (const entry of entries) {
1563
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
1564
+ const entryPath = join(scopeDir, entry.name);
1565
+ let resolved;
1566
+ try {
1567
+ resolved = realpathSync(entryPath);
1568
+ } catch {
1569
+ continue;
1570
+ }
1571
+ if (workspaceRealPaths.has(resolved)) continue;
1572
+ if (byRealPath.has(resolved)) continue;
1573
+ byRealPath.set(resolved, entryPath);
1574
+ }
1575
+ }
1576
+ const packages = [];
1577
+ for (const directory of byRealPath.values()) {
1578
+ const name = objectRecord(readJson(join(directory, "package.json"))).name;
1579
+ if (typeof name !== "string") continue;
1580
+ if (!SDK_PACKAGE_NAMES.has(name) && !name.startsWith("@happyvertical/smrt-")) continue;
1581
+ packages.push(readKnowledgePackage(rootDir, directory, includeDocs, { installed: true }));
1582
+ }
1583
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
832
1584
  }
833
1585
  function filterKnowledgePackages(packages, options) {
834
1586
  const packageQuery = options.packageName ?? options.package;
835
1587
  return packages.filter((pkg) => {
836
1588
  if (packageQuery && !packageMatches(pkg, packageQuery.toLowerCase())) return false;
837
1589
  switch (options.scope) {
838
- case "local": return pkg.kind !== "sdk" && !pkg.relativeDirectory.includes("node_modules");
839
- case "package": return pkg.kind !== "sdk";
1590
+ case "local": return pkg.kind !== "sdk" && !pkg.isInstalledDependency && !pkg.relativeDirectory.includes("node_modules");
1591
+ case "package": return pkg.kind !== "sdk" && !pkg.isInstalledDependency;
840
1592
  case "sdk": return pkg.kind === "sdk";
1593
+ case "installed": return pkg.isInstalledDependency;
841
1594
  case "project":
842
1595
  case void 0: return true;
843
1596
  default: return true;
844
1597
  }
845
1598
  });
846
1599
  }
847
- function readKnowledgePackage(rootDir, directory, includeDocs) {
1600
+ function readKnowledgePackage(rootDir, directory, includeDocs, options = {}) {
848
1601
  const packageJson = objectRecord(readJson(join(directory, "package.json")));
849
1602
  const dependencies = stringRecord(packageJson.dependencies);
850
1603
  const devDependencies = stringRecord(packageJson.devDependencies);
@@ -894,6 +1647,7 @@ function readKnowledgePackage(rootDir, directory, includeDocs) {
894
1647
  hasClaudeShim: claudeContent.trim() === "@AGENTS.md",
895
1648
  docSource,
896
1649
  agentDoc: includeDocs ? domainKnowledge?.content.agentDoc || (hasAgentsMd ? agentsContent : fallbackClaudeDoc || void 0) : void 0,
1650
+ agentDocSha256: hasAgentsMd ? createHash("sha256").update(agentsContent).digest("hex") : void 0,
897
1651
  moduleDocs: includeDocs ? domainKnowledge?.content.moduleDocs ?? readAgentModuleDocs(directory, agentsContent || void 0) : [],
898
1652
  hasDomainKnowledge: Boolean(domainKnowledge),
899
1653
  domainKnowledgePath: domainKnowledge?.path ? relative(rootDir, domainKnowledge.path) : void 0,
@@ -906,6 +1660,7 @@ function readKnowledgePackage(rootDir, directory, includeDocs) {
906
1660
  relationshipFeatures: relationshipFeatures(objects),
907
1661
  isWorkspaceRoot: resolve(directory) === resolve(rootDir),
908
1662
  isPrivate: packageJson.private === true,
1663
+ isInstalledDependency: options.installed === true,
909
1664
  objectSource: resolvedObjects.source,
910
1665
  objectSourceReason: resolvedObjects.reason,
911
1666
  checkedObjectPaths: resolvedObjects.checkedPaths
@@ -1547,7 +2302,7 @@ function selectPackages(index, options) {
1547
2302
  const selected = /* @__PURE__ */ new Set();
1548
2303
  const packageName = options.packageName?.toLowerCase();
1549
2304
  if (packageName) {
1550
- for (const pkg of domainPackages(index)) if (packageMatches(pkg, packageName)) selected.add(pkg);
2305
+ for (const pkg of domainPackages(index)) if (scopeAllowsPackage(pkg, options.scope) && packageMatches(pkg, packageName)) selected.add(pkg);
1551
2306
  }
1552
2307
  for (const pkg of selectPackagesForFiles(index, options.changedFiles ?? [])) if (scopeAllowsPackage(pkg, options.scope)) selected.add(pkg);
1553
2308
  const text = (options.text ?? "").toLowerCase();
@@ -1565,7 +2320,7 @@ function selectPackages(index, options) {
1565
2320
  "@happyvertical/smrt-dev-mcp"
1566
2321
  ]) {
1567
2322
  const pkg = index.smrtPackages.find((item) => item.name === name);
1568
- if (pkg) selected.add(pkg);
2323
+ if (pkg && scopeAllowsPackage(pkg, options.scope)) selected.add(pkg);
1569
2324
  }
1570
2325
  if (selected.size === 0) {
1571
2326
  const contributing = domainPackages(index).filter((pkg) => pkg.objects.length > 0 && scopeAllowsPackage(pkg, options.scope) && !pkg.relativeDirectory.includes("node_modules")).sort((a, b) => b.objects.length - a.objects.length).slice(0, MAX_FALLBACK_PACKAGES);
@@ -1580,28 +2335,41 @@ function selectSdkPackages(index, selectedPackages, texts, options = {}) {
1580
2335
  const text = texts.filter(Boolean).join("\n").toLowerCase();
1581
2336
  const packageName = options.packageName?.toLowerCase();
1582
2337
  for (const sdk of index.sdkPackages) {
2338
+ const selectedDependency = sdkNames.has(sdk.name);
2339
+ if (!scopeAllowsSdkPackage(sdk, options.scope, selectedDependency)) continue;
1583
2340
  const shortName = sdk.name.replace("@happyvertical/", "");
1584
- if (options.scope === "sdk" || sdkNames.has(sdk.name) || packageName && packageMatches(sdk, packageName) || text.includes(sdk.name.toLowerCase()) || includesToken(text, shortName)) selected.add(sdk);
2341
+ if (options.scope === "sdk" || selectedDependency || packageName && packageMatches(sdk, packageName) || text.includes(sdk.name.toLowerCase()) || includesToken(text, shortName)) selected.add(sdk);
1585
2342
  }
1586
- if (selected.size === 0) for (const name of [
2343
+ if (selected.size === 0 && !(packageName && selectedPackages.length === 0)) for (const name of [
1587
2344
  "@happyvertical/ai",
1588
2345
  "@happyvertical/sql",
1589
2346
  "@happyvertical/files",
1590
2347
  "@happyvertical/utils"
1591
2348
  ]) {
1592
- const sdk = index.sdkPackages.find((item) => item.name === name);
2349
+ const sdk = index.sdkPackages.find((item) => item.name === name && scopeAllowsSdkPackage(item, options.scope, false));
1593
2350
  if (sdk) selected.add(sdk);
1594
2351
  }
1595
2352
  return [...selected].sort((a, b) => a.name.localeCompare(b.name));
1596
2353
  }
2354
+ function scopeAllowsSdkPackage(pkg, scope, selectedDependency) {
2355
+ switch (scope) {
2356
+ case "installed": return pkg.isInstalledDependency;
2357
+ case "local":
2358
+ case "package": return !pkg.isInstalledDependency || selectedDependency;
2359
+ case "sdk":
2360
+ case "project":
2361
+ case void 0: return true;
2362
+ }
2363
+ }
1597
2364
  function domainPackages(index) {
1598
2365
  return index.packages.filter((pkg) => pkg.kind !== "sdk");
1599
2366
  }
1600
2367
  function scopeAllowsPackage(pkg, scope) {
1601
2368
  switch (scope) {
1602
2369
  case "sdk": return false;
1603
- case "local": return !pkg.relativeDirectory.includes("node_modules");
1604
- case "package":
2370
+ case "installed": return pkg.isInstalledDependency;
2371
+ case "local": return !pkg.isInstalledDependency && !pkg.relativeDirectory.includes("node_modules");
2372
+ case "package": return !pkg.isInstalledDependency;
1605
2373
  case "project":
1606
2374
  case void 0: return true;
1607
2375
  }
@@ -1834,6 +2602,6 @@ function sortJson(value) {
1834
2602
  return value;
1835
2603
  }
1836
2604
  //#endregion
1837
- export { checkKnowledgeFreshnessFromIndex as a, renderFreshnessResult as c, smrtReview as d, checkKnowledgeFreshness as i, renderKnowledgeIndexMarkdown as l, buildKnowledgeIndex as n, diffKnowledgeIndex as o, buildReviewContext as r, packageDocPaths as s, buildArchitectureContext as t, smrtArchitecture as u };
2605
+ export { checkKnowledgeFreshnessFromIndex as a, renderFreshnessResult as c, smrtReview as d, REVIEW_SKILL_NAME as f, checkKnowledgeFreshness as i, renderKnowledgeIndexMarkdown as l, buildKnowledgeIndex as n, diffKnowledgeIndex as o, TOOLS as p, buildReviewContext as r, packageDocPaths as s, buildArchitectureContext as t, smrtArchitecture as u };
1838
2606
 
1839
- //# sourceMappingURL=knowledge-DUUhTM5u.js.map
2607
+ //# sourceMappingURL=knowledge-A7-gIjW4.js.map