Merge pull request 'fix: feat: gardener bundles dust into ore before promoting to backlog (#74)' (#107) from fix/issue-74 into main
This commit is contained in:
commit
81e5e5aa50
3 changed files with 191 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -17,3 +17,4 @@ metrics/supervisor-metrics.jsonl
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dev/ci-fixes-*.json
|
dev/ci-fixes-*.json
|
||||||
|
gardener/dust.jsonl
|
||||||
|
|
|
||||||
50
gardener/PROMPT.md
Normal file
50
gardener/PROMPT.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Gardener Prompt — Dust vs Ore
|
||||||
|
|
||||||
|
> **Note:** This is human documentation. The actual LLM prompt is built
|
||||||
|
> inline in `gardener-poll.sh` (with dynamic context injection). This file
|
||||||
|
> documents the design rationale for reference.
|
||||||
|
|
||||||
|
## Rule
|
||||||
|
|
||||||
|
Don't promote trivial tech-debt individually. Each promotion costs a full
|
||||||
|
factory cycle: CI + dev-agent + review + merge. Don't fill minecarts with
|
||||||
|
dust — put ore inside.
|
||||||
|
|
||||||
|
## What is dust?
|
||||||
|
|
||||||
|
- Comment fix
|
||||||
|
- Variable rename
|
||||||
|
- Style-only change (whitespace, formatting)
|
||||||
|
- Single-line edit
|
||||||
|
- Trivial cleanup with no behavioral impact
|
||||||
|
|
||||||
|
## What is ore?
|
||||||
|
|
||||||
|
- Multi-file changes
|
||||||
|
- Behavioral fixes
|
||||||
|
- Architectural improvements
|
||||||
|
- Security or correctness issues
|
||||||
|
- Anything requiring design thought
|
||||||
|
|
||||||
|
## LLM output format
|
||||||
|
|
||||||
|
When a tech-debt issue is dust, the LLM outputs:
|
||||||
|
|
||||||
|
```
|
||||||
|
DUST: {"issue": NNN, "group": "<file-or-subsystem>", "title": "...", "reason": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `group` field clusters related dust by file or subsystem (e.g.
|
||||||
|
`"gardener"`, `"lib/env.sh"`, `"dev-poll"`).
|
||||||
|
|
||||||
|
## Bundling
|
||||||
|
|
||||||
|
The script collects dust items into `gardener/dust.jsonl`. When a group
|
||||||
|
accumulates 3+ items, the script automatically:
|
||||||
|
|
||||||
|
1. Creates one bundled backlog issue referencing all source issues
|
||||||
|
2. Closes the individual source issues with a cross-reference comment
|
||||||
|
3. Removes bundled items from the staging file
|
||||||
|
|
||||||
|
This converts N trivial issues into 1 actionable issue, saving N-1 factory
|
||||||
|
cycles.
|
||||||
|
|
@ -240,6 +240,13 @@ log "Invoking claude -p for grooming"
|
||||||
# Build issue summary for context (titles + labels + deps)
|
# Build issue summary for context (titles + labels + deps)
|
||||||
ISSUE_SUMMARY=$(echo "$ISSUES_JSON" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"')
|
ISSUE_SUMMARY=$(echo "$ISSUES_JSON" | jq -r '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"')
|
||||||
|
|
||||||
|
# Build list of issues already staged as dust (so LLM doesn't re-emit them)
|
||||||
|
DUST_FILE="$SCRIPT_DIR/dust.jsonl"
|
||||||
|
STAGED_DUST=""
|
||||||
|
if [ -s "$DUST_FILE" ]; then
|
||||||
|
STAGED_DUST=$(jq -r '"#\(.issue) (\(.group))"' "$DUST_FILE" 2>/dev/null | sort -u || true)
|
||||||
|
fi
|
||||||
|
|
||||||
PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Your job: keep the backlog clean, well-structured, and actionable.
|
PROMPT="You are the issue gardener for ${CODEBERG_REPO}. Your job: keep the backlog clean, well-structured, and actionable.
|
||||||
|
|
||||||
## Current open issues
|
## Current open issues
|
||||||
|
|
@ -274,6 +281,18 @@ Most open issues are raw review-bot findings labeled \`tech-debt\`. Convert them
|
||||||
|
|
||||||
Process up to 10 tech-debt issues per run (stay within API rate limits).
|
Process up to 10 tech-debt issues per run (stay within API rate limits).
|
||||||
|
|
||||||
|
## Dust vs Ore — bundle trivial tech-debt
|
||||||
|
Don't promote trivial tech-debt individually — each costs a full factory cycle (CI + dev-agent + review + merge). If an issue is dust (comment fix, rename, style-only, single-line change, trivial cleanup), output a DUST line instead of promoting:
|
||||||
|
|
||||||
|
DUST: {\"issue\": NNN, \"group\": \"<file-or-subsystem>\", \"title\": \"issue title\", \"reason\": \"why it's dust\"}
|
||||||
|
|
||||||
|
Group by file or subsystem (e.g. \"gardener\", \"lib/env.sh\", \"dev-poll\"). The script collects dust items into a staging file. When a group accumulates 3+ items, the script bundles them into one backlog issue automatically.
|
||||||
|
|
||||||
|
Only promote tech-debt that is substantial: multi-file changes, behavioral fixes, architectural improvements. Dust is any issue where the fix is a single-line edit, a rename, a comment tweak, or a style-only change.
|
||||||
|
$(if [ -n "$STAGED_DUST" ]; then echo "
|
||||||
|
These issues are ALREADY staged as dust — do NOT emit DUST lines for them again:
|
||||||
|
${STAGED_DUST}"; fi)
|
||||||
|
|
||||||
## Other rules
|
## Other rules
|
||||||
1. **Duplicates**: If confident (>80% overlap + same scope after reading bodies), close the newer one with a comment referencing the older. If unsure, ESCALATE.
|
1. **Duplicates**: If confident (>80% overlap + same scope after reading bodies), close the newer one with a comment referencing the older. If unsure, ESCALATE.
|
||||||
2. **Thin issues** (non-tech-debt): Add acceptance criteria. Read the body first.
|
2. **Thin issues** (non-tech-debt): Add acceptance criteria. Read the body first.
|
||||||
|
|
@ -292,6 +311,7 @@ ESCALATE
|
||||||
|
|
||||||
## Output format (MANDATORY — the script parses these exact prefixes)
|
## Output format (MANDATORY — the script parses these exact prefixes)
|
||||||
- After EVERY action you take, print exactly: ACTION: <description>
|
- After EVERY action you take, print exactly: ACTION: <description>
|
||||||
|
- For trivial tech-debt (dust), print exactly: DUST: {\"issue\": NNN, \"group\": \"<subsystem>\", \"title\": \"...\", \"reason\": \"...\"}
|
||||||
- For issues needing human decision, output EXACTLY:
|
- For issues needing human decision, output EXACTLY:
|
||||||
ESCALATE
|
ESCALATE
|
||||||
1. #NNN \"title\" — reason (a) option1 (b) option2
|
1. #NNN \"title\" — reason (a) option1 (b) option2
|
||||||
|
|
@ -347,6 +367,126 @@ if [ -n "$ACTIONS" ]; then
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Collect dust items ───────────────────────────────────────────────────
|
||||||
|
# DUST_FILE already set above (before prompt construction)
|
||||||
|
DUST_LINES=$(echo "$CLAUDE_OUTPUT" | grep "^DUST: " | sed 's/^DUST: //' || true)
|
||||||
|
if [ -n "$DUST_LINES" ]; then
|
||||||
|
# Build set of issue numbers already in dust.jsonl for dedup
|
||||||
|
EXISTING_DUST_ISSUES=""
|
||||||
|
if [ -s "$DUST_FILE" ]; then
|
||||||
|
EXISTING_DUST_ISSUES=$(jq -r '.issue' "$DUST_FILE" 2>/dev/null | sort -nu || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
DUST_COUNT=0
|
||||||
|
while IFS= read -r dust_json; do
|
||||||
|
[ -z "$dust_json" ] && continue
|
||||||
|
# Validate JSON
|
||||||
|
if ! echo "$dust_json" | jq -e '.issue and .group' >/dev/null 2>&1; then
|
||||||
|
log "WARNING: invalid dust JSON: $dust_json"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Deduplicate: skip if this issue is already staged
|
||||||
|
dust_issue_num=$(echo "$dust_json" | jq -r '.issue')
|
||||||
|
if echo "$EXISTING_DUST_ISSUES" | grep -qx "$dust_issue_num" 2>/dev/null; then
|
||||||
|
log "Skipping duplicate dust entry for issue #${dust_issue_num}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
EXISTING_DUST_ISSUES="${EXISTING_DUST_ISSUES}
|
||||||
|
${dust_issue_num}"
|
||||||
|
echo "$dust_json" | jq -c '. + {"ts": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}' >> "$DUST_FILE"
|
||||||
|
DUST_COUNT=$((DUST_COUNT + 1))
|
||||||
|
done <<< "$DUST_LINES"
|
||||||
|
log "Collected $DUST_COUNT dust item(s) (duplicates skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Expire stale dust entries (30-day TTL) ───────────────────────────────
|
||||||
|
if [ -s "$DUST_FILE" ]; then
|
||||||
|
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)
|
||||||
|
if [ -n "$CUTOFF" ]; then
|
||||||
|
BEFORE_COUNT=$(wc -l < "$DUST_FILE")
|
||||||
|
if jq -c --arg c "$CUTOFF" 'select(.ts >= $c)' "$DUST_FILE" > "${DUST_FILE}.ttl" 2>/dev/null; then
|
||||||
|
mv "${DUST_FILE}.ttl" "$DUST_FILE"
|
||||||
|
AFTER_COUNT=$(wc -l < "$DUST_FILE")
|
||||||
|
EXPIRED=$((BEFORE_COUNT - AFTER_COUNT))
|
||||||
|
[ "$EXPIRED" -gt 0 ] && log "Expired $EXPIRED stale dust entries (>30 days old)"
|
||||||
|
else
|
||||||
|
rm -f "${DUST_FILE}.ttl"
|
||||||
|
log "WARNING: TTL cleanup failed — dust.jsonl left unchanged"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Bundle dust groups with 3+ distinct issues ──────────────────────────
|
||||||
|
if [ -s "$DUST_FILE" ]; then
|
||||||
|
# Count distinct issues per group (not raw entries)
|
||||||
|
DUST_GROUPS=$(jq -r '[.group, (.issue | tostring)] | join("\t")' "$DUST_FILE" 2>/dev/null \
|
||||||
|
| sort -u | cut -f1 | sort | uniq -c | sort -rn || true)
|
||||||
|
while read -r count group; do
|
||||||
|
[ -z "$group" ] && continue
|
||||||
|
[ "$count" -lt 3 ] && continue
|
||||||
|
|
||||||
|
log "Bundling dust group '$group' ($count distinct issues)"
|
||||||
|
|
||||||
|
# Collect deduplicated issue references and details for this group
|
||||||
|
BUNDLE_ISSUES=$(jq -r --arg g "$group" 'select(.group == $g) | "#\(.issue) \(.title // "untitled") — \(.reason // "dust")"' "$DUST_FILE" | sort -u)
|
||||||
|
BUNDLE_ISSUE_NUMS=$(jq -r --arg g "$group" 'select(.group == $g) | .issue' "$DUST_FILE" | sort -nu)
|
||||||
|
DISTINCT_COUNT=$(echo "$BUNDLE_ISSUE_NUMS" | grep -c '.' || true)
|
||||||
|
|
||||||
|
bundle_title="fix: bundled dust cleanup — ${group}"
|
||||||
|
bundle_body="## Bundled dust cleanup — \`${group}\`
|
||||||
|
|
||||||
|
Gardener bundled ${DISTINCT_COUNT} trivial tech-debt items into one issue to save factory cycles.
|
||||||
|
|
||||||
|
### Items
|
||||||
|
$(echo "$BUNDLE_ISSUES" | sed 's/^/- /')
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
Fix all items above in a single PR. Each is a small change (rename, comment, style fix, single-line edit).
|
||||||
|
|
||||||
|
### Affected files
|
||||||
|
- Files in \`${group}\` subsystem
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
- [ ] All listed items resolved
|
||||||
|
- [ ] ShellCheck passes"
|
||||||
|
|
||||||
|
new_bundle=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CODEBERG_API}/issues" \
|
||||||
|
-d "$(jq -nc --arg t "$bundle_title" --arg b "$bundle_body" \
|
||||||
|
'{"title":$t,"body":$b,"labels":["backlog"]}')" 2>/dev/null | jq -r '.number // ""') || true
|
||||||
|
|
||||||
|
if [ -n "$new_bundle" ]; then
|
||||||
|
log "Created bundle issue #${new_bundle} for dust group '$group' ($DISTINCT_COUNT items)"
|
||||||
|
matrix_send "gardener" "📦 Bundled ${DISTINCT_COUNT} dust items (${group}) → #${new_bundle}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Close source issues with cross-reference
|
||||||
|
for src_issue in $BUNDLE_ISSUE_NUMS; do
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CODEBERG_API}/issues/${src_issue}/comments" \
|
||||||
|
-d "$(jq -nc --arg b "Bundled into #${new_bundle} (dust cleanup)" '{"body":$b}')" 2>/dev/null || true
|
||||||
|
curl -sf -X PATCH \
|
||||||
|
-H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CODEBERG_API}/issues/${src_issue}" \
|
||||||
|
-d '{"state":"closed"}' 2>/dev/null || true
|
||||||
|
log "Closed source issue #${src_issue} → bundled into #${new_bundle}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remove bundled items from dust.jsonl — only if jq succeeds
|
||||||
|
if jq -c --arg g "$group" 'select(.group != $g)' "$DUST_FILE" > "${DUST_FILE}.tmp" 2>/dev/null; then
|
||||||
|
mv "${DUST_FILE}.tmp" "$DUST_FILE"
|
||||||
|
else
|
||||||
|
rm -f "${DUST_FILE}.tmp"
|
||||||
|
log "WARNING: failed to prune bundled group '$group' from dust.jsonl"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$DUST_GROUPS"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Process dev-agent escalations (per-project) ──────────────────────────
|
# ── Process dev-agent escalations (per-project) ──────────────────────────
|
||||||
ESCALATION_FILE="${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.jsonl"
|
ESCALATION_FILE="${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.jsonl"
|
||||||
ESCALATION_DONE="${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.done.jsonl"
|
ESCALATION_DONE="${FACTORY_ROOT}/supervisor/escalations-${PROJECT_NAME}.done.jsonl"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue