hone-ai 0.2.0 → 0.9.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.
@@ -0,0 +1,900 @@
1
+ /**
2
+ * AGENTS.md generation functionality
3
+ * Core module for generating project documentation for AI agents
4
+ */
5
+
6
+ import { loadConfig, resolveModelForPhase, type HoneConfig, type AgentType } from './config'
7
+ import { readFile, writeFile, mkdir } from 'fs/promises'
8
+ import { join } from 'path'
9
+ import { existsSync } from 'fs'
10
+ import { AgentClient } from './agent-client'
11
+ import { log, logError, logVerbose, logVerboseError } from './logger'
12
+
13
+ /**
14
+ * Central constant for the agents documentation directory name
15
+ * This can be made configurable via config file in the future if needed
16
+ */
17
+ export const AGENTS_DOCS_DIR = '.agents-docs'
18
+
19
+ export interface AgentsMdGeneratorOptions {
20
+ projectPath?: string
21
+ overwrite?: boolean
22
+ agent?: AgentType
23
+ }
24
+
25
+ export interface ProjectAnalysis {
26
+ languages: string[]
27
+ buildSystems: string[]
28
+ testingFrameworks: string[]
29
+ dependencies: string[]
30
+ architecture: string[]
31
+ }
32
+
33
+ export interface GenerationResult {
34
+ success: boolean
35
+ mainFilePath?: string
36
+ agentsDirPath?: string
37
+ filesCreated: string[]
38
+ error?: Error
39
+ }
40
+
41
+ interface TemplateSection {
42
+ title: string
43
+ content: string
44
+ priority: number
45
+ detailFile?: string
46
+ }
47
+
48
+ /**
49
+ * Extract preservable content (gotchas, custom learnings) from existing AGENTS.md
50
+ * This function looks for custom sections that should be preserved when regenerating
51
+ */
52
+ function extractPreservableContent(existingContent: string): string | null {
53
+ const lines = existingContent.split('\n')
54
+ const preservedSections: string[] = []
55
+ let currentSection: string[] = []
56
+ let inPreservableSection = false
57
+ let sectionTitle = ''
58
+
59
+ for (const line of lines) {
60
+ if (!line) continue
61
+
62
+ // Look for section headers that might contain gotchas/learnings
63
+ if (line.startsWith('## ') || line.startsWith('# ')) {
64
+ // Save previous section if it was preservable
65
+ if (inPreservableSection && currentSection.length > 0) {
66
+ preservedSections.push(`## ${sectionTitle}\n\n${currentSection.join('\n').trim()}`)
67
+ }
68
+
69
+ // Check if this is a section we want to preserve
70
+ const title = line.replace(/^#+\s*/, '').toLowerCase()
71
+ sectionTitle = line.replace(/^#+\s*/, '')
72
+ inPreservableSection =
73
+ title.includes('gotcha') ||
74
+ title.includes('learning') ||
75
+ title.includes('note') ||
76
+ title.includes('warning') ||
77
+ title.includes('tip') ||
78
+ title.includes('custom') ||
79
+ title.includes('specific') ||
80
+ line.includes('PRESERVED CONTENT')
81
+
82
+ currentSection = []
83
+ } else if (inPreservableSection) {
84
+ currentSection.push(line)
85
+ }
86
+ }
87
+
88
+ // Don't forget the last section
89
+ if (inPreservableSection && currentSection.length > 0) {
90
+ preservedSections.push(`## ${sectionTitle}\n\n${currentSection.join('\n').trim()}`)
91
+ }
92
+
93
+ return preservedSections.length > 0 ? preservedSections.join('\n\n') : null
94
+ }
95
+
96
+ /**
97
+ * Analyze project structure and gather context for AGENTS.md generation
98
+ */
99
+ async function analyzeProject(projectPath: string): Promise<ProjectAnalysis> {
100
+ const analysis: ProjectAnalysis = {
101
+ languages: [],
102
+ buildSystems: [],
103
+ testingFrameworks: [],
104
+ dependencies: [],
105
+ architecture: [],
106
+ }
107
+
108
+ try {
109
+ // Package.json analysis for Node.js projects
110
+ const pkgPath = join(projectPath, 'package.json')
111
+ if (existsSync(pkgPath)) {
112
+ try {
113
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
114
+
115
+ // Detect languages based on dependencies
116
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
117
+ if (allDeps.typescript || existsSync(join(projectPath, 'tsconfig.json'))) {
118
+ analysis.languages.push('TypeScript')
119
+ } else {
120
+ analysis.languages.push('JavaScript')
121
+ }
122
+
123
+ // Detect frameworks and build systems
124
+ if (allDeps.react) analysis.dependencies.push('React')
125
+ if (allDeps.next) analysis.dependencies.push('Next.js')
126
+ if (allDeps.vue) analysis.dependencies.push('Vue.js')
127
+ if (allDeps.express) analysis.dependencies.push('Express')
128
+ if (allDeps.fastify) analysis.dependencies.push('Fastify')
129
+ if (allDeps['commander'] || allDeps['commander.js'])
130
+ analysis.dependencies.push('Commander.js')
131
+
132
+ // Build systems
133
+ if (pkg.scripts?.build) analysis.buildSystems.push('npm scripts')
134
+ if (existsSync(join(projectPath, 'webpack.config.js')))
135
+ analysis.buildSystems.push('Webpack')
136
+ if (
137
+ existsSync(join(projectPath, 'vite.config.ts')) ||
138
+ existsSync(join(projectPath, 'vite.config.js'))
139
+ ) {
140
+ analysis.buildSystems.push('Vite')
141
+ }
142
+
143
+ // Testing frameworks
144
+ if (allDeps.jest) analysis.testingFrameworks.push('Jest')
145
+ if (allDeps.vitest) analysis.testingFrameworks.push('Vitest')
146
+ if (allDeps.mocha) analysis.testingFrameworks.push('Mocha')
147
+ if (allDeps.bun) {
148
+ analysis.testingFrameworks.push('Bun Test')
149
+ analysis.buildSystems.push('Bun')
150
+ }
151
+ } catch (error) {
152
+ logVerbose(`Could not parse package.json: ${error}`)
153
+ }
154
+ }
155
+
156
+ // Python project detection
157
+ if (
158
+ existsSync(join(projectPath, 'requirements.txt')) ||
159
+ existsSync(join(projectPath, 'pyproject.toml')) ||
160
+ existsSync(join(projectPath, 'setup.py'))
161
+ ) {
162
+ analysis.languages.push('Python')
163
+
164
+ if (existsSync(join(projectPath, 'pyproject.toml'))) {
165
+ analysis.buildSystems.push('Poetry/setuptools')
166
+ }
167
+ }
168
+
169
+ // Java project detection
170
+ if (existsSync(join(projectPath, 'pom.xml'))) {
171
+ analysis.languages.push('Java')
172
+ analysis.buildSystems.push('Maven')
173
+ }
174
+ if (
175
+ existsSync(join(projectPath, 'build.gradle')) ||
176
+ existsSync(join(projectPath, 'build.gradle.kts'))
177
+ ) {
178
+ analysis.languages.push('Java/Kotlin')
179
+ analysis.buildSystems.push('Gradle')
180
+ }
181
+
182
+ // Go project detection
183
+ if (existsSync(join(projectPath, 'go.mod'))) {
184
+ analysis.languages.push('Go')
185
+ analysis.buildSystems.push('Go modules')
186
+ }
187
+
188
+ // Rust project detection
189
+ if (existsSync(join(projectPath, 'Cargo.toml'))) {
190
+ analysis.languages.push('Rust')
191
+ analysis.buildSystems.push('Cargo')
192
+ }
193
+
194
+ // Architecture patterns
195
+ if (existsSync(join(projectPath, 'src'))) {
196
+ analysis.architecture.push('src/ directory structure')
197
+ }
198
+ if (existsSync(join(projectPath, 'docker-compose.yml'))) {
199
+ analysis.architecture.push('Docker Compose')
200
+ }
201
+ if (existsSync(join(projectPath, 'Dockerfile'))) {
202
+ analysis.architecture.push('Docker containerization')
203
+ }
204
+ if (existsSync(join(projectPath, '.github/workflows'))) {
205
+ analysis.architecture.push('GitHub Actions CI/CD')
206
+ }
207
+
208
+ logVerbose(`[AgentsMd] Project analysis complete: ${JSON.stringify(analysis, null, 2)}`)
209
+ return analysis
210
+ } catch (error) {
211
+ logVerboseError(
212
+ `[AgentsMd] Error analyzing project: ${error instanceof Error ? error.message : error}`
213
+ )
214
+ return analysis // Return partial analysis on error
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Discovery prompts for analyzing different aspects of the project
220
+ */
221
+ const DISCOVERY_PROMPTS = {
222
+ languages: `Analyze this project's codebase to identify the primary programming languages used and their purposes.
223
+
224
+ IMPORTANT LANGUAGE DETECTION RULES:
225
+ - Look for source code files (.js, .ts, .py, .java, .go, .rs, .php, .rb, etc.)
226
+ - Check package.json, requirements.txt, go.mod, Cargo.toml, pom.xml, build.gradle for dependencies
227
+ - Identify language-specific configuration files (tsconfig.json, .eslintrc, setup.py, etc.)
228
+ - For TypeScript projects, note if it's primarily TypeScript or mixed JS/TS
229
+ - For frontend projects, distinguish between client-side and server-side languages
230
+
231
+ CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "PRIMARY LANGUAGES:".
232
+
233
+ PRIMARY LANGUAGES: [language 1, language 2, ...]
234
+ USAGE CONTEXT: [brief explanation of how each language is used in the project]`,
235
+
236
+ buildSystems: `Analyze this project to identify build systems, package managers, and compilation/bundling tools.
237
+
238
+ BUILD SYSTEM DETECTION RULES:
239
+ - npm/yarn/pnpm: Look for package.json, package-lock.json, yarn.lock, pnpm-lock.yaml
240
+ - Maven: Look for pom.xml, maven-wrapper files
241
+ - Gradle: Look for build.gradle, gradlew files
242
+ - Go modules: Look for go.mod, go.sum
243
+ - Cargo: Look for Cargo.toml, Cargo.lock
244
+ - Webpack: Look for webpack.config.js, webpack configurations
245
+ - Vite: Look for vite.config.ts/js
246
+ - Parcel: Look for .parcelrc, parcel configurations
247
+ - Build scripts in package.json (build, bundle, compile commands)
248
+ - Docker: Look for Dockerfile, docker-compose.yml
249
+ - Make: Look for Makefile
250
+ - Custom build scripts in various languages
251
+
252
+ CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "BUILD SYSTEMS:".
253
+
254
+ BUILD SYSTEMS: [system 1, system 2, ...]
255
+ BUILD COMMANDS: [key build commands developers should know]
256
+ BUNDLING: [bundling tools if applicable]`,
257
+
258
+ testing: `Identify testing frameworks, test organization patterns, and testing strategies used in this project.
259
+
260
+ TESTING FRAMEWORK DETECTION:
261
+ - JavaScript/TypeScript: Jest, Vitest, Mocha, Cypress, Playwright, Testing Library
262
+ - Python: pytest, unittest, nose, tox
263
+ - Java: JUnit, TestNG, Mockito, Spring Test
264
+ - Go: built-in testing, Testify, Ginkgo
265
+ - Rust: built-in testing, proptest, criterion
266
+ - Ruby: RSpec, minitest
267
+ - PHP: PHPUnit, Pest
268
+
269
+ Look for:
270
+ - Test files (*.test.*, *.spec.*, *_test.*, test_*.py)
271
+ - Test directories (/test, /tests, /__tests__)
272
+ - Configuration files (jest.config.js, vitest.config.ts, pytest.ini)
273
+ - CI/CD test configurations
274
+ - Mock/stub patterns
275
+ - E2E testing setup
276
+
277
+ CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "TESTING FRAMEWORKS:".
278
+
279
+ TESTING FRAMEWORKS: [framework 1, framework 2, ...]
280
+ TEST COMMANDS: [how to run tests]
281
+ TEST ORGANIZATION: [how tests are structured and organized]
282
+ E2E TESTING: [end-to-end testing approach if present]`,
283
+
284
+ architecture: `Analyze the project's architectural patterns, directory structure, and design decisions.
285
+
286
+ ARCHITECTURE ANALYSIS AREAS:
287
+ - Directory/folder structure and organization
288
+ - Design patterns (MVC, MVP, MVVM, layered architecture, microservices, etc.)
289
+ - Code organization (modules, packages, namespaces)
290
+ - Database integration patterns
291
+ - API design patterns (REST, GraphQL, RPC)
292
+ - Configuration management
293
+ - Dependency injection patterns
294
+ - Error handling patterns
295
+ - Logging and monitoring
296
+ - Security patterns
297
+ - Performance considerations
298
+
299
+ Examine:
300
+ - Source code organization in src/, lib/, app/ directories
301
+ - Configuration files and their patterns
302
+ - Database schema or ORM usage
303
+ - API endpoint definitions
304
+ - Middleware/interceptor patterns
305
+ - Shared utilities and common code
306
+
307
+ CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "ARCHITECTURE PATTERN:".
308
+
309
+ ARCHITECTURE PATTERN: [primary architectural pattern]
310
+ DIRECTORY STRUCTURE: [key organizational principles]
311
+ DESIGN PATTERNS: [notable design patterns in use]
312
+ DATABASE: [data layer architecture if applicable]
313
+ API DESIGN: [API architectural patterns if applicable]`,
314
+
315
+ deployment: `Analyze deployment strategies, infrastructure patterns, and operational considerations for this project.
316
+
317
+ DEPLOYMENT ANALYSIS:
318
+ - Containerization (Docker, Podman)
319
+ - Container orchestration (Kubernetes, Docker Swarm, Docker Compose)
320
+ - Cloud platforms (AWS, GCP, Azure, Vercel, Netlify, Railway)
321
+ - CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, CircleCI)
322
+ - Infrastructure as Code (Terraform, CloudFormation, Pulumi)
323
+ - Serverless deployment (Lambda, Cloud Functions, Vercel Functions)
324
+ - Static site deployment
325
+ - Database deployment and migrations
326
+ - Environment configuration management
327
+ - Monitoring and logging setup
328
+
329
+ Look for:
330
+ - Dockerfile, docker-compose.yml
331
+ - .github/workflows/, .gitlab-ci.yml, Jenkinsfile
332
+ - Cloud provider configuration files
333
+ - Deployment scripts
334
+ - Environment variable configurations (.env patterns)
335
+ - Database migration files
336
+ - Package.json deploy scripts
337
+
338
+ CRITICAL: Your response MUST start directly with the structured format below. NO preambles like "Based on my analysis..." or "Here's what I found..." - start IMMEDIATELY with "DEPLOYMENT STRATEGY:".
339
+
340
+ DEPLOYMENT STRATEGY: [primary deployment approach]
341
+ CONTAINERIZATION: [Docker/container usage]
342
+ CI/CD: [continuous integration/deployment setup]
343
+ HOSTING: [where the application is designed to be hosted]
344
+ ENVIRONMENT MANAGEMENT: [how environments are configured]`,
345
+ }
346
+
347
+ /**
348
+ * Execute a discovery prompt against the project using agent
349
+ */
350
+ async function executeDiscoveryPrompt(
351
+ projectPath: string,
352
+ promptKey: keyof typeof DISCOVERY_PROMPTS,
353
+ config: HoneConfig,
354
+ agent?: AgentType
355
+ ): Promise<string> {
356
+ const resolvedAgent = agent || config.defaultAgent
357
+ const model = resolveModelForPhase(config, 'agentsMd', resolvedAgent) // Use agentsMd phase model with resolved agent
358
+ const client = new AgentClient({
359
+ agent: resolvedAgent,
360
+ model,
361
+ workingDir: projectPath,
362
+ })
363
+
364
+ logVerbose(`[AgentsMd] Executing ${promptKey} discovery prompt`)
365
+
366
+ try {
367
+ const response = await client.messages.create({
368
+ max_tokens: 2000,
369
+ messages: [
370
+ {
371
+ role: 'user',
372
+ content: 'Analyze the project and provide the requested analysis.',
373
+ },
374
+ ],
375
+ system: DISCOVERY_PROMPTS[promptKey],
376
+ })
377
+
378
+ const content = response.content[0]
379
+ const result = content && content.type === 'text' ? content.text.trim() : ''
380
+
381
+ logVerbose(`[AgentsMd] Completed ${promptKey} discovery: ${result.substring(0, 100)}...`)
382
+ return result
383
+ } catch (error) {
384
+ logVerboseError(
385
+ `[AgentsMd] Failed ${promptKey} discovery: ${error instanceof Error ? error.message : error}`
386
+ )
387
+ return `Error analyzing ${promptKey}: ${error instanceof Error ? error.message : error}`
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Execute parallel agent-based project scanning
393
+ */
394
+ async function executeParallelScanning(
395
+ projectPath: string,
396
+ config: HoneConfig,
397
+ agent?: AgentType
398
+ ): Promise<Record<keyof typeof DISCOVERY_PROMPTS, string>> {
399
+ const promptKeys = Object.keys(DISCOVERY_PROMPTS) as (keyof typeof DISCOVERY_PROMPTS)[]
400
+
401
+ logVerbose(`[AgentsMd] Starting parallel scanning with ${promptKeys.length} discovery prompts`)
402
+
403
+ // Execute all discovery prompts in parallel to stay within 90-second limit
404
+ const results = await Promise.all(
405
+ promptKeys.map(async key => ({
406
+ key,
407
+ result: await executeDiscoveryPrompt(projectPath, key, config, agent),
408
+ }))
409
+ )
410
+
411
+ // Convert results array to object
412
+ const scanResults = Object.fromEntries(results.map(({ key, result }) => [key, result])) as Record<
413
+ keyof typeof DISCOVERY_PROMPTS,
414
+ string
415
+ >
416
+
417
+ logVerbose(`[AgentsMd] Parallel scanning completed with results for: ${promptKeys.join(', ')}`)
418
+ return scanResults
419
+ }
420
+
421
+ /**
422
+ * Create adaptive template sections based on discovered tech stack
423
+ */
424
+ function createTemplateSections(
425
+ scanResults: Record<keyof typeof DISCOVERY_PROMPTS, string>,
426
+ analysis: ProjectAnalysis
427
+ ): TemplateSection[] {
428
+ const sections: TemplateSection[] = []
429
+
430
+ // Build content using agent discovery results with static analysis fallback
431
+ const getContentWithFallback = (agentResult: string | undefined, fallbackData: string[]) => {
432
+ if (
433
+ agentResult &&
434
+ !agentResult.includes('failed to analyze') &&
435
+ !agentResult.includes('Error:')
436
+ ) {
437
+ return agentResult
438
+ }
439
+ return fallbackData.length > 0
440
+ ? `Static analysis detected: ${fallbackData.join(', ')}`
441
+ : 'Not available.'
442
+ }
443
+
444
+ // High priority sections (always include)
445
+ sections.push({
446
+ title: 'Project Overview',
447
+ content: getContentWithFallback(scanResults.languages || '', analysis.languages),
448
+ priority: 1,
449
+ detailFile: 'languages.md',
450
+ })
451
+
452
+ sections.push({
453
+ title: 'Build System',
454
+ content: getContentWithFallback(scanResults.buildSystems || '', analysis.buildSystems),
455
+ priority: 2,
456
+ detailFile: 'build.md',
457
+ })
458
+
459
+ // Medium priority sections (include if they have significant content)
460
+ const testingContent = getContentWithFallback(
461
+ scanResults.testing || '',
462
+ analysis.testingFrameworks
463
+ )
464
+ if (testingContent && !testingContent.includes('Not available')) {
465
+ sections.push({
466
+ title: 'Testing Framework',
467
+ content: testingContent,
468
+ priority: 3,
469
+ detailFile: 'testing.md',
470
+ })
471
+ }
472
+
473
+ const architectureContent = getContentWithFallback(
474
+ scanResults.architecture || '',
475
+ analysis.architecture
476
+ )
477
+ if (architectureContent && !architectureContent.includes('Not available')) {
478
+ sections.push({
479
+ title: 'Architecture',
480
+ content: architectureContent,
481
+ priority: 4,
482
+ detailFile: 'architecture.md',
483
+ })
484
+ }
485
+
486
+ // Lower priority sections (include if space allows)
487
+ const deploymentContent = scanResults.deployment || ''
488
+ if (deploymentContent && !deploymentContent.includes('not available')) {
489
+ sections.push({
490
+ title: 'Deployment',
491
+ content: deploymentContent,
492
+ priority: 5,
493
+ detailFile: 'deployment.md',
494
+ })
495
+ }
496
+
497
+ // Sort by priority
498
+ return sections.sort((a, b) => a.priority - b.priority)
499
+ }
500
+
501
+ /**
502
+ * Count lines in text content
503
+ */
504
+ function countLines(text: string): number {
505
+ return text.split('\n').length
506
+ }
507
+
508
+ /**
509
+ * Generate compact AGENTS.md content that fits within 100-line limit
510
+ */
511
+ function generateCompactContent(sections: TemplateSection[], useAgentsDir: boolean): string {
512
+ const header = `# AGENTS.md
513
+
514
+ Learnings and patterns for future agents working on this project.
515
+ `
516
+
517
+ if (!useAgentsDir) {
518
+ // Full content in main file
519
+ const fullSections = sections
520
+ .map(
521
+ section => `## ${section.title}
522
+
523
+ ${section.content}
524
+ `
525
+ )
526
+ .join('\n')
527
+
528
+ return (
529
+ header +
530
+ '\n' +
531
+ fullSections +
532
+ `
533
+ ---
534
+
535
+ *This AGENTS.md was generated using agent-based project discovery.*
536
+ `
537
+ )
538
+ }
539
+
540
+ // Compact version with references to ${AGENTS_DOCS_DIR}/ files
541
+ const compactSections = sections
542
+ .map(
543
+ section => `## ${section.title}
544
+
545
+ ${getFirstSentence(section.content)}
546
+
547
+ See [@${AGENTS_DOCS_DIR}/${section.detailFile}](${AGENTS_DOCS_DIR}/${section.detailFile}) for detailed information.
548
+ `
549
+ )
550
+ .join('\n')
551
+
552
+ return (
553
+ header +
554
+ '\n' +
555
+ compactSections +
556
+ `
557
+ ---
558
+
559
+ *This AGENTS.md was generated using agent-based project discovery.*
560
+ *Detailed information is available in the ${AGENTS_DOCS_DIR}/ directory.*
561
+ `
562
+ )
563
+ }
564
+
565
+ /**
566
+ * Extract concise, informative summary from agent-generated content
567
+ * Skips unhelpful preambles like "Based on my analysis..."
568
+ */
569
+ function getFirstSentence(content: string): string {
570
+ if (!content) return 'Information not available.'
571
+
572
+ // Skip common unhelpful agent preambles - comprehensive list of patterns
573
+ const skipPatterns = [
574
+ /^Based on (?:my |the )?(?:comprehensive |detailed |thorough )?(?:analysis|exploration|examination|review|investigation).*?[:,]\s*/gi,
575
+ /^(?:Here's|Here is).*?(?:analysis|overview|summary|breakdown).*?[:,]\s*/gi,
576
+ /^I(?:'ve|'ll| have| will).*?(?:analyze|explore|examine|review).*?[:,]\s*/gi,
577
+ /^(?:Looking|Examining|Reviewing|Analyzing) (?:at )?(?:the |this )?(?:project|codebase|code).*?[:,]\s*/gi,
578
+ /^After (?:analyzing|examining|reviewing|exploring).*?[:,]\s*/gi,
579
+ /^Let me (?:analyze|explore|examine|review).*?[:,]\s*/gi,
580
+ /^Upon (?:analysis|examination|review|exploration).*?[:,]\s*/gi,
581
+ /^(?:The |This )?(?:analysis|exploration|examination) (?:shows|reveals|indicates).*?[:,]\s*/gi,
582
+ ]
583
+
584
+ let cleanContent = content.trim()
585
+
586
+ // Remove matching preamble patterns (including following whitespace/newlines)
587
+ for (const pattern of skipPatterns) {
588
+ cleanContent = cleanContent.replace(pattern, '').trim()
589
+ }
590
+
591
+ // Look for structured information markers (uppercase patterns)
592
+ const lines = cleanContent.split('\n').filter(line => line.trim())
593
+
594
+ // Try to find lines with structured info like "**KEY**: value" or "KEY: value"
595
+ for (const line of lines) {
596
+ const trimmed = line.trim()
597
+ // Match **UPPERCASE**: value or UPPERCASE: value patterns
598
+ if (trimmed.match(/^\*\*[A-Z][A-Z\s_-]+\*\*\s*:|^[A-Z][A-Z\s_-]+:/)) {
599
+ if (trimmed.length <= 150) {
600
+ return trimmed
601
+ }
602
+ // If too long, extract just the key and first part of value
603
+ const colonIndex = trimmed.indexOf(':')
604
+ if (colonIndex > 0) {
605
+ const key = trimmed.substring(0, colonIndex + 1)
606
+ const value = trimmed.substring(colonIndex + 1).trim()
607
+ const shortValue = value.split(/[,;]/)[0]?.trim() || value.substring(0, 80)
608
+ return `${key} ${shortValue}`
609
+ }
610
+ }
611
+ }
612
+
613
+ // Try to get the first meaningful line that isn't a preamble
614
+ for (const line of lines) {
615
+ const trimmed = line.trim()
616
+ // Skip lines that look like preambles
617
+ if (
618
+ trimmed.toLowerCase().startsWith('based on') ||
619
+ trimmed.toLowerCase().startsWith("here's") ||
620
+ trimmed.toLowerCase().startsWith('here is') ||
621
+ trimmed.toLowerCase().startsWith('i ') ||
622
+ trimmed.toLowerCase().startsWith("i'") ||
623
+ trimmed.toLowerCase().startsWith('the analysis') ||
624
+ trimmed.toLowerCase().startsWith('looking at') ||
625
+ trimmed.toLowerCase().startsWith('after ')
626
+ ) {
627
+ continue
628
+ }
629
+ if (trimmed.length > 0 && trimmed.length <= 150) {
630
+ return trimmed
631
+ }
632
+ }
633
+
634
+ // If first line is acceptable, use it
635
+ const firstLine = lines[0]?.trim() ?? ''
636
+ if (firstLine.length > 0 && firstLine.length <= 150) {
637
+ return firstLine
638
+ }
639
+
640
+ // If first line is too long, try to get first sentence
641
+ const sentences = cleanContent.split(/[.!?]+/)
642
+ if (sentences.length > 0 && sentences[0]?.trim().length && sentences[0].trim().length <= 150) {
643
+ return sentences[0].trim() + '.'
644
+ }
645
+
646
+ // Fallback: truncate to reasonable length
647
+ return cleanContent.substring(0, 150).trim() + '...'
648
+ }
649
+
650
+ /**
651
+ * Generate AGENTS.md content based on agent-based discovery
652
+ */
653
+ async function generateContent(
654
+ projectPath: string,
655
+ analysis: ProjectAnalysis,
656
+ config: HoneConfig,
657
+ agent?: AgentType
658
+ ): Promise<{ mainContent: string; detailSections?: TemplateSection[]; useAgentsDir: boolean }> {
659
+ log('\nPhase 2: Agent Discovery')
660
+ log('-'.repeat(80))
661
+
662
+ process.stdout.write('Executing agent-based project discovery... ')
663
+
664
+ try {
665
+ // Execute parallel agent scanning for comprehensive project analysis
666
+ const scanResults = await executeParallelScanning(projectPath, config, agent)
667
+ process.stdout.write('✓\n')
668
+
669
+ logVerbose(
670
+ `[AgentsMd] Discovery completed with ${Object.keys(scanResults).length} analysis areas`
671
+ )
672
+
673
+ log('\nPhase 3: Content Generation')
674
+ log('-'.repeat(80))
675
+
676
+ // Create adaptive template sections based on discovered tech stack
677
+ const sections = createTemplateSections(scanResults, analysis)
678
+
679
+ // Generate initial content to check line count
680
+ const fullContent = generateCompactContent(sections, false)
681
+ const lineCount = countLines(fullContent)
682
+
683
+ logVerbose(`[AgentsMd] Generated content has ${lineCount} lines (limit: 100)`)
684
+
685
+ // Decide whether to use ${AGENTS_DOCS_DIR}/ subdirectory based on content length and complexity
686
+ const useAgentsDir = lineCount > 100 || sections.length > 5
687
+
688
+ if (useAgentsDir) {
689
+ if (lineCount > 100) {
690
+ log(
691
+ `Content exceeds 100-line limit. Creating ${AGENTS_DOCS_DIR}/ subdirectory for detailed information.`
692
+ )
693
+ } else {
694
+ log(
695
+ `Project has complex structure. Creating ${AGENTS_DOCS_DIR}/ subdirectory for better organization.`
696
+ )
697
+ }
698
+ }
699
+
700
+ logVerbose(`[AgentsMd] Using ${AGENTS_DOCS_DIR}/ directory: ${useAgentsDir}`)
701
+
702
+ const mainContent = generateCompactContent(sections, useAgentsDir)
703
+
704
+ return {
705
+ mainContent,
706
+ detailSections: useAgentsDir ? sections : undefined,
707
+ useAgentsDir,
708
+ }
709
+ } catch (error) {
710
+ process.stdout.write('✗\n')
711
+ throw error
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Main function to generate AGENTS.md documentation
717
+ */
718
+ export async function generateAgentsMd(
719
+ options: AgentsMdGeneratorOptions = {}
720
+ ): Promise<GenerationResult> {
721
+ const projectPath = options.projectPath || process.cwd()
722
+
723
+ try {
724
+ log('Phase 1: Project Analysis')
725
+ log('-'.repeat(80))
726
+
727
+ process.stdout.write('Analyzing project structure... ')
728
+ const analysis = await analyzeProject(projectPath)
729
+ process.stdout.write('✓\n')
730
+
731
+ process.stdout.write('Loading configuration... ')
732
+ const config = await loadConfig()
733
+ process.stdout.write('✓\n')
734
+
735
+ logVerbose(
736
+ `[AgentsMd] Found ${analysis.languages.length} languages, ${analysis.buildSystems.length} build systems, ${analysis.testingFrameworks.length} testing frameworks`
737
+ )
738
+
739
+ // Check if AGENTS.md already exists and handle accordingly
740
+ const agentsPath = join(projectPath, 'AGENTS.md')
741
+ const existingAgentsDirPath = join(projectPath, AGENTS_DOCS_DIR)
742
+
743
+ if (existsSync(agentsPath)) {
744
+ if (!options.overwrite) {
745
+ log('\n• AGENTS.md already exists. Use --overwrite to replace it.')
746
+ return {
747
+ success: false,
748
+ filesCreated: [],
749
+ error: new Error('AGENTS.md already exists'),
750
+ }
751
+ } else {
752
+ log('• AGENTS.md exists. Overwriting with new content.')
753
+ }
754
+ }
755
+
756
+ // Also check for existing ${AGENTS_DOCS_DIR}/ directory and inform user
757
+ if (existsSync(existingAgentsDirPath)) {
758
+ if (!options.overwrite) {
759
+ logVerbose(
760
+ `[AgentsMd] ${AGENTS_DOCS_DIR}/ directory already exists. Detail files will be skipped unless --overwrite is used.`
761
+ )
762
+ } else {
763
+ logVerbose(
764
+ `[AgentsMd] ${AGENTS_DOCS_DIR}/ directory exists. Detail files will be overwritten.`
765
+ )
766
+ }
767
+ }
768
+
769
+ const { mainContent, detailSections, useAgentsDir } = await generateContent(
770
+ projectPath,
771
+ analysis,
772
+ config,
773
+ options.agent
774
+ )
775
+
776
+ log('\nPhase 4: File Generation')
777
+ log('-'.repeat(80))
778
+
779
+ process.stdout.write('Writing AGENTS.md file... ')
780
+
781
+ // Preserve existing gotchas/learnings if overwriting
782
+ let finalContent = mainContent
783
+ if (options.overwrite && existsSync(agentsPath)) {
784
+ try {
785
+ const existingContent = await readFile(agentsPath, 'utf-8')
786
+ const preservedContent = extractPreservableContent(existingContent)
787
+ if (preservedContent) {
788
+ finalContent = `${mainContent}\n\n<!-- PRESERVED CONTENT FROM PREVIOUS VERSION -->\n${preservedContent}`
789
+ logVerbose('[AgentsMd] Preserved existing gotchas/learnings from previous AGENTS.md')
790
+ }
791
+ } catch (error) {
792
+ logVerboseError(`[AgentsMd] Could not preserve existing content: ${error}`)
793
+ }
794
+ }
795
+
796
+ await writeFile(agentsPath, finalContent, 'utf-8')
797
+ process.stdout.write('✓\n')
798
+
799
+ const filesCreated = [agentsPath]
800
+
801
+ // Create ${AGENTS_DOCS_DIR}/ directory and detail files if needed
802
+ let agentsDirPath: string | undefined
803
+ let detailFilesCreated = 0
804
+
805
+ if (useAgentsDir && detailSections) {
806
+ agentsDirPath = join(projectPath, AGENTS_DOCS_DIR)
807
+
808
+ // Handle existing ${AGENTS_DOCS_DIR}/ directory
809
+ if (existsSync(agentsDirPath)) {
810
+ if (!options.overwrite) {
811
+ log(
812
+ `• ${AGENTS_DOCS_DIR}/ directory already exists. Use --overwrite to replace existing detail files.`
813
+ )
814
+ } else {
815
+ log(`• ${AGENTS_DOCS_DIR}/ directory exists. Overwriting existing detail files.`)
816
+ }
817
+ } else {
818
+ await mkdir(agentsDirPath, { recursive: true })
819
+ logVerbose(`[AgentsMd] Created ${AGENTS_DOCS_DIR}/ directory for detailed information`)
820
+ }
821
+
822
+ process.stdout.write(`Creating ${detailSections.length} detail files... `)
823
+
824
+ // Write detail files
825
+ for (const section of detailSections) {
826
+ if (section.detailFile) {
827
+ const detailPath = join(agentsDirPath, section.detailFile)
828
+
829
+ // Check if detail file already exists
830
+ if (existsSync(detailPath) && !options.overwrite) {
831
+ logVerbose(`[AgentsMd] Skipping existing detail file: ${section.detailFile}`)
832
+ continue
833
+ }
834
+
835
+ try {
836
+ const detailContent = `# ${section.title}
837
+
838
+ ${section.content}
839
+
840
+ ---
841
+
842
+ *This file is part of the AGENTS.md documentation system.*
843
+ `
844
+ await writeFile(detailPath, detailContent, 'utf-8')
845
+ filesCreated.push(detailPath)
846
+ detailFilesCreated++
847
+ logVerbose(
848
+ `[AgentsMd] ${existsSync(detailPath) && options.overwrite ? 'Updated' : 'Created'} detail file: ${section.detailFile}`
849
+ )
850
+ } catch (fileError) {
851
+ logVerboseError(
852
+ `[AgentsMd] Failed to write detail file ${section.detailFile}: ${fileError instanceof Error ? fileError.message : fileError}`
853
+ )
854
+ // Continue with other files rather than failing completely
855
+ }
856
+ }
857
+ }
858
+
859
+ process.stdout.write('✓\n')
860
+ }
861
+
862
+ // Success message with details
863
+ log('')
864
+ if (useAgentsDir && detailSections) {
865
+ log(`✓ Generated AGENTS.md with ${detailSections.length} sections`)
866
+ log(`✓ Created ${detailFilesCreated} detail files in ${AGENTS_DOCS_DIR}/`)
867
+ } else {
868
+ log(
869
+ `✓ Generated AGENTS.md with ${analysis.languages.length + analysis.buildSystems.length + analysis.testingFrameworks.length} detected components`
870
+ )
871
+ }
872
+
873
+ // Next steps guidance
874
+ log('')
875
+ log('Next steps:')
876
+ log(' 1. Review the generated AGENTS.md for accuracy')
877
+ if (useAgentsDir) {
878
+ log(` 2. Check detailed information in the ${AGENTS_DOCS_DIR}/ directory`)
879
+ }
880
+ log(' 3. Edit and customize the documentation as needed')
881
+ log(' 4. Commit the changes to your repository')
882
+
883
+ return {
884
+ success: true,
885
+ mainFilePath: agentsPath,
886
+ agentsDirPath,
887
+ filesCreated,
888
+ }
889
+ } catch (error) {
890
+ const err = error instanceof Error ? error : new Error(String(error))
891
+ logError('\n✗ Failed to generate AGENTS.md')
892
+ logError(`Error: ${err.message}`)
893
+
894
+ return {
895
+ success: false,
896
+ filesCreated: [],
897
+ error: err,
898
+ }
899
+ }
900
+ }