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 CHANGED
@@ -10,6 +10,8 @@ Transform feature ideas into working code through autonomous development with hu
10
10
 
11
11
  ```bash
12
12
  npm install -g hone-ai
13
+ # or
14
+ bun add -g hone-ai
13
15
  ```
14
16
 
15
17
  2. **Install an AI agent** ([OpenCode](https://opencode.ai) or [Claude Code](https://docs.anthropic.com/claude/docs/claude-code))
@@ -41,6 +43,8 @@ That's it! hone will implement the feature, run tests, and commit changes automa
41
43
 
42
44
  ```bash
43
45
  npm install -g hone-ai
46
+ # or
47
+ bun add -g hone-ai
44
48
  ```
45
49
 
46
50
  ### From Source
@@ -55,11 +59,22 @@ Use `bun src/index.ts` instead of `hone` for all commands.
55
59
 
56
60
  ### Standalone Binary
57
61
 
58
- Build a self-contained executable:
62
+ Download pre-built binaries from [GitHub Releases](https://github.com/oskarhane/hone-ai/releases).
63
+
64
+ **macOS users**: Remove the quarantine attribute after downloading:
65
+
66
+ ```bash
67
+ unzip hone-v*-macos.zip
68
+ xattr -d com.apple.quarantine hone-v*-macos/hone
69
+ cp hone-v*-macos/hone /usr/local/bin/
70
+ ```
71
+
72
+ Or build from source:
59
73
 
60
74
  ```bash
61
75
  bun run build
62
- cp hone /usr/local/bin/
76
+ cp hone-macos /usr/local/bin/hone # macOS
77
+ cp hone-linux /usr/local/bin/hone # Linux
63
78
  ```
64
79
 
65
80
  ## Common Commands
@@ -72,6 +87,20 @@ hone prd-to-tasks .plans/prd-feature.md # Generate tasks
72
87
  hone run .plans/tasks-feature.yml -i 5 # Implement tasks
73
88
  ```
74
89
 
90
+ ### Reference files and URLs in PRDs
91
+
92
+ ```bash
93
+ # Reference local files in your PRD description
94
+ hone prd "Implement user authentication based on ./docs/auth-spec.md"
95
+ hone prd "Add dashboard following the component in ./src/components/Dashboard.tsx"
96
+
97
+ # Reference URLs for external specifications
98
+ hone prd "Create payment integration using https://stripe.com/docs/api"
99
+ hone prd "Build social login with https://developers.google.com/identity/protocols/oauth2"
100
+ ```
101
+
102
+ hone automatically reads referenced files and fetches URL content to inform PRD generation.
103
+
75
104
  ### Check progress
76
105
 
77
106
  ```bash
@@ -114,6 +143,22 @@ hone breaks feature development into 3 phases:
114
143
 
115
144
  Each `hone run` executes multiple iterations of this cycle automatically.
116
145
 
146
+ ### File and URL References in PRDs
147
+
148
+ When creating PRDs, you can reference files and URLs directly in your feature description:
149
+
150
+ **Local files:**
151
+ - `./docs/api-spec.md` - Read project documentation
152
+ - `src/components/Button.tsx` - Analyze existing components
153
+ - `./database/schema.sql` - Review database structure
154
+
155
+ **URLs:**
156
+ - `https://docs.stripe.com/api` - External API documentation
157
+ - `https://www.figma.com/design/123/App` - Design specifications
158
+ - `http://localhost:3000/dashboard` - Reference existing pages
159
+
160
+ The AI agent automatically reads files and fetches web content to generate more accurate PRDs.
161
+
117
162
  ## File Structure
118
163
 
119
164
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hone-ai",
3
- "version": "0.5.0",
3
+ "version": "0.10.0",
4
4
  "description": "AI coding agent orchestrator - orchestrate AI agents to implement features based on PRDs",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,7 +33,10 @@
33
33
  "hone": "src/index.ts"
34
34
  },
35
35
  "scripts": {
36
- "build": "bun build --compile --minify --sourcemap ./src/index.ts --outfile hone",
36
+ "build": "bun run build:linux && bun run build:macos",
37
+ "format": "prettier --write \"**/*.ts\"",
38
+ "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-x64 ./src/index.ts --outfile hone-linux",
39
+ "build:macos": "bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile hone-macos",
37
40
  "format:yaml": "prettier --write \"**/*.yml\" \"**/*.yaml\"",
38
41
  "lint:yaml": "yamllint -c .yamllint.yml **/*.yml **/*.yaml",
39
42
  "check:yaml": "bun run lint:yaml && prettier --check \"**/*.yml\" \"**/*.yaml\""
@@ -1,97 +1,95 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { AgentClient, type AgentMessageRequest } from './agent-client';
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { AgentClient, type AgentMessageRequest } from './agent-client'
3
3
 
4
4
  describe('AgentClient Integration', () => {
5
5
  // Note: These tests verify the API surface is correct.
6
6
  // Actual agent subprocess spawning is tested manually or in CI
7
7
  // where real agent binaries (opencode/claude) are available.
8
-
8
+
9
9
  describe('API compatibility', () => {
10
10
  test('mirrors Anthropic SDK interface', () => {
11
11
  const client = new AgentClient({
12
12
  agent: 'opencode',
13
- model: 'claude-sonnet-4-20250514'
14
- });
15
-
13
+ model: 'claude-sonnet-4-20250514',
14
+ })
15
+
16
16
  // Verify the API surface matches Anthropic SDK
17
- expect(client.messages).toBeDefined();
18
- expect(typeof client.messages.create).toBe('function');
19
-
17
+ expect(client.messages).toBeDefined()
18
+ expect(typeof client.messages.create).toBe('function')
19
+
20
20
  // Verify request structure matches Anthropic SDK
21
21
  const validRequest: AgentMessageRequest = {
22
22
  model: 'claude-sonnet-4-20250514',
23
23
  max_tokens: 4000,
24
- messages: [
25
- { role: 'user', content: 'Test message' }
26
- ],
27
- system: 'You are a helpful assistant'
28
- };
29
-
24
+ messages: [{ role: 'user', content: 'Test message' }],
25
+ system: 'You are a helpful assistant',
26
+ }
27
+
30
28
  // This shouldn't throw TypeScript errors
31
- expect(validRequest.messages).toHaveLength(1);
32
- });
33
-
29
+ expect(validRequest.messages).toHaveLength(1)
30
+ })
31
+
34
32
  test('supports minimal request format', () => {
35
33
  const client = new AgentClient({
36
34
  agent: 'claude',
37
- model: 'claude-sonnet-4-20250514'
38
- });
39
-
35
+ model: 'claude-sonnet-4-20250514',
36
+ })
37
+
40
38
  // Minimal valid request
41
39
  const minimalRequest: AgentMessageRequest = {
42
- messages: [{ role: 'user', content: 'Hello' }]
43
- };
44
-
45
- expect(minimalRequest.messages).toHaveLength(1);
46
- });
47
-
40
+ messages: [{ role: 'user', content: 'Hello' }],
41
+ }
42
+
43
+ expect(minimalRequest.messages).toHaveLength(1)
44
+ })
45
+
48
46
  test('supports multi-turn conversations', () => {
49
47
  const client = new AgentClient({
50
48
  agent: 'opencode',
51
- model: 'claude-sonnet-4-20250514'
52
- });
53
-
49
+ model: 'claude-sonnet-4-20250514',
50
+ })
51
+
54
52
  const multiTurnRequest: AgentMessageRequest = {
55
53
  messages: [
56
54
  { role: 'user', content: 'What is 2+2?' },
57
55
  { role: 'assistant', content: '4' },
58
- { role: 'user', content: 'What about 3+3?' }
59
- ]
60
- };
61
-
62
- expect(multiTurnRequest.messages).toHaveLength(3);
63
- });
64
- });
65
-
56
+ { role: 'user', content: 'What about 3+3?' },
57
+ ],
58
+ }
59
+
60
+ expect(multiTurnRequest.messages).toHaveLength(3)
61
+ })
62
+ })
63
+
66
64
  describe('configuration', () => {
67
65
  test('accepts opencode agent configuration', () => {
68
66
  const client = new AgentClient({
69
67
  agent: 'opencode',
70
68
  model: 'claude-opus-4-20250514',
71
- workingDir: '/custom/path'
72
- });
73
-
74
- expect(client).toBeDefined();
75
- expect(client.messages.create).toBeDefined();
76
- });
77
-
69
+ workingDir: '/custom/path',
70
+ })
71
+
72
+ expect(client).toBeDefined()
73
+ expect(client.messages.create).toBeDefined()
74
+ })
75
+
78
76
  test('accepts claude agent configuration', () => {
79
77
  const client = new AgentClient({
80
78
  agent: 'claude',
81
- model: 'claude-sonnet-4-20250514'
82
- });
83
-
84
- expect(client).toBeDefined();
85
- expect(client.messages.create).toBeDefined();
86
- });
87
-
79
+ model: 'claude-sonnet-4-20250514',
80
+ })
81
+
82
+ expect(client).toBeDefined()
83
+ expect(client.messages.create).toBeDefined()
84
+ })
85
+
88
86
  test('accepts optional model in config', () => {
89
87
  // Model can be omitted if it will be provided per-request
90
88
  const client = new AgentClient({
91
- agent: 'opencode'
92
- });
93
-
94
- expect(client).toBeDefined();
95
- });
96
- });
97
- });
89
+ agent: 'opencode',
90
+ })
91
+
92
+ expect(client).toBeDefined()
93
+ })
94
+ })
95
+ })
@@ -1,43 +1,43 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { AgentClient } from './agent-client';
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { AgentClient } from './agent-client'
3
3
 
