codeharness 0.6.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.
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env bash
2
+ # doc_gardener.sh — Documentation freshness scanner
3
+ # Finds: missing AGENTS.md, stale AGENTS.md, stale exec-plans.
4
+ # Used by the doc-gardener subagent and during retrospectives.
5
+ #
6
+ # Usage: ralph/doc_gardener.sh --project-dir DIR [--json]
7
+
8
+ set -e
9
+
10
+ PROJECT_DIR=""
11
+ JSON_OUTPUT=false
12
+ GENERATE_REPORT=false
13
+ COMPLEXITY_THRESHOLD=3 # Minimum source files to require AGENTS.md
14
+
15
+ show_help() {
16
+ cat << 'HELPEOF'
17
+ Doc-Gardener Scanner — find stale and missing documentation
18
+
19
+ Usage:
20
+ ralph/doc_gardener.sh --project-dir DIR [--json]
21
+
22
+ Checks:
23
+ 1. Modules with 3+ source files but no AGENTS.md
24
+ 2. AGENTS.md files older than corresponding source code changes
25
+ 3. Exec-plans in active/ for already-completed stories
26
+
27
+ Options:
28
+ --project-dir DIR Project root directory
29
+ --json Output findings as JSON (default: human-readable)
30
+ --report Generate quality-score.md and tech-debt-tracker.md
31
+ --threshold N Min source files to require AGENTS.md (default: 3)
32
+ -h, --help Show this help message
33
+ HELPEOF
34
+ }
35
+
36
+ while [[ $# -gt 0 ]]; do
37
+ case $1 in
38
+ -h|--help)
39
+ show_help
40
+ exit 0
41
+ ;;
42
+ --project-dir)
43
+ PROJECT_DIR="$2"
44
+ shift 2
45
+ ;;
46
+ --json)
47
+ JSON_OUTPUT=true
48
+ shift
49
+ ;;
50
+ --report)
51
+ GENERATE_REPORT=true
52
+ shift
53
+ ;;
54
+ --threshold)
55
+ COMPLEXITY_THRESHOLD="$2"
56
+ shift 2
57
+ ;;
58
+ *)
59
+ echo "Unknown option: $1" >&2
60
+ exit 1
61
+ ;;
62
+ esac
63
+ done
64
+
65
+ if [[ -z "$PROJECT_DIR" ]]; then
66
+ echo "Error: --project-dir is required" >&2
67
+ exit 1
68
+ fi
69
+
70
+ if [[ ! -d "$PROJECT_DIR" ]]; then
71
+ echo "Error: project directory not found: $PROJECT_DIR" >&2
72
+ exit 1
73
+ fi
74
+
75
+ # ─── Findings collection ─────────────────────────────────────────────────
76
+
77
+ FINDINGS_JSON="[]"
78
+
79
+ add_finding() {
80
+ local type="$1"
81
+ local path="$2"
82
+ local message="$3"
83
+
84
+ FINDINGS_JSON=$(echo "$FINDINGS_JSON" | jq \
85
+ --arg type "$type" \
86
+ --arg path "$path" \
87
+ --arg message "$message" \
88
+ '. += [{"type": $type, "path": $path, "message": $message}]')
89
+ }
90
+
91
+ # ─── Check 1: Missing AGENTS.md for modules above complexity threshold ────
92
+
93
+ check_missing_agents_md() {
94
+ # Find directories with source files but no AGENTS.md
95
+ # Exclude hidden dirs, node_modules, _bmad, .ralph, docs, tests
96
+ while IFS= read -r dir; do
97
+ [[ -z "$dir" ]] && continue
98
+
99
+ # Count source files (common extensions)
100
+ local src_count
101
+ src_count=$(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) 2>/dev/null | wc -l | tr -d ' ')
102
+
103
+ if [[ $src_count -ge $COMPLEXITY_THRESHOLD ]]; then
104
+ if [[ ! -f "$dir/AGENTS.md" ]]; then
105
+ local rel_path="${dir#$PROJECT_DIR/}"
106
+ add_finding "missing_agents_md" "$rel_path" "Module $rel_path has $src_count source files but no AGENTS.md"
107
+ fi
108
+ fi
109
+ done < <(find "$PROJECT_DIR" -type d \
110
+ -not -path "$PROJECT_DIR/.*" \
111
+ -not -path "*/node_modules/*" \
112
+ -not -path "*/_bmad/*" \
113
+ -not -path "*/.ralph/*" \
114
+ -not -path "*/docs/*" \
115
+ -not -path "*/tests/*" \
116
+ -not -path "$PROJECT_DIR" \
117
+ 2>/dev/null)
118
+ }
119
+
120
+ # ─── Check 2: Stale AGENTS.md (code changed after docs) ──────────────────
121
+
122
+ check_stale_agents_md() {
123
+ while IFS= read -r agents_file; do
124
+ [[ -z "$agents_file" ]] && continue
125
+
126
+ local dir
127
+ dir=$(dirname "$agents_file")
128
+ local rel_dir="${dir#$PROJECT_DIR/}"
129
+
130
+ # Get AGENTS.md last commit time
131
+ local agents_commit_time
132
+ agents_commit_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$agents_file" 2>/dev/null || echo "0")
133
+
134
+ # Get latest source file commit time in the same directory
135
+ local latest_src_time="0"
136
+ while IFS= read -r src_file; do
137
+ [[ -z "$src_file" ]] && continue
138
+ local src_time
139
+ src_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$src_file" 2>/dev/null || echo "0")
140
+ if [[ $src_time -gt $latest_src_time ]]; then
141
+ latest_src_time=$src_time
142
+ fi
143
+ done < <(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) -not -name "AGENTS.md" 2>/dev/null)
144
+
145
+ if [[ $latest_src_time -gt $agents_commit_time && $agents_commit_time -gt 0 ]]; then
146
+ add_finding "stale_agents_md" "$rel_dir" "AGENTS.md in $rel_dir is stale — source code changed after docs"
147
+ fi
148
+ done < <(find "$PROJECT_DIR" -name "AGENTS.md" \
149
+ -not -path "*/node_modules/*" \
150
+ -not -path "*/_bmad/*" \
151
+ -not -path "*/.ralph/*" \
152
+ 2>/dev/null)
153
+ }
154
+
155
+ # ─── Check 3: Stale exec-plans (completed stories still in active/) ──────
156
+
157
+ check_stale_exec_plans() {
158
+ local progress_file="$PROJECT_DIR/ralph/progress.json"
159
+ local active_dir="$PROJECT_DIR/docs/exec-plans/active"
160
+
161
+ if [[ ! -f "$progress_file" || ! -d "$active_dir" ]]; then
162
+ return 0
163
+ fi
164
+
165
+ # Find active exec-plans for completed stories
166
+ for plan_file in "$active_dir"/*.md; do
167
+ [[ -f "$plan_file" ]] || continue
168
+
169
+ local story_id
170
+ story_id=$(basename "$plan_file" .md)
171
+
172
+ local story_status
173
+ story_status=$(jq -r --arg id "$story_id" '.tasks[] | select(.id == $id) | .status // ""' "$progress_file" 2>/dev/null)
174
+
175
+ if [[ "$story_status" == "complete" ]]; then
176
+ add_finding "stale_exec_plan" "docs/exec-plans/active/$story_id.md" \
177
+ "Exec-plan for story $story_id is still in active/ but story is complete — should be in completed/"
178
+ fi
179
+ done
180
+ }
181
+
182
+ # ─── Quality scoring ──────────────────────────────────────────────────────
183
+
184
+ # Collect module info for quality grading
185
+ declare -A MODULE_GRADES
186
+
187
+ grade_modules() {
188
+ while IFS= read -r dir; do
189
+ [[ -z "$dir" ]] && continue
190
+
191
+ local src_count
192
+ src_count=$(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.rb" \) 2>/dev/null | wc -l | tr -d ' ')
193
+
194
+ # Skip directories with no source files
195
+ [[ $src_count -eq 0 ]] && continue
196
+
197
+ local rel_path="${dir#$PROJECT_DIR/}"
198
+ local has_agents="false"
199
+ local is_stale="false"
200
+
201
+ if [[ -f "$dir/AGENTS.md" ]]; then
202
+ has_agents="true"
203
+
204
+ # Check staleness
205
+ local agents_time
206
+ agents_time=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$dir/AGENTS.md" 2>/dev/null || echo "0")
207
+ local latest_src="0"
208
+ while IFS= read -r sf; do
209
+ [[ -z "$sf" ]] && continue
210
+ local st
211
+ st=$(git -C "$PROJECT_DIR" log -1 --format="%ct" -- "$sf" 2>/dev/null || echo "0")
212
+ [[ $st -gt $latest_src ]] && latest_src=$st
213
+ done < <(find "$dir" -maxdepth 1 \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.sh" \) -not -name "AGENTS.md" 2>/dev/null)
214
+
215
+ if [[ $latest_src -gt $agents_time && $agents_time -gt 0 ]]; then
216
+ is_stale="true"
217
+ fi
218
+ fi
219
+
220
+ # Grade: A = has fresh AGENTS.md, B = has stale AGENTS.md, F = missing
221
+ local grade="F"
222
+ if [[ "$has_agents" == "true" && "$is_stale" == "false" ]]; then
223
+ grade="A"
224
+ elif [[ "$has_agents" == "true" && "$is_stale" == "true" ]]; then
225
+ grade="B"
226
+ fi
227
+
228
+ MODULE_GRADES["$rel_path"]="$grade"
229
+ done < <(find "$PROJECT_DIR" -type d \
230
+ -not -path "$PROJECT_DIR/.*" \
231
+ -not -path "*/node_modules/*" \
232
+ -not -path "*/_bmad/*" \
233
+ -not -path "*/.ralph/*" \
234
+ -not -path "*/docs/*" \
235
+ -not -path "*/tests/*" \
236
+ -not -path "$PROJECT_DIR" \
237
+ 2>/dev/null)
238
+ }
239
+
240
+ generate_quality_report() {
241
+ local output_file="$PROJECT_DIR/docs/quality/quality-score.md"
242
+ mkdir -p "$(dirname "$output_file")"
243
+
244
+ local timestamp
245
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
246
+
247
+ {
248
+ echo "<!-- DO NOT EDIT MANUALLY — generated by doc-gardener -->"
249
+ echo ""
250
+ echo "# Documentation Quality Score"
251
+ echo ""
252
+ echo "**Generated:** $timestamp"
253
+ echo ""
254
+ echo "## Module Grades"
255
+ echo ""
256
+ echo "| Module | Grade | Status |"
257
+ echo "|--------|-------|--------|"
258
+
259
+ # Sort and output grades
260
+ for module in $(echo "${!MODULE_GRADES[@]}" | tr ' ' '\n' | sort); do
261
+ local grade="${MODULE_GRADES[$module]}"
262
+ local status_text
263
+ case "$grade" in
264
+ A) status_text="AGENTS.md present and current" ;;
265
+ B) status_text="AGENTS.md present but stale" ;;
266
+ F) status_text="AGENTS.md missing" ;;
267
+ esac
268
+ echo "| $module | $grade | $status_text |"
269
+ done
270
+
271
+ echo ""
272
+ echo "## Grade Legend"
273
+ echo ""
274
+ echo "- **A**: Module has current AGENTS.md"
275
+ echo "- **B**: Module has AGENTS.md but code changed since last update"
276
+ echo "- **F**: Module has no AGENTS.md (3+ source files)"
277
+ } > "$output_file"
278
+ }
279
+
280
+ generate_tech_debt_report() {
281
+ local output_file="$PROJECT_DIR/docs/exec-plans/tech-debt-tracker.md"
282
+ mkdir -p "$(dirname "$output_file")"
283
+
284
+ local timestamp
285
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
286
+
287
+ {
288
+ echo "<!-- DO NOT EDIT MANUALLY — generated by doc-gardener -->"
289
+ echo ""
290
+ echo "# Documentation Tech Debt"
291
+ echo ""
292
+ echo "**Generated:** $timestamp"
293
+ echo ""
294
+
295
+ local debt_count
296
+ debt_count=$(echo "$FINDINGS_JSON" | jq '. | length')
297
+
298
+ if [[ $debt_count -eq 0 ]]; then
299
+ echo "No documentation debt items."
300
+ else
301
+ echo "| # | Type | Path | Issue |"
302
+ echo "|---|------|------|-------|"
303
+
304
+ local i=1
305
+ echo "$FINDINGS_JSON" | jq -r '.[] | "\(.type)\t\(.path)\t\(.message)"' | while IFS=$'\t' read -r type path message; do
306
+ echo "| $i | $type | $path | $message |"
307
+ i=$((i + 1))
308
+ done
309
+ fi
310
+ } > "$output_file"
311
+ }
312
+
313
+ # ─── Run all checks ──────────────────────────────────────────────────────
314
+
315
+ check_missing_agents_md
316
+ check_stale_agents_md
317
+ check_stale_exec_plans
318
+
319
+ # Generate reports if requested
320
+ if [[ "$GENERATE_REPORT" == "true" ]]; then
321
+ grade_modules
322
+ generate_quality_report
323
+ generate_tech_debt_report
324
+ fi
325
+
326
+ # ─── Output ───────────────────────────────────────────────────────────────
327
+
328
+ finding_count=$(echo "$FINDINGS_JSON" | jq '. | length')
329
+
330
+ if [[ "$JSON_OUTPUT" == "true" ]]; then
331
+ jq -n \
332
+ --argjson findings "$FINDINGS_JSON" \
333
+ --argjson count "$finding_count" \
334
+ --arg scanned_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
335
+ '{
336
+ scanned_at: $scanned_at,
337
+ finding_count: $count,
338
+ findings: $findings
339
+ }'
340
+ else
341
+ echo "Doc-Gardener Scan"
342
+ echo ""
343
+
344
+ if [[ $finding_count -eq 0 ]]; then
345
+ echo "[OK] No documentation issues found"
346
+ else
347
+ echo "$FINDINGS_JSON" | jq -r '.[] | " [WARN] \(.type): \(.message)"'
348
+ fi
349
+
350
+ echo ""
351
+ echo "$finding_count finding(s) total"
352
+ fi
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code driver for codeharness Ralph loop
3
+ # Handles instance lifecycle: spawn, monitor, terminate
4
+ # Each iteration gets a fresh Claude Code instance with the codeharness plugin
5
+
6
+ # Driver identification
7
+ driver_name() {
8
+ echo "claude-code"
9
+ }
10
+
11
+ driver_display_name() {
12
+ echo "Claude Code"
13
+ }
14
+
15
+ driver_cli_binary() {
16
+ echo "claude"
17
+ }
18
+
19
+ driver_min_version() {
20
+ echo "2.0.76"
21
+ }
22
+
23
+ # Check if the CLI binary is available
24
+ driver_check_available() {
25
+ command -v "$(driver_cli_binary)" &>/dev/null
26
+ }
27
+
28
+ # Valid tool patterns for --allowedTools validation
29
+ driver_valid_tools() {
30
+ VALID_TOOL_PATTERNS=(
31
+ "Write"
32
+ "Read"
33
+ "Edit"
34
+ "MultiEdit"
35
+ "Glob"
36
+ "Grep"
37
+ "Task"
38
+ "TodoWrite"
39
+ "WebFetch"
40
+ "WebSearch"
41
+ "Bash"
42
+ "Bash(git *)"
43
+ "Bash(npm *)"
44
+ "Bash(bats *)"
45
+ "Bash(python *)"
46
+ "Bash(node *)"
47
+ "NotebookEdit"
48
+ )
49
+ }
50
+
51
+ # Build the CLI command arguments
52
+ # Populates global CLAUDE_CMD_ARGS array
53
+ # Parameters:
54
+ # $1 - prompt_file: path to the prompt file
55
+ # $2 - loop_context: context string for session continuity
56
+ # $3 - session_id: session ID for resume (empty for new session)
57
+ # $4 - plugin_dir: plugin directory (for --plugin-dir flag)
58
+ driver_build_command() {
59
+ local prompt_file=$1
60
+ local loop_context=$2
61
+ local session_id=$3
62
+ local plugin_dir=${4:-""}
63
+
64
+ CLAUDE_CMD_ARGS=("$(driver_cli_binary)")
65
+
66
+ if [[ ! -f "$prompt_file" ]]; then
67
+ echo "ERROR: Prompt file not found: $prompt_file" >&2
68
+ return 1
69
+ fi
70
+
71
+ # Plugin directory
72
+ if [[ -n "$plugin_dir" ]]; then
73
+ CLAUDE_CMD_ARGS+=("--plugin-dir" "$plugin_dir")
74
+ fi
75
+
76
+ # Output format
77
+ if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
78
+ CLAUDE_CMD_ARGS+=("--output-format" "json")
79
+ fi
80
+
81
+ # Allowed tools
82
+ if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
83
+ CLAUDE_CMD_ARGS+=("--allowedTools")
84
+ local IFS=','
85
+ read -ra tools_array <<< "$CLAUDE_ALLOWED_TOOLS"
86
+ for tool in "${tools_array[@]}"; do
87
+ tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
88
+ if [[ -n "$tool" ]]; then
89
+ CLAUDE_CMD_ARGS+=("$tool")
90
+ fi
91
+ done
92
+ fi
93
+
94
+ # Session resume
95
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
96
+ CLAUDE_CMD_ARGS+=("--resume" "$session_id")
97
+ fi
98
+
99
+ # Loop context as system prompt
100
+ if [[ -n "$loop_context" ]]; then
101
+ CLAUDE_CMD_ARGS+=("--append-system-prompt" "$loop_context")
102
+ fi
103
+
104
+ # Prompt content
105
+ local prompt_content
106
+ prompt_content=$(cat "$prompt_file")
107
+ CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
108
+ }
109
+
110
+ # Whether this driver supports session continuity
111
+ driver_supports_sessions() {
112
+ return 0
113
+ }
114
+
115
+ # Claude Code supports stream-json live output
116
+ driver_supports_live_output() {
117
+ return 0
118
+ }
119
+
120
+ # Prepare command arguments for live stream-json output
121
+ driver_prepare_live_command() {
122
+ LIVE_CMD_ARGS=()
123
+ local skip_next=false
124
+
125
+ for arg in "${CLAUDE_CMD_ARGS[@]}"; do
126
+ if [[ "$skip_next" == "true" ]]; then
127
+ LIVE_CMD_ARGS+=("stream-json")
128
+ skip_next=false
129
+ elif [[ "$arg" == "--output-format" ]]; then
130
+ LIVE_CMD_ARGS+=("$arg")
131
+ skip_next=true
132
+ else
133
+ LIVE_CMD_ARGS+=("$arg")
134
+ fi
135
+ done
136
+
137
+ if [[ "$skip_next" == "true" ]]; then
138
+ return 1
139
+ fi
140
+
141
+ LIVE_CMD_ARGS+=("--verbose" "--include-partial-messages")
142
+ }
143
+
144
+ # Stream filter for raw Claude stream-json events
145
+ driver_stream_filter() {
146
+ echo '
147
+ if .type == "stream_event" then
148
+ if .event.type == "content_block_delta" and .event.delta.type == "text_delta" then
149
+ .event.delta.text
150
+ elif .event.type == "content_block_start" and .event.content_block.type == "tool_use" then
151
+ "\n\n⚡ [" + .event.content_block.name + "]\n"
152
+ elif .event.type == "content_block_stop" then
153
+ "\n"
154
+ else
155
+ empty
156
+ end
157
+ else
158
+ empty
159
+ end'
160
+ }