vibe-forge 0.1.0 → 0.3.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/bin/forge.sh CHANGED
@@ -13,6 +13,11 @@
13
13
  # forge daemon - Start/stop the background daemon
14
14
  # forge help - Show help
15
15
  #
16
+ # Security Note:
17
+ # This script uses --dangerously-skip-permissions for Claude Code to enable
18
+ # seamless agent startup. This is intentional for the forge workflow.
19
+ # See docs/security.md for details.
20
+ #
16
21
 
17
22
  set -e
18
23
 
@@ -20,99 +25,24 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
25
  FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
21
26
  CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
22
27
 
23
- # Colors
24
- RED='\033[0;31m'
25
- GREEN='\033[0;32m'
26
- YELLOW='\033[1;33m'
27
- BLUE='\033[0;34m'
28
- NC='\033[0m'
29
-
30
28
  # =============================================================================
31
- # Helper Functions
29
+ # Load Shared Libraries
32
30
  # =============================================================================
33
31
 
34
- load_config() {
35
- if [[ ! -f "$CONFIG_FILE" ]]; then
36
- echo -e "${RED}Error: Vibe Forge not initialized.${NC}"
37
- echo "Run 'forge init' first."
38
- exit 1
39
- fi
40
-
41
- # Parse JSON config (basic extraction)
42
- PLATFORM=$(grep -o '"platform": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
43
- GIT_BASH_PATH=$(grep -o '"git_bash_path": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
44
-
45
- # Set environment for Windows
46
- # Claude Code on Windows needs backslashes in the path
47
- if [[ "$PLATFORM" == "windows" && -n "$GIT_BASH_PATH" ]]; then
48
- # Convert forward slashes to backslashes for Windows
49
- GIT_BASH_PATH_WIN="${GIT_BASH_PATH//\//\\}"
50
- export CLAUDE_CODE_GIT_BASH_PATH="$GIT_BASH_PATH_WIN"
51
-
52
- # Add common Windows paths that Git Bash might miss
53
- # npm global path (where claude is typically installed)
54
- NPM_PATH="/c/Users/$USER/AppData/Roaming/npm"
55
- if [[ -d "$NPM_PATH" ]] && [[ ":$PATH:" != *":$NPM_PATH:"* ]]; then
56
- export PATH="$NPM_PATH:$PATH"
57
- fi
58
-
59
- # Also try with USERPROFILE
60
- if [[ -n "$USERPROFILE" ]]; then
61
- NPM_PATH_ALT="$USERPROFILE/AppData/Roaming/npm"
62
- NPM_PATH_ALT="${NPM_PATH_ALT//\\//}" # Convert backslashes
63
- if [[ -d "$NPM_PATH_ALT" ]] && [[ ":$PATH:" != *":$NPM_PATH_ALT:"* ]]; then
64
- export PATH="$NPM_PATH_ALT:$PATH"
65
- fi
66
- fi
67
- fi
68
- }
69
-
70
- get_personality_path() {
71
- local agent="$1"
72
- local personality_file=""
73
-
74
- case "$agent" in
75
- "hub"|"planning"|"master"|"forge-master"|"")
76
- personality_file="$FORGE_ROOT/agents/planning-hub/personality.md"
77
- ;;
78
- "anvil")
79
- personality_file="$FORGE_ROOT/agents/anvil/personality.md"
80
- ;;
81
- "furnace")
82
- personality_file="$FORGE_ROOT/agents/furnace/personality.md"
83
- ;;
84
- "crucible")
85
- personality_file="$FORGE_ROOT/agents/crucible/personality.md"
86
- ;;
87
- "sentinel")
88
- personality_file="$FORGE_ROOT/agents/sentinel/personality.md"
89
- ;;
90
- "scribe")
91
- personality_file="$FORGE_ROOT/agents/scribe/personality.md"
92
- ;;
93
- "herald")
94
- personality_file="$FORGE_ROOT/agents/herald/personality.md"
95
- ;;
96
- "ember")
97
- personality_file="$FORGE_ROOT/agents/ember/personality.md"
98
- ;;
99
- "aegis")
100
- personality_file="$FORGE_ROOT/agents/aegis/personality.md"
101
- ;;
102
- *)
103
- echo -e "${RED}Unknown agent: $agent${NC}"
104
- echo "Available agents: anvil, furnace, crucible, sentinel, scribe, herald, ember, aegis"
105
- exit 1
106
- ;;
107
- esac
108
-
109
- if [[ ! -f "$personality_file" ]]; then
110
- echo -e "${RED}Error: Personality file not found: $personality_file${NC}"
111
- exit 1
112
- fi
113
-
114
- echo "$personality_file"
115
- }
32
+ # shellcheck source=lib/colors.sh
33
+ source "$SCRIPT_DIR/lib/colors.sh"
34
+ # shellcheck source=lib/constants.sh
35
+ source "$SCRIPT_DIR/lib/constants.sh"
36
+ # shellcheck source=lib/config.sh
37
+ source "$SCRIPT_DIR/lib/config.sh"
38
+ # shellcheck source=lib/agents.sh
39
+ source "$SCRIPT_DIR/lib/agents.sh"
40
+
41
+ # Load agent configuration from JSON if available
42
+ # This overwrites the fallback values in constants.sh
43
+ if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
44
+ load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null || true
45
+ fi
116
46
 
