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.
- package/.claude/commands/clear-attention.md +63 -63
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -171
- package/.claude/commands/need-help.md +77 -77
- package/.claude/commands/update-status.md +64 -64
- package/.claude/commands/worker-loop.md +106 -106
- package/.claude/hooks/worker-loop.js +217 -187
- package/.claude/scripts/setup-worker-loop.sh +45 -45
- package/.claude/settings.json +89 -0
- package/LICENSE +21 -21
- package/README.md +253 -232
- package/agents/aegis/personality.md +303 -269
- package/agents/anvil/personality.md +278 -240
- package/agents/architect/personality.md +260 -234
- package/agents/crucible/personality.md +362 -309
- package/agents/crucible-x/personality.md +210 -0
- package/agents/ember/personality.md +293 -265
- package/agents/flux/personality.md +248 -0
- package/agents/furnace/personality.md +342 -291
- package/agents/herald/personality.md +249 -247
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +284 -0
- package/agents/pixel/personality.md +140 -0
- package/agents/planning-hub/personality.md +473 -251
- package/agents/scribe/personality.md +253 -251
- package/agents/slag/personality.md +268 -0
- package/agents/temper/personality.md +270 -0
- package/bin/cli.js +372 -325
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +507 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
- package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +645 -0
- package/bin/forge-daemon.sh +477 -851
- package/bin/forge-setup.sh +661 -645
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.cmd +83 -83
- package/bin/forge.sh +566 -387
- package/bin/lib/agents.sh +177 -177
- package/bin/lib/check-aliases.js +50 -0
- package/bin/lib/colors.sh +44 -44
- package/bin/lib/config.sh +347 -313
- package/bin/lib/constants.sh +241 -206
- package/bin/lib/daemon/budgets.sh +107 -0
- package/bin/lib/daemon/dependencies.sh +146 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +273 -0
- package/bin/lib/daemon/routing.sh +93 -0
- package/bin/lib/daemon/state.sh +163 -0
- package/bin/lib/daemon/sync.sh +103 -0
- package/bin/lib/database.sh +357 -305
- package/bin/lib/frontmatter.js +106 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -258
- package/bin/lib/terminal.js +452 -446
- package/bin/lib/util.sh +126 -126
- package/bin/lib/vcs.js +349 -349
- package/config/agent-manifest.yaml +237 -243
- package/config/agents.json +207 -132
- package/config/task-template.md +159 -87
- package/config/task-types.yaml +111 -106
- package/config/templates/handoff-template.md +40 -0
- package/context/agent-overrides/README.md +41 -0
- package/context/architecture.md +42 -0
- package/context/modern-conventions.md +129 -129
- package/context/project-context-template.md +122 -122
- package/docs/agents.md +473 -409
- package/docs/architecture.md +194 -162
- package/docs/commands.md +451 -388
- package/docs/security.md +195 -144
- package/package.json +77 -50
- package/.claude/settings.local.json +0 -33
- package/agents/forge-master/capabilities.md +0 -144
- package/agents/forge-master/context-template.md +0 -128
- package/agents/forge-master/personality.md +0 -138
- package/agents/sentinel/personality.md +0 -194
- package/context/forge-state.yaml +0 -19
- package/docs/TODO.md +0 -150
- package/docs/getting-started.md +0 -243
- package/docs/npm-publishing.md +0 -95
- package/docs/workflows/README.md +0 -32
- package/docs/workflows/azure-devops.md +0 -108
- package/docs/workflows/bitbucket.md +0 -104
- package/docs/workflows/git-only.md +0 -130
- package/docs/workflows/gitea.md +0 -168
- package/docs/workflows/github.md +0 -103
- package/docs/workflows/gitlab.md +0 -105
- package/docs/workflows.md +0 -454
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
- package/tasks/completed/ARCH-009-test-organization.md +0 -78
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
- package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
- package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
- package/tasks/completed/CLEAN-001.md +0 -38
- package/tasks/completed/CLEAN-003.md +0 -47
- package/tasks/completed/CLEAN-004.md +0 -56
- package/tasks/completed/CLEAN-005.md +0 -75
- package/tasks/completed/CLEAN-006.md +0 -47
- package/tasks/completed/CLEAN-007.md +0 -34
- package/tasks/completed/CLEAN-008.md +0 -49
- package/tasks/completed/CLEAN-012.md +0 -58
- package/tasks/completed/CLEAN-013.md +0 -45
- package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
- package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
- package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
- package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
- package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
- package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
- package/tasks/pending/ARCH-006-task-template-location.md +0 -64
- package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
- package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
- package/tasks/pending/CLEAN-002.md +0 -29
- package/tasks/pending/CLEAN-009.md +0 -31
- package/tasks/pending/CLEAN-010.md +0 -30
- package/tasks/pending/CLEAN-011.md +0 -30
- package/tasks/pending/CLEAN-014.md +0 -32
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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();
|