4
4
  describe('AgentClient', () => {
5
5
  describe('constructor', () => {
6
6
  test('creates client with minimal config', () => {
7
7
  const client = new AgentClient({
8
8
  agent: 'opencode',
9
- model: 'claude-sonnet-4-20250514'
10
- });
11
-
12
- expect(client).toBeDefined();
13
- expect(client.messages).toBeDefined();
14
- expect(typeof client.messages.create).toBe('function');
15
- });
16
-
9
+ model: 'claude-sonnet-4-20250514',
10
+ })
11
+
12
+ expect(client).toBeDefined()
13
+ expect(client.messages).toBeDefined()
14
+ expect(typeof client.messages.create).toBe('function')
15
+ })
16
+
17
17
  test('creates client with full config', () => {
18
18
  const client = new AgentClient({
19
19
  agent: 'claude',
20
20
  model: 'claude-opus-4-20250514',
21
- workingDir: '/custom/path'
22
- });
23
-
24
- expect(client).toBeDefined();
25
- expect(client.messages).toBeDefined();
26
- });
27
- });
28
-
21
+ workingDir: '/custom/path',
22
+ })
23
+
24
+ expect(client).toBeDefined()
25
+ expect(client.messages).toBeDefined()
26
+ })
27
+ })
28
+
29
29
  describe('messages API', () => {
30
30
  test('provides messages.create method', () => {
31
31
  const client = new AgentClient({
32
32
  agent: 'opencode',
33
- model: 'claude-sonnet-4-20250514'
34
- });
35
-
36
- expect(client.messages.create).toBeDefined();
37
- expect(typeof client.messages.create).toBe('function');
38
- });
39
- });
40
-
33
+ model: 'claude-sonnet-4-20250514',
34
+ })
35
+
36
+ expect(client.messages.create).toBeDefined()
37
+ expect(typeof client.messages.create).toBe('function')
38
+ })
39
+ })
40
+
41
41
  // Note: Integration tests with actual agent spawning would test:
