@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.
- 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 +236 -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
|
@@ -69,49 +69,23 @@ clast_cmd_entries() {
|
|
|
69
69
|
|
|
70
70
|
# _clast_entries_read_frontmatter <path>
|
|
71
71
|
# Emit raw frontmatter lines (between the first two `---` fences) to stdout.
|
|
72
|
+
# Thin alias over the shared primitive in clast-lib.bash.
|
|
72
73
|
_clast_entries_read_frontmatter() {
|
|
73
|
-
|
|
74
|
-
awk '
|
|
75
|
-
BEGIN { in_fm = 0; seen = 0 }
|
|
76
|
-
/^---[[:space:]]*$/ {
|
|
77
|
-
if (!seen) { in_fm = 1; seen = 1; next }
|
|
78
|
-
if (in_fm) { exit }
|
|
79
|
-
}
|
|
80
|
-
in_fm { print }
|
|
81
|
-
' "$path"
|
|
74
|
+
clast_read_frontmatter "$1"
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
# _clast_entries_extract_title <path>
|
|
85
78
|
# Look for `# Session: <title>` as the first non-blank body line.
|
|
79
|
+
# Thin alias over the shared primitive in clast-lib.bash.
|
|
86
80
|
_clast_entries_extract_title() {
|
|
87
|
-
|
|
88
|
-
BEGIN { in_fm = 0; seen = 0; past = 0 }
|
|
89
|
-
/^---[[:space:]]*$/ {
|
|
90
|
-
if (!seen) { in_fm = 1; seen = 1; next }
|
|
91
|
-
if (in_fm) { in_fm = 0; past = 1; next }
|
|
92
|
-
}
|
|
93
|
-
in_fm { next }
|
|
94
|
-
past {
|
|
95
|
-
if ($0 ~ /^[[:space:]]*$/) next
|
|
96
|
-
if (sub(/^# Session: /, "")) { print; exit }
|
|
97
|
-
exit
|
|
98
|
-
}
|
|
99
|
-
' "$1"
|
|
81
|
+
clast_entry_title "$1"
|
|
100
82
|
}
|
|
101
83
|
|
|
102
84
|
# _clast_entries_unquote <string>
|
|
103
85
|
# Strip surrounding double quotes and unescape \", \\, \n.
|
|
86
|
+
# Thin alias over the shared primitive in clast-lib.bash.
|
|
104
87
|
_clast_entries_unquote() {
|
|
105
|
-
|
|
106
|
-
if [[ "${v:0:1}" == '"' && "${v: -1}" == '"' && ${#v} -ge 2 ]]; then
|
|
107
|
-
v="${v:1:${#v}-2}"
|
|
108
|
-
# Process escapes in order: \\ → placeholder, \" → ", \n → LF, placeholder → \
|
|
109
|
-
v="${v//\\\\/$'\x01'}"
|
|
110
|
-
v="${v//\\\"/\"}"
|
|
111
|
-
v="${v//\\n/$'\n'}"
|
|
112
|
-
v="${v//$'\x01'/\\}"
|
|
113
|
-
fi
|
|
114
|
-
printf '%s' "$v"
|
|
88
|
+
clast_yaml_unquote "$1"
|
|
115
89
|
}
|
|
116
90
|
|
|
117
91
|
# ---------------------------------------------------------------------------
|
|
@@ -290,7 +264,7 @@ _clast_entries_list_consider() {
|
|
|
290
264
|
fi
|
|
291
265
|
|
|
292
266
|
# Parse frontmatter.
|
|
293
|
-
local fm_date="" fm_time="" fm_day_bucket="" fm_project=""
|
|
267
|
+
local fm_date="" fm_time="" fm_day_bucket="" fm_project="" fm_label=""
|
|
294
268
|
local fm_session_id="" fm_session_slug="" fm_branch=""
|
|
295
269
|
local fm_tags_raw=""
|
|
296
270
|
local line key val
|
|
@@ -307,6 +281,11 @@ _clast_entries_list_consider() {
|
|
|
307
281
|
time) fm_time="$(_clast_entries_unquote "$val")" ;;
|
|
308
282
|
day_bucket) fm_day_bucket="$(_clast_entries_unquote "$val")" ;;
|
|
309
283
|
project) fm_project="$(_clast_entries_unquote "$val")" ;;
|
|
284
|
+
label)
|
|
285
|
+
if [[ "$val" != "null" ]]; then
|
|
286
|
+
fm_label="$(_clast_entries_unquote "$val")"
|
|
287
|
+
fi
|
|
288
|
+
;;
|
|
310
289
|
session_id) fm_session_id="$(_clast_entries_unquote "$val")" ;;
|
|
311
290
|
session_slug) fm_session_slug="$(_clast_entries_unquote "$val")" ;;
|
|
312
291
|
branch)
|
|
@@ -372,6 +351,7 @@ _clast_entries_list_consider() {
|
|
|
372
351
|
--arg time "$fm_time" \
|
|
373
352
|
--arg day_bucket "$bucket" \
|
|
374
353
|
--arg project "$fm_project" \
|
|
354
|
+
--arg label "$fm_label" \
|
|
375
355
|
--arg session_id "$fm_session_id" \
|
|
376
356
|
--arg session_slug "$fm_session_slug" \
|
|
377
357
|
--arg branch "$fm_branch" \
|
|
@@ -383,6 +363,7 @@ _clast_entries_list_consider() {
|
|
|
383
363
|
time: $time,
|
|
384
364
|
day_bucket: $day_bucket,
|
|
385
365
|
project: $project,
|
|
366
|
+
label: (if $label == "" then null else $label end),
|
|
386
367
|
session_id: $session_id,
|
|
387
368
|
session_slug: $session_slug,
|
|
388
369
|
branch: (if $branch == "" then null else $branch end),
|
|
@@ -512,9 +493,16 @@ _clast_entries_write() {
|
|
|
512
493
|
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
513
494
|
[[ -z "$trimmed" ]] && continue
|
|
514
495
|
trimmed="${trimmed,,}"
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
496
|
+
# Normalize to the tag charset rather than rejecting. LLM-suggested tags
|
|
497
|
+
# routinely carry dots or other separators (e.g. "php-8.5"), and failing
|
|
498
|
+
# the whole write would discard a curated entry over a cosmetic tag. Map
|
|
499
|
+
# runs of disallowed characters to a single hyphen, trim edge hyphens,
|
|
500
|
+
# and cap at 32 chars — same shape as _clast_wake_slugify. A tag that
|
|
501
|
+
# normalizes to nothing (all punctuation) is dropped.
|
|
502
|
+
trimmed="$(printf '%s' "$trimmed" | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//')"
|
|
503
|
+
trimmed="${trimmed:0:32}"
|
|
504
|
+
trimmed="${trimmed%-}"
|
|
505
|
+
[[ -z "$trimmed" ]] && continue
|
|
518
506
|
tags+=("$trimmed")
|
|
519
507
|
done
|
|
520
508
|
fi
|
|
@@ -534,20 +522,17 @@ _clast_entries_write() {
|
|
|
534
522
|
local seg
|
|
535
523
|
seg="$(awk -F/ 'NR==1{print $3}' <<<"$snapshot_rel")"
|
|
536
524
|
|
|
537
|
-
# Resolve
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
else
|
|
549
|
-
project_slug="$resolved_slug"
|
|
550
|
-
fi
|
|
525
|
+
# Resolve the *specific* registry line for this session's directory (by
|
|
526
|
+
# path), not the first line that shares the slug. A slug may span several
|
|
527
|
+
# directories (clones/worktrees), each with its own path and label;
|
|
528
|
+
# slug-first-match would stamp every entry with the first line's path.
|
|
529
|
+
local project_slug="" project_path="" project_remote="" project_label=""
|
|
530
|
+
local reg_line=""
|
|
531
|
+
if reg_line="$(clast_registry_line_for_path "$seg" 2>/dev/null)" && [[ -n "$reg_line" ]]; then
|
|
532
|
+
project_slug="$(jq -r '.slug // empty' <<<"$reg_line")"
|
|
533
|
+
project_path="$(jq -r '.path // empty' <<<"$reg_line")"
|
|
534
|
+
project_remote="$(jq -r '.remote // empty' <<<"$reg_line")"
|
|
535
|
+
project_label="$(jq -r '.label // empty' <<<"$reg_line")"
|
|
551
536
|
else
|
|
552
537
|
project_slug="$seg"
|
|
553
538
|
local -a decoded=()
|
|
@@ -619,6 +604,11 @@ _clast_entries_write() {
|
|
|
619
604
|
else
|
|
620
605
|
fm+="project_path: null"$'\n'
|
|
621
606
|
fi
|
|
607
|
+
if [[ -n "$project_label" ]]; then
|
|
608
|
+
fm+="label: $(_clast_entries_yaml_string "$project_label")"$'\n'
|
|
609
|
+
else
|
|
610
|
+
fm+="label: null"$'\n'
|
|
611
|
+
fi
|
|
622
612
|
if [[ -n "$project_remote" ]]; then
|
|
623
613
|
fm+="project_remote: $(_clast_entries_yaml_string "$project_remote")"$'\n'
|
|
624
614
|
else
|
|
@@ -169,13 +169,18 @@ clast_cmd_projects() {
|
|
|
169
169
|
done
|
|
170
170
|
|
|
171
171
|
# Build per-segment rows as JSON, applying registry + --unregistered.
|
|
172
|
+
# Resolve each segment to its OWN registry line (not slug-first-match) so
|
|
173
|
+
# that a project spanning multiple checkouts under one shared slug reports
|
|
174
|
+
# each segment's own path/remote, instead of collapsing every row onto the
|
|
175
|
+
# first checkout's path.
|
|
172
176
|
local -a rows=()
|
|
173
|
-
local slug path remote registered row decode_rc decoded
|
|
177
|
+
local slug path remote registered row decode_rc decoded line
|
|
174
178
|
for seg in "${!seg_session_count[@]}"; do
|
|
175
|
-
if
|
|
179
|
+
if line="$(clast_registry_line_for_path "$seg" 2>/dev/null)" && [[ -n "$line" ]]; then
|
|
176
180
|
registered=true
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
slug="$(jq -r '.slug // empty' <<<"$line")"
|
|
182
|
+
path="$(jq -r '.path // empty' <<<"$line")"
|
|
183
|
+
remote="$(jq -r '.remote // empty' <<<"$line")"
|
|
179
184
|
else
|
|
180
185
|
registered=false
|
|
181
186
|
slug=""
|
|
@@ -298,18 +303,3 @@ _clast_projects_window_filter() {
|
|
|
298
303
|
printf '%s' "$joined"
|
|
299
304
|
}
|
|
300
305
|
|
|
301
|
-
# _clast_projects_path_for_slug <slug>
|
|
302
|
-
# First registry path for <slug>, or empty.
|
|
303
|
-
_clast_projects_path_for_slug() {
|
|
304
|
-
local slug="$1"
|
|
305
|
-
clast_registry_list_json \
|
|
306
|
-
| jq -r --arg s "$slug" 'map(select(.slug == $s)) | .[0].path // empty'
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
# _clast_projects_remote_for_slug <slug>
|
|
310
|
-
# First non-empty registry remote for <slug>, or empty.
|
|
311
|
-
_clast_projects_remote_for_slug() {
|
|
312
|
-
local slug="$1"
|
|
313
|
-
clast_registry_list_json \
|
|
314
|
-
| jq -r --arg s "$slug" 'map(select(.slug == $s and (.remote // "") != "")) | .[0].remote // empty'
|
|
315
|
-
}
|
|
@@ -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
|
+
}
|