vibe-forge 0.4.0 → 0.8.1

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 (129) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -102
  4. package/.claude/commands/forge.md +218 -171
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +217 -187
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +253 -232
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -240
  15. package/agents/architect/personality.md +260 -234
  16. package/agents/crucible/personality.md +362 -309
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -265
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -291
  21. package/agents/herald/personality.md +249 -247
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +473 -251
  26. package/agents/scribe/personality.md +253 -251
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/temper/personality.md +270 -0
  29. package/bin/cli.js +372 -325
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +477 -851
  38. package/bin/forge-setup.sh +661 -645
  39. package/bin/forge-spawn.sh +164 -164
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -387
  42. package/bin/lib/agents.sh +177 -177
  43. package/bin/lib/check-aliases.js +50 -0
  44. package/bin/lib/colors.sh +44 -44
  45. package/bin/lib/config.sh +347 -313
  46. package/bin/lib/constants.sh +241 -206
  47. package/bin/lib/daemon/budgets.sh +107 -0
  48. package/bin/lib/daemon/dependencies.sh +146 -0
  49. package/bin/lib/daemon/display.sh +128 -0
  50. package/bin/lib/daemon/notifications.sh +273 -0
  51. package/bin/lib/daemon/routing.sh +93 -0
  52. package/bin/lib/daemon/state.sh +163 -0
  53. package/bin/lib/daemon/sync.sh +103 -0
  54. package/bin/lib/database.sh +357 -305
  55. package/bin/lib/frontmatter.js +106 -0
  56. package/bin/lib/heimdall-setup.js +113 -0
  57. package/bin/lib/heimdall.js +265 -0
  58. package/bin/lib/json.sh +264 -258
  59. package/bin/lib/terminal.js +452 -446
  60. package/bin/lib/util.sh +126 -126
  61. package/bin/lib/vcs.js +349 -349
  62. package/config/agent-manifest.yaml +237 -243
  63. package/config/agents.json +207 -132
  64. package/config/task-template.md +159 -87
  65. package/config/task-types.yaml +111 -106
  66. package/config/templates/handoff-template.md +40 -0
  67. package/context/agent-overrides/README.md +41 -0
  68. package/context/architecture.md +42 -0
  69. package/context/modern-conventions.md +129 -129
  70. package/context/project-context-template.md +122 -122
  71. package/docs/agents.md +473 -409
  72. package/docs/architecture.md +194 -162
  73. package/docs/commands.md +451 -388
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -50
  76. package/.claude/settings.local.json +0 -33
  77. package/agents/forge-master/capabilities.md +0 -144
  78. package/agents/forge-master/context-template.md +0 -128
  79. package/agents/forge-master/personality.md +0 -138
  80. package/agents/sentinel/personality.md +0 -194
  81. package/context/forge-state.yaml +0 -19
  82. package/docs/TODO.md +0 -150
  83. package/docs/getting-started.md +0 -243
  84. package/docs/npm-publishing.md +0 -95
  85. package/docs/workflows/README.md +0 -32
  86. package/docs/workflows/azure-devops.md +0 -108
  87. package/docs/workflows/bitbucket.md +0 -104
  88. package/docs/workflows/git-only.md +0 -130
  89. package/docs/workflows/gitea.md +0 -168
  90. package/docs/workflows/github.md +0 -103
  91. package/docs/workflows/gitlab.md +0 -105
  92. package/docs/workflows.md +0 -454
  93. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  94. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  95. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  96. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  97. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  98. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  99. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  100. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  101. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  102. package/tasks/completed/CLEAN-001.md +0 -38
  103. package/tasks/completed/CLEAN-003.md +0 -47
  104. package/tasks/completed/CLEAN-004.md +0 -56
  105. package/tasks/completed/CLEAN-005.md +0 -75
  106. package/tasks/completed/CLEAN-006.md +0 -47
  107. package/tasks/completed/CLEAN-007.md +0 -34
  108. package/tasks/completed/CLEAN-008.md +0 -49
  109. package/tasks/completed/CLEAN-012.md +0 -58
  110. package/tasks/completed/CLEAN-013.md +0 -45
  111. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  112. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  113. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  114. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  115. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  116. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  117. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  118. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  119. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  120. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  121. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  122. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  123. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  124. package/tasks/pending/CLEAN-002.md +0 -29
  125. package/tasks/pending/CLEAN-009.md +0 -31
  126. package/tasks/pending/CLEAN-010.md +0 -30
  127. package/tasks/pending/CLEAN-011.md +0 -30
  128. package/tasks/pending/CLEAN-014.md +0 -32
  129. package/tasks/review/task-001.md +0 -78
