@procrastivity/clast 0.0.4 → 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.
Files changed (32) hide show
  1. package/README.md +9 -9
  2. package/bin/clast +13 -2
  3. package/bin/clast-plumbing +7 -0
  4. package/examples/workflows/morning-briefing.md +6 -6
  5. package/lib/clast/clast-classify-lib.bash +68 -0
  6. package/lib/clast/clast-dismissed-lib.bash +70 -0
  7. package/lib/clast/clast-lib.bash +88 -6
  8. package/lib/clast/clast-manifest-lib.bash +35 -5
  9. package/lib/clast/clast-porcelain-lib.bash +29 -16
  10. package/lib/clast/clast-porcelain-subcommands/brief.bash +90 -21
  11. package/lib/clast/clast-porcelain-subcommands/retro.bash +236 -0
  12. package/lib/clast/clast-porcelain-subcommands/undismiss.bash +27 -0
  13. package/lib/clast/clast-porcelain-subcommands/wake.bash +78 -13
  14. package/lib/clast/clast-registry-lib.bash +132 -27
  15. package/lib/clast/clast-retro-lib.bash +397 -0
  16. package/lib/clast/clast-subcommands/doctor.bash +29 -13
  17. package/lib/clast/clast-subcommands/entries.bash +40 -50
  18. package/lib/clast/clast-subcommands/projects.bash +9 -19
  19. package/lib/clast/clast-subcommands/registry.bash +26 -11
  20. package/lib/clast/clast-subcommands/retro.bash +217 -0
  21. package/lib/clast/clast-subcommands/sessions.bash +104 -4
  22. package/lib/clast/clast-subcommands/show.bash +54 -8
  23. package/lib/clast/clast-subcommands/snapshot.bash +18 -10
  24. package/lib/clast/prompts/brief-system.md +11 -5
  25. package/lib/clast/prompts/brief-user.md +4 -1
  26. package/lib/clast/prompts/retro-summary-system.md +14 -0
  27. package/lib/clast/prompts/retro-summary-user.md +7 -0
  28. package/lib/clast/prompts/{day-wakeup-draft-system.md → wake-draft-system.md} +1 -1
  29. package/package.json +2 -1
  30. package/{.claude-plugin/skills/wakeup → skills/brief}/SKILL.md +9 -9
  31. package/{.claude-plugin/skills/day-wakeup → skills/wake}/SKILL.md +35 -12
  32. /package/lib/clast/prompts/{day-wakeup-draft-user.md → wake-draft-user.md} +0 -0
@@ -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] | @tsv
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
- first_prompt="$(printf '%s\n' "$user_msgs" | head -n1)"
122
- last_prompt="$(printf '%s\n' "$user_msgs" | tail -n1)"
123
- # If the stream was empty the head/tail of '\n' is empty keep as ''.
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
- obj="$(jq -c --argjson f "$first_turns_json" --argjson l "$last_turns_json" \
194
- '. + {first_turns:$f, last_turns:$l}' <<<"$obj")"
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-message text content, one per line. Empty lines dropped.
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
- local epoch="$1"
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
@@ -1,17 +1,23 @@
1
1
  You are synthesizing a project briefing for the user. They are about to start work on a project and want a tight summary of where they left off.
2
2
 
3
+ The recent entries may be grouped under `## Workspace: <label>` headers when
4
+ this project spans more than one directory (separate clones/worktrees of the
5
+ same repo). Treat each workspace as a distinct line of work — do NOT blend
6
+ them. When there are no workspace headers, the project is a single directory;
7
+ render exactly as the structure below.
8
+
3
9
  Produce a briefing using this structure (omit any section that has no content):
4
10
 
5
11
  ## Wakeup briefing — {project}
6
12
 
7
- **Active thread:** one-line from most recent entry's "Open threads", or "None"
13
+ **Active thread:** one-line from the most recent entry's "Open threads", or "None". When the entries are split across workspaces, pick the active thread from the **current workspace** (named in the user prompt) if it has any entries; otherwise from the most recent entry overall. State which workspace it came from, e.g. "(dev)".
8
14
 
9
- **Last session:** date, branch, one-line goal
10
- - Work done: 2-3 bullets condensed from most recent entry
15
+ **Last session:** date, workspace/branch, one-line goal
16
+ - Work done: 2-3 bullets condensed from the most recent entry
11
17
  - Open threads: bullets, if any