117
47
  # =============================================================================
118
48
  # Commands
@@ -124,19 +54,31 @@ cmd_init() {
124
54
 
125
55
  cmd_start() {
126
56
  local agent="${1:-hub}"
127
- load_config
128
57
 
58
+ # Load and validate config
59
+ require_forge_config "$FORGE_ROOT"
60
+
61
+ # Validate and resolve agent name (SECURITY: whitelist validation)
62
+ local resolved
63
+ resolved=$(resolve_agent "$agent") || {
64
+ log_error "Unknown agent: $agent"
65
+ echo ""
66
+ show_available_agents
67
+ exit 1
68
+ }
69
+
70
+ # Get personality path (SECURITY: path traversal protection)
129
71
  local personality_path
130
- personality_path=$(get_personality_path "$agent")
72
+ personality_path=$(get_agent_personality_path "$FORGE_ROOT" "$resolved") || {
73
+ log_error "Personality file not found for agent: $resolved"
74
+ exit 1
75
+ }
131
76
 
77
+ # Get display name
132
78
  local agent_name
133
- if [[ "$agent" == "hub" || "$agent" == "" ]]; then
134
- agent_name="Planning Hub"
135
- else
136
- agent_name="$agent"
137
- fi
79
+ agent_name=$(get_agent_display_name "$resolved")
138
80
 
139
- echo -e "${YELLOW}🔥 Starting $agent_name...${NC}"
81
+ log_header "🔥 Starting $agent_name..."
140
82
  echo ""
141
83
 
142
84
  # Build the system prompt from personality file
@@ -144,7 +86,7 @@ cmd_start() {
144
86
  system_prompt=$(cat "$personality_path")
145
87
 
146
88
  # Add project context if it exists
147
- local project_context="$FORGE_ROOT/context/project-context.md"
89
+ local project_context="$FORGE_ROOT/$CONTEXT_DIR/project-context.md"
148
90
  if [[ -f "$project_context" ]]; then
149
91
  system_prompt="$system_prompt
150
92
 
@@ -156,7 +98,7 @@ $(cat "$project_context")"
156
98
  fi
157
99
 
158
100
  # Add startup instructions based on agent type
159
- if [[ "$agent" == "hub" || "$agent" == "planning" || "$agent" == "" ]]; then
101
+ if [[ "$resolved" == "hub" ]]; then
160
102
  # Planning Hub startup - Party Mode Team
161
103
  system_prompt="$system_prompt
162
104
 
@@ -180,8 +122,8 @@ On startup: Announce yourself (name, icon, role), check tasks/pending/ and tasks
180
122
 
181
123
  # Launch Claude Code with the personality
182
124
  # --dangerously-skip-permissions avoids repeated prompts when starting agents
183
- # Use --resume with initial prompt to trigger welcome but stay interactive
184
- if [[ "$agent" == "hub" || "$agent" == "planning" || "$agent" == "" ]]; then
125
+ # This is documented in docs/security.md
126
+ if [[ "$resolved" == "hub" ]]; then
185
127
  claude --dangerously-skip-permissions --system-prompt "$system_prompt" "begin"
186
128
  else
187
129
  claude --dangerously-skip-permissions --system-prompt "$system_prompt" "startup"
@@ -189,13 +131,12 @@ On startup: Announce yourself (name, icon, role), check tasks/pending/ and tasks
189
131
  }
190
132
 
191
133
  cmd_status() {
192
- load_config
134
+ require_forge_config "$FORGE_ROOT"
193
135
 
194
- local state_file="$FORGE_ROOT/context/forge-state.yaml"
136
+ local state_file="$FORGE_ROOT/$CONTEXT_DIR/forge-state.yaml"
195
137
 
196
138
  echo ""
197
- echo -e "${YELLOW}🔥 Forge Status${NC}"
198
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
139
+ log_header "🔥 Forge Status"
199
140
 
200
141
  if [[ -f "$state_file" ]]; then
201
142
  cat "$state_file"
@@ -210,18 +151,18 @@ cmd_status() {
210
151
  }
211
152
 
212
153
  cmd_test() {
213
- load_config
154
+ require_forge_config "$FORGE_ROOT"
214
155
 
215
156
  echo ""
216
- echo -e "${YELLOW}🧪 Testing Vibe Forge setup...${NC}"
157
+ log_header "🧪 Testing Vibe Forge setup..."
217
158
  echo ""
218
159
 
219
160
  # Test Claude Code
220
161
  echo "Testing Claude Code..."
221
162
  if claude --version &> /dev/null; then
222
- echo -e "${GREEN}✅ Claude Code working${NC}"
163
+ log_success "Claude Code working"
223
164
  else
224
- echo -e "${RED}❌ Claude Code not working${NC}"
165
+ log_error "Claude Code not working"
225
166
  exit 1
226
167
  fi
227
168
 
@@ -232,24 +173,32 @@ cmd_test() {
232
173
  test_output=$(claude --system-prompt "You are a test. Respond with only: FORGE_TEST_OK" --print "test" 2>&1 | head -1)
233
174
 
234
175
  if [[ "$test_output" == *"FORGE_TEST_OK"* || "$test_output" == *"test"* ]]; then
235
- echo -e "${GREEN}✅ Personality loading working${NC}"
176
+ log_success "Personality loading working"
236
177
  else
237
- echo -e "${YELLOW}⚠️ Personality loading may have issues${NC}"
178
+ log_warn "Personality loading may have issues"
238
179
  echo " Output: $test_output"
239
180
  fi
240
181
 
241
182
  echo ""
242
- echo -e "${GREEN}🔥 Setup validated!${NC}"
183
+ log_success "🔥 Setup validated!"
243
184
  }
244
185
 
245
186
  cmd_spawn() {
246
187
  local agent="${1:-}"
247
188
 
248
189
  if [[ -z "$agent" ]]; then
249
- echo -e "${RED}Error: No agent specified.${NC}"
190
+ log_error "No agent specified."
250
191
  echo "Usage: forge spawn <agent>"
251
192
  echo ""
252
- echo "Available agents: anvil, furnace, crucible, sentinel, scribe, herald, ember, aegis"
193
+ show_available_agents
194
+ exit 1
195
+ fi
196
+
197
+ # Validate agent before passing to spawn script (SECURITY: whitelist check)
198
+ if ! is_valid_agent "$agent"; then
199
+ log_error "Unknown agent: $agent"
200
+ echo ""
201
+ show_available_agents
253
202
  exit 1
254
203
  fi
255
204
 
@@ -286,8 +235,7 @@ cmd_daemon() {
286
235
 
287
236
  cmd_help() {
288
237
  echo ""
289
- echo -e "${YELLOW}🔥 Vibe Forge${NC}"
290
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
238
+ log_header "🔥 Vibe Forge"
291
239
  echo ""
292
240
  echo "Usage: forge [command] [options]"
293
241
  echo ""
@@ -301,20 +249,21 @@ cmd_help() {
301
249
  echo " daemon <action> Manage background daemon (start|stop|status|notifications|clear)"
302
250
  echo " help Show this help message"
303
251
  echo ""
304
- echo "Agents:"
305
- echo " anvil Frontend Developer"
306
- echo " furnace Backend Developer"
307
- echo " crucible Tester / QA"
308
- echo " sentinel Code Reviewer"
309
- echo " scribe Documentation"
310
- echo " herald Release Manager"
311
- echo " ember DevOps"
312
- echo " aegis Security"
252
+ echo "Agents (with aliases):"
253
+ echo " anvil (frontend, ui, fe) - Frontend Developer"
254
+ echo " furnace (backend, api, be) - Backend Developer"
255
+ echo " crucible (test, testing, qa) - Tester / QA"
256
+ echo " sentinel (review, reviewer, cr) - Code Reviewer"
257
+ echo " scribe (docs, documentation) - Documentation"
258
+ echo " herald (release, deploy) - Release Manager"
259
+ echo " ember (devops, ops, infra) - DevOps"
260
+ echo " aegis (security, sec, appsec) - Security"
313
261
  echo ""
314
262
  echo "Examples:"
315
263
  echo " forge Start Planning Hub"
316
264
  echo " forge init Initialize for new project"
317
265
  echo " forge start anvil Start Anvil (frontend) agent"
266
+ echo " forge spawn fe Spawn frontend agent in new terminal"
318
267
  echo " forge status Check current status"
319
268
  echo ""
320
269
  }
@@ -357,7 +306,7 @@ main() {
357
306
  cmd_start "hub"
358
307
  ;;
359
308
  *)
360
- echo -e "${RED}Unknown command: $command${NC}"
309
+ log_error "Unknown command: $command"
361
310
  echo "Run 'forge help' for usage."
362
311
  exit 1
363
312
  ;;
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - Agent Resolution and Validation
4
+ # Source this file in other scripts: source "$SCRIPT_DIR/lib/agents.sh"
5
+ #
6
+ # SECURITY: This module provides safe agent name resolution with whitelist validation.
7
+ # Never pass user input directly to shell commands - always resolve through these functions.
8
+ #
9
+
10
+ # Ensure constants are loaded
11
+ if [[ -z "${VALID_AGENTS+x}" ]]; then
12
+ echo "Error: constants.sh must be sourced before agents.sh" >&2
13
+ exit 1
14
+ fi
15
+
16
+ # resolve_agent ALIAS
17
+ # Converts an agent alias to its canonical name.
18
+ # Returns: canonical name on stdout, empty string if not found
19
+ # Exit code: 0 if found, 1 if not found
20
+ #
21
+ # SECURITY: This function validates against a whitelist.
22
+ # The returned value is safe to use in file paths and commands.
23
+ resolve_agent() {
24
+ local input="$1"
25
+
26
+ # Normalize to lowercase for matching
27
+ local normalized
28
+ normalized=$(echo "$input" | tr '[:upper:]' '[:lower:]')
29
+
30
+ # Look up in alias map
31
+ local canonical="${AGENT_ALIASES[$normalized]:-}"
32
+
33
+ if [[ -n "$canonical" ]]; then
34
+ echo "$canonical"
35
+ return 0
36
+ fi
37
+
38
+ return 1
39
+ }
40
+
41
+ # is_valid_agent AGENT
42
+ # Checks if an agent name is valid (either canonical or alias)
43
+ # Returns: 0 if valid, 1 if invalid
44
+ is_valid_agent() {
45
+ local agent="$1"
46
+ resolve_agent "$agent" >/dev/null 2>&1
47
+ }
48
+
49
+ # get_agent_personality_path FORGE_ROOT AGENT
50
+ # Returns the path to an agent's personality file.
51
+ # Uses AGENT_PERSONALITY_FILES from constants.sh (or loaded from agents.json)
52
+ # SECURITY: Validates agent name before constructing path.
53
+ # Returns: full path on stdout
54
+ # Exit code: 0 on success, 1 if agent invalid or file missing
55
+ get_agent_personality_path() {
56
+ local forge_root="$1"
57
+ local agent="$2"
58
+
59
+ # Resolve and validate agent
60
+ local canonical
61
+ canonical=$(resolve_agent "$agent") || {
62
+ return 1
63
+ }
64
+
65
+ # Get personality file path from configuration
66
+ local relative_path="${AGENT_PERSONALITY_FILES[$canonical]:-}"
67
+ if [[ -z "$relative_path" ]]; then
68
+ # Fallback: construct path if not in config
69
+ relative_path="agents/$canonical/personality.md"
70
+ fi
71
+
72
+ local personality_path="$forge_root/$relative_path"
73
+
74
+ # Validate file exists
75
+ if [[ ! -f "$personality_path" ]]; then
76
+ return 1
77
+ fi
78
+
79
+ # SECURITY: Verify the resolved path is within agents directory
80
+ local real_path
81
+ real_path=$(cd "$(dirname "$personality_path")" 2>/dev/null && pwd)/$(basename "$personality_path")
82
+ local agents_dir
83
+ agents_dir=$(cd "$forge_root/agents" 2>/dev/null && pwd)
84
+
85
+ if [[ "$real_path" != "$agents_dir"/* ]]; then
86
+ echo "Security error: Path traversal detected" >&2
87
+ return 1
88
+ fi
89
+
90
+ echo "$personality_path"
91
+ return 0
92
+ }
93
+
94
+ # show_available_agents
95
+ # Prints a formatted list of available agents with aliases
96
+ # Uses data from AGENT_DISPLAY_NAMES, AGENT_ROLES, and AGENT_ALIASES
97
+ show_available_agents() {
98
+ echo "Available agents:"
99
+
100
+ # Iterate over valid agents (excluding hub for user display)
101
+ for agent in "${VALID_AGENTS[@]}"; do
102
+ if [[ "$agent" == "hub" ]]; then
103
+ continue
104
+ fi
105
+
106
+ # Get display info
107
+ local role="${AGENT_ROLES[$agent]:-}"
108
+
109
+ # Collect aliases for this agent
110
+ local aliases=()
111
+ for alias in "${!AGENT_ALIASES[@]}"; do
112
+ if [[ "${AGENT_ALIASES[$alias]}" == "$agent" && "$alias" != "$agent" ]]; then
113
+ aliases+=("$alias")
114
+ fi
115
+ done
116
+
117
+ # Format aliases (take first 3)
118
+ local alias_str=""
119
+ if [[ ${#aliases[@]} -gt 0 ]]; then
120
+ # Sort and take first 3
121
+ IFS=$'\n' sorted=($(printf '%s\n' "${aliases[@]}" | sort)); unset IFS
122
+ local display_aliases=("${sorted[@]:0:3}")
123
+ alias_str="($(IFS=", "; echo "${display_aliases[*]}"))"
124
+ fi
125
+
126
+ # Print formatted line
127
+ printf " %-9s %-25s - %s\n" "$agent" "$alias_str" "$role"
128
+ done
129
+ }
130
+
131
+ # get_agent_display_name AGENT
132
+ # Returns the display name for an agent (e.g., "Anvil" for "anvil")
133
+ # Uses AGENT_DISPLAY_NAMES from constants.sh (or loaded from agents.json)
134
+ get_agent_display_name() {
135
+ local agent="$1"
136
+ local canonical
137
+ canonical=$(resolve_agent "$agent") || return 1
138
+
139
+ # Look up in AGENT_DISPLAY_NAMES array
140
+ local display_name="${AGENT_DISPLAY_NAMES[$canonical]:-}"
141
+ if [[ -n "$display_name" ]]; then
142
+ echo "$display_name"
143
+ else
144
+ # Fallback to canonical name with first letter capitalized
145
+ echo "${canonical^}"
146
+ fi
147
+ }
148
+
149
+ # get_agent_role AGENT
150
+ # Returns the role description for an agent
151
+ get_agent_role() {
152
+ local agent="$1"
153
+ local canonical
154
+ canonical=$(resolve_agent "$agent") || return 1
155
+
156
+ echo "${AGENT_ROLES[$canonical]:-}"
157
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - Shared Color Definitions and Logging
4
+ # Source this file in other scripts: source "$SCRIPT_DIR/lib/colors.sh"
5
+ #
6
+
7
+ # Colors (only if terminal supports them)
8
+ if [[ -t 1 ]]; then
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ CYAN='\033[0;36m'
14
+ NC='\033[0m'
15
+ else
16
+ RED=''
17
+ GREEN=''
18
+ YELLOW=''
19
+ BLUE=''
20
+ CYAN=''
21
+ NC=''
22
+ fi
23
+
24
+ # Logging functions
25
+ log_error() {
26
+ echo -e "${RED}Error: $1${NC}" >&2
27
+ }
28
+
29
+ log_success() {
30
+ echo -e "${GREEN}✓ $1${NC}"
31
+ }
32
+
33
+ log_info() {
34
+ echo -e "${BLUE}ℹ $1${NC}"
35
+ }
36
+
37
+ log_warn() {
38
+ echo -e "${YELLOW}⚠ $1${NC}"
39
+ }
40
+
41
+ log_header() {
42
+ echo -e "${YELLOW}$1${NC}"
43
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
44
+ }