vibe-forge 0.8.1 → 0.8.2

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.
Files changed (51) hide show
  1. package/.claude/commands/configure-vcs.md +102 -102
  2. package/.claude/commands/forge.md +218 -218
  3. package/.claude/hooks/worker-loop.js +220 -217
  4. package/.claude/settings.json +89 -89
  5. package/README.md +149 -191
  6. package/agents/aegis/personality.md +303 -303
  7. package/agents/anvil/personality.md +278 -278
  8. package/agents/architect/personality.md +260 -260
  9. package/agents/crucible/personality.md +362 -362
  10. package/agents/crucible-x/personality.md +210 -210
  11. package/agents/ember/personality.md +293 -293
  12. package/agents/flux/personality.md +248 -248
  13. package/agents/furnace/personality.md +342 -342
  14. package/agents/herald/personality.md +249 -249
  15. package/agents/oracle/personality.md +284 -284
  16. package/agents/pixel/personality.md +140 -140
  17. package/agents/planning-hub/personality.md +473 -473
  18. package/agents/scribe/personality.md +253 -253
  19. package/agents/slag/personality.md +268 -268
  20. package/agents/temper/personality.md +270 -270
  21. package/bin/cli.js +372 -372
  22. package/bin/forge-daemon.sh +477 -477
  23. package/bin/forge-setup.sh +662 -661
  24. package/bin/forge-spawn.sh +164 -164
  25. package/bin/forge.sh +566 -566
  26. package/docs/commands.md +8 -8
  27. package/package.json +77 -77
  28. package/{bin → src}/lib/agents.sh +177 -177
  29. package/{bin → src}/lib/check-aliases.js +50 -50
  30. package/{bin → src}/lib/colors.sh +45 -44
  31. package/{bin → src}/lib/config.sh +347 -347
  32. package/{bin → src}/lib/constants.sh +241 -241
  33. package/{bin → src}/lib/daemon/budgets.sh +107 -107
  34. package/{bin → src}/lib/daemon/dependencies.sh +146 -146
  35. package/{bin → src}/lib/daemon/display.sh +128 -128
  36. package/{bin → src}/lib/daemon/notifications.sh +273 -273
  37. package/{bin → src}/lib/daemon/routing.sh +93 -93
  38. package/{bin → src}/lib/daemon/state.sh +163 -163
  39. package/{bin → src}/lib/daemon/sync.sh +103 -103
  40. package/{bin → src}/lib/database.sh +357 -357
  41. package/{bin → src}/lib/frontmatter.js +106 -106
  42. package/{bin → src}/lib/heimdall-setup.js +113 -113
  43. package/{bin → src}/lib/heimdall.js +265 -265
  44. package/src/lib/index.sh +25 -0
  45. package/{bin → src}/lib/json.sh +264 -264
  46. package/{bin → src}/lib/terminal.js +452 -452
  47. package/{bin → src}/lib/util.sh +126 -126
  48. package/{bin → src}/lib/vcs.js +349 -349
  49. package/{context → templates}/project-context-template.md +122 -122
  50. package/config/task-template.md +0 -159
  51. package/config/templates/handoff-template.md +0 -40