12
18
  - Dead ends to avoid: bullets, if any
13
19
 
14
- **Recent sessions:** (up to 5 entries)
20
+ **Recent sessions:** (up to ~8 entries) — when multiple workspaces are present, group these under a short `**<label>:**` subheading per workspace, newest first within each:
15
21
  - date [branch] slug: one-line goal
16
22
 
17
23
  **Today's breadcrumbs:** (if any)
@@ -24,7 +30,7 @@ Produce a briefing using this structure (omit any section that has no content):
24
30
 
25
31
  Be concise. Use the user's terminology. Don't repeat content across sections. The total briefing should be 2-5k tokens — if you're approaching that, summarize rather than list verbatim.
26
32
 
27
- For the "Suggested next step": prefer the most recent entry's "Open threads" content, then the most recent breadcrumb, then a synthesis of the recent work. If nothing concrete, say "No active thread."
33
+ For the "Suggested next step": prefer the active thread's workspace — its most recent entry's "Open threads" content, then the most recent breadcrumb, then a synthesis of the recent work. If nothing concrete, say "No active thread."
28
34
 
29
35
  End with one of:
30
36
  - "Resume? Active thread: '<thread>'. Suggested next step: <step>."
@@ -1,6 +1,9 @@
1
1
  Project: {{project}}
2
+ Current workspace (the directory you are in now): {{current_label}}
2
3
 
3
- Recent curated entries (newest first):
4
+ Recent curated entries, grouped by workspace (newest first). When a project
5
+ spans more than one directory, entries appear under `## Workspace: <label>`
6
+ headers; a single-workspace project has no headers:
4
7
  {{entries}}
5
8
 
6
9
  Today's breadcrumbs for this project:
@@ -0,0 +1,14 @@
1
+ You are condensing a single already-curated Claude Code session into a few tight retro bullets. The input is the body of a journal entry (or several merged entries for one session). Your output will appear in a work retrospective grouped by day and project, so it must be skimmable and factual.
2
+
3
+ Condense the session into at most 3-5 bullets, in this order, omitting any that have no content (do not write "N/A" or empty headers):
4
+
5
+ - **Shipped:** what actually got done — features, fixes, files, decisions. One bullet per distinct outcome.
6
+ - **Issues:** notable problems hit and how they were resolved (only if the body records them).
7
+ - **Open:** anything left unfinished, deferred, or flagged for next time.
8
+
9
+ Rules:
10
+ - Use only what the body states. Do not invent, infer motivation, or add detail that is not present.
11
+ - Keep the user's terminology — project names, file paths, branch names, ticket ids — verbatim.
12
+ - Be terse: bullets, not paragraphs. No preamble, no closing summary, no headers other than the bold lead-ins above.
13
+ - If the body is an interrupted session (a goal and open threads but nothing shipped), say so in one **Open:** bullet rather than overstating progress.
14
+ - Output the bullets only — no title line, no tags, no surrounding prose.
@@ -0,0 +1,7 @@
1
+ Session to condense:
2
+ - Project: {{project}}
3
+ - Work day: {{work_day}}
4
+ - Session id: {{session_id}}
5
+
6
+ Entry body:
7
+ {{body}}
@@ -26,4 +26,4 @@ One sentence describing what this session was trying to accomplish.
26
26
  Be concise. Prefer bullets over paragraphs. Use the user's terminology (project-specific names, file paths). Do not invent details. If you are uncertain about something, omit it rather than guess.
27
27
 
28
28
  After the entry, add a blank line and then: "Suggested tags: tag1, tag2, tag3"
29
- Tags must be lowercase kebab-case (regex: `^[a-z0-9][a-z0-9-]{0,31}$`). Examples: `adrs`, `symfony-bot`, `mr-umbrella`, `phase-0`. Never use uppercase letters.
29
+ Tags must be lowercase kebab-case (regex: `^[a-z0-9][a-z0-9-]{0,31}$`). Examples: `adrs`, `symfony-bot`, `mr-umbrella`, `phase-0`. Never use uppercase letters. Use hyphens, not dots or spaces, for separators — write versions like `php-8-5`, not `php-8.5`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@procrastivity/clast",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Capture, curate, and surface Claude Code session history across all your projects.",
5
5
  "homepage": "https://github.com/procrastivity/clast#readme",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "bin/",