42
42
  // - Prompt construction with system messages
43
43
  // - Model parameter passing to spawnAgent
@@ -45,4 +45,4 @@ describe('AgentClient', () => {
45
45
  // - Error handling for non-zero exit codes
46
46
  // - Retry logic with retryWithBackoff
47
47
  // These are covered in integration tests that mock child_process
48
- });
48
+ })
@@ -3,26 +3,32 @@
3
3
  * Mirrors Anthropic SDK API but routes through agent subprocess spawning
4
4
  */
5
5
 
6
- import { spawnAgent } from './agent';
7
- import { retryWithBackoff, parseAgentError, ErrorMessages, exitWithError, isNetworkError } from './errors';
8
- import type { AgentType } from './config';
9
- import { logVerbose, logVerboseError } from './logger';
6
+ import { spawnAgent } from './agent'
7
+ import {
8
+ retryWithBackoff,
9
+ parseAgentError,
10
+ ErrorMessages,
11
+ exitWithError,
12
+ isNetworkError,
13
+ } from './errors'
14
+ import type { AgentType } from './config'
15
+ import { logVerbose, logVerboseError } from './logger'
10
16
 
11
17
  export interface AgentClientConfig {
12
- agent: AgentType;
13
- model?: string;
14
- workingDir?: string;
18
+ agent: AgentType
19
+ model?: string
20
+ workingDir?: string
15
21
  }
