hone-ai 0.5.0 → 0.10.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.
- package/README.md +47 -2
- package/package.json +5 -2
- package/src/agent-client.integration.test.ts +57 -59
- package/src/agent-client.test.ts +27 -27
- package/src/agent-client.ts +109 -77
- package/src/agent.test.ts +16 -16
- package/src/agent.ts +103 -103
- package/src/agents-md-generator.test.ts +360 -0
- package/src/agents-md-generator.ts +900 -0
- package/src/config.test.ts +209 -224
- package/src/config.ts +84 -83
- package/src/errors.test.ts +211 -208
- package/src/errors.ts +107 -101
- package/src/index.integration.test.ts +327 -223
- package/src/index.ts +163 -100
- package/src/integration-test.ts +168 -137
- package/src/logger.test.ts +67 -67
- package/src/logger.ts +8 -8
- package/src/prd-generator.integration.test.ts +50 -50
- package/src/prd-generator.test.ts +66 -25
- package/src/prd-generator.ts +280 -194
- package/src/prds.test.ts +60 -65
- package/src/prds.ts +64 -62
- package/src/prompt.test.ts +154 -155
- package/src/prompt.ts +63 -65
- package/src/run.ts +147 -147
- package/src/status.test.ts +80 -80
- package/src/status.ts +40 -42
- package/src/task-generator.test.ts +93 -66
- package/src/task-generator.ts +125 -112
package/src/agent.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from 'child_process'
|
|
2
|
-
import type { AgentType } from './config'
|
|
3
|
-
import { exitWithError, ErrorMessages } from './errors'
|
|
4
|
-
import { logVerbose, logVerboseError } from './logger'
|
|
1
|
+
import { spawn, type ChildProcess } from 'child_process'
|
|
2
|
+
import type { AgentType } from './config'
|
|
3
|
+
import { exitWithError, ErrorMessages } from './errors'
|
|
4
|
+
import { logVerbose, logVerboseError } from './logger'
|
|
5
5
|
|
|
6
6
|
export interface SpawnAgentOptions {
|
|
7
|
-
agent: AgentType
|
|
8
|
-
prompt: string
|
|
9
|
-
workingDir?: string
|
|
10
|
-
model?: string
|
|
11
|
-
silent?: boolean
|
|
12
|
-
timeout?: number
|
|
7
|
+
agent: AgentType
|
|
8
|
+
prompt: string
|
|
9
|
+
workingDir?: string
|
|
10
|
+
model?: string
|
|
11
|
+
silent?: boolean
|
|
12
|
+
timeout?: number // Timeout in milliseconds
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface SpawnAgentResult {
|
|
16
|
-
exitCode: number
|
|
17
|
-
stdout: string
|
|
18
|
-
stderr: string
|
|
16
|
+
exitCode: number
|
|
17
|
+
stdout: string
|
|
18
|
+
stderr: string
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -24,155 +24,155 @@ export interface SpawnAgentResult {
|
|
|
24
24
|
* @returns Promise resolving to exit code and captured output
|
|
25
25
|
*/
|
|
26
26
|
export async function spawnAgent(options: SpawnAgentOptions): Promise<SpawnAgentResult> {
|
|
27
|
-
const { agent, prompt, workingDir = process.cwd(), model, silent = false, timeout } = options
|
|
28
|
-
|
|
27
|
+
const { agent, prompt, workingDir = process.cwd(), model, silent = false, timeout } = options
|
|
28
|
+
|
|
29
29
|
// Log agent spawn initiation
|
|
30
|
-
logVerbose(`[Agent] Spawning ${agent} agent${model ? ` with model ${model}` : ''}`)
|
|
31
|
-
logVerbose(`[Agent] Working directory: ${workingDir}`)
|
|
32
|
-
|
|
30
|
+
logVerbose(`[Agent] Spawning ${agent} agent${model ? ` with model ${model}` : ''}`)
|
|
31
|
+
logVerbose(`[Agent] Working directory: ${workingDir}`)
|
|
32
|
+
|
|
33
33
|
// Build command and args based on agent type
|
|
34
34
|
// opencode: opencode run [--model anthropic/<model>] "prompt text"
|
|
35
35
|
// claude: claude -p "prompt text" [--model <model>]
|
|
36
|
-
const command = agent === 'opencode' ? 'opencode' : 'claude'
|
|
37
|
-
const args: string[] = []
|
|
38
|
-
|
|
36
|
+
const command = agent === 'opencode' ? 'opencode' : 'claude'
|
|
37
|
+
const args: string[] = []
|
|
38
|
+
|
|
39
39
|
if (agent === 'opencode') {
|
|
40
|
-
args.push('run')
|
|
40
|
+
args.push('run')
|
|
41
41
|
if (model) {
|
|
42
|
-
args.push('--model', `anthropic/${model}`)
|
|
42
|
+
args.push('--model', `anthropic/${model}`)
|
|
43
43
|
}
|
|
44
|
-
args.push(prompt)
|
|
44
|
+
args.push(prompt)
|
|
45
45
|
} else {
|
|
46
|
-
args.push('-p', prompt)
|
|
46
|
+
args.push('-p', prompt)
|
|
47
47
|
if (model) {
|
|
48
|
-
args.push('--model', model)
|
|
48
|
+
args.push('--model', model)
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
// Log command being executed
|
|
53
|
-
const cmdString = `${command} ${args.slice(0, -1).join(' ')} "<prompt>"
|
|
54
|
-
logVerbose(`[Agent] Command: ${cmdString}`)
|
|
55
|
-
|
|
53
|
+
const cmdString = `${command} ${args.slice(0, -1).join(' ')} "<prompt>"`
|
|
54
|
+
logVerbose(`[Agent] Command: ${cmdString}`)
|
|
55
|
+
|
|
56
56
|
return new Promise((resolve, reject) => {
|
|
57
57
|
const child: ChildProcess = spawn(command, args, {
|
|
58
58
|
cwd: workingDir,
|
|
59
|
-
stdio: ['inherit', 'pipe', 'pipe']
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
let stdout = ''
|
|
63
|
-
let stderr = ''
|
|
64
|
-
let isKilled = false
|
|
65
|
-
let timeoutId: NodeJS.Timeout | undefined
|
|
66
|
-
|
|
59
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
let stdout = ''
|
|
63
|
+
let stderr = ''
|
|
64
|
+
let isKilled = false
|
|
65
|
+
let timeoutId: NodeJS.Timeout | undefined
|
|
66
|
+
|
|
67
67
|
// Stream stdout to console and capture
|
|
68
68
|
if (child.stdout) {
|
|
69
69
|
child.stdout.on('data', (data: Buffer) => {
|
|
70
|
-
const text = data.toString()
|
|
70
|
+
const text = data.toString()
|
|
71
71
|
if (!silent) {
|
|
72
|
-
process.stdout.write(text)
|
|
72
|
+
process.stdout.write(text)
|
|
73
73
|
}
|
|
74
|
-
stdout += text
|
|
75
|
-
})
|
|
74
|
+
stdout += text
|
|
75
|
+
})
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// Stream stderr to console and capture
|
|
79
79
|
if (child.stderr) {
|
|
80
80
|
child.stderr.on('data', (data: Buffer) => {
|
|
81
|
-
const text = data.toString()
|
|
81
|
+
const text = data.toString()
|
|
82
82
|
if (!silent) {
|
|
83
|
-
process.stderr.write(text)
|
|
83
|
+
process.stderr.write(text)
|
|
84
84
|
}
|
|
85
|
-
stderr += text
|
|
86
|
-
})
|
|
85
|
+
stderr += text
|
|
86
|
+
})
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
// Set up timeout if specified
|
|
90
90
|
if (timeout && timeout > 0) {
|
|
91
91
|
timeoutId = setTimeout(() => {
|
|
92
92
|
if (!isKilled && child.pid) {
|
|
93
|
-
logVerboseError(`[Agent] Process timed out after ${timeout}ms, killing...`)
|
|
94
|
-
isKilled = true
|
|
93
|
+
logVerboseError(`[Agent] Process timed out after ${timeout}ms, killing...`)
|
|
94
|
+
isKilled = true
|
|
95
95
|
try {
|
|
96
|
-
process.kill(-child.pid, 'SIGTERM')
|
|
96
|
+
process.kill(-child.pid, 'SIGTERM')
|
|
97
97
|
} catch (err) {
|
|
98
|
-
child.kill('SIGTERM')
|
|
98
|
+
child.kill('SIGTERM')
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
}, timeout)
|
|
101
|
+
}, timeout)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Handle SIGINT (ctrl+c) and SIGTERM to kill child process
|
|
105
105
|
const handleSignal = (signal: NodeJS.Signals) => {
|
|
106
106
|
if (!isKilled && child.pid) {
|
|
107
|
-
isKilled = true
|
|
107
|
+
isKilled = true
|
|
108
108
|
if (timeoutId) {
|
|
109
|
-
clearTimeout(timeoutId)
|
|
110
|
-
timeoutId = undefined
|
|
109
|
+
clearTimeout(timeoutId)
|
|
110
|
+
timeoutId = undefined
|
|
111
111
|
}
|
|
112
112
|
// Kill the child process group
|
|
113
113
|
try {
|
|
114
|
-
process.kill(-child.pid, signal)
|
|
114
|
+
process.kill(-child.pid, signal)
|
|
115
115
|
} catch (err) {
|
|
116
116
|
// Fallback to killing just the child
|
|
117
|
-
child.kill(signal)
|
|
117
|
+
child.kill(signal)
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
process.on('SIGINT', handleSignal)
|
|
123
|
-
process.on('SIGTERM', handleSignal)
|
|
124
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.on('SIGINT', handleSignal)
|
|
123
|
+
process.on('SIGTERM', handleSignal)
|
|
124
|
+
|
|
125
125
|
// Handle process exit
|
|
126
126
|
child.on('close', (code: number | null) => {
|
|
127
127
|
// Clean up signal handlers and timeout
|
|
128
|
-
process.off('SIGINT', handleSignal)
|
|
129
|
-
process.off('SIGTERM', handleSignal)
|
|
128
|
+
process.off('SIGINT', handleSignal)
|
|
129
|
+
process.off('SIGTERM', handleSignal)
|
|
130
130
|
if (timeoutId) {
|
|
131
|
-
clearTimeout(timeoutId)
|
|
132
|
-
timeoutId = undefined
|
|
131
|
+
clearTimeout(timeoutId)
|
|
132
|
+
timeoutId = undefined
|
|
133
133
|
}
|
|
134
|
-
|
|
135
|
-
const exitCode = code ?? 1
|
|
136
|
-
|
|
134
|
+
|
|
135
|
+
const exitCode = code ?? 1
|
|
136
|
+
|
|
137
137
|
// Check if process was killed due to timeout
|
|
138
138
|
if (isKilled && timeout) {
|
|
139
|
-
logVerboseError(`[Agent] Process was terminated due to timeout (${timeout}ms)`)
|
|
139
|
+
logVerboseError(`[Agent] Process was terminated due to timeout (${timeout}ms)`)
|
|
140
140
|
resolve({
|
|
141
141
|
exitCode: 124, // Standard timeout exit code
|
|
142
142
|
stdout,
|
|
143
|
-
stderr: stderr + '\nProcess timed out'
|
|
144
|
-
})
|
|
145
|
-
return
|
|
143
|
+
stderr: stderr + '\nProcess timed out',
|
|
144
|
+
})
|
|
145
|
+
return
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
// Log completion status
|
|
149
149
|
if (exitCode === 0) {
|
|
150
|
-
logVerbose(`[Agent] Process completed successfully (exit code 0)`)
|
|
150
|
+
logVerbose(`[Agent] Process completed successfully (exit code 0)`)
|
|
151
151
|
} else {
|
|
152
|
-
logVerboseError(`[Agent] Process exited with code ${exitCode}`)
|
|
152
|
+
logVerboseError(`[Agent] Process exited with code ${exitCode}`)
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
resolve({
|
|
156
156
|
exitCode,
|
|
157
157
|
stdout,
|
|
158
|
-
stderr
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
|
|
158
|
+
stderr,
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
162
|
// Handle spawn errors (e.g., command not found)
|
|
163
163
|
child.on('error', (error: Error) => {
|
|
164
164
|
// Clean up signal handlers and timeout
|
|
165
|
-
process.off('SIGINT', handleSignal)
|
|
166
|
-
process.off('SIGTERM', handleSignal)
|
|
165
|
+
process.off('SIGINT', handleSignal)
|
|
166
|
+
process.off('SIGTERM', handleSignal)
|
|
167
167
|
if (timeoutId) {
|
|
168
|
-
clearTimeout(timeoutId)
|
|
169
|
-
timeoutId = undefined
|
|
168
|
+
clearTimeout(timeoutId)
|
|
169
|
+
timeoutId = undefined
|
|
170
170
|
}
|
|
171
|
-
|
|
172
|
-
logVerboseError(`[Agent] Spawn error: ${error.message}`)
|
|
173
|
-
reject(new Error(`Failed to spawn ${agent}: ${error.message}`))
|
|
174
|
-
})
|
|
175
|
-
})
|
|
171
|
+
|
|
172
|
+
logVerboseError(`[Agent] Spawn error: ${error.message}`)
|
|
173
|
+
reject(new Error(`Failed to spawn ${agent}: ${error.message}`))
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -181,19 +181,19 @@ export async function spawnAgent(options: SpawnAgentOptions): Promise<SpawnAgent
|
|
|
181
181
|
* @returns Promise resolving to true if agent is available
|
|
182
182
|
*/
|
|
183
183
|
export async function isAgentAvailable(agent: AgentType): Promise<boolean> {
|
|
184
|
-
const command = agent === 'opencode' ? 'opencode' : 'claude'
|
|
185
|
-
|
|
186
|
-
return new Promise(
|
|
184
|
+
const command = agent === 'opencode' ? 'opencode' : 'claude'
|
|
185
|
+
|
|
186
|
+
return new Promise(resolve => {
|
|
187
187
|
const child = spawn(command === 'opencode' ? 'which' : 'which', [command], {
|
|
188
|
-
stdio: 'ignore'
|
|
189
|
-
})
|
|
190
|
-
|
|
188
|
+
stdio: 'ignore',
|
|
189
|
+
})
|
|
190
|
+
|
|
191
191
|
child.on('close', (code: number | null) => {
|
|
192
|
-
resolve(code === 0)
|
|
193
|
-
})
|
|
194
|
-
|
|
192
|
+
resolve(code === 0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
195
|
child.on('error', () => {
|
|
196
|
-
resolve(false)
|
|
197
|
-
})
|
|
198
|
-
})
|
|
196
|
+
resolve(false)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
199
|
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach, mock } from 'bun:test'
|
|
2
|
+
import { generateAgentsMd, AGENTS_DOCS_DIR } from './agents-md-generator'
|
|
3
|
+
import type { AgentsMdGeneratorOptions, GenerationResult } from './agents-md-generator'
|
|
4
|
+
import { existsSync, mkdirSync, rmSync } from 'fs'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import * as fs from 'fs/promises'
|
|
7
|
+
|
|
8
|
+
// Mock AgentClient
|
|
9
|
+
import { AgentClient } from './agent-client'
|
|
10
|
+
|
|
11
|
+
const mockAgentResponse = {
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: 'text' as const,
|
|
15
|
+
text: 'PRIMARY LANGUAGES: JavaScript, TypeScript\nUSAGE CONTEXT: TypeScript for main application code, JavaScript for configuration files',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const mockAgentClient = {
|
|
21
|
+
messages: {
|
|
22
|
+
create: mock(async () => mockAgentResponse),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Mock the AgentClient constructor
|
|
27
|
+
mock.module('./agent-client', () => ({
|
|
28
|
+
AgentClient: mock(() => mockAgentClient),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
// Test workspace setup
|
|
32
|
+
const TEST_WORKSPACE = join(process.cwd(), '.test-agents-md-workspace')
|
|
33
|
+
const originalCwd = process.cwd()
|
|
34
|
+
|
|
35
|
+
// Mock console and logger functions
|
|
36
|
+
const originalLog = console.log
|
|
37
|
+
const originalError = console.error
|
|
38
|
+
let logCalls: string[] = []
|
|
39
|
+
let errorCalls: string[] = []
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
// Reset call tracking
|
|
43
|
+
logCalls = []
|
|
44
|
+
errorCalls = []
|
|
45
|
+
|
|
46
|
+
// Create isolated test workspace
|
|
47
|
+
if (existsSync(TEST_WORKSPACE)) {
|
|
48
|
+
rmSync(TEST_WORKSPACE, { recursive: true, force: true })
|
|
49
|
+
}
|
|
50
|
+
mkdirSync(TEST_WORKSPACE, { recursive: true })
|
|
51
|
+
|
|
52
|
+
// Change to test workspace
|
|
53
|
+
process.chdir(TEST_WORKSPACE)
|
|
54
|
+
|
|
55
|
+
// Mock console functions
|
|
56
|
+
console.log = mock((message: string) => {
|
|
57
|
+
logCalls.push(message)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
console.error = mock((message: string) => {
|
|
61
|
+
errorCalls.push(message)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
// Restore console functions
|
|
67
|
+
console.log = originalLog
|
|
68
|
+
console.error = originalError
|
|
69
|
+
|
|
70
|
+
// Return to original directory and clean up
|
|
71
|
+
process.chdir(originalCwd)
|
|
72
|
+
if (existsSync(TEST_WORKSPACE)) {
|
|
73
|
+
rmSync(TEST_WORKSPACE, { recursive: true, force: true })
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('agents-md-generator', () => {
|
|
78
|
+
test('generateAgentsMd returns proper result structure', async () => {
|
|
79
|
+
const result: GenerationResult = await generateAgentsMd()
|
|
80
|
+
|
|
81
|
+
expect(result).toHaveProperty('success')
|
|
82
|
+
expect(result).toHaveProperty('filesCreated')
|
|
83
|
+
expect(typeof result.success).toBe('boolean')
|
|
84
|
+
expect(Array.isArray(result.filesCreated)).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('generateAgentsMd generates content with basic project analysis', async () => {
|
|
88
|
+
const result: GenerationResult = await generateAgentsMd()
|
|
89
|
+
|
|
90
|
+
if (result.success && result.mainFilePath) {
|
|
91
|
+
expect(existsSync(result.mainFilePath)).toBe(true)
|
|
92
|
+
const content = await fs.readFile(result.mainFilePath, 'utf-8')
|
|
93
|
+
expect(content).toContain('# AGENTS.md')
|
|
94
|
+
expect(content).toContain('## Project Overview')
|
|
95
|
+
expect(content).toContain('## Build System')
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('generateAgentsMd accepts custom project path', async () => {
|
|
100
|
+
const options: AgentsMdGeneratorOptions = {
|
|
101
|
+
projectPath: process.cwd(),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = await generateAgentsMd(options)
|
|
105
|
+
expect(typeof result.success).toBe('boolean')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('generateAgentsMd respects overwrite option when file exists', async () => {
|
|
109
|
+
// First generation should succeed
|
|
110
|
+
const firstResult = await generateAgentsMd()
|
|
111
|
+
expect(firstResult.success).toBe(true)
|
|
112
|
+
|
|
113
|
+
// Second generation without overwrite should fail
|
|
114
|
+
const secondResult = await generateAgentsMd()
|
|
115
|
+
expect(secondResult.success).toBe(false)
|
|
116
|
+
expect(secondResult.error?.message).toContain('already exists')
|
|
117
|
+
|
|
118
|
+
// Third generation with overwrite should succeed
|
|
119
|
+
const thirdResult = await generateAgentsMd({ overwrite: true })
|
|
120
|
+
expect(thirdResult.success).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('generateAgentsMd handles errors gracefully', async () => {
|
|
124
|
+
// Test with invalid project path
|
|
125
|
+
const result = await generateAgentsMd({ projectPath: '/nonexistent/path' })
|
|
126
|
+
|
|
127
|
+
// Should handle errors gracefully and return error result
|
|
128
|
+
if (!result.success) {
|
|
129
|
+
expect(result.error).toBeInstanceOf(Error)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('generateAgentsMd outputs expected log messages', async () => {
|
|
134
|
+
await generateAgentsMd()
|
|
135
|
+
|
|
136
|
+
expect(logCalls.some(msg => msg.includes('Phase 1: Project Analysis'))).toBe(true)
|
|
137
|
+
expect(logCalls.some(msg => msg.includes('✓ Generated AGENTS.md'))).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test(`generateAgentsMd creates ${AGENTS_DOCS_DIR}/ directory when content exceeds 100 lines`, async () => {
|
|
141
|
+
// Mock a response that will generate a very long output
|
|
142
|
+
const longMockResponse = {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text' as const,
|
|
146
|
+
text:
|
|
147
|
+
'PRIMARY LANGUAGES: JavaScript, TypeScript, Python, Java, Go, Rust, PHP, Ruby, C++, C#, Swift, Kotlin, Scala, Clojure, Elixir, Erlang, Haskell, OCaml, F#, R, MATLAB, Lua, Perl, Shell\n'.repeat(
|
|
148
|
+
20
|
|
149
|
+
) +
|
|
150
|
+
'This is a very detailed analysis that will definitely exceed the 100-line limit when combined with other sections. ' +
|
|
151
|
+
'It includes extensive information about the project structure, dependencies, build systems, testing frameworks, and deployment strategies. ' +
|
|
152
|
+
`The content is intentionally verbose to trigger the ${AGENTS_DOCS_DIR}/ subdirectory creation logic.`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Mock agent client to return long content
|
|
158
|
+
const longMockAgentClient = {
|
|
159
|
+
messages: {
|
|
160
|
+
create: mock(async () => longMockResponse),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Replace the mock temporarily
|
|
165
|
+
const originalMock = mockAgentClient.messages.create
|
|
166
|
+
mockAgentClient.messages.create = longMockAgentClient.messages.create
|
|
167
|
+
|
|
168
|
+
const result = await generateAgentsMd()
|
|
169
|
+
|
|
170
|
+
// Restore original mock
|
|
171
|
+
mockAgentClient.messages.create = originalMock
|
|
172
|
+
|
|
173
|
+
expect(result.success).toBe(true)
|
|
174
|
+
|
|
175
|
+
// Check if ${AGENTS_DOCS_DIR}/ directory was created
|
|
176
|
+
if (result.agentsDirPath) {
|
|
177
|
+
expect(existsSync(result.agentsDirPath)).toBe(true)
|
|
178
|
+
expect(result.filesCreated.length).toBeGreaterThan(1) // Main file + detail files
|
|
179
|
+
expect(logCalls.some(msg => msg.includes('exceeds 100-line limit'))).toBe(true)
|
|
180
|
+
expect(logCalls.some(msg => msg.includes('✓ Created') && msg.includes('detail files'))).toBe(
|
|
181
|
+
true
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test(`generateAgentsMd creates compact content with references when using ${AGENTS_DOCS_DIR}/ directory`, async () => {
|
|
187
|
+
// Create a package.json with many dependencies to ensure we have content
|
|
188
|
+
await fs.writeFile(
|
|
189
|
+
'package.json',
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
name: 'test-project',
|
|
192
|
+
scripts: { build: 'tsc', test: 'jest' },
|
|
193
|
+
dependencies: {
|
|
194
|
+
react: '^18.0.0',
|
|
195
|
+
typescript: '^5.0.0',
|
|
196
|
+
jest: '^29.0.0',
|
|
197
|
+
express: '^4.18.0',
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
'utf-8'
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const result = await generateAgentsMd()
|
|
204
|
+
expect(result.success).toBe(true)
|
|
205
|
+
|
|
206
|
+
if (result.mainFilePath) {
|
|
207
|
+
const content = await fs.readFile(result.mainFilePath, 'utf-8')
|
|
208
|
+
|
|
209
|
+
// Should contain section headers
|
|
210
|
+
expect(content).toContain('## Project Overview')
|
|
211
|
+
expect(content).toContain('## Build System')
|
|
212
|
+
|
|
213
|
+
// If ${AGENTS_DOCS_DIR}/ directory was used, should contain references
|
|
214
|
+
if (result.agentsDirPath && existsSync(result.agentsDirPath)) {
|
|
215
|
+
expect(content).toContain(AGENTS_DOCS_DIR)
|
|
216
|
+
expect(content).toContain('for detailed information')
|
|
217
|
+
|
|
218
|
+
// Check that detail files were created
|
|
219
|
+
const detailFiles = ['languages.md', 'build.md']
|
|
220
|
+
for (const file of detailFiles) {
|
|
221
|
+
const detailPath = join(result.agentsDirPath, file)
|
|
222
|
+
if (existsSync(detailPath)) {
|
|
223
|
+
const detailContent = await fs.readFile(detailPath, 'utf-8')
|
|
224
|
+
expect(detailContent).toContain('# ')
|
|
225
|
+
expect(detailContent).toContain('part of the AGENTS.md documentation system')
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('generateAgentsMd handles project with TypeScript configuration', async () => {
|
|
233
|
+
// Create TypeScript project files
|
|
234
|
+
await fs.writeFile(
|
|
235
|
+
'tsconfig.json',
|
|
236
|
+
JSON.stringify({
|
|
237
|
+
compilerOptions: {
|
|
238
|
+
target: 'ES2020',
|
|
239
|
+
module: 'commonjs',
|
|
240
|
+
strict: true,
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
'utf-8'
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
await fs.writeFile(
|
|
247
|
+
'package.json',
|
|
248
|
+
JSON.stringify({
|
|
249
|
+
name: 'typescript-project',
|
|
250
|
+
scripts: { build: 'tsc' },
|
|
251
|
+
devDependencies: {
|
|
252
|
+
typescript: '^5.0.0',
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
'utf-8'
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const result = await generateAgentsMd()
|
|
259
|
+
expect(result.success).toBe(true)
|
|
260
|
+
|
|
261
|
+
if (result.mainFilePath) {
|
|
262
|
+
const content = await fs.readFile(result.mainFilePath, 'utf-8')
|
|
263
|
+
expect(content).toContain('## Project Overview')
|
|
264
|
+
expect(content).toContain('## Build System')
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test(`generateAgentsMd handles existing ${AGENTS_DOCS_DIR}/ directory properly`, async () => {
|
|
269
|
+
// Create existing ${AGENTS_DOCS_DIR}/ directory with a file
|
|
270
|
+
const agentsDir = join(process.cwd(), AGENTS_DOCS_DIR)
|
|
271
|
+
if (!existsSync(agentsDir)) {
|
|
272
|
+
mkdirSync(agentsDir, { recursive: true })
|
|
273
|
+
}
|
|
274
|
+
await fs.writeFile(join(agentsDir, 'existing.md'), 'Existing content', 'utf-8')
|
|
275
|
+
|
|
276
|
+
// First generation without overwrite
|
|
277
|
+
const result1 = await generateAgentsMd()
|
|
278
|
+
expect(result1.success).toBe(true)
|
|
279
|
+
|
|
280
|
+
// Second generation with overwrite
|
|
281
|
+
const result2 = await generateAgentsMd({ overwrite: true })
|
|
282
|
+
expect(result2.success).toBe(true)
|
|
283
|
+
|
|
284
|
+
// Verify existing file is still there (we don't delete unrelated files)
|
|
285
|
+
expect(existsSync(join(agentsDir, 'existing.md'))).toBe(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test(`generateAgentsMd creates ${AGENTS_DOCS_DIR}/ directory for complex projects`, async () => {
|
|
289
|
+
// Create a project with many sections to trigger ${AGENTS_DOCS_DIR}/ creation
|
|
290
|
+
await fs.writeFile(
|
|
291
|
+
'package.json',
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
name: 'complex-project',
|
|
294
|
+
scripts: {
|
|
295
|
+
build: 'tsc',
|
|
296
|
+
test: 'jest',
|
|
297
|
+
deploy: 'docker build',
|
|
298
|
+
lint: 'eslint',
|
|
299
|
+
},
|
|
300
|
+
dependencies: {
|
|
301
|
+
react: '^18.0.0',
|
|
302
|
+
express: '^4.18.0',
|
|
303
|
+
typescript: '^5.0.0',
|
|
304
|
+
jest: '^29.0.0',
|
|
305
|
+
eslint: '^8.0.0',
|
|
306
|
+
docker: '^1.0.0',
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
'utf-8'
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// Create multiple config files
|
|
313
|
+
await fs.writeFile('Dockerfile', 'FROM node:18', 'utf-8')
|
|
314
|
+
await fs.writeFile('docker-compose.yml', 'version: "3"', 'utf-8')
|
|
315
|
+
|
|
316
|
+
const result = await generateAgentsMd()
|
|
317
|
+
expect(result.success).toBe(true)
|
|
318
|
+
|
|
319
|
+
// Should trigger ${AGENTS_DOCS_DIR}/ directory creation due to complexity
|
|
320
|
+
if (result.agentsDirPath) {
|
|
321
|
+
expect(existsSync(result.agentsDirPath)).toBe(true)
|
|
322
|
+
expect(result.filesCreated.length).toBeGreaterThan(1)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('should generate AGENTS.md without unhelpful agent preambles', async () => {
|
|
327
|
+
// Test that generated AGENTS.md files don't contain "Based on my..." text
|
|
328
|
+
// This validates the fix for task-011/012 where agent preambles were cluttering the output
|
|
329
|
+
|
|
330
|
+
// Create minimal test project
|
|
331
|
+
await fs.writeFile('package.json', JSON.stringify({ name: 'test-project' }), 'utf-8')
|
|
332
|
+
|
|
333
|
+
const result = await generateAgentsMd({ overwrite: true })
|
|
334
|
+
expect(result.success).toBe(true)
|
|
335
|
+
|
|
336
|
+
if (result.success && result.mainFilePath) {
|
|
337
|
+
const content = await fs.readFile(result.mainFilePath, 'utf-8')
|
|
338
|
+
|
|
339
|
+
// Verify that unhelpful agent preambles are NOT in the AGENTS.md summary
|
|
340
|
+
// Comprehensive check for all common preamble patterns
|
|
341
|
+
expect(content).not.toMatch(/Based on my analysis.*?here's.*?/i)
|
|
342
|
+
expect(content).not.toMatch(/Based on my architectural analysis.*?/i)
|
|
343
|
+
expect(content).not.toMatch(/Based on my exploration.*?/i)
|
|
344
|
+
expect(content).not.toMatch(/Based on my comprehensive analysis.*?/i)
|
|
345
|
+
expect(content).not.toMatch(/Based on the.*?analysis.*?/i)
|
|
346
|
+
expect(content).not.toMatch(/Here's.*?analysis.*?:/i)
|
|
347
|
+
expect(content).not.toMatch(/Here's what I found.*?:/i)
|
|
348
|
+
expect(content).not.toMatch(/I've analyzed.*?:/i)
|
|
349
|
+
expect(content).not.toMatch(/I'll analyze.*?:/i)
|
|
350
|
+
expect(content).not.toMatch(/Looking at the project.*?:/i)
|
|
351
|
+
expect(content).not.toMatch(/After analyzing.*?:/i)
|
|
352
|
+
expect(content).not.toMatch(/Upon examination.*?:/i)
|
|
353
|
+
expect(content).not.toMatch(/Let me analyze.*?:/i)
|
|
354
|
+
|
|
355
|
+
// The content should have meaningful section headers
|
|
356
|
+
expect(content).toMatch(/## Project Overview/)
|
|
357
|
+
expect(content).toMatch(/## Build System/)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
})
|