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.
@@ -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 ""
@@ -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}"