12
12
  "lib/",
13
13
  ".claude-plugin/",
14
+ "skills/",
14
15
  "hooks/",
15
16
  "examples/",
16
17
  "README.md",
@@ -1,16 +1,16 @@
1
1
  ---
2
- name: wakeup
2
+ name: brief
3
3
  description: |
4
- Synthesize a briefing for the current project (or a named one) so the user can resume work without re-explaining context. Use when the user says "/wakeup", "wakeup", "wake up", "catch me up", "where was I", "what was I working on", "load last session", "resume", or otherwise signals they want prior context for the project they're about to work on. Optionally accepts a project slug like "/wakeup xesapps". Reads recent curated entries and today's breadcrumbs from `~/.claude/journal/` and produces a 2–5k-token briefing. This is the per-project read flow; for cross-project daily curation use /day-wakeup; for mid-session pivots use session-brief.
4
+ Synthesize a briefing for the current project (or a named one) so the user can resume work without re-explaining context. Use when the user says "/brief", "brief me", "catch me up", "where was I", "what was I working on", "load last session", "resume", or otherwise signals they want prior context for the project they're about to work on. Optionally accepts a project slug like "/brief xesapps". Reads recent curated entries and today's breadcrumbs from `~/.claude/journal/` and produces a 2–5k-token briefing. This is the per-project read flow; for cross-project daily curation use /wake; for mid-session pivots use session-brief.
5
5
  ---
6
6
 
7
- # Wakeup
7
+ # Brief
8
8
 
9
9
  Synthesize a briefing for the current (or named) project so the user can resume without re-explaining context.
10
10
 
11
11
  ## Why this exists
12
12
 
13
- `/day-wakeup` curates yesterday's work into entries. `/wakeup` reads those entries back when starting work in a specific repo. The two are complementary: one writes, one reads.
13
+ `/wake` curates yesterday's work into entries. `/brief` reads those entries back when starting work in a specific repo. The two are complementary: one writes, one reads.
14
14
 
15
15
  ## Step 0: Resolve the clast-plumbing binary
16
16
 
@@ -39,13 +39,13 @@ Use `$CLAST_BIN` in place of bare `clast-plumbing` for all commands in this skil
39
39
 
40
40
  ## Step 1: Resolve the project
41
41
 
42
- If the user passed a slug as an argument (`/wakeup xesapps`), use it directly. Otherwise resolve from current working directory:
42
+ If the user passed a slug as an argument (`/brief xesapps`), use it directly. Otherwise resolve from current working directory:
43
43
 
44
44
  ```bash
45
45
  $CLAST_BIN registry resolve "$(pwd)"
46
46
  ```
47
47
 
48
- If `pwd` doesn't resolve and no slug was given: print "Not in a registered project. Run `clast-plumbing registry add .` first, or invoke as `/wakeup <slug>`." and stop.
48
+ If `pwd` doesn't resolve and no slug was given: print "Not in a registered project. Run `clast-plumbing registry add .` first, or invoke as `/brief <slug>`." and stop.
49
49
 
50
50
  ## Step 2: Gather data
51
51
 
@@ -73,7 +73,7 @@ $CLAST_BIN entries read <entry-path>
73
73
  Using the **synthesis prompt** (see below), produce a briefing of 2–5k tokens. Structure:
74
74
 