@@ -1,305 +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
- # Initialize the database with schema
55
- # Requires: FORGE_DB must be set before calling
56
- db_init() {
57
- db_require_init || return 1
58
- local db_dir
59
- db_dir=$(dirname "$FORGE_DB")
60
-
61
- # Create directory if needed
62
- mkdir -p "$db_dir"
63
- chmod 700 "$db_dir"
64
-
65
- # Create tables if they don't exist
66
- sqlite3 "$FORGE_DB" <<'SQL'
67
- -- Daemon configuration (single row)
68
- CREATE TABLE IF NOT EXISTS daemon_config (
69
- id INTEGER PRIMARY KEY CHECK (id = 1),
70
- state TEXT DEFAULT 'idle',
71
- poll_interval_ms INTEGER DEFAULT 5000,
72
- last_activity_at TEXT,
73
- updated_at TEXT DEFAULT CURRENT_TIMESTAMP
74
- );
75
-
76
- -- Polling presets
77
- CREATE TABLE IF NOT EXISTS poll_presets (
78
- state TEXT PRIMARY KEY,
79
- interval_ms INTEGER NOT NULL
80
- );
81
-
82
- -- Insert default presets if not exist
83
- INSERT OR IGNORE INTO poll_presets (state, interval_ms) VALUES
84
- ('active', 5000),
85
- ('idle', 30000),
86
- ('stopped', 60000);
87
-
88
- -- Initialize daemon_config if empty
89
- INSERT OR IGNORE INTO daemon_config (id, state, poll_interval_ms)
90
- VALUES (1, 'idle', 30000);
91
-
92
- -- Agent status (aggregated from JSON files)
93
- CREATE TABLE IF NOT EXISTS agent_status (
94
- agent TEXT PRIMARY KEY,
95
- status TEXT DEFAULT 'unknown',
96
- task TEXT,
97
- message TEXT,
98
- updated_at TEXT,
99
- file_mtime INTEGER DEFAULT 0
100
- );
101
-
102
- -- Status history for metrics (optional, pruned periodically)
103
- CREATE TABLE IF NOT EXISTS status_history (
104
- id INTEGER PRIMARY KEY AUTOINCREMENT,
105
- agent TEXT NOT NULL,
106
- status TEXT NOT NULL,
107
- task TEXT,
108
- recorded_at TEXT DEFAULT CURRENT_TIMESTAMP
109
- );
110
-
111
- -- Index for history queries
112
- CREATE INDEX IF NOT EXISTS idx_history_agent ON status_history(agent);
113
- CREATE INDEX IF NOT EXISTS idx_history_recorded ON status_history(recorded_at);
114
- SQL
115
-
116
- # Set secure permissions on database file
117
- chmod 600 "$FORGE_DB" 2>/dev/null || true
118
- }
119
-
120
- # =============================================================================
121
- # Daemon State Operations
122
- # =============================================================================
123
-
124
- # Get current daemon state (active/idle/stopped)
125
- db_get_daemon_state() {
126
- db_require_init || return 1
127
- sqlite3 "$FORGE_DB" "SELECT state FROM daemon_config WHERE id = 1;"
128
- }
129
-
130
- # Set daemon state and update poll interval accordingly
131
- db_set_daemon_state() {
132
- db_require_init || return 1
133
- local new_state="$1"
134
- local interval
135
-
136
- # SECURITY: Validate state is a known value
137
- case "$new_state" in
138
- active|idle|stopped) ;;
139
- *)
140
- echo "Invalid daemon state: $new_state" >&2
141
- return 1
142
- ;;
143
- esac
144
-
145
- # Get interval for this state from presets (safe - validated above)
146
- interval=$(sqlite3 "$FORGE_DB" "SELECT interval_ms FROM poll_presets WHERE state = '$new_state';")
147
- interval="${interval:-30000}"
148
-
149
- sqlite3 "$FORGE_DB" <<SQL
150
- UPDATE daemon_config
151
- SET state = '$new_state',
152
- poll_interval_ms = $interval,
153
- updated_at = datetime('now')
154
- WHERE id = 1;
155
- SQL
156
- }
157
-
158
- # Get current poll interval in milliseconds
159
- db_get_poll_interval_ms() {
160
- sqlite3 "$FORGE_DB" "SELECT poll_interval_ms FROM daemon_config WHERE id = 1;"
161
- }
162
-
163
- # Update last activity timestamp
164
- db_touch_activity() {
165
- sqlite3 "$FORGE_DB" <<SQL
166
- UPDATE daemon_config
167
- SET last_activity_at = datetime('now'),
168
- updated_at = datetime('now')
169
- WHERE id = 1;
170
- SQL
171
- }
172
-
173
- # =============================================================================
174
- # Agent Status Operations
175
- # =============================================================================
176
-
177
- # Upsert agent status (from JSON file data)
178
- db_upsert_agent_status() {
179
- db_require_init || return 1
180
- local agent="$1"
181
- local status="$2"
182
- local task="$3"
183
- local message="$4"
184
- local updated_at="$5"
185
- local file_mtime="$6"
186
-
187
- # SECURITY: Escape all string inputs to prevent SQL injection
188
- agent=$(db_escape "$agent")
189
- status=$(db_escape "$status")
190
- task=$(db_escape "$task")
191
- message=$(db_escape "$message")
192
- updated_at=$(db_escape "$updated_at")
193
-
194
- # Validate file_mtime is numeric
195
- if ! [[ "$file_mtime" =~ ^[0-9]+$ ]]; then
196
- file_mtime=0
197
- fi
198
-
199
- sqlite3 "$FORGE_DB" <<SQL
200
- INSERT INTO agent_status (agent, status, task, message, updated_at, file_mtime)
201
- VALUES ('$agent', '$status', '$task', '$message', '$updated_at', $file_mtime)
202
- ON CONFLICT(agent) DO UPDATE SET
203
- status = excluded.status,
204
- task = excluded.task,
205
- message = excluded.message,
206
- updated_at = excluded.updated_at,
207
- file_mtime = excluded.file_mtime;
208
- SQL
209
- }
210
-
211
- # Get stored mtime for an agent's status file
212
- db_get_agent_mtime() {
213
- local agent="$1"
214
- # SECURITY: Escape agent name to prevent SQL injection
215
- agent=$(db_escape "$agent")
216
- sqlite3 "$FORGE_DB" "SELECT file_mtime FROM agent_status WHERE agent = '$agent';" 2>/dev/null || echo "0"
217
- }
218
-
219
- # Get all agent statuses
220
- db_get_all_agent_statuses() {
221
- sqlite3 -separator '|' "$FORGE_DB" <<SQL
222
- SELECT agent, status, task, message, updated_at
223
- FROM agent_status
224
- ORDER BY agent;
225
- SQL
226
- }
227
-
228
- # Get agents with specific status
229
- db_get_agents_by_status() {
230
- local status="$1"
231
- # SECURITY: Escape status to prevent SQL injection
232
- status=$(db_escape "$status")
233
- sqlite3 "$FORGE_DB" "SELECT agent FROM agent_status WHERE status = '$status';"
234
- }
235
-
236
- # Count active workers (status = 'working')
237
- db_count_active_workers() {
238
- sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status WHERE status = 'working';"
239
- }
240
-
241
- # Delete stale agent status entries
242
- db_cleanup_stale_agents() {
243
- local minutes="${1:-30}"
244
- # SECURITY: Validate minutes is numeric to prevent SQL injection
245
- if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then
246
- minutes=30
247
- fi
248
- sqlite3 "$FORGE_DB" <<SQL
249
- DELETE FROM agent_status
250
- WHERE updated_at < datetime('now', '-$minutes minutes');
251
- SQL
252
- }
253
-
254
- # =============================================================================
255
- # History Operations (for future metrics)
256
- # =============================================================================
257
-
258
- # Record status change in history
259
- db_record_status_history() {
260
- local agent="$1"
261
- local status="$2"
262
- local task="$3"
263
-
264
- # SECURITY: Escape all inputs to prevent SQL injection
265
- agent=$(db_escape "$agent")
266
- status=$(db_escape "$status")
267
- task=$(db_escape "$task")
268
-
269
- sqlite3 "$FORGE_DB" <<SQL
270
- INSERT INTO status_history (agent, status, task)
271
- VALUES ('$agent', '$status', '$task');
272
- SQL
273
- }
274
-
275
- # Prune old history entries (keep last N days)
276
- db_prune_history() {
277
- local days="${1:-7}"
278
- # SECURITY: Validate days is numeric to prevent SQL injection
279
- if ! [[ "$days" =~ ^[0-9]+$ ]]; then
280
- days=7
281
- fi
282
- sqlite3 "$FORGE_DB" <<SQL
283
- DELETE FROM status_history
284
- WHERE recorded_at < datetime('now', '-$days days');
285
- SQL
286
- }
287
-
288
- # =============================================================================
289
- # Utility Functions
290
- # =============================================================================
291
-
292
- # Check if database exists and is valid
293
- db_exists() {
294
- [[ -n "$FORGE_DB" ]] && [[ -f "$FORGE_DB" ]] && sqlite3 "$FORGE_DB" "SELECT 1 FROM daemon_config LIMIT 1;" &>/dev/null
295
- }
296
-
297
- # Get database stats for debugging
298
- db_stats() {
299
- db_require_init || return 1
300
- echo "Database: $FORGE_DB"
301
- echo "Agent count: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;")"
302
- echo "History entries: $(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM status_history;")"
303
- echo "Current state: $(db_get_daemon_state)"
304
- echo "Poll interval: $(db_get_poll_interval_ms)ms"
305
- }
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
+ }