ralph-tool 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +40 -0
- package/README.md +148 -0
- package/bin/ralph.js +357 -0
- package/docker-compose.yaml +40 -0
- package/helpers/constants.sh +22 -0
- package/helpers/cost_helpers.sh +73 -0
- package/helpers/display_helpers.sh +312 -0
- package/helpers/extract_subagent_map.jq +32 -0
- package/helpers/iteration_helpers.sh +69 -0
- package/helpers/output_formatting.jq +294 -0
- package/package.json +48 -0
- package/scripts/check_feature_completion.sh +116 -0
- package/scripts/cleanup.sh +179 -0
- package/scripts/clear.sh +38 -0
- package/scripts/git_feature_complete.sh +133 -0
- package/scripts/loop.sh +206 -0
- package/scripts/plan-loop.sh +233 -0
- package/scripts/postinstall.js +155 -0
- package/scripts/review.sh +90 -0
- package/scripts/sandbox.sh +15 -0
- package/scripts/start.sh +25 -0
- package/scripts/status.sh +108 -0
- package/templates/AGENT.md +27 -0
- package/templates/feature-spec-template.md +115 -0
- package/templates/plan-cleanup-prompt.md +61 -0
- package/templates/plan-prompt.md +114 -0
- package/templates/pr-review-prompt.md +14 -0
- package/templates/prd.json +22 -0
- package/templates/prompt.md +124 -0
- package/templates/roles/ralph-helper.md +73 -0
- package/templates/roles/ralph-plan-feature.md +74 -0
- package/templates/roles/ralph-plan-vision.md +46 -0
- package/templates/testing-harness.md +42 -0
- package/templates/vision.md +43 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Output formatting filter for ralph loop
|
|
2
|
+
# This file contains the jq filter for formatting Claude's JSON stream output
|
|
3
|
+
|
|
4
|
+
# Color escape codes
|
|
5
|
+
def esc(s): "\u001b[" + s + "m";
|
|
6
|
+
def reset: if $COLOR==1 then esc("0") else "" end;
|
|
7
|
+
def bold: if $COLOR==1 then esc("1") else "" end;
|
|
8
|
+
def cyan: if $COLOR==1 then esc("36") else "" end;
|
|
9
|
+
def yellow: if $COLOR==1 then esc("33") else "" end;
|
|
10
|
+
def green: if $COLOR==1 then esc("32") else "" end;
|
|
11
|
+
def red: if $COLOR==1 then esc("31") else "" end;
|
|
12
|
+
def mag: if $COLOR==1 then esc("35") else "" end;
|
|
13
|
+
def dim: if $COLOR==1 then esc("2") else "" end;
|
|
14
|
+
def blue: if $COLOR==1 then esc("34") else "" end;
|
|
15
|
+
|
|
16
|
+
def hr: "────────────────────────────────────────────────────────────\n";
|
|
17
|
+
|
|
18
|
+
# Extract file path from tool input
|
|
19
|
+
def extract_file_path(input):
|
|
20
|
+
if input.file_path? then input.file_path
|
|
21
|
+
elif input.target_file? then input.target_file
|
|
22
|
+
elif input.file? then input.file
|
|
23
|
+
elif input.path? then input.path
|
|
24
|
+
else null
|
|
25
|
+
end;
|
|
26
|
+
|
|
27
|
+
# Check if command is ralph status
|
|
28
|
+
def is_ralph_status_cmd(cmd):
|
|
29
|
+
(cmd | test("ralph\\s+status"));
|
|
30
|
+
|
|
31
|
+
# Extract minimal tool info for display (main agent)
|
|
32
|
+
def tool_info_minimal(input; name):
|
|
33
|
+
if input.command? then
|
|
34
|
+
(input.command|tostring) as $cmd
|
|
35
|
+
| if is_ralph_status_cmd($cmd) then
|
|
36
|
+
"BASH: " + $cmd
|
|
37
|
+
else
|
|
38
|
+
($cmd | split("\n")) as $lines
|
|
39
|
+
| if ($lines | length) > 4 then
|
|
40
|
+
(($lines[0:4] | join("\n")) + "\n...")
|
|
41
|
+
else
|
|
42
|
+
$cmd
|
|
43
|
+
end
|
|
44
|
+
| "BASH: " + .
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
(extract_file_path(input) // "") as $file
|
|
48
|
+
| if $file != null and $file != "" then
|
|
49
|
+
name + " " + $file
|
|
50
|
+
else
|
|
51
|
+
name
|
|
52
|
+
end
|
|
53
|
+
end;
|
|
54
|
+
|
|
55
|
+
# Extract tool info for subagents (more detailed)
|
|
56
|
+
def tool_info_subagent(input; name):
|
|
57
|
+
if input.command? then
|
|
58
|
+
(input.command|tostring) as $cmd
|
|
59
|
+
| if is_ralph_status_cmd($cmd) then
|
|
60
|
+
"BASH: " + $cmd
|
|
61
|
+
else
|
|
62
|
+
($cmd | split("\n")) as $lines
|
|
63
|
+
| if ($lines | length) > 4 then
|
|
64
|
+
(($lines[0:4] | join("\n")) + "\n...")
|
|
65
|
+
else
|
|
66
|
+
$cmd
|
|
67
|
+
end
|
|
68
|
+
| "BASH: " + .
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
(extract_file_path(input) // "") as $file
|
|
72
|
+
| if $file != null and $file != "" then
|
|
73
|
+
name + " → " + $file
|
|
74
|
+
elif input.pattern? then
|
|
75
|
+
name + " → pattern: " + (input.pattern|tostring)
|
|
76
|
+
elif input.query? then
|
|
77
|
+
name + " → query: " + (input.query|tostring)
|
|
78
|
+
elif input.glob_pattern? then
|
|
79
|
+
name + " → glob: " + (input.glob_pattern|tostring)
|
|
80
|
+
else
|
|
81
|
+
name
|
|
82
|
+
end
|
|
83
|
+
end;
|
|
84
|
+
|
|
85
|
+
# Extract parent_tool_use_id from various possible locations
|
|
86
|
+
def get_parent_id:
|
|
87
|
+
if .parent_tool_use_id? and (.parent_tool_use_id != "") then
|
|
88
|
+
.parent_tool_use_id
|
|
89
|
+
elif .event?.parent_tool_use_id? and (.event.parent_tool_use_id != "") then
|
|
90
|
+
.event.parent_tool_use_id
|
|
91
|
+
elif .event?.content_block?.parent_tool_use_id? and (.event.content_block.parent_tool_use_id != "") then
|
|
92
|
+
.event.content_block.parent_tool_use_id
|
|
93
|
+
elif .message?.parent_tool_use_id? and (.message.parent_tool_use_id != "") then
|
|
94
|
+
.message.parent_tool_use_id
|
|
95
|
+
else
|
|
96
|
+
null
|
|
97
|
+
end;
|
|
98
|
+
|
|
99
|
+
# Check if this is a subagent task
|
|
100
|
+
def is_subagent: (get_parent_id != null);
|
|
101
|
+
|
|
102
|
+
# Get subagent number from mapping
|
|
103
|
+
def get_subagent_num:
|
|
104
|
+
if is_subagent and ($SUBAGENT_MAP | type == "object") then
|
|
105
|
+
get_parent_id as $id
|
|
106
|
+
| if $id and $SUBAGENT_MAP[$id] then
|
|
107
|
+
$SUBAGENT_MAP[$id]
|
|
108
|
+
else
|
|
109
|
+
null
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
null
|
|
113
|
+
end;
|
|
114
|
+
|
|
115
|
+
# Subagent name list
|
|
116
|
+
def subagent_names:
|
|
117
|
+
["BOB", "BEN", "TIBO", "ALONSO", "JULIAN", "JULIA", "MELISSA", "TALIA", "CENK", "VIKRAM", "ANKIT", "JULIETTE", "ANICA", "CHUBS", "STILGARD", "GERARD"];
|
|
118
|
+
|
|
119
|
+
# Get short ID suffix from parent_tool_use_id
|
|
120
|
+
def get_short_id:
|
|
121
|
+
get_parent_id as $id
|
|
122
|
+
| if $id and ($id | length) > 4 then
|
|
123
|
+
$id[-4:]
|
|
124
|
+
elif $id then
|
|
125
|
+
$id
|
|
126
|
+
else
|
|
127
|
+
null
|
|
128
|
+
end;
|
|
129
|
+
|
|
130
|
+
# Get deterministic name from parent_tool_use_id
|
|
131
|
+
def get_subagent_name:
|
|
132
|
+
get_parent_id as $id
|
|
133
|
+
| if $id then
|
|
134
|
+
($id | explode | add) as $hash
|
|
135
|
+
| subagent_names[$hash % (subagent_names | length)]
|
|
136
|
+
else
|
|
137
|
+
null
|
|
138
|
+
end;
|
|
139
|
+
|
|
140
|
+
# Format subagent label
|
|
141
|
+
def subagent_label:
|
|
142
|
+
if is_subagent then
|
|
143
|
+
get_subagent_num as $num
|
|
144
|
+
| if $num then
|
|
145
|
+
blue + bold + "[SUBAGENT #\($num)] " + reset
|
|
146
|
+
else
|
|
147
|
+
get_subagent_name as $name
|
|
148
|
+
| get_short_id as $short
|
|
149
|
+
| if $name and $short then
|
|
150
|
+
blue + bold + "[\($name) @\($short)] " + reset
|
|
151
|
+
elif $name then
|
|
152
|
+
blue + bold + "[\($name)] " + reset
|
|
153
|
+
elif $short then
|
|
154
|
+
blue + bold + "[SUBAGENT @\($short)] " + reset
|
|
155
|
+
else
|
|
156
|
+
blue + bold + "[SUBAGENT] " + reset
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
""
|
|
161
|
+
end;
|
|
162
|
+
|
|
163
|
+
# Format tool result content
|
|
164
|
+
def format_tool_result(content; is_ralph_status):
|
|
165
|
+
if content | type == "string" then
|
|
166
|
+
if is_ralph_status then
|
|
167
|
+
(content | split("\n") | .[0:4] | join("\n"))
|
|
168
|
+
else
|
|
169
|
+
(content | split("\n") | .[0]) as $first
|
|
170
|
+
| if ($first|length) > 100 then
|
|
171
|
+
$first[0:100] + "..."
|
|
172
|
+
else
|
|
173
|
+
$first
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
"[OK]"
|
|
178
|
+
end;
|
|
179
|
+
|
|
180
|
+
# Format duration from milliseconds
|
|
181
|
+
def format_duration(ms):
|
|
182
|
+
if ms == null or ms == 0 then
|
|
183
|
+
"0s"
|
|
184
|
+
else
|
|
185
|
+
(ms / 1000 | floor) as $total_secs
|
|
186
|
+
| ($total_secs / 3600 | floor) as $hours
|
|
187
|
+
| (($total_secs % 3600) / 60 | floor) as $minutes
|
|
188
|
+
| ($total_secs % 60) as $seconds
|
|
189
|
+
| (if $hours > 0 then "\($hours)h " else "" end) +
|
|
190
|
+
(if $minutes > 0 then "\($minutes)m " else "" end) +
|
|
191
|
+
"\($seconds)s"
|
|
192
|
+
end;
|
|
193
|
+
|
|
194
|
+
# Format cost to 2 decimal places
|
|
195
|
+
def format_cost(cost):
|
|
196
|
+
if cost == null then
|
|
197
|
+
"$0.00"
|
|
198
|
+
else
|
|
199
|
+
(cost * 100 | round / 100) as $rounded
|
|
200
|
+
| ($rounded | tostring) as $str
|
|
201
|
+
| if ($str | contains(".")) then
|
|
202
|
+
($str | split(".")) as $parts
|
|
203
|
+
| "$" + $parts[0] + "." + (($parts[1] // "") | .[0:2] | if length < 2 then . + ("0" * (2 - length)) else . end)
|
|
204
|
+
else
|
|
205
|
+
"$" + $str + ".00"
|
|
206
|
+
end
|
|
207
|
+
end;
|
|
208
|
+
|
|
209
|
+
# Main filter
|
|
210
|
+
try
|
|
211
|
+
if .type=="system" and .subtype=="init" then
|
|
212
|
+
bold + cyan + "[MODEL] " + reset + bold + (.model // "unknown") + reset + "\n"
|
|
213
|
+
|
|
214
|
+
elif .type=="stream_event"
|
|
215
|
+
and .event.type=="content_block_start"
|
|
216
|
+
and .event.content_block.type=="text"
|
|
217
|
+
and .event.index == 0 then
|
|
218
|
+
"\n" + dim + ">>> " + reset
|
|
219
|
+
|
|
220
|
+
elif .type=="stream_event"
|
|
221
|
+
and .event.type=="content_block_start"
|
|
222
|
+
and .event.content_block.type=="tool_use" then
|
|
223
|
+
subagent_label +
|
|
224
|
+
mag + bold + "TOOL: " + reset + mag +
|
|
225
|
+
(if (is_subagent) then
|
|
226
|
+
tool_info_subagent(.event.content_block.input; .event.content_block.name)
|
|
227
|
+
else
|
|
228
|
+
tool_info_minimal(.event.content_block.input; .event.content_block.name)
|
|
229
|
+
end) +
|
|
230
|
+
reset + "\n"
|
|
231
|
+
|
|
232
|
+
elif .type=="assistant"
|
|
233
|
+
and (.message | type == "object")
|
|
234
|
+
and (.message.content | type == "array")
|
|
235
|
+
and (.message.content[]?.type=="tool_use") then
|
|
236
|
+
subagent_label +
|
|
237
|
+
(.message.content[]
|
|
238
|
+
| select(.type=="tool_use")
|
|
239
|
+
| if (.input.command? != null) then
|
|
240
|
+
yellow + bold + "BASH: " + reset + yellow +
|
|
241
|
+
(.input.command|tostring) + reset + "\n"
|
|
242
|
+
elif (is_subagent) then
|
|
243
|
+
yellow + bold + "TOOL: " + reset + yellow +
|
|
244
|
+
tool_info_subagent(.input; .name) + reset + "\n"
|
|
245
|
+
elif (.input.file_path? or .input.target_file? or .input.file?) then
|
|
246
|
+
yellow + bold + "TOOL: " + reset + yellow +
|
|
247
|
+
tool_info_minimal(.input; .name) + reset + "\n"
|
|
248
|
+
else empty end)
|
|
249
|
+
|
|
250
|
+
elif .type=="user"
|
|
251
|
+
and (.message | type == "object")
|
|
252
|
+
and (.message.content | type == "array")
|
|
253
|
+
and (.message.content[0]?.type=="tool_result") then
|
|
254
|
+
(if (is_subagent) then
|
|
255
|
+
empty
|
|
256
|
+
else
|
|
257
|
+
(.message.content[0].content | type == "string" and test("╔|╗|╚|╝|║|═|Feature:|Task:|Status:")) as $is_ralph_status
|
|
258
|
+
| cyan + bold + "RESULT:" + reset +
|
|
259
|
+
(if $is_ralph_status then
|
|
260
|
+
"\n" + format_tool_result(.message.content[0].content; $is_ralph_status)
|
|
261
|
+
elif (.message.content[0].content | type == "string") then
|
|
262
|
+
" " + format_tool_result(.message.content[0].content; false)
|
|
263
|
+
else
|
|
264
|
+
" [OK]"
|
|
265
|
+
end) + "\n"
|
|
266
|
+
end)
|
|
267
|
+
|
|
268
|
+
elif .type=="stream_event"
|
|
269
|
+
and .event.type=="content_block_delta"
|
|
270
|
+
and .event.delta.type=="text_delta"
|
|
271
|
+
and .event.index == 0 then
|
|
272
|
+
.event.delta.text | gsub("\n"; "\n ")
|
|
273
|
+
|
|
274
|
+
elif .type=="stream_event"
|
|
275
|
+
and .event.type=="content_block_stop"
|
|
276
|
+
and .event.index == 0 then
|
|
277
|
+
"\n\n"
|
|
278
|
+
|
|
279
|
+
elif .type=="stream_event" and .event.type=="message_stop" then
|
|
280
|
+
empty
|
|
281
|
+
|
|
282
|
+
elif .type=="result" then
|
|
283
|
+
hr +
|
|
284
|
+
green + bold + "[COMPLETE] ITERATION COMPLETED" + reset + "\n" +
|
|
285
|
+
" Turns: \(.num_turns) | Duration: \(format_duration(.duration_ms)) | Cost: \(format_cost(.total_cost_usd))\n" +
|
|
286
|
+
dim +
|
|
287
|
+
" Tokens: in=\(.usage.input_tokens) out=\(.usage.output_tokens) cache_read=\(.usage.cache_read_input_tokens) cache_write=\(.usage.cache_creation_input_tokens)\n" +
|
|
288
|
+
reset +
|
|
289
|
+
hr
|
|
290
|
+
|
|
291
|
+
else
|
|
292
|
+
empty
|
|
293
|
+
end
|
|
294
|
+
catch empty
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ralph-tool",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered task automation CLI for software development",
|
|
5
|
+
"main": "bin/ralph.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ralph": "./bin/ralph.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/postinstall.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"automation",
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"cli",
|
|
18
|
+
"task-runner",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"code-generation",
|
|
21
|
+
"orchestration"
|
|
22
|
+
],
|
|
23
|
+
"author": "Thibault Knobloch",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Thibault-Knobloch/ralph-tool.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/Thibault-Knobloch/ralph-tool#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Thibault-Knobloch/ralph-tool/issues"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"chalk": "^4.1.2"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"bin/",
|
|
42
|
+
"scripts/",
|
|
43
|
+
"helpers/",
|
|
44
|
+
"templates/",
|
|
45
|
+
"Dockerfile",
|
|
46
|
+
"docker-compose.yaml"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Ralph Auto Feature Completion Check
|
|
5
|
+
# Called automatically after each loop iteration
|
|
6
|
+
# Environment: RALPH_HOME, PROJECT_RALPH_DIR
|
|
7
|
+
|
|
8
|
+
RALPH_NAME="${1:-RALPH}"
|
|
9
|
+
|
|
10
|
+
RALPH_HOME="${RALPH_HOME:?RALPH_HOME not set}"
|
|
11
|
+
PROJECT_RALPH_DIR="${PROJECT_RALPH_DIR:?PROJECT_RALPH_DIR not set}"
|
|
12
|
+
|
|
13
|
+
PRD_FILE="${PROJECT_RALPH_DIR}/tasks/prd.json"
|
|
14
|
+
PROGRESS_FILE="${PROJECT_RALPH_DIR}/tasks/progress.txt"
|
|
15
|
+
PROGRESS_ARCHIVE_DIR="${PROJECT_RALPH_DIR}/logs/progress"
|
|
16
|
+
|
|
17
|
+
# Verify PRD file exists
|
|
18
|
+
if [[ ! -f "$PRD_FILE" ]]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Check if multi-feature format
|
|
23
|
+
IS_MULTI_FEATURE=$(jq 'has("features")' "$PRD_FILE" 2>/dev/null)
|
|
24
|
+
if [[ "$IS_MULTI_FEATURE" != "true" ]]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Find current feature (first incomplete one)
|
|
29
|
+
CURRENT_FEATURE=$(jq -r '.features[] | select(.feature_completed == false) | .id' "$PRD_FILE" 2>/dev/null | head -1)
|
|
30
|
+
|
|
31
|
+
if [[ -z "$CURRENT_FEATURE" ]]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Check if all tasks in current feature are completed
|
|
36
|
+
ALL_TASKS_DONE=$(jq --arg id "$CURRENT_FEATURE" '
|
|
37
|
+
.features[] | select(.id == $id) |
|
|
38
|
+
(.tasks | length > 0) and (.tasks | all(.completed == true))
|
|
39
|
+
' "$PRD_FILE" 2>/dev/null)
|
|
40
|
+
|
|
41
|
+
if [[ "$ALL_TASKS_DONE" != "true" ]]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Check if feature is already marked complete
|
|
46
|
+
FEATURE_ALREADY_COMPLETE=$(jq --arg id "$CURRENT_FEATURE" '
|
|
47
|
+
.features[] | select(.id == $id) | .feature_completed
|
|
48
|
+
' "$PRD_FILE" 2>/dev/null)
|
|
49
|
+
|
|
50
|
+
if [[ "$FEATURE_ALREADY_COMPLETE" == "true" ]]; then
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# === All tasks done, feature not yet marked complete - proceed ===
|
|
55
|
+
|
|
56
|
+
# Get feature name for archive filename
|
|
57
|
+
FEATURE_NAME=$(jq -r --arg id "$CURRENT_FEATURE" '.features[] | select(.id == $id) | .name' "$PRD_FILE" 2>/dev/null)
|
|
58
|
+
FEATURE_NAME_KEBAB=$(echo "$FEATURE_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
|
|
59
|
+
|
|
60
|
+
echo ""
|
|
61
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
62
|
+
echo " AUTO-COMPLETING FEATURE: $FEATURE_NAME"
|
|
63
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# Step 1: Update prd.json - set feature_completed: true
|
|
67
|
+
echo "[1/4] Marking feature as completed in prd.json..."
|
|
68
|
+
jq --arg id "$CURRENT_FEATURE" '(.features[] | select(.id == $id) | .feature_completed) = true' "$PRD_FILE" > "${PRD_FILE}.tmp"
|
|
69
|
+
mv "${PRD_FILE}.tmp" "$PRD_FILE"
|
|
70
|
+
echo " ✓ Feature $CURRENT_FEATURE marked as completed"
|
|
71
|
+
|
|
72
|
+
# Step 2: Git operations
|
|
73
|
+
if [[ "${RALPH_LOCAL:-}" == "1" ]]; then
|
|
74
|
+
echo "[2/4] Committing changes locally (--local mode, skipping branch/push/PR)..."
|
|
75
|
+
git add -A -- ':!.ralph'
|
|
76
|
+
git commit -m "RALPH: ${FEATURE_NAME}" || echo " ⓘ Nothing to commit"
|
|
77
|
+
echo " ✓ Changes committed on current branch"
|
|
78
|
+
else
|
|
79
|
+
echo "[2/4] Creating git branch and PR for feature..."
|
|
80
|
+
"${RALPH_HOME}/scripts/git_feature_complete.sh" "$FEATURE_NAME" "$FEATURE_NAME_KEBAB" "$RALPH_NAME" "$PROGRESS_FILE"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Step 3: Archive progress.txt
|
|
84
|
+
echo "[3/4] Archiving progress.txt..."
|
|
85
|
+
mkdir -p "$PROGRESS_ARCHIVE_DIR"
|
|
86
|
+
|
|
87
|
+
if [[ -f "$PROGRESS_FILE" && -s "$PROGRESS_FILE" ]]; then
|
|
88
|
+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
89
|
+
ARCHIVE_FILE="${PROGRESS_ARCHIVE_DIR}/${FEATURE_NAME_KEBAB}-progress-${TIMESTAMP}.txt"
|
|
90
|
+
cp "$PROGRESS_FILE" "$ARCHIVE_FILE"
|
|
91
|
+
echo " ✓ Archived to: $ARCHIVE_FILE"
|
|
92
|
+
else
|
|
93
|
+
echo " ⓘ No progress.txt to archive (empty or missing)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Step 4: Clear progress.txt for next feature
|
|
97
|
+
echo "[4/4] Clearing progress.txt for next feature..."
|
|
98
|
+
: > "$PROGRESS_FILE"
|
|
99
|
+
echo " ✓ progress.txt cleared"
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
103
|
+
echo " FEATURE '$FEATURE_NAME' AUTO-COMPLETED"
|
|
104
|
+
echo "═══════════════════════════════════════════════════════════════"
|
|
105
|
+
|
|
106
|
+
# Check if there's a next feature
|
|
107
|
+
NEXT_FEATURE=$(jq -r '.features[] | select(.feature_completed == false) | .name' "$PRD_FILE" 2>/dev/null | head -1)
|
|
108
|
+
if [[ -n "$NEXT_FEATURE" ]]; then
|
|
109
|
+
echo " Next feature: $NEXT_FEATURE"
|
|
110
|
+
echo ""
|
|
111
|
+
exit 0
|
|
112
|
+
else
|
|
113
|
+
echo " 🎉 ALL FEATURES COMPLETED!"
|
|
114
|
+
echo ""
|
|
115
|
+
exit 99
|
|
116
|
+
fi
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Ralph Cleanup - Archive Completed Features
|
|
5
|
+
# Environment: RALPH_HOME, PROJECT_RALPH_DIR
|
|
6
|
+
|
|
7
|
+
RALPH_HOME="${RALPH_HOME:?RALPH_HOME not set}"
|
|
8
|
+
PROJECT_RALPH_DIR="${PROJECT_RALPH_DIR:?PROJECT_RALPH_DIR not set}"
|
|
9
|
+
|
|
10
|
+
TASKS_DIR="${PROJECT_RALPH_DIR}/tasks"
|
|
11
|
+
NEW_TASKS_DIR="${TASKS_DIR}/1_new_tasks"
|
|
12
|
+
DONE_TASKS_DIR="${TASKS_DIR}/2_done_tasks"
|
|
13
|
+
PROGRESS_DIR="${PROJECT_RALPH_DIR}/logs/progress"
|
|
14
|
+
PRD_FILE="${TASKS_DIR}/prd.json"
|
|
15
|
+
|
|
16
|
+
FEATURE_ARG="${1:-}"
|
|
17
|
+
|
|
18
|
+
# Colors
|
|
19
|
+
RED='\033[0;31m'
|
|
20
|
+
GREEN='\033[0;32m'
|
|
21
|
+
YELLOW='\033[1;33m'
|
|
22
|
+
LIGHT_BLUE='\033[1;36m'
|
|
23
|
+
NC='\033[0m'
|
|
24
|
+
|
|
25
|
+
echo ""
|
|
26
|
+
echo -e "${LIGHT_BLUE}Ralph Cleanup - Archive Completed Features${NC}"
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# 1. Clear iteration logs
|
|
30
|
+
echo "[1/4] Clearing iteration logs..."
|
|
31
|
+
"${RALPH_HOME}/scripts/clear.sh" > /dev/null 2>&1 || true
|
|
32
|
+
echo -e " ${GREEN}✓${NC} Logs cleared"
|
|
33
|
+
|
|
34
|
+
# 2. Parse prd.json and find completed features
|
|
35
|
+
echo "[2/4] Finding completed features..."
|
|
36
|
+
|
|
37
|
+
if [[ ! -f "$PRD_FILE" ]]; then
|
|
38
|
+
echo -e "${RED}Error: prd.json not found${NC}"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Get completed features as JSON array
|
|
43
|
+
COMPLETED_FEATURES=$(jq '[.features[] | select(.feature_completed == true)]' "$PRD_FILE")
|
|
44
|
+
COMPLETED_COUNT=$(echo "$COMPLETED_FEATURES" | jq 'length')
|
|
45
|
+
|
|
46
|
+
if [[ "$COMPLETED_COUNT" -eq 0 ]]; then
|
|
47
|
+
echo -e "${YELLOW}No completed features to cleanup${NC}"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# If feature arg provided, filter to just that feature
|
|
52
|
+
if [[ -n "$FEATURE_ARG" ]]; then
|
|
53
|
+
ARG_LOWER=$(echo "$FEATURE_ARG" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
|
54
|
+
|
|
55
|
+
MATCHED_FEATURE=$(echo "$COMPLETED_FEATURES" | jq --arg arg "$ARG_LOWER" '
|
|
56
|
+
[.[] | select(
|
|
57
|
+
(.name | ascii_downcase | gsub(" "; "-")) == $arg or
|
|
58
|
+
(.name | ascii_downcase | gsub(" "; "-") | contains($arg)) or
|
|
59
|
+
(.id | ascii_downcase) == $arg
|
|
60
|
+
)] | .[0] // empty
|
|
61
|
+
')
|
|
62
|
+
|
|
63
|
+
if [[ -z "$MATCHED_FEATURE" || "$MATCHED_FEATURE" == "null" ]]; then
|
|
64
|
+
echo -e "${RED}Feature '${FEATURE_ARG}' not found in completed features${NC}"
|
|
65
|
+
echo "Completed features:"
|
|
66
|
+
echo "$COMPLETED_FEATURES" | jq -r '.[].name' | sed 's/^/ - /'
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
COMPLETED_FEATURES="[$MATCHED_FEATURE]"
|
|
71
|
+
COMPLETED_COUNT=1
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo -e " ${GREEN}✓${NC} Found ${COMPLETED_COUNT} completed feature(s)"
|
|
75
|
+
|
|
76
|
+
# Helper: Convert feature name to slug
|
|
77
|
+
to_slug() {
|
|
78
|
+
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed 's/[^a-z0-9-]//g'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Helper: Convert slug to camelCase
|
|
82
|
+
to_camel_case() {
|
|
83
|
+
echo "$1" | awk -F'-' '{
|
|
84
|
+
for(i=1; i<=NF; i++) {
|
|
85
|
+
if(i==1) printf "%s", $i
|
|
86
|
+
else printf "%s", toupper(substr($i,1,1)) substr($i,2)
|
|
87
|
+
}
|
|
88
|
+
}'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# 3. Process each feature
|
|
92
|
+
echo "[3/4] Archiving features..."
|
|
93
|
+
CLEANED_FEATURES=()
|
|
94
|
+
CLEANED_IDS=()
|
|
95
|
+
|
|
96
|
+
for i in $(seq 0 $((COMPLETED_COUNT - 1))); do
|
|
97
|
+
FEATURE=$(echo "$COMPLETED_FEATURES" | jq ".[$i]")
|
|
98
|
+
FEATURE_NAME=$(echo "$FEATURE" | jq -r '.name')
|
|
99
|
+
FEATURE_ID=$(echo "$FEATURE" | jq -r '.id')
|
|
100
|
+
SPEC_FILE=$(echo "$FEATURE" | jq -r '.tasks[0].specFile // empty')
|
|
101
|
+
|
|
102
|
+
SLUG=$(to_slug "$FEATURE_NAME")
|
|
103
|
+
CAMEL_CASE=$(to_camel_case "$SLUG")
|
|
104
|
+
|
|
105
|
+
echo " Processing: ${FEATURE_NAME}"
|
|
106
|
+
|
|
107
|
+
# Create done folder
|
|
108
|
+
DONE_FOLDER="${DONE_TASKS_DIR}/${CAMEL_CASE}"
|
|
109
|
+
mkdir -p "$DONE_FOLDER"
|
|
110
|
+
|
|
111
|
+
# Move spec file
|
|
112
|
+
SPEC_MOVED=false
|
|
113
|
+
|
|
114
|
+
if [[ -n "$SPEC_FILE" ]]; then
|
|
115
|
+
SPEC_PATH="${PROJECT_RALPH_DIR}/../${SPEC_FILE}"
|
|
116
|
+
if [[ -f "$SPEC_PATH" ]]; then
|
|
117
|
+
mv "$SPEC_PATH" "$DONE_FOLDER/"
|
|
118
|
+
SPEC_MOVED=true
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
if [[ "$SPEC_MOVED" == false ]]; then
|
|
123
|
+
FOUND_SPEC=$(find "$NEW_TASKS_DIR" -maxdepth 1 -name "*${SLUG}*" -type f 2>/dev/null | head -1)
|
|
124
|
+
if [[ -n "$FOUND_SPEC" && -f "$FOUND_SPEC" ]]; then
|
|
125
|
+
mv "$FOUND_SPEC" "$DONE_FOLDER/"
|
|
126
|
+
SPEC_MOVED=true
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [[ "$SPEC_MOVED" == true ]]; then
|
|
131
|
+
echo -e " ${GREEN}✓${NC} Moved spec file"
|
|
132
|
+
else
|
|
133
|
+
echo -e " ${YELLOW}⚠${NC} Spec file not found"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Move progress file(s)
|
|
137
|
+
if [[ -d "$PROGRESS_DIR" ]]; then
|
|
138
|
+
PROGRESS_MOVED=false
|
|
139
|
+
while IFS= read -r -d '' pfile; do
|
|
140
|
+
if [[ -f "$pfile" ]]; then
|
|
141
|
+
mv "$pfile" "$DONE_FOLDER/"
|
|
142
|
+
PROGRESS_MOVED=true
|
|
143
|
+
fi
|
|
144
|
+
done < <(find "$PROGRESS_DIR" -maxdepth 1 -name "*${SLUG}*" -type f -print0 2>/dev/null)
|
|
145
|
+
|
|
146
|
+
if [[ "$PROGRESS_MOVED" == true ]]; then
|
|
147
|
+
echo -e " ${GREEN}✓${NC} Moved progress file(s)"
|
|
148
|
+
else
|
|
149
|
+
echo -e " ${YELLOW}⚠${NC} No progress files found"
|
|
150
|
+
fi
|
|
151
|
+
else
|
|
152
|
+
echo -e " ${YELLOW}⚠${NC} Progress directory not found"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
CLEANED_FEATURES+=("$CAMEL_CASE")
|
|
156
|
+
CLEANED_IDS+=("$FEATURE_ID")
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
# 4. Update prd.json - remove cleaned features
|
|
160
|
+
echo "[4/4] Updating prd.json..."
|
|
161
|
+
|
|
162
|
+
if [[ -n "$FEATURE_ARG" ]]; then
|
|
163
|
+
FEATURE_ID=$(echo "$COMPLETED_FEATURES" | jq -r '.[0].id')
|
|
164
|
+
jq --arg id "$FEATURE_ID" '.features = [.features[] | select(.id != $id)]' "$PRD_FILE" > "${PRD_FILE}.tmp"
|
|
165
|
+
else
|
|
166
|
+
jq '.features = [.features[] | select(.feature_completed != true)]' "$PRD_FILE" > "${PRD_FILE}.tmp"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
mv "${PRD_FILE}.tmp" "$PRD_FILE"
|
|
170
|
+
echo -e " ${GREEN}✓${NC} prd.json updated"
|
|
171
|
+
|
|
172
|
+
# Summary
|
|
173
|
+
echo ""
|
|
174
|
+
echo -e "${GREEN}Cleanup complete!${NC}"
|
|
175
|
+
echo "Archived ${#CLEANED_FEATURES[@]} feature(s):"
|
|
176
|
+
for f in "${CLEANED_FEATURES[@]}"; do
|
|
177
|
+
echo " - 2_done_tasks/${f}/"
|
|
178
|
+
done
|
|
179
|
+
echo ""
|
package/scripts/clear.sh
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Ralph Clear - Delete all log files (keeps progress folder)
|
|
5
|
+
# Environment: PROJECT_RALPH_DIR
|
|
6
|
+
|
|
7
|
+
PROJECT_RALPH_DIR="${PROJECT_RALPH_DIR:?PROJECT_RALPH_DIR not set}"
|
|
8
|
+
LOG_DIR="${PROJECT_RALPH_DIR}/logs"
|
|
9
|
+
|
|
10
|
+
# Color support (only if TTY)
|
|
11
|
+
if [[ -t 1 ]]; then
|
|
12
|
+
GREEN="\033[32m"
|
|
13
|
+
YELLOW="\033[33m"
|
|
14
|
+
RESET="\033[0m"
|
|
15
|
+
else
|
|
16
|
+
GREEN=""
|
|
17
|
+
YELLOW=""
|
|
18
|
+
RESET=""
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if logs directory exists
|
|
22
|
+
if [[ ! -d "$LOG_DIR" ]]; then
|
|
23
|
+
echo "Logs directory not found: $LOG_DIR"
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Count files to be deleted (excluding progress directory)
|
|
28
|
+
FILE_COUNT=$(find "$LOG_DIR" -maxdepth 1 -type f | wc -l | tr -d ' ')
|
|
29
|
+
|
|
30
|
+
if [[ "$FILE_COUNT" -eq 0 ]]; then
|
|
31
|
+
echo -e "${YELLOW}No log files to clear.${RESET}"
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Delete all files in logs directory (but not subdirectories like progress)
|
|
36
|
+
find "$LOG_DIR" -maxdepth 1 -type f -delete
|
|
37
|
+
|
|
38
|
+
echo -e "${GREEN}Cleared ${FILE_COUNT} log files.${RESET}"
|