@cyperx/clawforge 1.2.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/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- package/tui/styles.go +69 -0
package/bin/learn.sh
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# learn.sh — Module 9: Capture learnings from completed tasks
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
LEARNINGS_FILE="${CLAWFORGE_DIR}/registry/learnings.jsonl"
|
|
9
|
+
MEMORY_DIR="$HOME/.openclaw/agents/builder/memory"
|
|
10
|
+
|
|
11
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: learn.sh [options]
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--task-id <id> Task ID to learn from (required unless --summary)
|
|
18
|
+
--auto Auto-generate notes from task data
|
|
19
|
+
--notes <text> Manual notes to attach
|
|
20
|
+
--tags <t1,t2> Comma-separated pattern tags
|
|
21
|
+
--summary Output summary of all learnings
|
|
22
|
+
--memory Also append to Builder's daily memory
|
|
23
|
+
--help Show this help
|
|
24
|
+
EOF
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
28
|
+
TASK_ID="" AUTO=false NOTES="" TAGS="" SUMMARY=false MEMORY=false
|
|
29
|
+
|
|
30
|
+
while [[ $# -gt 0 ]]; do
|
|
31
|
+
case "$1" in
|
|
32
|
+
--task-id) TASK_ID="$2"; shift 2 ;;
|
|
33
|
+
--auto) AUTO=true; shift ;;
|
|
34
|
+
--notes) NOTES="$2"; shift 2 ;;
|
|
35
|
+
--tags) TAGS="$2"; shift 2 ;;
|
|
36
|
+
--summary) SUMMARY=true; shift ;;
|
|
37
|
+
--memory) MEMORY=true; shift ;;
|
|
38
|
+
--help|-h) usage; exit 0 ;;
|
|
39
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
mkdir -p "$(dirname "$LEARNINGS_FILE")"
|
|
44
|
+
|
|
45
|
+
# ── Summary mode ─────────────────────────────────────────────────────
|
|
46
|
+
if $SUMMARY; then
|
|
47
|
+
if [[ ! -f "$LEARNINGS_FILE" ]]; then
|
|
48
|
+
echo "No learnings recorded yet."
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
TOTAL=$(wc -l < "$LEARNINGS_FILE" | tr -d ' ')
|
|
53
|
+
SUCCESSES=$(grep -c '"success":true' "$LEARNINGS_FILE" 2>/dev/null || echo 0)
|
|
54
|
+
FAILURES=$(grep -c '"success":false' "$LEARNINGS_FILE" 2>/dev/null || echo 0)
|
|
55
|
+
|
|
56
|
+
echo "=== Learning Summary ==="
|
|
57
|
+
echo "Total entries: $TOTAL"
|
|
58
|
+
echo "Successes: $SUCCESSES"
|
|
59
|
+
echo "Failures: $FAILURES"
|
|
60
|
+
if [[ "$TOTAL" -gt 0 ]]; then
|
|
61
|
+
RATE=$(python3 -c "print(f'{($SUCCESSES/$TOTAL)*100:.0f}%')" 2>/dev/null || echo "N/A")
|
|
62
|
+
echo "Success rate: $RATE"
|
|
63
|
+
fi
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# Average duration
|
|
67
|
+
AVG_DURATION=$(cat "$LEARNINGS_FILE" | jq -s '[.[].duration_minutes | select(. != null and . > 0)] | if length > 0 then (add / length | floor) else 0 end' 2>/dev/null || echo 0)
|
|
68
|
+
echo "Avg duration: ${AVG_DURATION} min"
|
|
69
|
+
|
|
70
|
+
# Agent breakdown
|
|
71
|
+
echo ""
|
|
72
|
+
echo "By agent:"
|
|
73
|
+
cat "$LEARNINGS_FILE" | jq -r '.agent' | sort | uniq -c | sort -rn | while read count agent; do
|
|
74
|
+
echo " $agent: $count"
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# Model breakdown
|
|
78
|
+
echo ""
|
|
79
|
+
echo "By model:"
|
|
80
|
+
cat "$LEARNINGS_FILE" | jq -r '.model' | sort | uniq -c | sort -rn | while read count model; do
|
|
81
|
+
echo " $model: $count"
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
# Recent entries
|
|
85
|
+
echo ""
|
|
86
|
+
echo "Recent (last 5):"
|
|
87
|
+
tail -5 "$LEARNINGS_FILE" | jq -r '" [\(.taskId)] \(.agent)/\(.model) — \(.success | if . then "✅" else "❌" end) \(.duration_minutes // "?")min — \(.notes // "no notes")"' 2>/dev/null || true
|
|
88
|
+
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# ── Learn from task ──────────────────────────────────────────────────
|
|
93
|
+
[[ -z "$TASK_ID" ]] && { log_error "--task-id is required (or use --summary)"; usage; exit 1; }
|
|
94
|
+
|
|
95
|
+
TASK_DATA=$(registry_get "$TASK_ID")
|
|
96
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
97
|
+
log_error "Task '$TASK_ID' not found in registry"
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
AGENT=$(echo "$TASK_DATA" | jq -r '.agent // "unknown"')
|
|
102
|
+
MODEL=$(echo "$TASK_DATA" | jq -r '.model // "unknown"')
|
|
103
|
+
RETRIES=$(echo "$TASK_DATA" | jq -r '.retries // 0')
|
|
104
|
+
STATUS=$(echo "$TASK_DATA" | jq -r '.status // "unknown"')
|
|
105
|
+
STARTED=$(echo "$TASK_DATA" | jq -r '.startedAt // 0')
|
|
106
|
+
COMPLETED=$(echo "$TASK_DATA" | jq -r '.completedAt // 0')
|
|
107
|
+
BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // ""')
|
|
108
|
+
CHECKS=$(echo "$TASK_DATA" | jq '.checks // {}')
|
|
109
|
+
DESC=$(echo "$TASK_DATA" | jq -r '.description // ""')
|
|
110
|
+
|
|
111
|
+
# Calculate duration
|
|
112
|
+
DURATION_MIN=0
|
|
113
|
+
if [[ "$STARTED" -gt 0 && "$COMPLETED" -gt 0 ]]; then
|
|
114
|
+
DURATION_MS=$((COMPLETED - STARTED))
|
|
115
|
+
DURATION_MIN=$((DURATION_MS / 60000))
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Determine success
|
|
119
|
+
SUCCESS=false
|
|
120
|
+
if [[ "$STATUS" == "done" || "$STATUS" == "archived" ]]; then
|
|
121
|
+
SUCCESS=true
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Reviews passed
|
|
125
|
+
REVIEWS_PASSED=$(echo "$CHECKS" | jq '[to_entries[] | select(.value | test("APPROVE"; "i")) | .key]' 2>/dev/null || echo '[]')
|
|
126
|
+
|
|
127
|
+
# Auto-generate notes
|
|
128
|
+
if $AUTO && [[ -z "$NOTES" ]]; then
|
|
129
|
+
if [[ "$RETRIES" -eq 0 ]] && $SUCCESS; then
|
|
130
|
+
NOTES="One-shot success."
|
|
131
|
+
elif [[ "$RETRIES" -gt 0 ]] && $SUCCESS; then
|
|
132
|
+
NOTES="Succeeded after $RETRIES retries."
|
|
133
|
+
elif ! $SUCCESS; then
|
|
134
|
+
NOTES="Failed. Status: $STATUS."
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
if [[ "$DURATION_MIN" -gt 60 ]]; then
|
|
138
|
+
NOTES+=" Long-running task (${DURATION_MIN}min)."
|
|
139
|
+
elif [[ "$DURATION_MIN" -gt 0 && "$DURATION_MIN" -le 10 ]]; then
|
|
140
|
+
NOTES+=" Quick task (${DURATION_MIN}min)."
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Parse tags
|
|
145
|
+
TAGS_JSON="[]"
|
|
146
|
+
if [[ -n "$TAGS" ]]; then
|
|
147
|
+
TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | jq -R . | jq -s .)
|
|
148
|
+
else
|
|
149
|
+
# Auto-tag from branch name
|
|
150
|
+
TAGS_JSON="[]"
|
|
151
|
+
if [[ "$BRANCH" == feat/* || "$BRANCH" == feature/* ]]; then
|
|
152
|
+
TAGS_JSON='["feature"]'
|
|
153
|
+
elif [[ "$BRANCH" == fix/* || "$BRANCH" == bugfix/* ]]; then
|
|
154
|
+
TAGS_JSON='["bugfix"]'
|
|
155
|
+
elif [[ "$BRANCH" == refactor/* ]]; then
|
|
156
|
+
TAGS_JSON='["refactor"]'
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Build learning entry
|
|
161
|
+
NOW=$(epoch_ms)
|
|
162
|
+
LEARNING=$(jq -cn \
|
|
163
|
+
--argjson timestamp "$NOW" \
|
|
164
|
+
--arg taskId "$TASK_ID" \
|
|
165
|
+
--arg agent "$AGENT" \
|
|
166
|
+
--arg model "$MODEL" \
|
|
167
|
+
--argjson duration_minutes "$DURATION_MIN" \
|
|
168
|
+
--argjson retries "$RETRIES" \
|
|
169
|
+
--argjson success "$SUCCESS" \
|
|
170
|
+
--argjson reviews_passed "$REVIEWS_PASSED" \
|
|
171
|
+
--arg branch "$BRANCH" \
|
|
172
|
+
--argjson pattern_tags "$TAGS_JSON" \
|
|
173
|
+
--arg notes "${NOTES:-}" \
|
|
174
|
+
'{
|
|
175
|
+
timestamp: $timestamp,
|
|
176
|
+
taskId: $taskId,
|
|
177
|
+
agent: $agent,
|
|
178
|
+
model: $model,
|
|
179
|
+
duration_minutes: $duration_minutes,
|
|
180
|
+
retries: $retries,
|
|
181
|
+
success: $success,
|
|
182
|
+
reviews_passed: $reviews_passed,
|
|
183
|
+
branch: $branch,
|
|
184
|
+
pattern_tags: $pattern_tags,
|
|
185
|
+
notes: $notes
|
|
186
|
+
}')
|
|
187
|
+
|
|
188
|
+
# Write learning
|
|
189
|
+
echo "$LEARNING" >> "$LEARNINGS_FILE"
|
|
190
|
+
log_info "Learning recorded for task: $TASK_ID"
|
|
191
|
+
|
|
192
|
+
# Output
|
|
193
|
+
echo "$LEARNING" | jq .
|
|
194
|
+
|
|
195
|
+
# Append to clawforge memory with source=learn
|
|
196
|
+
CLAWFORGE_MEMORY_BASE="$HOME/.clawforge/memory"
|
|
197
|
+
LEARN_REPO=$(echo "$TASK_DATA" | jq -r '.repo // empty')
|
|
198
|
+
if [[ -n "$LEARN_REPO" ]]; then
|
|
199
|
+
LEARN_REPO_NAME=$(basename "$LEARN_REPO")
|
|
200
|
+
LEARN_REMOTE=$(git -C "$LEARN_REPO" config --get remote.origin.url 2>/dev/null || true)
|
|
201
|
+
[[ -n "$LEARN_REMOTE" ]] && LEARN_REPO_NAME=$(basename "$LEARN_REMOTE" .git)
|
|
202
|
+
LEARN_MEMORY_FILE="${CLAWFORGE_MEMORY_BASE}/${LEARN_REPO_NAME}.jsonl"
|
|
203
|
+
mkdir -p "$CLAWFORGE_MEMORY_BASE"
|
|
204
|
+
MEMORY_TEXT="[learn] ${DESC}: ${NOTES:-$AGENT/$MODEL, ${DURATION_MIN}min, retries=$RETRIES}"
|
|
205
|
+
LEARN_MEM_ENTRY=$(jq -cn \
|
|
206
|
+
--arg id "learn-$(date +%s)" \
|
|
207
|
+
--arg text "$MEMORY_TEXT" \
|
|
208
|
+
--argjson tags "$TAGS_JSON" \
|
|
209
|
+
--arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
210
|
+
--arg source "learn" \
|
|
211
|
+
'{id:$id, text:$text, tags:$tags, created:$created, source:$source}')
|
|
212
|
+
echo "$LEARN_MEM_ENTRY" >> "$LEARN_MEMORY_FILE"
|
|
213
|
+
log_info "Appended to clawforge memory: $LEARN_MEMORY_FILE"
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
# Append to Builder's daily memory
|
|
217
|
+
if $MEMORY; then
|
|
218
|
+
mkdir -p "$MEMORY_DIR"
|
|
219
|
+
TODAY=$(date +%Y-%m-%d)
|
|
220
|
+
MEMORY_FILE="${MEMORY_DIR}/${TODAY}.md"
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
echo ""
|
|
224
|
+
echo "## Learning: $TASK_ID"
|
|
225
|
+
echo "- Agent: $AGENT / $MODEL"
|
|
226
|
+
echo "- Duration: ${DURATION_MIN}min | Retries: $RETRIES | Success: $SUCCESS"
|
|
227
|
+
[[ -n "$NOTES" ]] && echo "- Notes: $NOTES"
|
|
228
|
+
} >> "$MEMORY_FILE"
|
|
229
|
+
log_info "Appended to memory: $MEMORY_FILE"
|
|
230
|
+
fi
|
package/bin/logs.sh
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# logs.sh — Capture and display agent output from tmux pane
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
usage() {
|
|
9
|
+
cat <<EOF
|
|
10
|
+
Usage: clawforge logs <id> [options]
|
|
11
|
+
|
|
12
|
+
Capture output from a running agent's tmux session without attaching.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
<id> Task ID or short ID (e.g., 1 or sprint-add-jwt)
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--lines <N> Number of lines to capture (default: 50)
|
|
19
|
+
--follow Stream output continuously (Ctrl+C to stop)
|
|
20
|
+
--raw Don't strip ANSI escape codes
|
|
21
|
+
--save <path> Save output to file
|
|
22
|
+
--help Show this help
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
clawforge logs 1
|
|
26
|
+
clawforge logs 1 --lines 100
|
|
27
|
+
clawforge logs 1 --follow
|
|
28
|
+
clawforge logs sprint-jwt --save /tmp/agent-output.log
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
TASK_REF="" LINES=50 FOLLOW=false RAW=false SAVE_PATH=""
|
|
33
|
+
|
|
34
|
+
while [[ $# -gt 0 ]]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--lines) LINES="$2"; shift 2 ;;
|
|
37
|
+
--follow) FOLLOW=true; shift ;;
|
|
38
|
+
--raw) RAW=true; shift ;;
|
|
39
|
+
--save) SAVE_PATH="$2"; shift 2 ;;
|
|
40
|
+
--help|-h) usage; exit 0 ;;
|
|
41
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
42
|
+
*) TASK_REF="$1"; shift ;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
[[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
|
|
47
|
+
|
|
48
|
+
# Resolve task: try short ID first, then full ID
|
|
49
|
+
_ensure_registry
|
|
50
|
+
TASK_DATA=""
|
|
51
|
+
if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
|
|
52
|
+
TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
53
|
+
fi
|
|
54
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
55
|
+
TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
|
|
56
|
+
fi
|
|
57
|
+
if [[ -z "$TASK_DATA" ]]; then
|
|
58
|
+
log_error "Task '$TASK_REF' not found in registry"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
TMUX_SESSION=$(echo "$TASK_DATA" | jq -r '.tmuxSession // empty')
|
|
63
|
+
TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
|
|
64
|
+
|
|
65
|
+
if [[ -z "$TMUX_SESSION" ]]; then
|
|
66
|
+
TMUX_SESSION="agent-${TASK_ID}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Check tmux session exists
|
|
70
|
+
if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
|
|
71
|
+
log_error "tmux session '$TMUX_SESSION' not found (agent may have exited)"
|
|
72
|
+
echo "Tip: Use 'clawforge history' to see completed task records."
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Capture function
|
|
77
|
+
capture_output() {
|
|
78
|
+
local output
|
|
79
|
+
output=$(tmux capture-pane -t "$TMUX_SESSION" -p -S "-${LINES}" 2>/dev/null || true)
|
|
80
|
+
if [[ -z "$output" ]]; then
|
|
81
|
+
echo "(no output captured)"
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
if ! $RAW; then
|
|
85
|
+
# Strip ANSI escape codes
|
|
86
|
+
output=$(printf '%s' "$output" | sed $'s/\x1b\[[0-9;]*[a-zA-Z]//g')
|
|
87
|
+
fi
|
|
88
|
+
echo "$output"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Follow mode
|
|
92
|
+
if $FOLLOW; then
|
|
93
|
+
echo "Following output from agent #${TASK_REF} (${TMUX_SESSION}). Ctrl+C to stop."
|
|
94
|
+
echo "────────────────────────────────────────"
|
|
95
|
+
LAST_HASH=""
|
|
96
|
+
while true; do
|
|
97
|
+
OUTPUT=$(capture_output)
|
|
98
|
+
HASH=$(printf '%s' "$OUTPUT" | md5 2>/dev/null || printf '%s' "$OUTPUT" | md5sum 2>/dev/null | cut -d' ' -f1 || echo "x")
|
|
99
|
+
if [[ "$HASH" != "$LAST_HASH" ]]; then
|
|
100
|
+
clear 2>/dev/null || true
|
|
101
|
+
echo "Following agent #${TASK_REF} (${TMUX_SESSION}) — $(date +%H:%M:%S)"
|
|
102
|
+
echo "────────────────────────────────────────"
|
|
103
|
+
echo "$OUTPUT"
|
|
104
|
+
LAST_HASH="$HASH"
|
|
105
|
+
fi
|
|
106
|
+
sleep 1
|
|
107
|
+
# Stop if session dies
|
|
108
|
+
if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
|
|
109
|
+
echo ""
|
|
110
|
+
echo "── Session ended ──"
|
|
111
|
+
break
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# One-shot capture
|
|
118
|
+
OUTPUT=$(capture_output)
|
|
119
|
+
|
|
120
|
+
if [[ -n "$SAVE_PATH" ]]; then
|
|
121
|
+
echo "$OUTPUT" > "$SAVE_PATH"
|
|
122
|
+
echo "Saved ${LINES} lines to $SAVE_PATH"
|
|
123
|
+
else
|
|
124
|
+
echo "── Agent #${TASK_REF} (${TMUX_SESSION}) ──"
|
|
125
|
+
echo "$OUTPUT"
|
|
126
|
+
fi
|
package/bin/memory.sh
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# memory.sh — Agent memory: per-repo JSONL knowledge base
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
7
|
+
|
|
8
|
+
MEMORY_BASE="$HOME/.clawforge/memory"
|
|
9
|
+
|
|
10
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<EOF
|
|
13
|
+
Usage: memory.sh [subcommand] [options]
|
|
14
|
+
|
|
15
|
+
Subcommands:
|
|
16
|
+
(none) Show memory stats for current repo
|
|
17
|
+
show List all memories for current repo
|
|
18
|
+
add <text> Add a memory entry
|
|
19
|
+
search <query> Search memories by text
|
|
20
|
+
forget --id <id> Remove a specific memory
|
|
21
|
+
clear Wipe all memories for current repo
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--tags <t1,t2> Tags for the memory (with add)
|
|
25
|
+
--source <src> Source label (default: manual)
|
|
26
|
+
--repo-name <name> Override auto-detected repo name
|
|
27
|
+
--help Show this help
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ── Detect repo name ──────────────────────────────────────────────────
|
|
32
|
+
get_repo_name() {
|
|
33
|
+
local override="${REPO_NAME_OVERRIDE:-}"
|
|
34
|
+
if [[ -n "$override" ]]; then
|
|
35
|
+
echo "$override"
|
|
36
|
+
return
|
|
37
|
+
fi
|
|
38
|
+
# Try git remote
|
|
39
|
+
local remote_url
|
|
40
|
+
remote_url=$(git config --get remote.origin.url 2>/dev/null || true)
|
|
41
|
+
if [[ -n "$remote_url" ]]; then
|
|
42
|
+
basename "$remote_url" .git
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
# Fallback: directory name
|
|
46
|
+
basename "$(pwd)"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
memory_file() {
|
|
50
|
+
local name
|
|
51
|
+
name=$(get_repo_name)
|
|
52
|
+
echo "${MEMORY_BASE}/${name}.jsonl"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ── Generate UUID ─────────────────────────────────────────────────────
|
|
56
|
+
gen_id() {
|
|
57
|
+
python3 -c 'import uuid; print(uuid.uuid4().hex[:12])' 2>/dev/null || \
|
|
58
|
+
head -c 12 /dev/urandom | xxd -p 2>/dev/null | head -c 12 || \
|
|
59
|
+
echo "$(date +%s)$$"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ── Subcommands ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
cmd_stats() {
|
|
65
|
+
local file
|
|
66
|
+
file=$(memory_file)
|
|
67
|
+
local name
|
|
68
|
+
name=$(get_repo_name)
|
|
69
|
+
if [[ ! -f "$file" ]] || [[ ! -s "$file" ]]; then
|
|
70
|
+
echo "No memories for repo: $name"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
local count
|
|
74
|
+
count=$(wc -l < "$file" | tr -d ' ')
|
|
75
|
+
local sources
|
|
76
|
+
sources=$(jq -r '.source' "$file" | sort | uniq -c | sort -rn | head -5)
|
|
77
|
+
echo "Memory: $name ($count entries)"
|
|
78
|
+
echo "File: $file"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "By source:"
|
|
81
|
+
echo "$sources" | while read -r cnt src; do
|
|
82
|
+
echo " $src: $cnt"
|
|
83
|
+
done
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cmd_show() {
|
|
87
|
+
local file
|
|
88
|
+
file=$(memory_file)
|
|
89
|
+
if [[ ! -f "$file" ]] || [[ ! -s "$file" ]]; then
|
|
90
|
+
echo "No memories for repo: $(get_repo_name)"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
jq -r '"[\(.id)] [\(.source)] \(.text)" + if (.tags | length) > 0 then " [" + (.tags | join(",")) + "]" else "" end' "$file"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cmd_add() {
|
|
97
|
+
local text="$1"
|
|
98
|
+
local tags_csv="${TAGS:-}"
|
|
99
|
+
local source="${SOURCE:-manual}"
|
|
100
|
+
local file
|
|
101
|
+
file=$(memory_file)
|
|
102
|
+
mkdir -p "$(dirname "$file")"
|
|
103
|
+
|
|
104
|
+
local tags_json="[]"
|
|
105
|
+
if [[ -n "$tags_csv" ]]; then
|
|
106
|
+
tags_json=$(echo "$tags_csv" | tr ',' '\n' | jq -R . | jq -s .)
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
local entry
|
|
110
|
+
entry=$(jq -cn \
|
|
111
|
+
--arg id "$(gen_id)" \
|
|
112
|
+
--arg text "$text" \
|
|
113
|
+
--argjson tags "$tags_json" \
|
|
114
|
+
--arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
115
|
+
--arg source "$source" \
|
|
116
|
+
'{id:$id, text:$text, tags:$tags, created:$created, source:$source}')
|
|
117
|
+
|
|
118
|
+
echo "$entry" >> "$file"
|
|
119
|
+
log_info "Memory added for $(get_repo_name)"
|
|
120
|
+
echo "$entry" | jq .
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cmd_search() {
|
|
124
|
+
local query="$1"
|
|
125
|
+
local file
|
|
126
|
+
file=$(memory_file)
|
|
127
|
+
if [[ ! -f "$file" ]]; then
|
|
128
|
+
echo "No memories for repo: $(get_repo_name)"
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
grep -i "$query" "$file" | jq -r '"[\(.id)] \(.text)"' 2>/dev/null || echo "No matches."
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cmd_forget() {
|
|
135
|
+
local target_id="$1"
|
|
136
|
+
local file
|
|
137
|
+
file=$(memory_file)
|
|
138
|
+
if [[ ! -f "$file" ]]; then
|
|
139
|
+
log_error "No memory file for $(get_repo_name)"
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
local before
|
|
143
|
+
before=$(wc -l < "$file" | tr -d ' ')
|
|
144
|
+
local tmp
|
|
145
|
+
tmp=$(mktemp)
|
|
146
|
+
jq -c "select(.id != \"$target_id\")" "$file" > "$tmp"
|
|
147
|
+
mv "$tmp" "$file"
|
|
148
|
+
local after
|
|
149
|
+
after=$(wc -l < "$file" | tr -d ' ')
|
|
150
|
+
if [[ "$before" -eq "$after" ]]; then
|
|
151
|
+
echo "No memory with id: $target_id"
|
|
152
|
+
return 1
|
|
153
|
+
fi
|
|
154
|
+
echo "Removed memory: $target_id ($before → $after entries)"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
cmd_clear() {
|
|
158
|
+
local file
|
|
159
|
+
file=$(memory_file)
|
|
160
|
+
if [[ -f "$file" ]]; then
|
|
161
|
+
rm -f "$file"
|
|
162
|
+
echo "Cleared all memories for $(get_repo_name)"
|
|
163
|
+
else
|
|
164
|
+
echo "No memories to clear for $(get_repo_name)"
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# ── Parse args ────────────────────────────────────────────────────────
|
|
169
|
+
SUBCMD="" TEXT="" FORGET_ID="" TAGS="" SOURCE="manual" REPO_NAME_OVERRIDE=""
|
|
170
|
+
|
|
171
|
+
# First pass: extract global flags
|
|
172
|
+
POSITIONAL=()
|
|
173
|
+
while [[ $# -gt 0 ]]; do
|
|
174
|
+
case "$1" in
|
|
175
|
+
--tags) TAGS="$2"; shift 2 ;;
|
|
176
|
+
--source) SOURCE="$2"; shift 2 ;;
|
|
177
|
+
--repo-name) REPO_NAME_OVERRIDE="$2"; shift 2 ;;
|
|
178
|
+
--id) FORGET_ID="$2"; shift 2 ;;
|
|
179
|
+
--help|-h) usage; exit 0 ;;
|
|
180
|
+
*) POSITIONAL+=("$1"); shift ;;
|
|
181
|
+
esac
|
|
182
|
+
done
|
|
183
|
+
set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
|
|
184
|
+
|
|
185
|
+
SUBCMD="${1:-}"
|
|
186
|
+
shift 2>/dev/null || true
|
|
187
|
+
|
|
188
|
+
case "$SUBCMD" in
|
|
189
|
+
"") cmd_stats ;;
|
|
190
|
+
show) cmd_show ;;
|
|
191
|
+
add)
|
|
192
|
+
TEXT="${1:-}"
|
|
193
|
+
[[ -z "$TEXT" ]] && { log_error "Usage: memory add <text>"; exit 1; }
|
|
194
|
+
cmd_add "$TEXT"
|
|
195
|
+
;;
|
|
196
|
+
search)
|
|
197
|
+
QUERY="${1:-}"
|
|
198
|
+
[[ -z "$QUERY" ]] && { log_error "Usage: memory search <query>"; exit 1; }
|
|
199
|
+
cmd_search "$QUERY"
|
|
200
|
+
;;
|
|
201
|
+
forget)
|
|
202
|
+
[[ -z "$FORGET_ID" ]] && { log_error "Usage: memory forget --id <id>"; exit 1; }
|
|
203
|
+
cmd_forget "$FORGET_ID"
|
|
204
|
+
;;
|
|
205
|
+
clear) cmd_clear ;;
|
|
206
|
+
*) log_error "Unknown subcommand: $SUBCMD"; usage; exit 1 ;;
|
|
207
|
+
esac
|