16
22
 
17
23
  export interface AgentMessageRequest {
18
- model?: string;
19
- max_tokens?: number;
20
- messages: Array<{ role: 'user' | 'assistant'; content: string }>;
21
- system?: string;
24
+ model?: string
25
+ max_tokens?: number
26
+ messages: Array<{ role: 'user' | 'assistant'; content: string }>
27
+ system?: string
22
28
  }
23
29
 
24
30
  export interface AgentMessageResponse {
25
- content: Array<{ type: 'text'; text: string }>;
31
+ content: Array<{ type: 'text'; text: string }>
26
32
  }
27
33
 
28
34
  /**
@@ -30,12 +36,12 @@ export interface AgentMessageResponse {
30
36
  * Routes message creation through agent subprocess calls
31
37
  */
32
38
  export class AgentClient {
33
- private config: AgentClientConfig;
34
-
39
+ private config: AgentClientConfig
40
+
35
41
  constructor(config: AgentClientConfig) {
36
- this.config = config;
42
+ this.config = config
37
43
  }
38
-
44
+
39
45
  /**
40
46
  * Messages API with create method
41
47
  * Mirrors Anthropic client.messages.create() interface
@@ -44,19 +50,23 @@ export class AgentClient {
44
50
  return {
45
51
  create: async (request: AgentMessageRequest): Promise<AgentMessageResponse> => {
46
52
  // Use model from request, fall back to client config
47
- const model = request.model || this.config.model;
48
-
53
+ const model = request.model || this.config.model
54
+
49
55
  // Log agent request initiation
50
- logVerbose(`[AgentClient] Initiating request to ${this.config.agent}${model ? ` with model ${model}` : ''}`);
51
-
56
+ logVerbose(
57
+ `[AgentClient] Initiating request to ${this.config.agent}${model ? ` with model ${model}` : ''}`
58
+ )
59
+
52
60
  // Construct prompt from request
53
- const prompt = constructPromptFromRequest(request);
54
-
61
+ const prompt = constructPromptFromRequest(request)
62
+
55
63
  // Log request details
56
- const messageCount = request.messages.length;
57
- const hasSystem = !!request.system;
58
- logVerbose(`[AgentClient] Request: ${messageCount} message(s)${hasSystem ? ' + system prompt' : ''}`);
59
-
64
+ const messageCount = request.messages.length
65
+ const hasSystem = !!request.system
66
+ logVerbose(
67
+ `[AgentClient] Request: ${messageCount} message(s)${hasSystem ? ' + system prompt' : ''}`
68
+ )
69
+
60
70
  // Execute with retry logic for network errors only
61
71
  try {
62
72
  const result = await retryWithBackoff(async () => {
@@ -66,63 +76,83 @@ export class AgentClient {
66
76
  workingDir: this.config.workingDir || process.cwd(),
67
77
  model,
68
78
  silent: true,
69
- timeout: 120000 // 2 minute timeout for PRD questions
70
- });
71
-
79
+ timeout: 300000, // 5 minute timeout for PRD questions
80
+ })
81
+
72
82
  // Only retry network errors, throw immediately for other failures
73
83
  if (spawnResult.exitCode !== 0) {
74
84
  // Parse error type from stderr
75
- const errorInfo = parseAgentError(spawnResult.stderr, spawnResult.exitCode);
76
- logVerboseError(`[AgentClient] Agent exited with code ${spawnResult.exitCode}, error type: ${errorInfo.type}`);
77
-
85
+ const errorInfo = parseAgentError(spawnResult.stderr, spawnResult.exitCode)
86
+ logVerboseError(
87
+ `[AgentClient] Agent exited with code ${spawnResult.exitCode}, error type: ${errorInfo.type}`
88
+ )
89
+
78
90
  // Handle specific error types with user-friendly messages
79
91
  if (errorInfo.type === 'model_unavailable') {
80
- const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(model || 'unknown', this.config.agent);
81
- exitWithError(message, details);
92
+ const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(
93
+ model || 'unknown',
94
+ this.config.agent
95
+ )
96
+ exitWithError(message, details)
82
97
  } else if (errorInfo.type === 'rate_limit') {
83
- const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(this.config.agent, errorInfo.retryAfter);
84
- exitWithError(message, details);
98
+ const { message, details } = ErrorMessages.RATE_LIMIT_ERROR(
99
+ this.config.agent,
100
+ errorInfo.retryAfter
101
+ )
102
+ exitWithError(message, details)
85
103
  } else if (errorInfo.type === 'spawn_failed') {
86
- const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(this.config.agent, spawnResult.stderr);
87
- exitWithError(message, details);
104
+ const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED(
105
+ this.config.agent,
106
+ spawnResult.stderr
107
+ )
108
+ exitWithError(message, details)
88
109
  } else if (errorInfo.type === 'timeout') {
89
- const timeoutMs = 120000; // Default timeout
90
- const { message, details } = ErrorMessages.AGENT_TIMEOUT(this.config.agent, timeoutMs);
91
- exitWithError(message, details);
110
+ const timeoutMs = 300000 // Default timeout
111
+ const { message, details } = ErrorMessages.AGENT_TIMEOUT(
112
+ this.config.agent,
113
+ timeoutMs
114
+ )
115
+ exitWithError(message, details)
92
116
  }
93
-
117
+
94
118
  // For network errors, allow retry
95
119
  if (errorInfo.retryable) {
96
- logVerbose(`[AgentClient] Network error detected, will retry`);
97
- throw new Error(`Agent exited with code ${spawnResult.exitCode}: ${spawnResult.stderr}`);
120
+ logVerbose(`[AgentClient] Network error detected, will retry`)
121
+ throw new Error(
122
+ `Agent exited with code ${spawnResult.exitCode}: ${spawnResult.stderr}`
123
+ )
98
124
  }
99
-
125
+
100
126
  // For other unknown errors, provide generic agent error message
101
- const { message, details } = ErrorMessages.AGENT_ERROR(this.config.agent, spawnResult.exitCode, spawnResult.stderr);
102
- exitWithError(message, details);
127
+ const { message, details } = ErrorMessages.AGENT_ERROR(
128
+ this.config.agent,
129
+ spawnResult.exitCode,
130
+ spawnResult.stderr
131
+ )
132
+ exitWithError(message, details)
103
133
  }
104
-
105
- logVerbose(`[AgentClient] Request completed successfully`);
106
- return spawnResult;
107
- });
108
-
134
+
135
+ logVerbose(`[AgentClient] Request completed successfully`)
136
+ return spawnResult
137
+ })
138
+
109
139
  // Log response details
110
- const responseLength = result.stdout.trim().length;
111
- logVerbose(`[AgentClient] Response: ${responseLength} characters`);
112
-
140
+ const responseLength = result.stdout.trim().length
141
+ logVerbose(`[AgentClient] Response: ${responseLength} characters`)
142
+
113
143
  // Parse response into Anthropic-compatible format
114
- return parseAgentResponse(result.stdout);
144
+ return parseAgentResponse(result.stdout)
115
145
  } catch (error) {
116
146
  // Network errors that exhausted retries
117
147
  if (error instanceof Error && error.message.includes('Agent exited with code')) {
118
- logVerboseError(`[AgentClient] All retry attempts exhausted`);
119
- const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error);
120
- exitWithError(message, details);
148
+ logVerboseError(`[AgentClient] All retry attempts exhausted`)
149
+ const { message, details } = ErrorMessages.NETWORK_ERROR_FINAL(error)
150
+ exitWithError(message, details)
121
151
  }
122
- throw error;
152
+ throw error
123
153
  }
124
- }
125
- };
154
+ },
155
+ }
126
156
  }
127
157
  }
128
158
 
@@ -131,26 +161,26 @@ export class AgentClient {
131
161
  * Converts Anthropic message format to agent prompt string
132
162
  */
133
163
  function constructPromptFromRequest(request: AgentMessageRequest): string {
134
- const parts: string[] = [];
135
-
164
+ const parts: string[] = []
165
+
136
166
  // Add system prompt if present
137
167
  if (request.system) {
138
- parts.push('# System');
139
- parts.push(request.system);
140
- parts.push('');
168
+ parts.push('# System')
169
+ parts.push(request.system)
170
+ parts.push('')
141
171
  }
142
-
172
+
143
173
  // Add conversation messages
144
174
  for (const message of request.messages) {
145
175
  if (message.role === 'user') {
146
- parts.push(message.content);
176
+ parts.push(message.content)
147
177
  } else {
148
178
  // Handle assistant messages (for multi-turn conversations)
149
- parts.push(`Previous response: ${message.content}`);
179
+ parts.push(`Previous response: ${message.content}`)
150
180
  }
151
181
  }
152
-
153
- return parts.join('\n');
182
+
183
+ return parts.join('\n')
154
184
  }
155
185
 
156
186
  /**
@@ -158,9 +188,11 @@ function constructPromptFromRequest(request: AgentMessageRequest): string {
158
188
  */
159
189
  function parseAgentResponse(stdout: string): AgentMessageResponse {
160
190
  return {
161
- content: [{
162
- type: 'text',
163
- text: stdout.trim()
164
- }]
165
- };
191
+ content: [
192
+ {
193
+ type: 'text',
194
+ text: stdout.trim(),
195
+ },
196
+ ],
197
+ }
166
198
  }
package/src/agent.test.ts CHANGED
@@ -1,26 +1,26 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { isAgentAvailable } from './agent';
1
+ import { describe, test, expect } from 'bun:test'
2
+ import { isAgentAvailable } from './agent'
3
3
 
4
4
  describe('Agent utilities', () => {
5
5
  describe('isAgentAvailable', () => {
6
6
  test('should check if claude is available', async () => {
7
- const available = await isAgentAvailable('claude');
8
- expect(typeof available).toBe('boolean');
9
- });
10
-
7
+ const available = await isAgentAvailable('claude')
8
+ expect(typeof available).toBe('boolean')
9
+ })
10
+
11
11
  test('should check if opencode is available', async () => {
12
- const available = await isAgentAvailable('opencode');
13
- expect(typeof available).toBe('boolean');
14
- });
15
- });
16
-
12
+ const available = await isAgentAvailable('opencode')
13
+ expect(typeof available).toBe('boolean')
14
+ })
15
+ })
16
+
17
17
  describe('spawnAgent', () => {
18
18
  // Note: spawnAgent tests require actual agent binaries to be installed
19
19
  // and would spawn interactive processes. Testing is done manually or via
20
20
  // integration tests with mocked child_process.
21
21
  test('should export spawnAgent function', () => {
22
- const { spawnAgent } = require('./agent');
23
- expect(typeof spawnAgent).toBe('function');
24
- });
25
- });
26
- });
22
+ const { spawnAgent } = require('./agent')
23
+ expect(typeof spawnAgent).toBe('function')
24
+ })
25
+ })
26
+ })