@@ -1,357 +1,357 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Vibe Forge - SQLite Database Operations
4
- # Handles daemon state persistence and agent status aggregation
5
- #
6
-
7
- # Database file location (set after FORGE_ROOT is known)
8
- FORGE_DB=""
9
-
10
- # =============================================================================
11
- # Security Functions
12
- # =============================================================================
13
-
14
- # Escape a string for safe use in SQLite queries
15
- # Escapes single quotes by doubling them (SQL standard)
16
- # Also removes null bytes and other dangerous characters
17
- db_escape() {
18
- local input="$1"
19
- # Remove null bytes
20
- input="${input//$'\0'/}"
21
- # Escape single quotes by doubling them
22
- input="${input//\'/\'\'}"
23
- # Remove backslashes that could escape quotes
24
- input="${input//\\/}"
25
- echo "$input"
26
- }
27
-
28
- # Validate that input contains only safe characters for identifiers
29
- # Returns 0 if safe, 1 if unsafe
30
- db_validate_identifier() {
31
- local input="$1"
32
- # Allow only alphanumeric, underscore, hyphen
33
- if [[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
34
- return 0
35
- fi
36
- return 1
37
- }
38
-
39
- # =============================================================================
40
- # Database Initialization
41
- # =============================================================================
42
-
43
- # Require FORGE_DB to be set before database operations
44
- # Call this at the start of database functions that need the path
45
- # Usage: db_require_init || return 1
46
- db_require_init() {
47
- if [[ -z "$FORGE_DB" ]]; then
48
- echo "Error: FORGE_DB not set. Set FORGE_DB path before calling database functions." >&2
49
- return 1
50
- fi
51
- return 0
52
- }
53
-
54
- # Check if sqlite3 is available; attempt install if missing
55
- db_require_sqlite3() {
56
- if command -v sqlite3 &>/dev/null; then
57
- return 0
58
- fi
59
-
60
- echo "sqlite3 not found. Attempting to install..." >&2
61
-
62
- # Try platform-appropriate package manager
63
- if command -v apt-get &>/dev/null; then
64
- echo "Installing sqlite3 via apt (may require sudo password)..." >&2
65
- sudo apt-get install -y sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
66
- elif command -v brew &>/dev/null; then
67
- echo "Installing sqlite3 via Homebrew..." >&2
68
- brew install sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
69
- elif command -v pacman &>/dev/null; then
70
- echo "Installing sqlite via pacman (may require sudo password)..." >&2
71
- sudo pacman -S --noconfirm sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
72
- elif command -v dnf &>/dev/null; then
73
- echo "Installing sqlite via dnf (may require sudo password)..." >&2
74
- sudo dnf install -y sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
75
- elif command -v winget &>/dev/null; then
76
- echo "Installing SQLite via winget. You may see a UAC prompt; please approve it." >&2
77
- winget install --id SQLite.SQLite -e --silent 2>/dev/null
78
- # winget installs to Program Files but doesn't add to Git Bash PATH
79
- local winget_sqlite="/c/Program Files/SQLite/sqlite3.exe"
80
- if [[ -f "$winget_sqlite" ]]; then
81
- export PATH="$PATH:/c/Program Files/SQLite"
82
- command -v sqlite3 &>/dev/null && return 0
83
- fi
84
- elif command -v choco &>/dev/null; then
85
- echo "Installing sqlite via Chocolatey. You may see a UAC prompt; please approve it." >&2
86
- choco install sqlite -y 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
87
- fi
88
-
89
- echo "ERROR: sqlite3 is required but could not be installed automatically." >&2
90
- echo "Please install sqlite3 manually:" >&2
91
- echo " macOS: brew install sqlite3" >&2
92
- echo " Ubuntu: sudo apt-get install sqlite3" >&2
93
- echo " Fedora: sudo dnf install sqlite" >&2
94
- echo " Windows: choco install sqlite" >&2
95
- echo " or: winget install SQLite.SQLite" >&2
96
- echo " (then add the install folder to your PATH)" >&2
97
- return 1
98
- }
99
-
100
- # Initialize the database with schema
101
- # Requires: FORGE_DB must be set before calling
102
- db_init() {
103
- db_require_init || return 1
104
- db_require_sqlite3 || return 1
105
- local db_dir
106
- db_dir=$(dirname "$FORGE_DB")
107
-
108
- # Create directory if needed
109
- mkdir -p "$db_dir"
110
- chmod 700 "$db_dir"
111
-
112
- # Enable WAL mode for concurrent reads without blocking writes.
113
- # WAL + NORMAL synchronous is the recommended pairing for local tooling:
114
- # faster than DELETE+FULL while still durable against OS crashes.
115
- sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
116
-
117
- # Create tables if they don't exist
118
- sqlite3 "$FORGE_DB" <<'SQL'
119
- -- Daemon configuration (single row)
120
- CREATE TABLE IF NOT EXISTS daemon_config (
121
- id INTEGER PRIMARY KEY CHECK (id = 1),
122
- state TEXT DEFAULT 'idle',
123
- poll_interval_ms INTEGER DEFAULT 5000,
124
- last_activity_at TEXT,
125
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
126
- );
127
-
128
- -- Polling presets
129
- CREATE TABLE IF NOT EXISTS poll_presets (
130
- state TEXT PRIMARY KEY,
131
- interval_ms INTEGER NOT NULL
132
- );
133
-
134
- -- Insert default presets if not exist
135
- INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
136
- ('active', 5000),
137
- ('idle', 30000),
138
- ('stopped', 60000);
139
-
140
- -- Initialize daemon_config if empty
141
- INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
142
- VALUES (1, 'idle', 30000);
143
-
144
- -- Agent status (aggregated from JSON files)
145
- CREATE TABLE IF NOT EXISTS agent_status (
146
- agent TEXT PRIMARY KEY,
147
- status TEXT DEFAULT 'unknown',
148
- task TEXT,
149
- message TEXT,
150
- updated_at TEXT,
151
- file_mtime INTEGER DEFAULT 0
152
- );
153
-
154
- -- Status history for metrics (optional, pruned periodically)
155
- CREATE TABLE IF NOT EXISTS status_history (
156
- id INTEGER PRIMARY KEY AUTOINCREMENT,
157
- agent TEXT NOT NULL,
158
- status TEXT NOT NULL,
159
- task TEXT,
160
- recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
161
- );
162
-
163
- -- Index for history queries
164
- CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
165
- CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
166
- SQL
167
-
168
- # Set secure permissions on database file
169
- chmod 600 "$FORGE_DB" 2>/dev/null || true
170
- }
171
-
172
- # =============================================================================
173
- # Daemon State Operations
174
- # =============================================================================
175
-
176
- # Get current daemon state (active/idle/stopped)
177
- db_get_daemon_state() {
178
- db_require_init || return 1
179
- sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
180
- }
181
-
182
- # Set daemon state and update poll interval accordingly
183
- db_set_daemon_state() {
184
- db_require_init || return 1
185
- local new_state="$1"
186
- local interval
187
-
188
- # SECURITY: Validate state is a known value
189
- case "$new_state" in
190
- active|idle|stopped) ;;
191
- *)
192
- echo "Invalid daemon state: $new_state" >&2
193
- return 1
194
- ;;
195
- esac
196
-
197
- # Get interval for this state from presets (safe - validated above)
198
- interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
199
- interval="${interval:-30000}"
200
-
201
- sqlite3 "$FORGE_DB" <<SQL
202
- UPDATE daemon_config
203
- SET state = '$new_state',
204
- poll_interval_ms = $interval,
205
- updated_at = datetime('now')
206
- WHERE id = 1;
207
- SQL
208
- }
209
-
210
- # Get current poll interval in milliseconds
211
- db_get_poll_interval_ms() {
212
- sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
213
- }
214
-
215
- # Update last activity timestamp
216
- db_touch_activity() {
217
- sqlite3 "$FORGE_DB" <<SQL
218
- UPDATE daemon_config
219
- SET last_activity_at = datetime('now'),
220
- updated_at = datetime('now')
221
- WHERE id = 1;
222
- SQL
223
- }
224
-
225
- # =============================================================================
226
- # Agent Status Operations
227
- # =============================================================================
228
-
229
- # Upsert agent status (from JSON file data)
230
- db_upsert_agent_status() {
231
- db_require_init || return 1
232
- local agent="$1"
233
- local status="$2"
234
- local task="$3"
235
- local message="$4"
236
- local updated_at="$5"
237
- local file_mtime="$6"
238
-
239
- # SECURITY: Escape all string inputs to prevent SQL injection
240
- agent=$(db_escape "$agent")
241
- status=$(db_escape "$status")
242
- task=$(db_escape "$task")
243
- message=$(db_escape "$message")
244
- updated_at=$(db_escape "$updated_at")
245
-
246
- # Validate file_mtime is numeric
247
- if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
248
- file_mtime=0
249
- fi
250
-
251
- sqlite3 "$FORGE_DB" <<SQL
252
- INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
253
- VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
254
- ON CONFLICT(agent) DO UPDATE SET
255
- status = excluded.status,
256
- task = excluded.task,
257
- message = excluded.message,
258
- updated_at = excluded.updated_at,
259
- file_mtime = excluded.file_mtime;
260
- SQL
261
- }
262
-
263
- # Get stored mtime for an agent's status file
264
- db_get_agent_mtime() {
265
- local agent="$1"
266
- # SECURITY: Escape agent name to prevent SQL injection
267
- agent=$(db_escape "$agent")
268
- sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
269
- }
270
-
271
- # Get all agent statuses
272
- db_get_all_agent_statuses() {
273
- sqlite3 -separator '|' "$FORGE_DB" <<SQL
274
- SELECT agent, status, task, message, updated_at
275
- FROM agent_status
276
- ORDER BY agent;
277
- SQL
278
- }
279
-
280
- # Get agents with specific status
281
- db_get_agents_by_status() {
282
- local status="$1"
283
- # SECURITY: Escape status to prevent SQL injection
284
- status=$(db_escape "$status")
285
- sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
286
- }
287
-
288
- # Count active workers (status = 'working')
289
- db_count_active_workers() {
290
- sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
291
- }
292
-
293
- # Delete stale agent status entries
294
- db_cleanup_stale_agents() {
295
- local minutes="${1:-30}"
296
- # SECURITY: Validate minutes is numeric to prevent SQL injection
297
- if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
298
- minutes=30
299
- fi
300
- sqlite3 "$FORGE_DB" <<SQL
301
- DELETE FROM agent_status
302
- WHERE updated_at < datetime('now', '-$minutes minutes');
303
- SQL
304
- }
305
-
306
- # =============================================================================
307
- # History Operations (for future metrics)
308
- # =============================================================================
309
-
310
- # Record status change in history
311
- db_record_status_history() {
312
- local agent="$1"
313
- local status="$2"
314
- local task="$3"
315
-
316
- # SECURITY: Escape all inputs to prevent SQL injection
317
- agent=$(db_escape "$agent")
318
- status=$(db_escape "$status")
319
- task=$(db_escape "$task")
320
-
321
- sqlite3 "$FORGE_DB" <<SQL
322
- INSERT INTO status_history (agent, status, task)
323
- VALUES ('$agent', '$status', '$task');
324
- SQL
325
- }
326
-
327
- # Prune old history entries (keep last N days)
328
- db_prune_history() {
329
- local days="${1:-7}"
330
- # SECURITY: Validate days is numeric to prevent SQL injection
331
- if ! [[ "$days" =~ ^[0-9]+$ ]]; then
332
- days=7
333
- fi
334
- sqlite3 "$FORGE_DB" <<SQL
335
- DELETE FROM status_history
336
- WHERE recorded_at < datetime('now', '-$days days');
337
- SQL
338
- }
339
-
340
- # =============================================================================
341
- # Utility Functions
342
- # =============================================================================
343
-
344
- # Check if database exists and is valid
345
- db_exists() {
346
- [[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
347
- }
348
-
349
- # Get database stats for debugging
350
- db_stats() {
351
- db_require_init || return 1
352
- echo "Database: $FORGE_DB"
353
- echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
354
- echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
355
- echo "Current state: $(db_get_daemon_state)"
356
- echo "Poll interval: $(db_get_poll_interval_ms)ms"
357
- }
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - SQLite Database Operations
4
+ # Handles daemon state persistence and agent status aggregation
5
+ #
6
+
7
+ # Database file location (set after FORGE_ROOT is known)
8
+ FORGE_DB=""
9
+
10
+ # =============================================================================
11
+ # Security Functions
12
+ # =============================================================================
13
+
14
+ # Escape a string for safe use in SQLite queries
15
+ # Escapes single quotes by doubling them (SQL standard)
16
+ # Also removes null bytes and other dangerous characters
17
+ db_escape() {
18
+ local input="$1"
19
+ # Remove null bytes
20
+ input="${input//$'\0'/}"
21
+ # Escape single quotes by doubling them
22
+ input="${input//\'/\'\'}"
23
+ # Remove backslashes that could escape quotes
24
+ input="${input//\\/}"
25
+ echo "$input"
26
+ }
27
+
28
+ # Validate that input contains only safe characters for identifiers
29
+ # Returns 0 if safe, 1 if unsafe
30
+ db_validate_identifier() {
31
+ local input="$1"
32
+ # Allow only alphanumeric, underscore, hyphen
33
+ if [[ "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
34
+ return 0
35
+ fi
36
+ return 1
37
+ }
38
+
39
+ # =============================================================================
40
+ # Database Initialization
41
+ # =============================================================================
42
+
43
+ # Require FORGE_DB to be set before database operations
44
+ # Call this at the start of database functions that need the path
45
+ # Usage: db_require_init || return 1
46
+ db_require_init() {
47
+ if [[ -z "$FORGE_DB" ]]; then
48
+ echo "Error: FORGE_DB not set. Set FORGE_DB path before calling database functions." >&2
49
+ return 1
50
+ fi
51
+ return 0
52
+ }
53
+
54
+ # Check if sqlite3 is available; attempt install if missing
55
+ db_require_sqlite3() {
56
+ if command -v sqlite3 &>/dev/null; then
57
+ return 0
58
+ fi
59
+
60
+ echo "sqlite3 not found. Attempting to install..." >&2
61
+
62
+ # Try platform-appropriate package manager
63
+ if command -v apt-get &>/dev/null; then
64
+ echo "Installing sqlite3 via apt (may require sudo password)..." >&2
65
+ sudo apt-get install -y sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
66
+ elif command -v brew &>/dev/null; then
67
+ echo "Installing sqlite3 via Homebrew..." >&2
68
+ brew install sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
69
+ elif command -v pacman &>/dev/null; then
70
+ echo "Installing sqlite via pacman (may require sudo password)..." >&2
71
+ sudo pacman -S --noconfirm sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
72
+ elif command -v dnf &>/dev/null; then
73
+ echo "Installing sqlite via dnf (may require sudo password)..." >&2
74
+ sudo dnf install -y sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
75
+ elif command -v winget &>/dev/null; then
76
+ echo "Installing SQLite via winget. You may see a UAC prompt; please approve it." >&2
77
+ winget install --id SQLite.SQLite -e --silent 2>/dev/null
78
+ # winget installs to Program Files but doesn't add to Git Bash PATH
79
+ local winget_sqlite="/c/Program Files/SQLite/sqlite3.exe"
80
+ if [[ -f "$winget_sqlite" ]]; then
81
+ export PATH="$PATH:/c/Program Files/SQLite"
82
+ command -v sqlite3 &>/dev/null && return 0
83
+ fi
84
+ elif command -v choco &>/dev/null; then
85
+ echo "Installing sqlite via Chocolatey. You may see a UAC prompt; please approve it." >&2
86
+ choco install sqlite -y 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
87
+ fi
88
+
89
+ echo "ERROR: sqlite3 is required but could not be installed automatically." >&2
90
+ echo "Please install sqlite3 manually:" >&2
91
+ echo " macOS: brew install sqlite3" >&2
92
+ echo " Ubuntu: sudo apt-get install sqlite3" >&2
93
+ echo " Fedora: sudo dnf install sqlite" >&2
94
+ echo " Windows: choco install sqlite" >&2
95
+ echo " or: winget install SQLite.SQLite" >&2
96
+ echo " (then add the install folder to your PATH)" >&2
97
+ return 1
98
+ }
99
+
100
+ # Initialize the database with schema
101
+ # Requires: FORGE_DB must be set before calling
102
+ db_init() {
103
+ db_require_init || return 1
104
+ db_require_sqlite3 || return 1
105
+ local db_dir
106
+ db_dir=$(dirname "$FORGE_DB")
107
+
108
+ # Create directory if needed
109
+ mkdir -p "$db_dir"
110
+ chmod 700 "$db_dir"
111
+
112
+ # Enable WAL mode for concurrent reads without blocking writes.
113
+ # WAL + NORMAL synchronous is the recommended pairing for local tooling:
114
+ # faster than DELETE+FULL while still durable against OS crashes.
115
+ sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
116
+
117
+ # Create tables if they don't exist
118
+ sqlite3 "$FORGE_DB" <<'SQL'
119
+ -- Daemon configuration (single row)
120
+ CREATE TABLE IF NOT EXISTS daemon_config (
121
+ id INTEGER PRIMARY KEY CHECK (id = 1),
122
+ state TEXT DEFAULT 'idle',
123
+ poll_interval_ms INTEGER DEFAULT 5000,
124
+ last_activity_at TEXT,
125
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
126
+ );
127
+
128
+ -- Polling presets
129
+ CREATE TABLE IF NOT EXISTS poll_presets (
130
+ state TEXT PRIMARY KEY,
131
+ interval_ms INTEGER NOT NULL
132
+ );
133
+
134
+ -- Insert default presets if not exist
135
+ INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
136
+ ('active', 5000),
137
+ ('idle', 30000),
138
+ ('stopped', 60000);
139
+
140
+ -- Initialize daemon_config if empty
141
+ INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
142
+ VALUES (1, 'idle', 30000);
143
+
144
+ -- Agent status (aggregated from JSON files)
145
+ CREATE TABLE IF NOT EXISTS agent_status (
146
+ agent TEXT PRIMARY KEY,
147
+ status TEXT DEFAULT 'unknown',
148
+ task TEXT,
149
+ message TEXT,
150
+ updated_at TEXT,
151
+ file_mtime INTEGER DEFAULT 0
152
+ );
153
+
154
+ -- Status history for metrics (optional, pruned periodically)
155
+ CREATE TABLE IF NOT EXISTS status_history (
156
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
157
+ agent TEXT NOT NULL,
158
+ status TEXT NOT NULL,
159
+ task TEXT,
160
+ recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
161
+ );
162
+
163
+ -- Index for history queries
164
+ CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
165
+ CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
166
+ SQL
167
+
168
+ # Set secure permissions on database file
169
+ chmod 600 "$FORGE_DB" 2>/dev/null || true
170
+ }
171
+
172
+ # =============================================================================
173
+ # Daemon State Operations
174
+ # =============================================================================
175
+
176
+ # Get current daemon state (active/idle/stopped)
177
+ db_get_daemon_state() {
178
+ db_require_init || return 1
179
+ sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
180
+ }
181
+
182
+ # Set daemon state and update poll interval accordingly
183
+ db_set_daemon_state() {
184
+ db_require_init || return 1
185
+ local new_state="$1"
186
+ local interval
187
+
188
+ # SECURITY: Validate state is a known value
189
+ case "$new_state" in
190
+ active|idle|stopped) ;;
191
+ *)
192
+ echo "Invalid daemon state: $new_state" >&2
193
+ return 1
194
+ ;;
195
+ esac
196
+
197
+ # Get interval for this state from presets (safe - validated above)
198
+ interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
199
+ interval="${interval:-30000}"
200
+
201
+ sqlite3 "$FORGE_DB" <<SQL
202
+ UPDATE daemon_config
203
+ SET state = '$new_state',
204
+ poll_interval_ms = $interval,
205
+ updated_at = datetime('now')
206
+ WHERE id = 1;
207
+ SQL
208
+ }
209
+
210
+ # Get current poll interval in milliseconds
211
+ db_get_poll_interval_ms() {
212
+ sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
213
+ }
214
+
215
+ # Update last activity timestamp
216
+ db_touch_activity() {
217
+ sqlite3 "$FORGE_DB" <<SQL
218
+ UPDATE daemon_config
219
+ SET last_activity_at = datetime('now'),
220
+ updated_at = datetime('now')
221
+ WHERE id = 1;
222
+ SQL
223
+ }
224
+
225
+ # =============================================================================
226
+ # Agent Status Operations
227
+ # =============================================================================
228
+
229
+ # Upsert agent status (from JSON file data)
230
+ db_upsert_agent_status() {
231
+ db_require_init || return 1
232
+ local agent="$1"
233
+ local status="$2"
234
+ local task="$3"
235
+ local message="$4"
236
+ local updated_at="$5"
237
+ local file_mtime="$6"
238
+
239
+ # SECURITY: Escape all string inputs to prevent SQL injection
240
+ agent=$(db_escape "$agent")
241
+ status=$(db_escape "$status")
242
+ task=$(db_escape "$task")
243
+ message=$(db_escape "$message")
244
+ updated_at=$(db_escape "$updated_at")
245
+
246
+ # Validate file_mtime is numeric
247
+ if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
248
+ file_mtime=0
249
+ fi
250
+
251
+ sqlite3 "$FORGE_DB" <<SQL
252
+ INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
253
+ VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
254
+ ON CONFLICT(agent) DO UPDATE SET
255
+ status = excluded.status,
256
+ task = excluded.task,
257
+ message = excluded.message,
258
+ updated_at = excluded.updated_at,
259
+ file_mtime = excluded.file_mtime;
260
+ SQL
261
+ }
262
+
263
+ # Get stored mtime for an agent's status file
264
+ db_get_agent_mtime() {
265
+ local agent="$1"
266
+ # SECURITY: Escape agent name to prevent SQL injection
267
+ agent=$(db_escape "$agent")
268
+ sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
269
+ }
270
+
271
+ # Get all agent statuses
272
+ db_get_all_agent_statuses() {
273
+ sqlite3 -separator '|' "$FORGE_DB" <<SQL
274
+ SELECT agent, status, task, message, updated_at
275
+ FROM agent_status
276
+ ORDER BY agent;
277
+ SQL
278
+ }
279
+
280
+ # Get agents with specific status
281
+ db_get_agents_by_status() {
282
+ local status="$1"
283
+ # SECURITY: Escape status to prevent SQL injection
284
+ status=$(db_escape "$status")
285
+ sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
286
+ }
287
+
288
+ # Count active workers (status = 'working')
289
+ db_count_active_workers() {
290
+ sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
291
+ }
292
+
293
+ # Delete stale agent status entries
294
+ db_cleanup_stale_agents() {
295
+ local minutes="${1:-30}"
296
+ # SECURITY: Validate minutes is numeric to prevent SQL injection
297
+ if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
298
+ minutes=30
299
+ fi
300
+ sqlite3 "$FORGE_DB" <<SQL
301
+ DELETE FROM agent_status
302
+ WHERE updated_at < datetime('now', '-$minutes minutes');
303
+ SQL
304
+ }
305
+
306
+ # =============================================================================
307
+ # History Operations (for future metrics)
308
+ # =============================================================================
309
+
310
+ # Record status change in history
311
+ db_record_status_history() {
312
+ local agent="$1"
313
+ local status="$2"
314
+ local task="$3"
315
+
316
+ # SECURITY: Escape all inputs to prevent SQL injection
317
+ agent=$(db_escape "$agent")
318
+ status=$(db_escape "$status")
319
+ task=$(db_escape "$task")
320
+
321
+ sqlite3 "$FORGE_DB" <<SQL
322
+ INSERT INTO status_history (agent, status, task)
323
+ VALUES ('$agent', '$status', '$task');
324
+ SQL
325
+ }
326
+
327
+ # Prune old history entries (keep last N days)
328
+ db_prune_history() {
329
+ local days="${1:-7}"
330
+ # SECURITY: Validate days is numeric to prevent SQL injection
331
+ if ! [[ "$days" =~ ^[0-9]+$ ]]; then
332
+ days=7
333
+ fi
334
+ sqlite3 "$FORGE_DB" <<SQL
335
+ DELETE FROM status_history
336
+ WHERE recorded_at < datetime('now', '-$days days');
337
+ SQL
338
+ }
339
+
340
+ # =============================================================================
341
+ # Utility Functions
342
+ # =============================================================================
343
+
344
+ # Check if database exists and is valid
345
+ db_exists() {
346
+ [[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
347
+ }
348
+
349
+ # Get database stats for debugging
350
+ db_stats() {
351
+ db_require_init || return 1
352
+ echo "Database: $FORGE_DB"
353
+ echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
354
+ echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
355
+ echo "Current state: $(db_get_daemon_state)"
356
+ echo "Poll interval: $(db_get_poll_interval_ms)ms"
357
+ }