75
75
  ```
76
- ## Wakeup briefing — <project>
76
+ ## Brief — <project>
77
77
 
78
78
  **Active thread:** <one-line from most recent entry's "Open threads" section, or "None">
79
79
 
@@ -101,11 +101,11 @@ End with one of:
101
101
 
102
102
  ## Step 4: Don't write anything
103
103
 
104
- Wakeup is read-only. Never invoke write-form subcommands (`entries write`, `breadcrumb '<text>'`, `snapshot`) from this skill.
104
+ Brief is read-only. Never invoke write-form subcommands (`entries write`, `breadcrumb '<text>'`, `snapshot`) from this skill.
105
105
 
106
106
  ## Edge cases
107
107
 
108
- - **No entries for project**: print "No curated entries for `<slug>` yet. Run `/day-wakeup` to process recent sessions, or run `clast-plumbing sessions --project <slug>` to see what's available." and stop.
108
+ - **No entries for project**: print "No curated entries for `<slug>` yet. Run `/wake` to process recent sessions, or run `clast-plumbing sessions --project <slug>` to see what's available." and stop.
109
109
  - **Slug resolves but no entries and no sessions**: print "Project `<slug>` registered but has no journal activity yet."
110
110
  - **Today's session count > 5**: summarize ("worked 12 sessions today, most recent 16:22 on branch `loop-guard-ngram`") rather than listing all.
111
111
 
@@ -1,17 +1,17 @@
1
1
  ---
2
- name: day-wakeup
3
- description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/day-wakeup", "day wakeup", "morning briefing", "catch me up on yesterday", "what did I work on yesterday", "review my day", "process yesterday''s sessions", or otherwise signals they want to curate prior work across projects at the start of a new day. Runs `clast-plumbing snapshot` to ensure fresh data, then walks through each uncurated session from yesterday and proposes a draft entry the user can accept, edit, or skip. Prompts for promotion of decisions, common-issues, and workflows per accepted session. This is the once-per-day curation flow; for per-project briefings use /wakeup; for mid-session pivots use session-brief.'
2
+ name: wake
3
+ description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/wake", "wake", "morning briefing", "catch me up on yesterday", "what did I work on yesterday", "review my day", "process yesterday''s sessions", or otherwise signals they want to curate prior work across projects at the start of a new day. Runs `clast-plumbing snapshot` to ensure fresh data, then walks through each uncurated session from yesterday and proposes a draft entry the user can accept, edit, or skip. Prompts for promotion of decisions, common-issues, and workflows per accepted session. This is the once-per-day curation flow; for per-project briefings use /brief; for mid-session pivots use session-brief.'
4
4
  ---
5
5
 
6
- # Day Wakeup
6
+ # Wake
7
7
 
8
8
  Process yesterday's Claude Code sessions across all projects. For each session, generate a draft journal entry and walk the user through accepting/editing/skipping it.
9
9
 
10
10
  ## Why this exists
11
11
 
12
- Curation at end-of-session has high friction (the user wants to stop, not summarize). Curation at start-of-next-day has lower friction (fresh eyes, easier to decide what's worth keeping). `/day-wakeup` is that start-of-next-day flow.
12
+ Curation at end-of-session has high friction (the user wants to stop, not summarize). Curation at start-of-next-day has lower friction (fresh eyes, easier to decide what's worth keeping). `/wake` is that start-of-next-day flow.
13
13
 
14
- The transcripts themselves are captured automatically by the SessionStart hook + cron — the user never has to remember to log anything. What `/day-wakeup` does is **curate the captured transcripts into durable entries the user controls**, and prompt for promotion of decisions, common-issues, and workflows along the way.
14
+ The transcripts themselves are captured automatically by the SessionStart hook + cron — the user never has to remember to log anything. What `/wake` does is **curate the captured transcripts into durable entries the user controls**, and prompt for promotion of decisions, common-issues, and workflows along the way.
15
15
 
16
16
  ## Step 0: Resolve the clast-plumbing binary
17
17
 
@@ -58,6 +58,27 @@ $CLAST_BIN --json sessions --since -30d
58
58
 
59
59
  Filter to sessions with `curated: false` or `stale: true` (stale sessions were curated but their transcript was updated since). If none remain, print "Nothing to curate — all sessions are curated or dismissed." and stop.
60
60
 
61
+ ### Step 2a: Auto-dismiss no-op sessions (deterministic, no LLM)
62
+
63
+ Each session row carries a deterministic `substantive` flag (computed by
64
+ `clast-plumbing` from the transcript, cached at snapshot time). A session is
65
+ `substantive: false` when **Claude never replied** — an empty session, one that
66
+ contains only slash commands like `/clear`, `/model`, `/config`, or one abandoned
67
+ before any response. These are worthless to curate. (A session driven by a *custom*
68
+ slash command still has assistant replies, so it stays `substantive: true`.)
69
+
70
+ Before generating any drafts, auto-dismiss every uncurated session with
71
+ `substantive == false` (skip this if the user set `CLAST_WAKE_AUTODISMISS_NOOP=0`):
72
+
73
+ ```bash
74
+ $CLAST_BIN sessions dismiss <session-id> --reason "auto: no substantive content (empty / slash-command-only)"
75
+ ```
76
+
77
+ Do **not** call the LLM for these. Dismissal is reversible via `clast undismiss <id>`.
78
+ Remove them from the working set, keep a count, and report it in the final summary
79
+ (e.g. "Auto-dismissed 5 no-op session(s)."). If nothing substantive remains after this,
80
+ print "Nothing to curate — all remaining sessions were empty or slash-command-only." and stop.
81
+
61
82
  ### Triage when multiple days have uncurated sessions
62
83
 
63
84
  If uncurated sessions span more than one day (e.g., after a weekend or break), present a triage step before processing. Show a per-day breakdown:
@@ -113,15 +134,16 @@ For each session in the list:
113
134
  - **Accept** (any combination of accept-flavored options): pipe the draft to `clast-plumbing entries write` via stdin.
114
135
  - **Edit**: prompt the user for what to change, regenerate the draft incorporating their feedback, loop.
115
136
  - **Skip**: do not write.
116
- - **Stop here**: end the entire `/day-wakeup` flow, leaving remaining sessions uncurated (user can resume tomorrow).
137
+ - **Stop here**: end the entire `/wake` flow, leaving remaining sessions uncurated (user can resume tomorrow).
117
138
 
118
139
  ## Step 4: Final summary
119
140
 
120
141
  After all sessions are processed (or user stopped early), print a summary:
121
142
 
122
143
  ```
