@procrastivity/clast 0.0.3 → 0.0.5
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/README.md +41 -38
- package/bin/clast +31 -122
- package/bin/clast-plumbing +180 -0
- package/examples/cron/clast-snapshot.service +2 -2
- package/examples/cron/clast-snapshot.timer +1 -1
- package/examples/cron/crontab.sample +4 -4
- package/examples/workflows/morning-briefing.md +17 -17
- package/hooks/snapshot.sh +5 -5
- package/lib/clast/clast-classify-lib.bash +68 -0
- package/lib/clast/clast-dismissed-lib.bash +76 -5
- package/lib/clast/clast-lib.bash +93 -8
- package/lib/clast/clast-manifest-lib.bash +58 -5
- package/lib/clast/clast-porcelain-lib.bash +230 -0
- package/lib/clast/clast-porcelain-subcommands/brief.bash +236 -0
- package/lib/clast/clast-porcelain-subcommands/retro.bash +236 -0
- package/lib/clast/clast-porcelain-subcommands/undismiss.bash +27 -0
- package/lib/clast/clast-porcelain-subcommands/wake.bash +514 -0
- package/lib/clast/clast-registry-lib.bash +164 -41
- package/lib/clast/clast-retro-lib.bash +397 -0
- package/lib/clast/clast-subcommands/doctor.bash +29 -13
- package/lib/clast/clast-subcommands/entries.bash +40 -50
- package/lib/clast/clast-subcommands/projects.bash +16 -20
- package/lib/clast/clast-subcommands/registry.bash +26 -11
- package/lib/clast/clast-subcommands/retro.bash +217 -0
- package/lib/clast/clast-subcommands/sessions.bash +193 -48
- package/lib/clast/clast-subcommands/show.bash +69 -12
- package/lib/clast/clast-subcommands/snapshot.bash +29 -11
- package/lib/clast/clast-subcommands/stats.bash +7 -2
- package/lib/clast/prompts/brief-system.md +11 -5
- package/lib/clast/prompts/brief-user.md +4 -1
- package/lib/clast/prompts/retro-summary-system.md +14 -0
- package/lib/clast/prompts/retro-summary-user.md +7 -0
- package/lib/clast/prompts/{day-wakeup-draft-system.md → wake-draft-system.md} +1 -1
- package/package.json +3 -3
- package/{.claude-plugin/skills/wakeup → skills/brief}/SKILL.md +22 -20
- package/{.claude-plugin/skills/day-wakeup → skills/wake}/SKILL.md +54 -29
- package/bin/clast-brief +0 -305
- package/bin/clast-wake +0 -579
- /package/lib/clast/prompts/{day-wakeup-draft-user.md → wake-draft-user.md} +0 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# clast wake — LLM-powered interactive day curation.
|
|
2
|
+
#
|
|
3
|
+
# Replicates the /wake plugin skill using an OpenAI-compatible chat
|
|
4
|
+
# completions endpoint. Calls clast-plumbing for data, assembles prompts,
|
|
5
|
+
# calls the LLM via curl, presents drafts interactively.
|
|
6
|
+
#
|
|
7
|
+
# Usage: clast wake
|
|
8
|
+
# shellcheck shell=bash
|
|
9
|
+
|
|
10
|
+
# --- Small helpers -----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
_clast_wake_slugify() {
|
|
13
|
+
local s="$1"
|
|
14
|
+
s="${s,,}"
|
|
15
|
+
s="$(printf '%s' "$s" | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//')"
|
|
16
|
+
printf '%s' "${s:0:60}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_clast_wake_separator() {
|
|
20
|
+
local label="$1"
|
|
21
|
+
local width=72
|
|
22
|
+
local pad=$(( width - ${#label} - 4 ))
|
|
23
|
+
(( pad < 2 )) && pad=2
|
|
24
|
+
local dashes=""
|
|
25
|
+
local j
|
|
26
|
+
for (( j = 0; j < pad; j++ )); do dashes+="─"; done
|
|
27
|
+
printf '── %s %s\n' "$label" "$dashes"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# --- Preflight ---------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_clast_wake_preflight() {
|
|
33
|
+
if [[ ! -t 0 ]]; then
|
|
34
|
+
clast_porcelain_die "clast wake requires an interactive terminal (stdin is not a tty)."
|
|
35
|
+
fi
|
|
36
|
+
clast_porcelain_preflight_llm
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# --- User prompt -------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
_clast_wake_build_user_prompt() {
|
|
42
|
+
local project="$1" branch="$2" start="$3" end="$4" msg_count="$5"
|
|
43
|
+
local first_turns="$6" last_turns="$7" breadcrumbs="$8"
|
|
44
|
+
|
|
45
|
+
local template_file template
|
|
46
|
+
template_file="$(clast_porcelain_user_prompt_file wake-draft-user)"
|
|
47
|
+
|
|
48
|
+
if [[ -n "$template_file" ]]; then
|
|
49
|
+
template="$(cat "$template_file")"
|
|
50
|
+
template="${template//\{\{project\}\}/${project}}"
|
|
51
|
+
template="${template//\{\{branch\}\}/${branch:-unknown}}"
|
|
52
|
+
template="${template//\{\{start\}\}/${start}}"
|
|
53
|
+
template="${template//\{\{end\}\}/${end}}"
|
|
54
|
+
template="${template//\{\{msg_count\}\}/${msg_count}}"
|
|
55
|
+
template="${template//\{\{first_turns\}\}/${first_turns}}"
|
|
56
|
+
template="${template//\{\{last_turns\}\}/${last_turns}}"
|
|
57
|
+
template="${template//\{\{breadcrumbs\}\}/${breadcrumbs:-None.}}"
|
|
58
|
+
printf '%s' "$template"
|
|
59
|
+
else
|
|
60
|
+
clast_porcelain_warn "user prompt template not found: wake-draft-user.md — using inline fallback"
|
|
61
|
+
cat <<EOF
|
|
62
|
+
Session metadata:
|
|
63
|
+
- Project: ${project}
|
|
64
|
+
- Branch: ${branch:-unknown}
|
|
65
|
+
- Start: ${start}
|
|
66
|
+
- End: ${end}
|
|
67
|
+
- Approximate messages: ${msg_count}
|
|
68
|
+
|
|
69
|
+
First turns of the session:
|
|
70
|
+
${first_turns}
|
|
71
|
+
|
|
72
|
+
Last turns of the session:
|
|
73
|
+
${last_turns}
|
|
74
|
+
|
|
75
|
+
Breadcrumbs the user left during this session's day:
|
|
76
|
+
${breadcrumbs:-None.}
|
|
77
|
+
EOF
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- Draft parsing -----------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
_clast_wake_extract_title() {
|
|
84
|
+
local draft="$1"
|
|
85
|
+
grep -m1 '^# Session:' <<<"$draft" | sed 's/^# Session:[[:space:]]*//'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_clast_wake_extract_tags() {
|
|
89
|
+
local draft="$1"
|
|
90
|
+
local tags
|
|
91
|
+
tags="$(grep -i '^Suggested tags:' <<<"$draft" | sed 's/^[Ss]uggested tags:[[:space:]]*//')"
|
|
92
|
+
printf '%s' "$tags" | sed 's/[[:space:]]*,[[:space:]]*/,/g'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_clast_wake_strip_tags_trailer() {
|
|
96
|
+
local draft="$1"
|
|
97
|
+
printf '%s' "$draft" | sed '/^$/{ N; /\n[Ss]uggested tags:/d; }' | sed '/^[Ss]uggested tags:/d'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# --- Session slug ------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
_clast_wake_get_session_slug() {
|
|
103
|
+
local snapshot_path="$1" draft_title="$2"
|
|
104
|
+
local journal_dir
|
|
105
|
+
journal_dir="$(clast-plumbing whereami 2>/dev/null | grep '^journal_dir:' | awk '{print $2}')" || true
|
|
106
|
+
if [[ -z "$journal_dir" ]]; then
|
|
107
|
+
journal_dir="${HOME}/.claude/journal"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
local abs_path="$journal_dir/$snapshot_path"
|
|
111
|
+
|
|
112
|
+
if [[ -r "$abs_path" ]]; then
|
|
113
|
+
local ai_title
|
|
114
|
+
ai_title="$(jq -r 'select(.type == "ai-title") | .aiTitle' "$abs_path" 2>/dev/null | tail -1)" || true
|
|
115
|
+
if [[ -n "$ai_title" && "$ai_title" != "null" ]]; then
|
|
116
|
+
_clast_wake_slugify "$ai_title"
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [[ -n "$draft_title" ]]; then
|
|
122
|
+
_clast_wake_slugify "$draft_title"
|
|
123
|
+
return
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
printf 'session'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# --- Interactive menu --------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
_clast_wake_prompt_choice() {
|
|
132
|
+
local choice
|
|
133
|
+
printf '\n [a] Accept [e] Edit [d] Dismiss [s] Skip [q] Stop here\n' >/dev/tty
|
|
134
|
+
printf ' Choice: ' >/dev/tty
|
|
135
|
+
read -r -n1 choice </dev/tty
|
|
136
|
+
printf '\n' >/dev/tty
|
|
137
|
+
printf '%s' "$choice"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_clast_wake_prompt_edit_feedback() {
|
|
141
|
+
local feedback
|
|
142
|
+
printf '\n What should change? ' >/dev/tty
|
|
143
|
+
read -r feedback </dev/tty
|
|
144
|
+
printf '%s' "$feedback"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# --- Triage ------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
_clast_wake_triage() {
|
|
150
|
+
local uncurated="$1" total="$2" day_count="$3"
|
|
151
|
+
local first_day="$4" last_day="$5" project_count="$6"
|
|
152
|
+
|
|
153
|
+
clast_porcelain_info "Found $total uncurated session(s) across $day_count day(s) ($first_day to $last_day)." >/dev/tty
|
|
154
|
+
printf '\n' >/dev/tty
|
|
155
|
+
|
|
156
|
+
local breakdown
|
|
157
|
+
breakdown="$(jq -r '
|
|
158
|
+
group_by(.day_bucket) | sort_by(.[0].day_bucket) | .[] |
|
|
159
|
+
" \(.[0].day_bucket) \(length) session(s)"
|
|
160
|
+
' <<<"$uncurated")"
|
|
161
|
+
printf '%s\n' "$breakdown" >/dev/tty
|
|
162
|
+
printf '\n' >/dev/tty
|
|
163
|
+
|
|
164
|
+
local choice
|
|
165
|
+
printf ' [a] Process all %s sessions\n' "$total" >/dev/tty
|
|
166
|
+
printf ' [y] Process yesterday only\n' >/dev/tty
|
|
167
|
+
printf ' [n] Choose how many days back\n' >/dev/tty
|
|
168
|
+
printf ' [o] Dismiss everything older, then process the rest\n' >/dev/tty
|
|
169
|
+
printf ' [q] Quit\n' >/dev/tty
|
|
170
|
+
printf ' Choice: ' >/dev/tty
|
|
171
|
+
read -r -n1 choice </dev/tty
|
|
172
|
+
printf '\n' >/dev/tty
|
|
173
|
+
|
|
174
|
+
case "$choice" in
|
|
175
|
+
a|A)
|
|
176
|
+
printf '%s' "$uncurated"
|
|
177
|
+
;;
|
|
178
|
+
y|Y)
|
|
179
|
+
local yesterday
|
|
180
|
+
yesterday="$(date -d 'yesterday' +%Y-%m-%d)"
|
|
181
|
+
jq -c "[.[] | select(.day_bucket == \"$yesterday\")]" <<<"$uncurated"
|
|
182
|
+
;;
|
|
183
|
+
n|N)
|
|
184
|
+
local days
|
|
185
|
+
printf ' How many days back? ' >/dev/tty
|
|
186
|
+
read -r days </dev/tty
|
|
187
|
+
if ! [[ "$days" =~ ^[1-9][0-9]*$ ]]; then
|
|
188
|
+
clast_porcelain_warn "invalid number — processing all sessions"
|
|
189
|
+
printf '%s' "$uncurated"
|
|
190
|
+
return
|
|
191
|
+
fi
|
|
192
|
+
local cutoff
|
|
193
|
+
cutoff="$(date -d "-${days} days" +%Y-%m-%d 2>/dev/null)" || {
|
|
194
|
+
clast_porcelain_warn "failed to compute date — processing all sessions"
|
|
195
|
+
printf '%s' "$uncurated"
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
jq -c "[.[] | select(.day_bucket >= \"$cutoff\")]" <<<"$uncurated"
|
|
199
|
+
;;
|
|
200
|
+
o|O)
|
|
201
|
+
local days_keep
|
|
202
|
+
printf ' Keep how many days? (dismiss everything older) ' >/dev/tty
|
|
203
|
+
read -r days_keep </dev/tty
|
|
204
|
+
if ! [[ "$days_keep" =~ ^[1-9][0-9]*$ ]]; then
|
|
205
|
+
clast_porcelain_warn "invalid number — processing all sessions"
|
|
206
|
+
printf '%s' "$uncurated"
|
|
207
|
+
return
|
|
208
|
+
fi
|
|
209
|
+
local cutoff_keep
|
|
210
|
+
cutoff_keep="$(date -d "-${days_keep} days" +%Y-%m-%d 2>/dev/null)" || {
|
|
211
|
+
clast_porcelain_warn "failed to compute date — processing all sessions"
|
|
212
|
+
printf '%s' "$uncurated"
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
local old_ids
|
|
216
|
+
old_ids="$(jq -r "[.[] | select(.day_bucket < \"$cutoff_keep\")] | .[].session_id" <<<"$uncurated")"
|
|
217
|
+
local dismiss_count=0
|
|
218
|
+
local old_id
|
|
219
|
+
while IFS= read -r old_id; do
|
|
220
|
+
[[ -z "$old_id" ]] && continue
|
|
221
|
+
clast-plumbing sessions dismiss "$old_id" --reason "bulk dismiss via clast wake triage" 2>/dev/null
|
|
222
|
+
dismiss_count=$(( dismiss_count + 1 ))
|
|
223
|
+
done <<<"$old_ids"
|
|
224
|
+
if (( dismiss_count > 0 )); then
|
|
225
|
+
clast_porcelain_info " Dismissed $dismiss_count older session(s)." >/dev/tty
|
|
226
|
+
fi
|
|
227
|
+
jq -c "[.[] | select(.day_bucket >= \"$cutoff_keep\")]" <<<"$uncurated"
|
|
228
|
+
;;
|
|
229
|
+
q|Q)
|
|
230
|
+
printf '[]'
|
|
231
|
+
;;
|
|
232
|
+
*)
|
|
233
|
+
clast_porcelain_warn "unknown choice — processing all sessions"
|
|
234
|
+
printf '%s' "$uncurated"
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# --- Main --------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
clast_cmd_wake() {
|
|
242
|
+
_clast_wake_preflight
|
|
243
|
+
|
|
244
|
+
clast_porcelain_info "Snapshotting fresh transcripts..."
|
|
245
|
+
if ! clast-plumbing snapshot 2>/dev/null; then
|
|
246
|
+
clast_porcelain_warn "clast-plumbing snapshot failed — proceeding with existing data"
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
clast_porcelain_info "Checking for uncurated or stale sessions..."
|
|
250
|
+
# Window is configurable; a shorter default keeps the scan fast on large
|
|
251
|
+
# journals (clast sessions cost scales with sessions in the window).
|
|
252
|
+
local since="${CLAST_WAKE_SINCE:--14d}"
|
|
253
|
+
local sessions_json
|
|
254
|
+
sessions_json="$(clast-plumbing --json sessions --since "$since" 2>/dev/null)" || {
|
|
255
|
+
clast_porcelain_die "failed to list sessions"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Auto-dismiss no-op sessions before any LLM work: sessions where Claude
|
|
259
|
+
# never replied (substantive == false) — empty sessions, slash-command-only
|
|
260
|
+
# sessions (/clear, /model, /config), and sessions abandoned before any
|
|
261
|
+
# response. This is a deterministic pre-filter — the LLM is never called for
|
|
262
|
+
# them. Sessions driven by a custom slash command still have assistant
|
|
263
|
+
# replies, so they are kept. Reversible via `clast undismiss <id>`. Opt out
|
|
264
|
+
# by setting CLAST_WAKE_AUTODISMISS_NOOP=0.
|
|
265
|
+
local auto_dismissed_count=0
|
|
266
|
+
if [[ "${CLAST_WAKE_AUTODISMISS_NOOP:-1}" != "0" ]]; then
|
|
267
|
+
local noop_ids noop_id
|
|
268
|
+
noop_ids="$(jq -r '
|
|
269
|
+
.[] | select(.substantive == false and .curated == false and .dismissed == false)
|
|
270
|
+
| .session_id
|
|
271
|
+
' <<<"$sessions_json")"
|
|
272
|
+
while IFS= read -r noop_id; do
|
|
273
|
+
[[ -z "$noop_id" ]] && continue
|
|
274
|
+
if clast-plumbing sessions dismiss "$noop_id" \
|
|
275
|
+
--reason "auto: no substantive content (empty / slash-command-only)" >/dev/null 2>&1; then
|
|
276
|
+
auto_dismissed_count=$(( auto_dismissed_count + 1 ))
|
|
277
|
+
fi
|
|
278
|
+
done <<<"$noop_ids"
|
|
279
|
+
if (( auto_dismissed_count > 0 )); then
|
|
280
|
+
clast_porcelain_info "Auto-dismissed $auto_dismissed_count no-op session(s) (empty / slash-command-only)."
|
|
281
|
+
# Drop the just-dismissed rows from the working set so they don't reappear.
|
|
282
|
+
sessions_json="$(jq -c '[.[] | select(.substantive != false or .curated == true)]' <<<"$sessions_json")"
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
local uncurated
|
|
287
|
+
uncurated="$(jq -c '[.[] | select(.curated == false or .stale == true)]' <<<"$sessions_json")"
|
|
288
|
+
local total
|
|
289
|
+
total="$(jq 'length' <<<"$uncurated")"
|
|
290
|
+
|
|
291
|
+
if (( total == 0 )); then
|
|
292
|
+
clast_porcelain_info "Nothing to curate — all sessions are curated or dismissed."
|
|
293
|
+
return 0
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
local day_count first_day last_day project_count
|
|
297
|
+
day_count="$(jq '[.[].day_bucket] | unique | length' <<<"$uncurated")"
|
|
298
|
+
first_day="$(jq -r '[.[].day_bucket] | sort | first' <<<"$uncurated")"
|
|
299
|
+
last_day="$(jq -r '[.[].day_bucket] | sort | last' <<<"$uncurated")"
|
|
300
|
+
project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
|
|
301
|
+
|
|
302
|
+
if (( day_count > 1 )); then
|
|
303
|
+
uncurated="$(_clast_wake_triage "$uncurated" "$total" "$day_count" "$first_day" "$last_day" "$project_count")"
|
|
304
|
+
total="$(jq 'length' <<<"$uncurated")"
|
|
305
|
+
if (( total == 0 )); then
|
|
306
|
+
clast_porcelain_info "Nothing left to curate."
|
|
307
|
+
return 0
|
|
308
|
+
fi
|
|
309
|
+
project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
clast_porcelain_info "Processing $total session(s) across $project_count project(s)."
|
|
313
|
+
printf '\n'
|
|
314
|
+
|
|
315
|
+
local curated_count=0 skipped_count=0 dismissed_count=0
|
|
316
|
+
local -a curated_projects=()
|
|
317
|
+
local i=0 stop=0
|
|
318
|
+
|
|
319
|
+
while (( i < total && stop == 0 )); do
|
|
320
|
+
local session
|
|
321
|
+
session="$(jq -c ".[$i]" <<<"$uncurated")"
|
|
322
|
+
|
|
323
|
+
local sid project branch start_ts end_ts msg_count snapshot_path
|
|
324
|
+
sid="$(jq -r '.session_id' <<<"$session")"
|
|
325
|
+
project="$(jq -r '.project' <<<"$session")"
|
|
326
|
+
branch="$(jq -r '.branch // ""' <<<"$session")"
|
|
327
|
+
start_ts="$(jq -r '.start' <<<"$session")"
|
|
328
|
+
end_ts="$(jq -r '.end' <<<"$session")"
|
|
329
|
+
msg_count="$(jq -r '.msg_count_approx' <<<"$session")"
|
|
330
|
+
snapshot_path="$(jq -r '.snapshot_path' <<<"$session")"
|
|
331
|
+
|
|
332
|
+
# Recorded date + time range so the reviewer can tell which day's work
|
|
333
|
+
# this is (BDS-54). Render the session's own start/end instant in the
|
|
334
|
+
# local timezone. Deliberately NOT day_bucket: that is clast's
|
|
335
|
+
# cutoff-adjusted *filing* day, which differs from the instant's calendar
|
|
336
|
+
# date for pre-cutoff sessions — pairing it with a clock time (and a "UTC"
|
|
337
|
+
# label) yielded a timestamp wrong by a day. Fall back to the raw UTC
|
|
338
|
+
# substrings if `date` can't parse the timestamp.
|
|
339
|
+
local rec_date start_short end_short tz
|
|
340
|
+
rec_date="$(date -d "$start_ts" +%Y-%m-%d 2>/dev/null)" || rec_date=""
|
|
341
|
+
[[ -z "$rec_date" ]] && rec_date="${start_ts:0:10}"
|
|
342
|
+
start_short="$(date -d "$start_ts" +%H:%M 2>/dev/null)" || start_short=""
|
|
343
|
+
[[ -z "$start_short" ]] && start_short="${start_ts:11:5}"
|
|
344
|
+
end_short="$(date -d "$end_ts" +%H:%M 2>/dev/null)" || end_short=""
|
|
345
|
+
[[ -z "$end_short" ]] && end_short="${end_ts:11:5}"
|
|
346
|
+
tz="$(date -d "$start_ts" +%Z 2>/dev/null)" || tz="UTC"
|
|
347
|
+
[[ -z "$tz" ]] && tz="UTC"
|
|
348
|
+
|
|
349
|
+
local recorded="$rec_date"
|
|
350
|
+
if [[ -n "$start_short" ]]; then
|
|
351
|
+
recorded="$recorded $start_short"
|
|
352
|
+
[[ -n "$end_short" && "$end_short" != "$start_short" ]] && recorded="$recorded–$end_short"
|
|
353
|
+
recorded="$recorded $tz"
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
local is_stale
|
|
357
|
+
is_stale="$(jq -r '.stale // false' <<<"$session")"
|
|
358
|
+
local label="Session $((i+1))/$total: $project"
|
|
359
|
+
[[ "$is_stale" == "true" ]] && label="$label [STALE]"
|
|
360
|
+
[[ -n "$rec_date" ]] && label="$label ($rec_date $start_short"
|
|
361
|
+
[[ -n "$branch" && "$branch" != "null" ]] && label="$label, $branch"
|
|
362
|
+
label="$label)"
|
|
363
|
+
|
|
364
|
+
_clast_wake_separator "$label"
|
|
365
|
+
# Full session ID + recorded window: identifies exactly which session is
|
|
366
|
+
# being reviewed (e.g. for `clast-plumbing sessions dismiss/undismiss <id>`).
|
|
367
|
+
clast_porcelain_info " id: $sid"
|
|
368
|
+
clast_porcelain_info " recorded: $recorded"
|
|
369
|
+
clast_porcelain_info "Gathering context..."
|
|
370
|
+
|
|
371
|
+
local show_json
|
|
372
|
+
show_json="$(clast-plumbing --json show "$sid" --full --turns 8 2>/dev/null)" || {
|
|
373
|
+
local rc=$? reason
|
|
374
|
+
reason="$(jq -r '.error // empty' <<<"$show_json" 2>/dev/null || true)"
|
|
375
|
+
[[ -z "$reason" ]] && reason="exit $rc"
|
|
376
|
+
clast_porcelain_warn "failed to read session $sid ($reason) — skipping"
|
|
377
|
+
skipped_count=$(( skipped_count + 1 ))
|
|
378
|
+
i=$(( i + 1 ))
|
|
379
|
+
continue
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Cap each turn's text: a single pathological turn (e.g. a huge pasted
|
|
383
|
+
# blob or tool dump) would otherwise bloat the prompt — costly and liable
|
|
384
|
+
# to exceed the model's context. show --full keeps the full text; only the
|
|
385
|
+
# LLM-bound copy is bounded.
|
|
386
|
+
local first_turns last_turns turn_cap=2000
|
|
387
|
+
first_turns="$(jq -r --argjson cap "$turn_cap" '
|
|
388
|
+
.first_turns // [] | .[] |
|
|
389
|
+
(.text // "") as $t | ($t | length) as $n |
|
|
390
|
+
"[\(.role)] \(if $n > $cap then $t[0:$cap] + "… [\($n - $cap) more chars truncated]" else $t end)"
|
|
391
|
+
' <<<"$show_json" 2>/dev/null)" || true
|
|
392
|
+
|
|
393
|
+
last_turns="$(jq -r --argjson cap "$turn_cap" '
|
|
394
|
+
.last_turns // [] | .[] |
|
|
395
|
+
(.text // "") as $t | ($t | length) as $n |
|
|
396
|
+
"[\(.role)] \(if $n > $cap then $t[0:$cap] + "… [\($n - $cap) more chars truncated]" else $t end)"
|
|
397
|
+
' <<<"$show_json" 2>/dev/null)" || true
|
|
398
|
+
|
|
399
|
+
local breadcrumbs=""
|
|
400
|
+
breadcrumbs="$(clast-plumbing breadcrumb --read --project "$project" --day yesterday 2>/dev/null)" || true
|
|
401
|
+
|
|
402
|
+
local user_prompt
|
|
403
|
+
user_prompt="$(_clast_wake_build_user_prompt "$project" "$branch" "$start_ts" "$end_ts" "$msg_count" \
|
|
404
|
+
"$first_turns" "$last_turns" "$breadcrumbs")"
|
|
405
|
+
|
|
406
|
+
local system_prompt
|
|
407
|
+
system_prompt="$(clast_porcelain_load_system_prompt wake-draft-system)"
|
|
408
|
+
|
|
409
|
+
local draft="" edit_extra=""
|
|
410
|
+
local drafting=1
|
|
411
|
+
|
|
412
|
+
while (( drafting )); do
|
|
413
|
+
local full_system="$system_prompt"
|
|
414
|
+
local full_user="$user_prompt"
|
|
415
|
+
if [[ -n "$edit_extra" ]]; then
|
|
416
|
+
full_user="${full_user}
|
|
417
|
+
|
|
418
|
+
Revisions requested by user: ${edit_extra}"
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
clast_porcelain_info "Generating draft..."
|
|
422
|
+
if ! draft="$(clast_porcelain_llm_chat "$full_system" "$full_user")"; then
|
|
423
|
+
printf '\n'
|
|
424
|
+
clast_porcelain_warn "LLM call failed for session $sid"
|
|
425
|
+
local retry
|
|
426
|
+
printf ' [r] Retry [s] Skip [q] Stop\n Choice: '
|
|
427
|
+
read -r -n1 retry </dev/tty
|
|
428
|
+
printf '\n'
|
|
429
|
+
case "$retry" in
|
|
430
|
+
r|R) continue ;;
|
|
431
|
+
q|Q) stop=1; break ;;
|
|
432
|
+
*) skipped_count=$(( skipped_count + 1 )); break ;;
|
|
433
|
+
esac
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
printf '\n%s\n' "$draft"
|
|
437
|
+
printf '\n'
|
|
438
|
+
|
|
439
|
+
local choice
|
|
440
|
+
choice="$(_clast_wake_prompt_choice)"
|
|
441
|
+
|
|
442
|
+
case "$choice" in
|
|
443
|
+
a|A)
|
|
444
|
+
local title tags body slug
|
|
445
|
+
title="$(_clast_wake_extract_title "$draft")"
|
|
446
|
+
tags="$(_clast_wake_extract_tags "$draft")"
|
|
447
|
+
body="$(_clast_wake_strip_tags_trailer "$draft")"
|
|
448
|
+
slug="$(_clast_wake_get_session_slug "$snapshot_path" "$title")"
|
|
449
|
+
|
|
450
|
+
local write_args=(entries write --session "$sid" --slug "$slug" --body-stdin)
|
|
451
|
+
[[ -n "$tags" ]] && write_args+=(--tags "$tags")
|
|
452
|
+
[[ -n "$title" ]] && write_args+=(--title "$title")
|
|
453
|
+
|
|
454
|
+
local write_result
|
|
455
|
+
if write_result="$(printf '%s\n' "$body" | clast-plumbing "${write_args[@]}" 2>&1)"; then
|
|
456
|
+
clast_porcelain_info " $write_result"
|
|
457
|
+
curated_count=$(( curated_count + 1 ))
|
|
458
|
+
curated_projects+=("$project")
|
|
459
|
+
else
|
|
460
|
+
clast_porcelain_warn "failed to write entry: $write_result"
|
|
461
|
+
fi
|
|
462
|
+
drafting=0
|
|
463
|
+
;;
|
|
464
|
+
e|E)
|
|
465
|
+
edit_extra="$(_clast_wake_prompt_edit_feedback)"
|
|
466
|
+
;;
|
|
467
|
+
d|D)
|
|
468
|
+
if clast-plumbing sessions dismiss "$sid" --reason "dismissed via clast wake" 2>/dev/null; then
|
|
469
|
+
clast_porcelain_info " Dismissed."
|
|
470
|
+
dismissed_count=$(( dismissed_count + 1 ))
|
|
471
|
+
else
|
|
472
|
+
clast_porcelain_warn "failed to dismiss session $sid"
|
|
473
|
+
fi
|
|
474
|
+
drafting=0
|
|
475
|
+
;;
|
|
476
|
+
s|S)
|
|
477
|
+
skipped_count=$(( skipped_count + 1 ))
|
|
478
|
+
drafting=0
|
|
479
|
+
;;
|
|
480
|
+
q|Q)
|
|
481
|
+
stop=1
|
|
482
|
+
drafting=0
|
|
483
|
+
;;
|
|
484
|
+
*)
|
|
485
|
+
clast_porcelain_info " Unknown choice '$choice' — skipping."
|
|
486
|
+
skipped_count=$(( skipped_count + 1 ))
|
|
487
|
+
drafting=0
|
|
488
|
+
;;
|
|
489
|
+
esac
|
|
490
|
+
done
|
|
491
|
+
|
|
492
|
+
i=$(( i + 1 ))
|
|
493
|
+
printf '\n'
|
|
494
|
+
done
|
|
495
|
+
|
|
496
|
+
local remaining=$(( total - curated_count - skipped_count - dismissed_count ))
|
|
497
|
+
local unique_projects
|
|
498
|
+
unique_projects="$(printf '%s\n' "${curated_projects[@]}" 2>/dev/null | sort -u | wc -l | tr -d ' ')" || true
|
|
499
|
+
[[ -z "$unique_projects" || "$unique_projects" == "0" ]] && unique_projects=0
|
|
500
|
+
|
|
501
|
+
printf '\n'
|
|
502
|
+
_clast_wake_separator "Summary"
|
|
503
|
+
clast_porcelain_info " Curated: $curated_count session(s) across $unique_projects project(s)"
|
|
504
|
+
if (( auto_dismissed_count > 0 )); then
|
|
505
|
+
clast_porcelain_info " Auto-dismissed (no-op): $auto_dismissed_count session(s)"
|
|
506
|
+
fi
|
|
507
|
+
if (( dismissed_count > 0 )); then
|
|
508
|
+
clast_porcelain_info " Dismissed: $dismissed_count session(s)"
|
|
509
|
+
fi
|
|
510
|
+
clast_porcelain_info " Skipped: $skipped_count session(s)"
|
|
511
|
+
if (( remaining > 0 )); then
|
|
512
|
+
clast_porcelain_info " Remaining: $remaining session(s) (stopped early)"
|
|
513
|
+
fi
|
|
514
|
+
}
|