@procrastivity/clast 0.0.4 → 0.0.6
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 +9 -9
- package/bin/clast +13 -2
- package/bin/clast-plumbing +7 -0
- package/examples/workflows/morning-briefing.md +6 -6
- package/lib/clast/clast-classify-lib.bash +68 -0
- package/lib/clast/clast-dismissed-lib.bash +70 -0
- package/lib/clast/clast-lib.bash +88 -6
- package/lib/clast/clast-manifest-lib.bash +35 -5
- package/lib/clast/clast-porcelain-lib.bash +29 -16
- package/lib/clast/clast-porcelain-subcommands/brief.bash +90 -21
- package/lib/clast/clast-porcelain-subcommands/retro.bash +317 -0
- package/lib/clast/clast-porcelain-subcommands/undismiss.bash +27 -0
- package/lib/clast/clast-porcelain-subcommands/wake.bash +78 -13
- package/lib/clast/clast-registry-lib.bash +132 -27
- 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 +9 -19
- 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 +104 -4
- package/lib/clast/clast-subcommands/show.bash +54 -8
- package/lib/clast/clast-subcommands/snapshot.bash +18 -10
- 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 +2 -1
- package/{.claude-plugin/skills/wakeup → skills/brief}/SKILL.md +9 -9
- package/{.claude-plugin/skills/day-wakeup → skills/wake}/SKILL.md +35 -12
- /package/lib/clast/prompts/{day-wakeup-draft-user.md → wake-draft-user.md} +0 -0
|
@@ -63,15 +63,16 @@ EOF
|
|
|
63
63
|
return 0
|
|
64
64
|
fi
|
|
65
65
|
|
|
66
|
-
printf '%-17s %-33s %-43s %s\n' slug path remote aliases
|
|
67
|
-
local n i slug path remote aliases
|
|
66
|
+
printf '%-17s %-13s %-33s %-43s %s\n' slug label path remote aliases
|
|
67
|
+
local n i slug label path remote aliases
|
|
68
68
|
n="$(jq 'length' <<<"$arr")"
|
|
69
69
|
for (( i = 0; i < n; i++ )); do
|
|
70
70
|
slug="$(jq -r ".[$i].slug // \"\"" <<<"$arr")"
|
|
71
|
+
label="$(jq -r ".[$i].label // \"\" | if . == \"\" then \"(none)\" else . end" <<<"$arr")"
|
|
71
72
|
path="$(jq -r ".[$i].path // \"\"" <<<"$arr")"
|
|
72
73
|
remote="$(jq -r ".[$i].remote // \"\"" <<<"$arr")"
|
|
73
74
|
aliases="$(jq -r ".[$i].aliases // [] | if length == 0 then \"(none)\" else join(\",\") end" <<<"$arr")"
|
|
74
|
-
printf '%-17s %-33s %-43s %s\n' "$slug" "$path" "$remote" "$aliases"
|
|
75
|
+
printf '%-17s %-13s %-33s %-43s %s\n' "$slug" "$label" "$path" "$remote" "$aliases"
|
|
75
76
|
done
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -83,18 +84,18 @@ _clast_registry_op_add() {
|
|
|
83
84
|
--json) json=1; shift ;;
|
|
84
85
|
-h|--help)
|
|
85
86
|
cat <<'EOF'
|
|
86
|
-
Usage: clast registry add <path> [--slug NAME] [--remote URL] [--json]
|
|
87
|
+
Usage: clast registry add <path> [--slug NAME] [--label NAME] [--remote URL] [--json]
|
|
87
88
|
EOF
|
|
88
89
|
return 0
|
|
89
90
|
;;
|
|
90
91
|
# TODO(v1.1): interactive --slug prompt when stdin is a TTY.
|
|
91
|
-
--slug|--remote)
|
|
92
|
+
--slug|--label|--remote)
|
|
92
93
|
if [[ $# -lt 2 ]]; then
|
|
93
94
|
clast_log_error "registry add: $1 requires a value"
|
|
94
95
|
return 2
|
|
95
96
|
fi
|
|
96
97
|
passthrough+=("$1" "$2"); shift 2 ;;
|
|
97
|
-
--slug=*|--remote=*)
|
|
98
|
+
--slug=*|--label=*|--remote=*)
|
|
98
99
|
passthrough+=("$1"); shift ;;
|
|
99
100
|
*)
|
|
100
101
|
passthrough+=("$1"); shift ;;
|
|
@@ -110,10 +111,15 @@ EOF
|
|
|
110
111
|
if [[ -n "$json" ]]; then
|
|
111
112
|
printf '%s\n' "$line"
|
|
112
113
|
else
|
|
113
|
-
local slug path
|
|
114
|
+
local slug label path
|
|
114
115
|
slug="$(jq -r '.slug' <<<"$line")"
|
|
116
|
+
label="$(jq -r '.label // ""' <<<"$line")"
|
|
115
117
|
path="$(jq -r '.path' <<<"$line")"
|
|
116
|
-
|
|
118
|
+
if [[ -n "$label" ]]; then
|
|
119
|
+
printf 'registered %s (%s) → %s\n' "$slug" "$label" "$path"
|
|
120
|
+
else
|
|
121
|
+
printf 'registered %s → %s\n' "$slug" "$path"
|
|
122
|
+
fi
|
|
117
123
|
fi
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -126,6 +132,9 @@ _clast_registry_op_resolve() {
|
|
|
126
132
|
-h|--help)
|
|
127
133
|
cat <<'EOF'
|
|
128
134
|
Usage: clast registry resolve <path-or-segment> [--json]
|
|
135
|
+
|
|
136
|
+
Prints the resolved slug. With --json, also includes the directory's
|
|
137
|
+
`label` when the matched line has one.
|
|
129
138
|
EOF
|
|
130
139
|
return 0
|
|
131
140
|
;;
|
|
@@ -144,10 +153,16 @@ EOF
|
|
|
144
153
|
return 2
|
|
145
154
|
fi
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
# Resolve to the specific line so --json can surface the per-directory
|
|
157
|
+
# label (not just the slug). Human output stays slug-only.
|
|
158
|
+
local line
|
|
159
|
+
if line="$(clast_registry_line_for_path "$input")" && [[ -n "$line" ]]; then
|
|
160
|
+
local slug label
|
|
161
|
+
slug="$(jq -r '.slug // empty' <<<"$line")"
|
|
162
|
+
label="$(jq -r '.label // empty' <<<"$line")"
|
|
149
163
|
if [[ -n "$json" ]]; then
|
|
150
|
-
jq -cn --arg slug "$slug"
|
|
164
|
+
jq -cn --arg slug "$slug" --arg label "$label" \
|
|
165
|
+
'{slug: $slug} + (if $label == "" then {} else {label: $label} end)'
|
|
151
166
|
else
|
|
152
167
|
printf '%s\n' "$slug"
|
|
153
168
|
fi
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# clast-subcommands/retro.bash — `clast-plumbing retro`.
|
|
2
|
+
#
|
|
3
|
+
# Round 1 of the retro feature: run the deterministic day→project manifest
|
|
4
|
+
# (clast_retro_manifest) and render it from the raw entry bodies, in work-day
|
|
5
|
+
# order. No LLM. `--json` emits the manifest verbatim; the human render is the
|
|
6
|
+
# deterministic retro document the porcelain `clast retro` will later condense.
|
|
7
|
+
# See .wip/initiatives/clast-retro/.
|
|
8
|
+
# shellcheck shell=bash
|
|
9
|
+
# shellcheck source=lib/clast/clast-lib.bash
|
|
10
|
+
# shellcheck source=lib/clast/clast-retro-lib.bash
|
|
11
|
+
source "$CLAST_LIB/clast-retro-lib.bash"
|
|
12
|
+
|
|
13
|
+
_clast_retro_usage() {
|
|
14
|
+
cat <<'EOF'
|
|
15
|
+
Usage: clast retro [--from DATE] [--to DATE] [--window work-days|file-dates]
|
|
16
|
+
|
|
17
|
+
Summarize work grouped by the day it actually happened → project, from the
|
|
18
|
+
curated journal entries. Deterministic; no model call.
|
|
19
|
+
|
|
20
|
+
Flags:
|
|
21
|
+
--from DATE Start of the window (inclusive). Default: corpus start.
|
|
22
|
+
--to DATE End of the window (inclusive). Default: corpus end.
|
|
23
|
+
--window WHICH work-days (default): keep sessions whose work day is in range.
|
|
24
|
+
file-dates: keep entries whose filename date is in range
|
|
25
|
+
(pulls earlier work days reachable from those files).
|
|
26
|
+
--bodies With --json only: add each session's merged entry `body`.
|
|
27
|
+
-h, --help Print this usage and exit.
|
|
28
|
+
|
|
29
|
+
DATE accepts ISO (YYYY-MM-DD), `today`, `yesterday`, `last-week`, `-Nd`, `-Nw`.
|
|
30
|
+
With --json, emits the raw day→project manifest instead of the rendered report.
|
|
31
|
+
EOF
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_clast_retro_err() {
|
|
35
|
+
local msg="$1" code="${2:-2}"
|
|
36
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
37
|
+
jq -cn --arg m "$msg" --argjson c "$code" '{error:$m, code:$c}'
|
|
38
|
+
else
|
|
39
|
+
clast_log_error "retro: $msg"
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clast_cmd_retro() {
|
|
44
|
+
local from="" to="" window="work-days" bodies=0
|
|
45
|
+
|
|
46
|
+
while [[ $# -gt 0 ]]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--bodies) bodies=1; shift ;;
|
|
49
|
+
--from)
|
|
50
|
+
if [[ $# -lt 2 ]]; then _clast_retro_err "--from requires a value"; return 2; fi
|
|
51
|
+
if ! from="$(clast_parse_date "$2" 2>/dev/null)"; then
|
|
52
|
+
_clast_retro_err "invalid date '$2'"; return 2
|
|
53
|
+
fi
|
|
54
|
+
shift 2 ;;
|
|
55
|
+
--from=*)
|
|
56
|
+
if ! from="$(clast_parse_date "${1#*=}" 2>/dev/null)"; then
|
|
57
|
+
_clast_retro_err "invalid date '${1#*=}'"; return 2
|
|
58
|
+
fi
|
|
59
|
+
shift ;;
|
|
60
|
+
--to)
|
|
61
|
+
if [[ $# -lt 2 ]]; then _clast_retro_err "--to requires a value"; return 2; fi
|
|
62
|
+
if ! to="$(clast_parse_date "$2" 2>/dev/null)"; then
|
|
63
|
+
_clast_retro_err "invalid date '$2'"; return 2
|
|
64
|
+
fi
|
|
65
|
+
shift 2 ;;
|
|
66
|
+
--to=*)
|
|
67
|
+
if ! to="$(clast_parse_date "${1#*=}" 2>/dev/null)"; then
|
|
68
|
+
_clast_retro_err "invalid date '${1#*=}'"; return 2
|
|
69
|
+
fi
|
|
70
|
+
shift ;;
|
|
71
|
+
--window)
|
|
72
|
+
if [[ $# -lt 2 ]]; then _clast_retro_err "--window requires a value"; return 2; fi
|
|
73
|
+
window="$2"; shift 2 ;;
|
|
74
|
+
--window=*) window="${1#*=}"; shift ;;
|
|
75
|
+
-h|--help) _clast_retro_usage; return 0 ;;
|
|
76
|
+
--) shift; break ;;
|
|
77
|
+
-*) _clast_retro_err "unknown flag '$1'"; return 2 ;;
|
|
78
|
+
*) _clast_retro_err "unexpected positional '$1'"; return 2 ;;
|
|
79
|
+
esac
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
case "$window" in
|
|
83
|
+
work-days|file-dates) ;;
|
|
84
|
+
*) _clast_retro_err "--window must be 'work-days' or 'file-dates'"; return 2 ;;
|
|
85
|
+
esac
|
|
86
|
+
|
|
87
|
+
if [[ -n "$from" && -n "$to" && "$from" > "$to" ]]; then
|
|
88
|
+
_clast_retro_err "--from must be <= --to"; return 2
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
local manifest
|
|
92
|
+
manifest="$(clast_retro_manifest \
|
|
93
|
+
${from:+--from "$from"} ${to:+--to "$to"} --window "$window")" || return $?
|
|
94
|
+
|
|
95
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
96
|
+
if (( bodies )); then
|
|
97
|
+
manifest="$(_clast_retro_inject_bodies "$manifest")"
|
|
98
|
+
fi
|
|
99
|
+
printf '%s\n' "$manifest"
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
if (( bodies )); then
|
|
103
|
+
_clast_retro_err "--bodies is only meaningful with --json"; return 2
|
|
104
|
+
fi
|
|
105
|
+
if [[ -n "${CLAST_QUIET:-}" ]]; then
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
_clast_retro_render "$manifest"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# _clast_retro_inject_bodies <manifest-json>
|
|
113
|
+
# Add a `body` string (the merged trimmed entry bodies) to every session,
|
|
114
|
+
# for `--json --bodies`. Reuses clast_retro_session_body so the body matches
|
|
115
|
+
# the human render byte-for-byte.
|
|
116
|
+
_clast_retro_inject_bodies() {
|
|
117
|
+
local manifest="$1"
|
|
118
|
+
local -a pairs=()
|
|
119
|
+
local sess key body entry0 title interrupted
|
|
120
|
+
while IFS= read -r sess; do
|
|
121
|
+
[[ -z "$sess" ]] && continue
|
|
122
|
+
# Key by session_id, falling back to the first entry path when a
|
|
123
|
+
# legacy/hand-curated session has no id (JSON null): entries[0] is unique
|
|
124
|
+
# per session, so id-less sessions don't collide on a shared "null" key.
|
|
125
|
+
key="$(jq -r '.session_id // .entries[0]' <<<"$sess")"
|
|
126
|
+
body="$(clast_retro_session_body "$sess")"
|
|
127
|
+
entry0="$(jq -r '.entries[0]' <<<"$sess")"
|
|
128
|
+
title=""
|
|
129
|
+
[[ -r "$entry0" ]] && title="$(clast_entry_title "$entry0")"
|
|
130
|
+
if printf '%s' "$body" | clast_retro_is_interrupted; then
|
|
131
|
+
interrupted=true
|
|
132
|
+
else
|
|
133
|
+
interrupted=false
|
|
134
|
+
fi
|
|
135
|
+
# Body via --rawfile (process substitution), never argv: a single merged
|
|
136
|
+
# body can exceed MAX_ARG_STRLEN (~128 KiB) and silently fail the exec.
|
|
137
|
+
pairs+=("$(jq -cn --arg k "$key" --rawfile b <(printf '%s' "$body") \
|
|
138
|
+
--arg t "$title" --argjson i "$interrupted" \
|
|
139
|
+
'{key:$k, body:$b, title:$t, interrupted:$i}')")
|
|
140
|
+
done < <(jq -c '.days[].projects[].sessions[]' <<<"$manifest")
|
|
141
|
+
|
|
142
|
+
if (( ${#pairs[@]} == 0 )); then
|
|
143
|
+
printf '%s' "$manifest"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Manifest on stdin and the body-bearing pairs via --slurpfile — both can be
|
|
148
|
+
# large, so neither goes through argv. The `session_id // .entries[0]` key is
|
|
149
|
+
# always a non-null string, so this can't `$x[null]` abort jq ("Cannot index
|
|
150
|
+
# object with null") and id-less sessions don't collide on a shared key.
|
|
151
|
+
printf '%s' "$manifest" | jq -c --slurpfile pairs <(printf '%s\n' "${pairs[@]}") '
|
|
152
|
+
(reduce $pairs[] as $p ({}; .[$p.key] = $p)) as $x
|
|
153
|
+
| .days |= map(.projects |= map(.sessions |= map(
|
|
154
|
+
(.session_id // .entries[0]) as $k
|
|
155
|
+
| . + {body: ($x[$k].body // null),
|
|
156
|
+
title: ($x[$k].title // null),
|
|
157
|
+
interrupted: ($x[$k].interrupted // false)} )))
|
|
158
|
+
'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# _clast_retro_render <manifest-json>
|
|
162
|
+
# Render the day→project report to stdout.
|
|
163
|
+
_clast_retro_render() {
|
|
164
|
+
local manifest="$1"
|
|
165
|
+
local from to window
|
|
166
|
+
from="$(jq -r '.from // "(start)"' <<<"$manifest")"
|
|
167
|
+
to="$(jq -r '.to // "(end)"' <<<"$manifest")"
|
|
168
|
+
window="$(jq -r '.window' <<<"$manifest")"
|
|
169
|
+
printf 'Retro: %s -> %s (%s)\n' "$from" "$to" "$window"
|
|
170
|
+
|
|
171
|
+
local nd
|
|
172
|
+
nd="$(jq '.days | length' <<<"$manifest")"
|
|
173
|
+
if (( nd == 0 )); then
|
|
174
|
+
printf '\n(no sessions in range)\n'
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
local di pj si
|
|
179
|
+
local day np project ns sess sid shortsid title entry note body flag
|
|
180
|
+
for (( di = 0; di < nd; di++ )); do
|
|
181
|
+
day="$(jq -r ".days[$di].day" <<<"$manifest")"
|
|
182
|
+
printf '\n== %s ==\n' "$day"
|
|
183
|
+
note="$(_clast_retro_provenance_note "$day" "$(jq -c ".days[$di].curation_dates // []" <<<"$manifest")")"
|
|
184
|
+
[[ -n "$note" ]] && printf '%s\n' "$note"
|
|
185
|
+
np="$(jq ".days[$di].projects | length" <<<"$manifest")"
|
|
186
|
+
for (( pj = 0; pj < np; pj++ )); do
|
|
187
|
+
project="$(jq -r ".days[$di].projects[$pj].project_name // .days[$di].projects[$pj].project_path // \"(no project)\"" <<<"$manifest")"
|
|
188
|
+
printf '\n[%s]\n' "$project"
|
|
189
|
+
ns="$(jq ".days[$di].projects[$pj].sessions | length" <<<"$manifest")"
|
|
190
|
+
for (( si = 0; si < ns; si++ )); do
|
|
191
|
+
sess="$(jq -c ".days[$di].projects[$pj].sessions[$si]" <<<"$manifest")"
|
|
192
|
+
sid="$(jq -r '.session_id' <<<"$sess")"
|
|
193
|
+
shortsid="${sid:0:8}"
|
|
194
|
+
entry="$(jq -r '.entries[0]' <<<"$sess")"
|
|
195
|
+
title=""
|
|
196
|
+
[[ -r "$entry" ]] && title="$(clast_entry_title "$entry")"
|
|
197
|
+
[[ -z "$title" ]] && title="(untitled)"
|
|
198
|
+
body="$(clast_retro_session_body "$sess")"
|
|
199
|
+
flag=""
|
|
200
|
+
printf '%s' "$body" | clast_retro_is_interrupted && flag=" [interrupted]"
|
|
201
|
+
printf '\n * %s (%s)%s\n' "$title" "$shortsid" "$flag"
|
|
202
|
+
printf '%s\n' "$body"
|
|
203
|
+
done
|
|
204
|
+
done
|
|
205
|
+
done
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# _clast_retro_provenance_note <work-day> <curation_dates-json>
|
|
209
|
+
# One-line note when a day's entries were curated on other date(s) than the
|
|
210
|
+
# work day. Empty when they match (or there's nothing to say).
|
|
211
|
+
_clast_retro_provenance_note() {
|
|
212
|
+
local day="$1" cd_json="$2"
|
|
213
|
+
jq -r --arg d "$day" '
|
|
214
|
+
if . == [] or . == [$d] then empty
|
|
215
|
+
else " (filed " + (join(", ")) + "; work day reconstructed from session snapshots)"
|
|
216
|
+
end' <<<"$cd_json"
|
|
217
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# shellcheck shell=bash
|
|
7
7
|
# shellcheck source=lib/clast/clast-lib.bash
|
|
8
8
|
# shellcheck source=lib/clast/clast-manifest-lib.bash
|
|
9
|
+
# shellcheck source=lib/clast/clast-classify-lib.bash
|
|
9
10
|
# shellcheck source=lib/clast/clast-registry-lib.bash
|
|
10
11
|
# shellcheck source=lib/clast/clast-decode-lib.bash
|
|
11
12
|
# shellcheck source=lib/clast/clast-dismissed-lib.bash
|
|
@@ -14,11 +15,13 @@ _clast_sessions_usage() {
|
|
|
14
15
|
cat <<'EOF'
|
|
15
16
|
Usage: clast sessions [--day DATE] [--since DATE] [--until DATE] [--project SLUG]
|
|
16
17
|
clast sessions dismiss <session-id> [<session-id>...] [--reason TEXT]
|
|
18
|
+
clast sessions undismiss <session-id> [<session-id>...]
|
|
17
19
|
|
|
18
20
|
List sessions captured in a date window.
|
|
19
21
|
|
|
20
22
|
Subcommands:
|
|
21
23
|
dismiss ID... Mark session(s) as dismissed (excluded from future queries).
|
|
24
|
+
undismiss ID... Restore previously dismissed session(s).
|
|
22
25
|
|
|
23
26
|
Flags:
|
|
24
27
|
--day DATE Single-day window (default: today). Mutually exclusive
|
|
@@ -98,6 +101,54 @@ _clast_sessions_dismiss() {
|
|
|
98
101
|
fi
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
# _clast_sessions_undismiss <id> [<id>...]
|
|
105
|
+
# Reverse a dismissal so the session reappears in queries.
|
|
106
|
+
_clast_sessions_undismiss() {
|
|
107
|
+
source "$CLAST_LIB/clast-dismissed-lib.bash"
|
|
108
|
+
|
|
109
|
+
local -a ids=()
|
|
110
|
+
while [[ $# -gt 0 ]]; do
|
|
111
|
+
case "$1" in
|
|
112
|
+
-h|--help)
|
|
113
|
+
_clast_sessions_usage; return 0 ;;
|
|
114
|
+
-*)
|
|
115
|
+
_clast_sessions_err "undismiss: unknown flag '$1'"; return 2 ;;
|
|
116
|
+
*)
|
|
117
|
+
ids+=("$1"); shift ;;
|
|
118
|
+
esac
|
|
119
|
+
done
|
|
120
|
+
|
|
121
|
+
if [[ ${#ids[@]} -eq 0 ]]; then
|
|
122
|
+
_clast_sessions_err "undismiss requires at least one session ID"
|
|
123
|
+
return 2
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
local id count=0 rc
|
|
127
|
+
for id in "${ids[@]}"; do
|
|
128
|
+
if ! [[ "$id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
|
129
|
+
_clast_sessions_err "undismiss: '$id' is not a valid UUID"
|
|
130
|
+
return 2
|
|
131
|
+
fi
|
|
132
|
+
# `|| rc=$?` keeps the non-zero return from tripping `set -e` (the normal
|
|
133
|
+
# "not dismissed" path returns 1); rc stays 0 on success.
|
|
134
|
+
rc=0
|
|
135
|
+
clast_dismissed_remove "$id" >/dev/null || rc=$?
|
|
136
|
+
case "$rc" in
|
|
137
|
+
0) count=$(( count + 1 )) ;;
|
|
138
|
+
1) [[ -z "${CLAST_QUIET:-}" ]] && clast_log_info "not dismissed: $id" ;;
|
|
139
|
+
# Any other code is a real failure (temp file / rewrite / mv). Don't
|
|
140
|
+
# report success while the session is still dismissed — surface it.
|
|
141
|
+
*) _clast_sessions_err "undismiss: failed to restore $id"; return 1 ;;
|
|
142
|
+
esac
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
146
|
+
jq -cn --argjson count "$count" '{undismissed: $count}'
|
|
147
|
+
elif [[ -z "${CLAST_QUIET:-}" ]]; then
|
|
148
|
+
clast_log_info "Restored $count session(s)."
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
101
152
|
clast_cmd_sessions() {
|
|
102
153
|
# Handle "sessions dismiss" sub-action before flag parsing.
|
|
103
154
|
if [[ "${1:-}" == "dismiss" ]]; then
|
|
@@ -106,6 +157,13 @@ clast_cmd_sessions() {
|
|
|
106
157
|
return $?
|
|
107
158
|
fi
|
|
108
159
|
|
|
160
|
+
# Handle "sessions undismiss" sub-action before flag parsing.
|
|
161
|
+
if [[ "${1:-}" == "undismiss" ]]; then
|
|
162
|
+
shift
|
|
163
|
+
_clast_sessions_undismiss "$@"
|
|
164
|
+
return $?
|
|
165
|
+
fi
|
|
166
|
+
|
|
109
167
|
local day_filter="" since_date="" until_date=""
|
|
110
168
|
local project_filter=""
|
|
111
169
|
local include_dismissed=0
|
|
@@ -246,11 +304,12 @@ clast_cmd_sessions() {
|
|
|
246
304
|
# source_mtime.
|
|
247
305
|
local -a rows=()
|
|
248
306
|
local sid snapshot day_bucket mtime seg abs_path msgs
|
|
249
|
-
local cached_msgs cached_first cached_last
|
|
307
|
+
local cached_msgs cached_first cached_last cached_user cached_assistant
|
|
308
|
+
local user_count assistant_count substantive
|
|
250
309
|
local first_ts last_ts start_ts end_ts curated stale branch slug
|
|
251
310
|
declare -A seg_slug=()
|
|
252
311
|
|
|
253
|
-
while IFS=$'\t' read -r sid snapshot day_bucket mtime cached_msgs cached_first cached_last; do
|
|
312
|
+
while IFS=$'\t' read -r sid snapshot day_bucket mtime cached_msgs cached_first cached_last cached_user cached_assistant; do
|
|
254
313
|
[[ -z "$sid" ]] && continue
|
|
255
314
|
# Skip dismissed sessions early, before any per-session file work.
|
|
256
315
|
if [[ -n "${dismissed_ids[$sid]:-}" ]]; then
|
|
@@ -293,6 +352,40 @@ clast_cmd_sessions() {
|
|
|
293
352
|
start_ts="${first_ts:-$mtime}"
|
|
294
353
|
end_ts="${last_ts:-$mtime}"
|
|
295
354
|
|
|
355
|
+
# Session classification (clast-classify-lib.bash). Prefer the cached
|
|
356
|
+
# counts on the manifest line; recompute from the transcript for legacy
|
|
357
|
+
# lines that predate them. When neither is available (snapshot missing),
|
|
358
|
+
# leave the counts unknown and fall SAFE — substantive=true — so wake
|
|
359
|
+
# never auto-dismisses a session we couldn't classify. Note: a line may
|
|
360
|
+
# carry cached_msgs (step 21) yet lack these counts, so resolve them
|
|
361
|
+
# independently of the msg_count branch above.
|
|
362
|
+
if [[ -n "$cached_user" ]]; then
|
|
363
|
+
user_count="$cached_user"
|
|
364
|
+
assistant_count="$cached_assistant"
|
|
365
|
+
elif [[ -r "$abs_path" ]]; then
|
|
366
|
+
IFS=$'\t' read -r user_count assistant_count < <(clast_session_msg_counts "$abs_path")
|
|
367
|
+
[[ "$user_count" =~ ^[0-9]+$ ]] || user_count=""
|
|
368
|
+
[[ "$assistant_count" =~ ^[0-9]+$ ]] || assistant_count=""
|
|
369
|
+
else
|
|
370
|
+
user_count=""
|
|
371
|
+
assistant_count=""
|
|
372
|
+
fi
|
|
373
|
+
# substantive iff Claude actually produced a reply. assistant_msg_count==0
|
|
374
|
+
# captures BOTH no-op shapes the feature targets: empty / slash-command-only
|
|
375
|
+
# sessions (/clear, /model, /config) AND sessions where the user typed but
|
|
376
|
+
# quit before any response. Deliberately NOT gated on user_msg_count: a
|
|
377
|
+
# custom slash command (e.g. /review) leaves zero prose prompts yet drives
|
|
378
|
+
# real assistant work — those must be kept. Unknown count → SAFE (true).
|
|
379
|
+
if [[ "$assistant_count" =~ ^[0-9]+$ ]]; then
|
|
380
|
+
if (( assistant_count > 0 )); then
|
|
381
|
+
substantive=true
|
|
382
|
+
else
|
|
383
|
+
substantive=false
|
|
384
|
+
fi
|
|
385
|
+
else
|
|
386
|
+
substantive=true
|
|
387
|
+
fi
|
|
388
|
+
|
|
296
389
|
# Resolve the slug once per unique segment (segments repeat heavily
|
|
297
390
|
# across sessions; each resolve forks several jq calls).
|
|
298
391
|
if [[ -n "${seg_slug[$seg]+x}" ]]; then
|
|
@@ -338,6 +431,9 @@ clast_cmd_sessions() {
|
|
|
338
431
|
--argjson curated "$curated" \
|
|
339
432
|
--argjson stale "$stale" \
|
|
340
433
|
--argjson dismissed "$is_dismissed" \
|
|
434
|
+
--arg user_msg_count "$user_count" \
|
|
435
|
+
--arg assistant_msg_count "$assistant_count" \
|
|
436
|
+
--argjson substantive "$substantive" \
|
|
341
437
|
'{
|
|
342
438
|
session_id: $session_id,
|
|
343
439
|
project: $project,
|
|
@@ -350,7 +446,10 @@ clast_cmd_sessions() {
|
|
|
350
446
|
day_bucket: $day_bucket,
|
|
351
447
|
curated: $curated,
|
|
352
448
|
stale: $stale,
|
|
353
|
-
dismissed: $dismissed
|
|
449
|
+
dismissed: $dismissed,
|
|
450
|
+
user_msg_count: (if $user_msg_count == "" then null else ($user_msg_count | tonumber) end),
|
|
451
|
+
assistant_msg_count: (if $assistant_msg_count == "" then null else ($assistant_msg_count | tonumber) end),
|
|
452
|
+
substantive: $substantive
|
|
354
453
|
}')")
|
|
355
454
|
done < <(
|
|
356
455
|
if [[ -f "$manifest_path" ]]; then
|
|
@@ -361,7 +460,8 @@ clast_cmd_sessions() {
|
|
|
361
460
|
({}; .[$l.session_id] = $l)
|
|
362
461
|
| to_entries[] | .value
|
|
363
462
|
| [.session_id, .snapshot, .day_bucket, .source_mtime,
|
|
364
|
-
.msg_count, .first_ts, .last_ts
|
|
463
|
+
.msg_count, .first_ts, .last_ts,
|
|
464
|
+
.user_msg_count, .assistant_msg_count] | @tsv
|
|
365
465
|
' "$manifest_path"
|
|
366
466
|
fi
|
|
367
467
|
)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# shellcheck shell=bash
|
|
6
6
|
# shellcheck source=lib/clast/clast-lib.bash
|
|
7
7
|
# shellcheck source=lib/clast/clast-manifest-lib.bash
|
|
8
|
+
# shellcheck source=lib/clast/clast-classify-lib.bash
|
|
8
9
|
# shellcheck source=lib/clast/clast-registry-lib.bash
|
|
9
10
|
# shellcheck source=lib/clast/clast-decode-lib.bash
|
|
10
11
|
|
|
@@ -113,14 +114,35 @@ clast_cmd_show() {
|
|
|
113
114
|
start_ts="${first_ts:-$source_mtime}"
|
|
114
115
|
end_ts="${last_ts:-$source_mtime}"
|
|
115
116
|
|
|
117
|
+
# Session classification (clast-classify-lib.bash): prefer cached counts on
|
|
118
|
+
# the manifest line, else recompute from the transcript (always readable
|
|
119
|
+
# here — the -r guard above already returned otherwise). substantive is true
|
|
120
|
+
# iff Claude produced at least one reply (assistant_msg_count > 0) — this is
|
|
121
|
+
# the no-op signal wake keys on; see sessions.bash for the full rationale.
|
|
122
|
+
local user_count assistant_count substantive
|
|
123
|
+
IFS=$'\t' read -r user_count assistant_count \
|
|
124
|
+
< <(jq -r '[(.user_msg_count // ""), (.assistant_msg_count // "")] | @tsv' <<<"$line")
|
|
125
|
+
if ! [[ "$user_count" =~ ^[0-9]+$ && "$assistant_count" =~ ^[0-9]+$ ]]; then
|
|
126
|
+
IFS=$'\t' read -r user_count assistant_count < <(clast_session_msg_counts "$abs_path")
|
|
127
|
+
[[ "$user_count" =~ ^[0-9]+$ ]] || user_count=0
|
|
128
|
+
[[ "$assistant_count" =~ ^[0-9]+$ ]] || assistant_count=0
|
|
129
|
+
fi
|
|
130
|
+
if (( assistant_count > 0 )); then
|
|
131
|
+
substantive=true
|
|
132
|
+
else
|
|
133
|
+
substantive=false
|
|
134
|
+
fi
|
|
135
|
+
|
|
116
136
|
# first_prompt / last_prompt — best-effort scan of user messages.
|
|
117
137
|
# These pipelines must never abort the command: malformed JSONL lines are
|
|
118
138
|
# routine, and the fields are explicitly documented as best-effort.
|
|
119
139
|
local first_prompt last_prompt user_msgs=""
|
|
120
140
|
user_msgs="$(_clast_show_user_messages "$abs_path" 2>/dev/null || true)"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
# Pure parameter expansion — no subshell/pipe, so a >64KB multi-line
|
|
142
|
+
# $user_msgs can never SIGPIPE a `printf | head` and abort under pipefail.
|
|
143
|
+
first_prompt="${user_msgs%%$'\n'*}" # text up to the first newline
|
|
144
|
+
last_prompt="${user_msgs##*$'\n'}" # text after the last newline
|
|
145
|
+
# If the stream was empty the expansions yield '' anyway — keep as ''.
|
|
124
146
|
[[ -z "$user_msgs" ]] && first_prompt="" && last_prompt=""
|
|
125
147
|
first_prompt="$(_clast_show_truncate "$first_prompt")"
|
|
126
148
|
last_prompt="$(_clast_show_truncate "$last_prompt")"
|
|
@@ -174,6 +196,9 @@ clast_cmd_show() {
|
|
|
174
196
|
--argjson curated "$curated" \
|
|
175
197
|
--arg first_prompt "$first_prompt" \
|
|
176
198
|
--arg last_prompt "$last_prompt" \
|
|
199
|
+
--argjson user_msg_count "$user_count" \
|
|
200
|
+
--argjson assistant_msg_count "$assistant_count" \
|
|
201
|
+
--argjson substantive "$substantive" \
|
|
177
202
|
'{
|
|
178
203
|
session_id: $session_id,
|
|
179
204
|
project: $project,
|
|
@@ -187,11 +212,19 @@ clast_cmd_show() {
|
|
|
187
212
|
day_bucket: $day_bucket,
|
|
188
213
|
curated: $curated,
|
|
189
214
|
first_prompt: (if $first_prompt == "" then null else $first_prompt end),
|
|
190
|
-
last_prompt: (if $last_prompt == "" then null else $last_prompt end)
|
|
215
|
+
last_prompt: (if $last_prompt == "" then null else $last_prompt end),
|
|
216
|
+
user_msg_count: $user_msg_count,
|
|
217
|
+
assistant_msg_count: $assistant_msg_count,
|
|
218
|
+
substantive: $substantive
|
|
191
219
|
}')"
|
|
192
220
|
if (( include_turns == 1 )); then
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
# Feed the (potentially multi-hundred-KB) turn arrays via stdin, not as
|
|
222
|
+
# --argjson argv: a single argument >128KB (MAX_ARG_STRLEN) fails with
|
|
223
|
+
# "Argument list too long" even when total ARG_MAX is far larger. printf
|
|
224
|
+
# is a builtin, so it has no argv size limit; jq reads three JSON values.
|
|
225
|
+
obj="$(printf '%s\n%s\n%s\n' "$obj" "$first_turns_json" "$last_turns_json" \
|
|
226
|
+
| jq -cn 'input as $o | input as $f | input as $l
|
|
227
|
+
| $o + {first_turns:$f, last_turns:$l}')"
|
|
195
228
|
fi
|
|
196
229
|
printf '%s\n' "$obj"
|
|
197
230
|
return 0
|
|
@@ -208,6 +241,13 @@ clast_cmd_show() {
|
|
|
208
241
|
printf 'duration: %s\n' "$duration_str"
|
|
209
242
|
fi
|
|
210
243
|
printf 'msg_count: %s (approx)\n' "$msgs"
|
|
244
|
+
printf 'user_msgs: %s\n' "$user_count"
|
|
245
|
+
printf 'assistant_msgs: %s\n' "$assistant_count"
|
|
246
|
+
if [[ "$substantive" == "true" ]]; then
|
|
247
|
+
printf 'substantive: yes\n'
|
|
248
|
+
else
|
|
249
|
+
printf 'substantive: no (no-op — empty / slash-command-only)\n'
|
|
250
|
+
fi
|
|
211
251
|
printf 'snapshot: %s\n' "$abs_path"
|
|
212
252
|
if [[ "$curated" == "true" ]]; then
|
|
213
253
|
printf 'curated: yes\n'
|
|
@@ -231,14 +271,20 @@ clast_cmd_show() {
|
|
|
231
271
|
}
|
|
232
272
|
|
|
233
273
|
# _clast_show_user_messages <abs_path>
|
|
234
|
-
# Stream user-
|
|
274
|
+
# Stream real user-prompt text content, one per line. Empty lines, meta
|
|
275
|
+
# messages, and slash-command wrappers (`/clear`, `/model`, …) are dropped
|
|
276
|
+
# so first_prompt/last_prompt reflect what the user actually typed — not a
|
|
277
|
+
# `<local-command-caveat>…` marker. Shares CLAST_COMMAND_MARKER_RE with the
|
|
278
|
+
# classifier (clast-classify-lib.bash) so the two can't drift.
|
|
235
279
|
_clast_show_user_messages() {
|
|
236
280
|
local path="$1"
|
|
237
|
-
jq -r '
|
|
281
|
+
jq -r --arg cmd_re "$CLAST_COMMAND_MARKER_RE" '
|
|
238
282
|
select((.role // .message.role // .type) == "user")
|
|
283
|
+
| select((.isMeta // false) != true)
|
|
239
284
|
| (.message.content // .content // empty)
|
|
240
285
|
| if type == "array" then map(.text? // "") | join(" ") else . end
|
|
241
286
|
| select(. != null and . != "")
|
|
287
|
+
| select(test($cmd_re) | not)
|
|
242
288
|
' "$path" 2>/dev/null
|
|
243
289
|
}
|
|
244
290
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# shellcheck shell=bash
|
|
8
8
|
# shellcheck source=lib/clast/clast-lib.bash
|
|
9
9
|
# shellcheck source=lib/clast/clast-manifest-lib.bash
|
|
10
|
+
# shellcheck source=lib/clast/clast-classify-lib.bash
|
|
10
11
|
# shellcheck source=lib/clast/clast-decode-lib.bash
|
|
11
12
|
# shellcheck source=lib/clast/clast-registry-lib.bash
|
|
12
13
|
|
|
@@ -28,16 +29,10 @@ EOF
|
|
|
28
29
|
|
|
29
30
|
# _clast_snapshot_bucket_for_epoch <epoch>
|
|
30
31
|
# Mirror of clast_today's cutoff math against an arbitrary epoch. Local
|
|
31
|
-
# time per docs/explanation/conventions.md.
|
|
32
|
+
# time per docs/explanation/conventions.md. Thin alias over the shared
|
|
33
|
+
# primitive in clast-lib.bash.
|
|
32
34
|
_clast_snapshot_bucket_for_epoch() {
|
|
33
|
-
|
|
34
|
-
local cutoff="${CLAST_DAY_CUTOFF:-04:00}"
|
|
35
|
-
local h="${cutoff%%:*}" m="${cutoff##*:}"
|
|
36
|
-
h=$((10#$h))
|
|
37
|
-
m=$((10#$m))
|
|
38
|
-
local off=$((h * 3600 + m * 60))
|
|
39
|
-
# GNU `date -d` — BSD date not supported, per overview.md.
|
|
40
|
-
date -d "@$((epoch - off))" +%Y-%m-%d
|
|
35
|
+
clast_day_bucket_for_epoch "$1"
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
clast_cmd_snapshot() {
|
|
@@ -118,6 +113,7 @@ clast_cmd_snapshot() {
|
|
|
118
113
|
if [[ -d "$projects_dir" ]]; then
|
|
119
114
|
local source segment session_id mtime_epoch mtime_iso source_size
|
|
120
115
|
local first_ts ts_epoch day_bucket last_ts msg_count
|
|
116
|
+
local user_msg_count assistant_msg_count
|
|
121
117
|
local dest_rel dest dest_dir tmp
|
|
122
118
|
while IFS= read -r -d '' source; do
|
|
123
119
|
segment="$(basename "$(dirname "$source")")"
|
|
@@ -181,6 +177,18 @@ clast_cmd_snapshot() {
|
|
|
181
177
|
continue
|
|
182
178
|
fi
|
|
183
179
|
|
|
180
|
+
# Session classification (clast-classify-lib.bash): count real user
|
|
181
|
+
# prompts and assistant replies so wake can auto-dismiss no-op sessions
|
|
182
|
+
# (empty / slash-command-only) without an LLM call. Cached on the
|
|
183
|
+
# manifest line alongside msg_count so readers never re-open the file.
|
|
184
|
+
# Computed AFTER the dedupe check: unlike the head/tail/wc reads above,
|
|
185
|
+
# this streams the whole transcript through jq, so we must not pay it for
|
|
186
|
+
# already-captured sessions skipped on every wake/hook run.
|
|
187
|
+
IFS=$'\t' read -r user_msg_count assistant_msg_count \
|
|
188
|
+
< <(clast_session_msg_counts "$source")
|
|
189
|
+
[[ "$user_msg_count" =~ ^[0-9]+$ ]] || user_msg_count=0
|
|
190
|
+
[[ "$assistant_msg_count" =~ ^[0-9]+$ ]] || assistant_msg_count=0
|
|
191
|
+
|
|
184
192
|
dest_rel="transcripts/$day_bucket/$segment/$session_id.jsonl"
|
|
185
193
|
dest="$journal_dir/$dest_rel"
|
|
186
194
|
|
|
@@ -209,7 +217,7 @@ clast_cmd_snapshot() {
|
|
|
209
217
|
# Invariant: a manifest line implies the dest file exists. If the
|
|
210
218
|
# append fails the dest is an orphan; doctor (step 10) will reap
|
|
211
219
|
# it, and a re-run of snapshot overwrites + retries the append.
|
|
212
|
-
if ! clast_manifest_append "$session_id" "$source" "$dest_rel" "$mtime_iso" "$source_size" "$day_bucket" "$msg_count" "$first_ts" "$last_ts"; then
|
|
220
|
+
if ! clast_manifest_append "$session_id" "$source" "$dest_rel" "$mtime_iso" "$source_size" "$day_bucket" "$msg_count" "$first_ts" "$last_ts" "$user_msg_count" "$assistant_msg_count"; then
|
|
213
221
|
error_lines+=("$(jq -cn --arg f "$source" --arg r "manifest append failed" '{file:$f,reason:$r}')")
|
|
214
222
|
continue
|
|
215
223
|
fi
|