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,106 +1,106 @@
1
- ---
2
- description: Start a persistent worker loop (Ralph-style)
3
- argument-hint: <agent> [--max-idle 10] | stop
4
- ---
5
-
6
- # Worker Loop Command
7
-
8
- Start a persistent worker loop for the specified agent. The worker will:
9
- 1. Check for assigned tasks
10
- 2. Work on tasks until complete
11
- 3. Loop back and check for more tasks
12
- 4. Only exit after N idle checks with no work found
13
-
14
- ## Activation Modes
15
-
16
- Worker Loop can be activated in two ways:
17
-
18
- ### 1. Config-based (Recommended)
19
-
20
- Enable during `forge init` or toggle anytime:
21
-
22
- ```bash
23
- forge config worker-loop on # Enable for all workers
24
- forge config worker-loop off # Disable
25
- forge config worker-loop # Check status
26
- ```
27
-
28
- When enabled, ALL workers will automatically stay running and check for new tasks.
29
-
30
- ### 2. Runtime (Per-session)
31
-
32
- Use this command for fine-grained control:
33
-
34
- ```
35
- /worker-loop anvil # Start Anvil in persistent loop
36
- /worker-loop furnace --max-idle 20 # Custom idle limit
37
- /worker-loop stop # Stop the current loop
38
- ```
39
-
40
- ## Arguments
41
-
42
- - `$1` - Agent name (anvil, furnace, crucible, etc.) or "stop"
43
- - `--max-idle N` - Exit after N checks with no tasks (default: 10)
44
-
45
- ## Implementation
46
-
47
- Based on `$ARGUMENTS`:
48
-
49
- ### If first argument is "stop"
50
-
51
- Remove the worker loop state file and confirm:
52
-
53
- ```bash
54
- rm -f "${CLAUDE_LOCAL_DIR:-$HOME/.claude}/forge-worker-loop.json"
55
- ```
56
-
57
- Output: "Worker loop stopped."
58
-
59
- ### Otherwise, start a loop
60
-
61
- 1. Resolve the agent name to canonical form
62
- 2. Create state file at `${CLAUDE_LOCAL_DIR}/forge-worker-loop.json`:
63
-
64
- ```json
65
- {
66
- "agent": "<resolved-agent>",
67
- "max_idle_checks": 10,
68
- "idle_count": 0,
69
- "poll_interval": 5,
70
- "started_at": "<ISO timestamp>"
71
- }
72
- ```
73
-
74
- 3. Load the agent's personality file
75
- 4. Start working with this system prompt addition:
76
-
77
- ```
78
- You are in PERSISTENT WORKER MODE. After completing each task:
79
- 1. Check for more tasks assigned to you in tasks/pending/ and tasks/needs-changes/
80
- 2. If tasks found, immediately begin working
81
- 3. If no tasks, announce you are idle and waiting
82
-
83
- Do NOT ask permission to continue - work autonomously until no tasks remain.
84
- ```
85
-
86
- 5. The stop hook (hooks/worker-loop.js) will intercept exit attempts and:
87
- - If tasks exist: feed prompt to continue working
88
- - If no tasks: increment idle counter
89
- - If max idle reached: allow exit
90
-
91
- ## How It Works
92
-
93
- The Worker Loop uses Claude Code's **Stop Hook** feature:
94
-
95
- 1. When a worker tries to exit, the hook script runs
96
- 2. It checks for pending tasks in `tasks/pending/` and `tasks/needs-changes/`
97
- 3. If tasks found: blocks exit and feeds a prompt to continue working
98
- 4. If no tasks: allows exit (or waits, depending on config)
99
-
100
- ## Benefits
101
-
102
- - Workers stay active and pick up new tasks automatically
103
- - No need to manually respawn workers
104
- - Tasks can be added to pending/ and workers will find them
105
- - Config-based mode requires zero per-session setup
106
- - Configurable idle timeout prevents infinite waiting
1
+ ---
2
+ description: Start a persistent worker loop (Ralph-style)
3
+ argument-hint: <agent> [--max-idle 10] | stop
4
+ ---
5
+
6
+ # Worker Loop Command
7
+
8
+ Start a persistent worker loop for the specified agent. The worker will:
9
+ 1. Check for assigned tasks
10
+ 2. Work on tasks until complete
11
+ 3. Loop back and check for more tasks
12
+ 4. Only exit after N idle checks with no work found
13
+
14
+ ## Activation Modes
15
+
16
+ Worker Loop can be activated in two ways:
17
+
18
+ ### 1. Config-based (Recommended)
19
+
20
+ Enable during `forge init` or toggle anytime:
21
+
22
+ ```bash
23
+ forge config worker-loop on # Enable for all workers
24
+ forge config worker-loop off # Disable
25
+ forge config worker-loop # Check status
26
+ ```
27
+
28
+ When enabled, ALL workers will automatically stay running and check for new tasks.
29
+
30
+ ### 2. Runtime (Per-session)
31
+
32
+ Use this command for fine-grained control:
33
+
34
+ ```
35
+ /worker-loop anvil # Start Anvil in persistent loop
36
+ /worker-loop furnace --max-idle 20 # Custom idle limit
37
+ /worker-loop stop # Stop the current loop
38
+ ```
39
+
40
+ ## Arguments
41
+
42
+ - `$1` - Agent name (anvil, furnace, crucible, etc.) or "stop"
43
+ - `--max-idle N` - Exit after N checks with no tasks (default: 10)
44
+
45
+ ## Implementation
46
+
47
+ Based on `$ARGUMENTS`:
48
+
49
+ ### If first argument is "stop"
50
+
51
+ Remove the worker loop state file and confirm:
52
+
53
+ ```bash
54
+ rm -f "${CLAUDE_LOCAL_DIR:-$HOME/.claude}/forge-worker-loop.json"
55
+ ```
56
+
57
+ Output: "Worker loop stopped."
58
+
59
+ ### Otherwise, start a loop
60
+
61
+ 1. Resolve the agent name to canonical form
62
+ 2. Create state file at `${CLAUDE_LOCAL_DIR}/forge-worker-loop.json`:
63
+
64
+ ```json
65
+ {
66
+ "agent": "<resolved-agent>",
67
+ "max_idle_checks": 10,
68
+ "idle_count": 0,
69
+ "poll_interval": 5,
70
+ "started_at": "<ISO timestamp>"
71
+ }
72
+ ```
73
+
74
+ 3. Load the agent's personality file
75
+ 4. Start working with this system prompt addition:
76
+
77
+ ```
78
+ You are in PERSISTENT WORKER MODE. After completing each task:
79
+ 1. Check for more tasks assigned to you in tasks/pending/ and tasks/needs-changes/
80
+ 2. If tasks found, immediately begin working
81
+ 3. If no tasks, announce you are idle and waiting
82
+
83
+ Do NOT ask permission to continue - work autonomously until no tasks remain.
84
+ ```
85
+
86
+ 5. The stop hook (hooks/worker-loop.js) will intercept exit attempts and:
87
+ - If tasks exist: feed prompt to continue working
88
+ - If no tasks: increment idle counter
89
+ - If max idle reached: allow exit
90
+
91
+ ## How It Works
92
+
93
+ The Worker Loop uses Claude Code's **Stop Hook** feature:
94
+
95
+ 1. When a worker tries to exit, the hook script runs
96
+ 2. It checks for pending tasks in `tasks/pending/` and `tasks/needs-changes/`
97
+ 3. If tasks found: blocks exit and feeds a prompt to continue working
98
+ 4. If no tasks: allows exit (or waits, depending on config)
99
+
100
+ ## Benefits
101
+
102
+ - Workers stay active and pick up new tasks automatically
103
+ - No need to manually respawn workers
104
+ - Tasks can be added to pending/ and workers will find them
105
+ - Config-based mode requires zero per-session setup
106
+ - Configurable idle timeout prevents infinite waiting
@@ -1,187 +1,217 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Vibe Forge Worker Loop - Stop Hook (Cross-Platform)
4
- *
5
- * Implements the Ralph Loop technique for Vibe Forge workers.
6
- * When a worker tries to exit, this hook checks if there are pending tasks
7
- * and feeds the worker prompt back to continue working.
8
- *
9
- * Based on the Ralph Loop plugin by Anthropic.
10
- *
11
- * Activation modes:
12
- * 1. Config-based: worker_loop_enabled=true in .forge/config.json
13
- * 2. Runtime: State file created by /worker-loop command
14
- */
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const os = require('os');
19
-
20
- // State file location (runtime toggle)
21
- const claudeLocalDir = process.env.CLAUDE_LOCAL_DIR || path.join(os.homedir(), '.claude');
22
- const stateFile = path.join(claudeLocalDir, 'forge-worker-loop.json');
23
- const forgeRoot = process.env.FORGE_ROOT || process.cwd();
24
-
25
- // Helper to safely parse JSON
26
- function safeJsonParse(filePath, defaultValue = {}) {
27
- try {
28
- if (fs.existsSync(filePath)) {
29
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
30
- }
31
- } catch (e) {
32
- // Ignore parse errors
33
- }
34
- return defaultValue;
35
- }
36
-
37
- // Helper to count matching files
38
- function countFiles(dir, pattern = '*.md') {
39
- try {
40
- if (!fs.existsSync(dir)) return 0;
41
- const files = fs.readdirSync(dir);
42
- return files.filter(f => f.endsWith('.md')).length;
43
- } catch (e) {
44
- return 0;
45
- }
46
- }
47
-
48
- // Helper to count files with specific content
49
- function countFilesWithContent(dir, searchPattern) {
50
- try {
51
- if (!fs.existsSync(dir)) return 0;
52
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
53
- let count = 0;
54
- for (const file of files) {
55
- try {
56
- const content = fs.readFileSync(path.join(dir, file), 'utf8');
57
- if (content.includes(searchPattern)) {
58
- count++;
59
- }
60
- } catch (e) {
61
- // Skip files we can't read
62
- }
63
- }
64
- return count;
65
- } catch (e) {
66
- return 0;
67
- }
68
- }
69
-
70
- // Output result and exit
71
- function output(result) {
72
- console.log(JSON.stringify(result));
73
- process.exit(0);
74
- }
75
-
76
- // Main logic
77
- function main() {
78
- let loopActive = false;
79
- let workerAgent = '';
80
- let maxIdleChecks = 10;
81
- let idleCount = 0;
82
- let pollInterval = 5;
83
-
84
- // Check for runtime state file first (takes precedence)
85
- if (fs.existsSync(stateFile)) {
86
- const state = safeJsonParse(stateFile);
87
- loopActive = true;
88
- workerAgent = state.agent || '';
89
- maxIdleChecks = state.max_idle_checks || 10;
90
- idleCount = state.idle_count || 0;
91
- pollInterval = state.poll_interval || 5;
92
- }
93
-
94
- // If no runtime state, check config-based setting
95
- if (!loopActive) {
96
- const configFile = path.join(forgeRoot, '.forge', 'config.json');
97
- const config = safeJsonParse(configFile);
98
- if (config.worker_loop_enabled === true) {
99
- loopActive = true;
100
- workerAgent = 'any';
101
- }
102
- }
103
-
104
- // Check if worker loop is active
105
- if (!loopActive) {
106
- return output({ decision: 'approve' });
107
- }
108
-
109
- if (!workerAgent) {
110
- // Invalid state, clean up and allow exit
111
- try { fs.unlinkSync(stateFile); } catch (e) {}
112
- return output({ decision: 'approve' });
113
- }
114
-
115
- // Check for pending tasks
116
- const tasksDir = path.join(forgeRoot, '_vibe-forge', 'tasks');
117
- let pendingCount = 0;
118
- let needsChangesCount = 0;
119
-
120
- const pendingDir = path.join(tasksDir, 'pending');
121
- const needsChangesDir = path.join(tasksDir, 'needs-changes');
122
-
123
- if (workerAgent === 'any') {
124
- // Config mode: count all tasks
125
- pendingCount = countFiles(pendingDir);
126
- needsChangesCount = countFiles(needsChangesDir);
127
- } else {
128
- // Runtime mode: count only tasks assigned to specific worker
129
- const searchPattern = `assigned_to: ${workerAgent}`;
130
- pendingCount = countFilesWithContent(pendingDir, searchPattern);
131
- needsChangesCount = countFilesWithContent(needsChangesDir, searchPattern);
132
- }
133
-
134
- const totalTasks = pendingCount + needsChangesCount;
135
-
136
- if (totalTasks > 0) {
137
- // Tasks found! Reset idle counter and continue
138
- if (fs.existsSync(stateFile)) {
139
- try {
140
- const state = safeJsonParse(stateFile);
141
- state.idle_count = 0;
142
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
143
- } catch (e) {}
144
- }
145
-
146
- return output({
147
- decision: 'block',
148
- message: `[Forge Loop] Found ${totalTasks} pending task(s). Continuing work...`,
149
- prompt: 'Check _vibe-forge/tasks/pending/ and _vibe-forge/tasks/needs-changes/ for tasks assigned to you and begin working on them immediately.'
150
- });
151
- }
152
-
153
- // No tasks - handle idle state
154
- if (fs.existsSync(stateFile)) {
155
- idleCount++;
156
-
157
- try {
158
- const state = safeJsonParse(stateFile);
159
- state.idle_count = idleCount;
160
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
161
- } catch (e) {}
162
-
163
- if (idleCount >= maxIdleChecks) {
164
- // Max idle checks reached, clean up and allow exit
165
- try { fs.unlinkSync(stateFile); } catch (e) {}
166
- return output({
167
- decision: 'approve',
168
- message: `[Forge Loop] No tasks found after ${maxIdleChecks} checks. Exiting.`
169
- });
170
- }
171
-
172
- // Still within idle limit - wait and check again
173
- return output({
174
- decision: 'block',
175
- message: `[Forge Loop] No tasks available. Idle check ${idleCount}/${maxIdleChecks}. Waiting...`,
176
- prompt: 'No tasks currently assigned to you. Wait briefly, then check _vibe-forge/tasks/pending/ and _vibe-forge/tasks/needs-changes/ again for new work. If still no tasks, announce you are idle and ready.'
177
- });
178
- }
179
-
180
- // Config-based mode - no idle tracking, just allow exit when no tasks
181
- return output({
182
- decision: 'approve',
183
- message: '[Forge Loop] No pending tasks. Worker exiting.'
184
- });
185
- }
186
-
187
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Vibe Forge Worker Loop - Stop Hook (Cross-Platform)
4
+ *
5
+ * Implements the Ralph Loop technique for Vibe Forge workers.
6
+ * When a worker tries to exit, this hook checks if there are pending tasks
7
+ * and feeds the worker prompt back to continue working.
8
+ *
9
+ * Based on the Ralph Loop plugin by Anthropic.
10
+ *
11
+ * Activation modes:
12
+ * 1. Config-based: worker_loop_enabled=true in .forge/config.json
13
+ * 2. Runtime: State file created by /worker-loop command
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+
20
+ // State file location (runtime toggle)
21
+ const claudeLocalDir = process.env.CLAUDE_LOCAL_DIR || path.join(os.homedir(), '.claude');
22
+ const stateFile = path.join(claudeLocalDir, 'forge-worker-loop.json');
23
+ // SEC-005: Validate FORGE_ROOT to prevent path traversal via attacker-controlled env
24
+ const rawForgeRoot = process.env.FORGE_ROOT || process.cwd();
25
+ const forgeRoot = validateForgeRoot(rawForgeRoot) ? rawForgeRoot : null;
26
+
27
+ function validateForgeRoot(dir) {
28
+ try {
29
+ // Must contain tasks/ and .forge/ (or config/) to be a valid forge project
30
+ const hasTasksDir = fs.existsSync(path.join(dir, 'tasks'));
31
+ const hasForgeDir = fs.existsSync(path.join(dir, '.forge')) || fs.existsSync(path.join(dir, 'config'));
32
+ return hasTasksDir && hasForgeDir;
33
+ } catch (_) {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ // Helper to safely parse JSON
39
+ function safeJsonParse(filePath, defaultValue = {}) {
40
+ try {
41
+ if (fs.existsSync(filePath)) {
42
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
43
+ }
44
+ } catch (e) {
45
+ // Ignore parse errors
46
+ }
47
+ return defaultValue;
48
+ }
49
+
50
+ // Helper to count matching files
51
+ function countFiles(dir, pattern = '*.md') {
52
+ try {
53
+ if (!fs.existsSync(dir)) return 0;
54
+ const files = fs.readdirSync(dir);
55
+ return files.filter(f => f.endsWith('.md')).length;
56
+ } catch (e) {
57
+ return 0;
58
+ }
59
+ }
60
+
61
+ // Helper to count files with specific content
62
+ function countFilesWithContent(dir, searchPattern) {
63
+ try {
64
+ if (!fs.existsSync(dir)) return 0;
65
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
66
+ let count = 0;
67
+ for (const file of files) {
68
+ try {
69
+ const content = fs.readFileSync(path.join(dir, file), 'utf8');
70
+ if (content.includes(searchPattern)) {
71
+ count++;
72
+ }
73
+ } catch (e) {
74
+ // Skip files we can't read
75
+ }
76
+ }
77
+ return count;
78
+ } catch (e) {
79
+ return 0;
80
+ }
81
+ }
82
+
83
+ // Output result and exit
84
+ function output(result) {
85
+ console.log(JSON.stringify(result));
86
+ process.exit(0);
87
+ }
88
+
89
+ // Main logic
90
+ function main() {
91
+ // SEC-005: Abort safely if forge root is invalid
92
+ if (!forgeRoot) {
93
+ output({ decision: 'allow' });
94
+ return;
95
+ }
96
+
97
+ let loopActive = false;
98
+ let workerAgent = '';
99
+ let maxIdleChecks = 10;
100
+ let idleCount = 0;
101
+ let pollInterval = 5;
102
+
103
+ // Check for runtime state file first (takes precedence)
104
+ if (fs.existsSync(stateFile)) {
105
+ const state = safeJsonParse(stateFile);
106
+ loopActive = true;
107
+ workerAgent = state.agent || '';
108
+ maxIdleChecks = state.max_idle_checks || 10;
109
+ idleCount = state.idle_count || 0;
110
+ pollInterval = state.poll_interval || 5;
111
+ }
112
+
113
+ // If no runtime state, check config-based setting
114
+ if (!loopActive) {
115
+ const configFile = path.join(forgeRoot, '.forge', 'config.json');
116
+ const config = safeJsonParse(configFile);
117
+ if (config.worker_loop_enabled === true) {
118
+ loopActive = true;
119
+ workerAgent = 'any';
120
+ }
121
+ }
122
+
123
+ // Check if worker loop is active
124
+ if (!loopActive) {
125
+ return output({ decision: 'approve' });
126
+ }
127
+
128
+ if (!workerAgent) {
129
+ // Invalid state, clean up and allow exit
130
+ try { fs.unlinkSync(stateFile); } catch (e) {}
131
+ return output({ decision: 'approve' });
132
+ }
133
+
134
+ // Determine tasks directory location
135
+ // When running from vibe-forge repo: tasks are at ./tasks/
136
+ // When running from a project using vibe-forge as submodule: tasks are at ./_vibe-forge/tasks/
137
+ let tasksDir = path.join(forgeRoot, 'tasks');
138
+ if (!fs.existsSync(tasksDir)) {
139
+ // Fall back to submodule location
140
+ tasksDir = path.join(forgeRoot, '_vibe-forge', 'tasks');
141
+ }
142
+
143
+ // Check for pending tasks
144
+ let pendingCount = 0;
145
+ let needsChangesCount = 0;
146
+
147
+ const pendingDir = path.join(tasksDir, 'pending');
148
+ const needsChangesDir = path.join(tasksDir, 'needs-changes');
149
+
150
+ if (workerAgent === 'any') {
151
+ // Config mode: count all tasks
152
+ pendingCount = countFiles(pendingDir);
153
+ needsChangesCount = countFiles(needsChangesDir);
154
+ } else {
155
+ // Runtime mode: count only tasks assigned to specific worker
156
+ const searchPattern = `assigned_to: ${workerAgent}`;
157
+ pendingCount = countFilesWithContent(pendingDir, searchPattern);
158
+ needsChangesCount = countFilesWithContent(needsChangesDir, searchPattern);
159
+ }
160
+
161
+ const totalTasks = pendingCount + needsChangesCount;
162
+
163
+ // Get relative task path for prompts (relative to forgeRoot)
164
+ const relativeTasksPath = path.relative(forgeRoot, tasksDir);
165
+
166
+ if (totalTasks > 0) {
167
+ // Tasks found! Reset idle counter and continue
168
+ if (fs.existsSync(stateFile)) {
169
+ try {
170
+ const state = safeJsonParse(stateFile);
171
+ state.idle_count = 0;
172
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
173
+ } catch (e) {}
174
+ }
175
+
176
+ return output({
177
+ decision: 'block',
178
+ message: `[Forge Loop] Found ${totalTasks} pending task(s). Continuing work...`,
179
+ prompt: `Check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ for tasks assigned to you and begin working on them immediately.`
180
+ });
181
+ }
182
+
183
+ // No tasks - handle idle state
184
+ if (fs.existsSync(stateFile)) {
185
+ idleCount++;
186
+
187
+ try {
188
+ const state = safeJsonParse(stateFile);
189
+ state.idle_count = idleCount;
190
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
191
+ } catch (e) {}
192
+
193
+ if (idleCount >= maxIdleChecks) {
194
+ // Max idle checks reached, clean up and allow exit
195
+ try { fs.unlinkSync(stateFile); } catch (e) {}
196
+ return output({
197
+ decision: 'approve',
198
+ message: `[Forge Loop] No tasks found after ${maxIdleChecks} checks. Exiting.`
199
+ });
200
+ }
201
+
202
+ // Still within idle limit - wait and check again
203
+ return output({
204
+ decision: 'block',
205
+ message: `[Forge Loop] No tasks available. Idle check ${idleCount}/${maxIdleChecks}. Waiting...`,
206
+ prompt: `No tasks currently assigned to you. Wait briefly, then check ${relativeTasksPath}/pending/ and ${relativeTasksPath}/needs-changes/ again for new work. If still no tasks, announce you are idle and ready.`
207
+ });
208
+ }
209
+
210
+ // Config-based mode - no idle tracking, just allow exit when no tasks
211
+ return output({
212
+ decision: 'approve',
213
+ message: '[Forge Loop] No pending tasks. Worker exiting.'
214
+ });
215
+ }
216
+
217
+ main();