123
- Day wakeup complete.
144
+ Wake complete.
124
145
  Curated: 3 sessions across 2 projects.
146
+ Auto-dismissed (no-op): 5 sessions.
125
147
  Skipped: 1 session.
126
148
  Remaining uncurated: 0.
127
149
 
@@ -130,15 +152,15 @@ Promoted:
130
152
  Common-issues: 0
131
153
  Workflows: 1
132
154
 
133
- Run `/wakeup <project>` to start working on a specific project today.
155
+ Run `/brief <project>` to start working on a specific project today.
134
156
  ```
135
157
 
136
158
  ## Draft generation prompt
137
159
 
138
- The prompt templates live in `lib/clast/prompts/` so they are shared with the porcelain `clast wake` subcommand:
160
+ The prompt templates are installed alongside the plugin under `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/`:
139
161
 
140
- - **System prompt:** `lib/clast/prompts/day-wakeup-draft-system.md`
141
- - **User prompt template:** `lib/clast/prompts/day-wakeup-draft-user.md` (uses `{{placeholder}}` syntax)
162
+ - **System prompt:** `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/wake-draft-system.md`
163
+ - **User prompt template:** `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/wake-draft-user.md` (uses `{{placeholder}}` syntax)
142
164
 
143
165
  When generating each draft, read those files and substitute the placeholders with session data. The user prompt template uses these placeholders: `{{project}}`, `{{branch}}`, `{{start}}`, `{{end}}`, `{{msg_count}}`, `{{first_turns}}`, `{{last_turns}}`, `{{breadcrumbs}}`.
144
166
 
@@ -156,7 +178,7 @@ After showing the draft, present:
156
178
  - `Accept + promote workflow` — also write a workflow file
157
179
  - `Edit` — user wants to revise; will prompt for changes
158
180
  - `Skip` — do not write this entry
159
- - `Stop here` — end /day-wakeup entirely, leave remaining sessions uncurated
181
+ - `Stop here` — end /wake entirely, leave remaining sessions uncurated
160
182
 
161
183
  If `Skip` and `Stop here` are both selected, treat as `Stop here`. If `Edit` is selected alongside any accept option, treat as `Edit` first (the user wants to revise before accepting).
162
184
 
@@ -199,6 +221,7 @@ For promoted items (decisions, common-issues, workflows): currently these are tr
199
221
  ## Edge cases
200
222
 
201
223
  - **No uncurated sessions**: print "Nothing to curate — all sessions are curated or dismissed." and stop.
224
+ - **All uncurated sessions are no-ops**: after Step 2a auto-dismisses them, nothing substantive remains — print "Nothing to curate — all remaining sessions were empty or slash-command-only." and stop.
202
225
  - **Multi-day backlog**: present the triage step (Step 2) so the user can choose scope before processing.
203
226
  - **`clast-plumbing snapshot` fails**: warn the user, then attempt to proceed with whatever's already in the manifest.
204
227
  - **`clast-plumbing show` fails for a specific session**: skip that session, note it in the final summary, continue with the rest.