@indigoai-us/hq-cli 5.316.0 → 5.317.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.317.0] — 2026-10-02
6
+ - Add native `hq core session-title`, `session-title-config`, `session-journal` and `session-project` on the fast route behind the default-off `core-native-hook-helpers` flag.
7
+ - Build required npm self-updates outside the active prefix, then activate only after the staged package is ready, so an interrupted download or unpack leaves the installed CLI runnable.
8
+
5
9
  ## [5.316.0] — 2026-10-02
6
10
  - Add a default-off autopilot notice for registered lead headlines that remain stale for more than 55 minutes, using `headline_at` or the timestamp prefix in `now`.
7
11
  - Add a flag-gated per-job option to complete successful scheduled bot runs without an owner DM; failures still report normally.
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env bash
2
+ # session-journal.sh — minimal helper for session-level journal entries.
3
+ # Spec: core/knowledge/public/hq-core/session-journal-spec.md
4
+ #
5
+ # Usage:
6
+ # session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
7
+ # session-journal.sh list [--date YYYY-MM-DD]
8
+ # session-journal.sh read <NNN> [--date YYYY-MM-DD]
9
+ # session-journal.sh index-path [--date YYYY-MM-DD] # prints the INDEX.md path
10
+ # session-journal.sh dir-path [--date YYYY-MM-DD] # prints the journal dir
11
+ # session-journal.sh tool-counter increment [--session KEY] # used by PostToolUse hook
12
+ # session-journal.sh tool-counter read [--session KEY] # current count
13
+ # session-journal.sh tool-counter reset [--session KEY]
14
+ #
15
+ # The tool counter is per-session: state lives at
16
+ # workspace/threads/journal/<date>/.tool-count-<session-key>. The key comes from
17
+ # --session, else $HQ_JOURNAL_SESSION, else $HQ_HOOK_SESSION_ID, else
18
+ # "unscoped". A date-global counter let concurrent Claude and Codex tasks
19
+ # trigger and reset each other's journal reminders.
20
+ #
21
+ # All commands fail-soft: warn to stderr, exit 0 — never block the caller.
22
+
23
+ set -uo pipefail
24
+
25
+ HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
26
+
27
+ today() { date -u +%Y-%m-%d; }
28
+ now_iso_z() { date -u +%Y-%m-%dT%H:%M:%SZ; }
29
+ now_hhmm_z() { date -u +%H:%MZ; }
30
+
31
+ journal_dir_for() {
32
+ local d="${1:-$(today)}"
33
+ printf '%s/workspace/threads/journal/%s' "$HQ_ROOT" "$d"
34
+ }
35
+
36
+ warn() { echo "session-journal: $*" >&2; }
37
+
38
+ slugify() {
39
+ # Lowercase, replace non-alnum with hyphen, collapse hyphens, trim.
40
+ printf '%s' "$1" \
41
+ | tr '[:upper:]' '[:lower:]' \
42
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' \
43
+ | cut -c1-40
44
+ }
45
+
46
+ # Counters are per-session. A date-global counter lets concurrent Claude and
47
+ # Codex tasks trip (and reset) each other's journal reminders.
48
+ counter_key() {
49
+ local raw="${1:-}"
50
+ raw="$(printf '%s' "$raw" | tr -c 'A-Za-z0-9._-' '_')"
51
+ raw="${raw#_}"; raw="${raw%_}"
52
+ [ -z "$raw" ] && raw="unscoped"
53
+ [ "${#raw}" -gt 96 ] && raw="${raw:0:96}"
54
+ printf '%s' "$raw"
55
+ }
56
+
57
+ resolve_session_key() {
58
+ counter_key "${1:-${HQ_JOURNAL_SESSION:-${HQ_HOOK_SESSION_ID:-}}}"
59
+ }
60
+
61
+ # Read-modify-write on the counter must not interleave across concurrent tasks.
62
+ with_counter_lock() {
63
+ local lockdir="$1"; shift
64
+ local i=0
65
+ while ! mkdir "$lockdir" 2>/dev/null; do
66
+ i=$((i + 1))
67
+ [ "$i" -ge 50 ] && break
68
+ sleep 0.02 2>/dev/null || sleep 1
69
+ done
70
+ "$@"
71
+ rmdir "$lockdir" 2>/dev/null || true
72
+ }
73
+
74
+ counter_read_raw() {
75
+ local counter="$1" n=0
76
+ [ -f "$counter" ] && n=$(tr -dc '0-9' < "$counter" 2>/dev/null)
77
+ [ -z "$n" ] && n=0
78
+ printf '%d' "$n"
79
+ }
80
+
81
+ next_seq() {
82
+ local dir="$1"
83
+ # Highest NNN in the dir, +1, zero-padded to 3.
84
+ local hi
85
+ hi=$(ls "$dir" 2>/dev/null \
86
+ | grep -E '^[0-9]{3}-' \
87
+ | sed -E 's/^([0-9]+)-.*/\1/' \
88
+ | sort -n \
89
+ | tail -1)
90
+ if [ -z "$hi" ]; then printf '001'; else printf '%03d' $((10#$hi + 1)); fi
91
+ }
92
+
93
+ acquire_journal_write_lock() {
94
+ local lock="$1/.session-journal-write.lock" owner=0 i=0
95
+ while [ "$i" -lt 250 ]; do
96
+ if mkdir "$lock" 2>/dev/null; then
97
+ printf '%s\n' "$$" > "$lock/pid" || { rm -rf "$lock"; return 1; }
98
+ JOURNAL_WRITE_LOCK="$lock"
99
+ return 0
100
+ fi
101
+ owner="$(cat "$lock/pid" 2>/dev/null || true)"
102
+ if [[ "$owner" =~ ^[0-9]+$ ]] && [ "$owner" -gt 0 ] && ! kill -0 "$owner" 2>/dev/null; then
103
+ if [ "$(cat "$lock/pid" 2>/dev/null || true)" = "$owner" ]; then rm -rf "$lock" 2>/dev/null || true; fi
104
+ fi
105
+ i=$((i + 1))
106
+ sleep 0.02 2>/dev/null || sleep 1
107
+ done
108
+ return 1
109
+ }
110
+
111
+ release_journal_write_lock() {
112
+ [ -n "${JOURNAL_WRITE_LOCK:-}" ] || return 0
113
+ if [ "$(cat "$JOURNAL_WRITE_LOCK/pid" 2>/dev/null || true)" = "$$" ]; then
114
+ rm -rf "$JOURNAL_WRITE_LOCK" 2>/dev/null || true
115
+ fi
116
+ }
117
+
118
+ cmd="${1:-}"; shift || true
119
+
120
+ case "$cmd" in
121
+ write)
122
+ title="${1:-}"; shift || true
123
+ files_csv=""
124
+ body_file=""
125
+ session_arg=""
126
+ while [ $# -gt 0 ]; do
127
+ case "$1" in
128
+ --files) files_csv="${2:-}"; shift 2 || break ;;
129
+ --body-file) body_file="${2:-}"; shift 2 || break ;;
130
+ --session) session_arg="${2:-}"; shift 2 || break ;;
131
+ *) shift ;;
132
+ esac
133
+ done
134
+
135
+ [ -z "$title" ] && { warn "write: title required"; exit 0; }
136
+
137
+ d="$(today)"
138
+ dir="$(journal_dir_for "$d")"
139
+ mkdir -p "$dir" 2>/dev/null || { warn "write: mkdir failed: $dir"; exit 0; }
140
+
141
+ slug="$(slugify "$title")"
142
+ [ -z "$slug" ] && slug="entry"
143
+ JOURNAL_WRITE_LOCK=""
144
+ acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
145
+ trap release_journal_write_lock EXIT
146
+ seq=""
147
+ out=""
148
+ created=false
149
+ attempt=0
150
+ while [ "$attempt" -lt 10000 ]; do
151
+ attempt=$((attempt + 1))
152
+ seq="$(next_seq "$dir")"
153
+ out="$dir/${seq}-${slug}.md"
154
+ write_status=0
155
+ (
156
+ set -o noclobber
157
+ if ! exec 3>"$out" 2>/dev/null; then
158
+ if [ -e "$out" ] || [ -L "$out" ]; then exit 17; fi
159
+ exit 16
160
+ fi
161
+ {
162
+ printf -- '---\n'
163
+ printf 'ts: %s\n' "$(now_iso_z)"
164
+ printf 'title: %s\n' "$title"
165
+ printf 'slug: %s\n' "$slug"
166
+ if [ -n "$files_csv" ]; then
167
+ printf 'files:\n'
168
+ old_ifs="$IFS"
169
+ IFS=','
170
+ set -- $files_csv
171
+ IFS="$old_ifs"
172
+ for f in "$@"; do
173
+ [ -n "$f" ] && printf ' - %s\n' "$f"
174
+ done
175
+ fi
176
+ printf 'status: closed\n'
177
+ printf -- '---\n\n'
178
+ printf '# %s — %s\n\n' "$seq" "$title"
179
+ if [ -n "$body_file" ] && [ -f "$body_file" ]; then
180
+ cat "$body_file"
181
+ else
182
+ printf '## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n'
183
+ fi
184
+ } >&3 || exit 18
185
+ )
186
+ write_status=$?
187
+ if [ "$write_status" -eq 0 ]; then
188
+ created=true
189
+ break
190
+ elif [ "$write_status" -eq 17 ]; then
191
+ continue
192
+ else
193
+ warn "write: failed to write $out"
194
+ exit 0
195
+ fi
196
+ done
197
+ [ "$created" = true ] || { warn "write: failed to write $out"; exit 0; }
198
+
199
+ # Update INDEX.md
200
+ idx="$dir/INDEX.md"
201
+ if [ ! -f "$idx" ]; then
202
+ printf '# Session journal — %s\n\n' "$d" > "$idx"
203
+ fi
204
+ printf -- '- `%s` %s — %s\n' "$seq" "$(now_hhmm_z)" "$title" >> "$idx" \
205
+ || warn "write: INDEX update failed"
206
+
207
+ # Reset the tool counter for THIS session only — journaling one task's
208
+ # milestone must not clear a concurrent task's progress toward its own.
209
+ "$0" tool-counter reset --session "$(resolve_session_key "$session_arg")" >/dev/null 2>&1 || true
210
+
211
+ printf '%s\n' "$out"
212
+ ;;
213
+
214
+ list)
215
+ date_arg="$(today)"
216
+ while [ $# -gt 0 ]; do
217
+ case "$1" in --date) date_arg="${2:-$(today)}"; shift 2 ;; *) shift ;; esac
218
+ done
219
+ dir="$(journal_dir_for "$date_arg")"
220
+ idx="$dir/INDEX.md"
221
+ if [ -f "$idx" ]; then
222
+ cat "$idx"
223
+ else
224
+ echo "(no journal for $date_arg)"
225
+ fi
226
+ ;;
227
+
228
+ read)
229
+ seq="${1:-}"; shift || true
230
+ date_arg="$(today)"
231
+ while [ $# -gt 0 ]; do
232
+ case "$1" in --date) date_arg="${2:-$(today)}"; shift 2 ;; *) shift ;; esac
233
+ done
234
+ [ -z "$seq" ] && { warn "read: NNN required"; exit 0; }
235
+ [[ "$seq" =~ ^[0-9]{1,18}$ ]] || { warn "read: NNN required"; exit 0; }
236
+ seq=$(printf '%03d' $((10#$seq)) 2>/dev/null) || seq="$seq"
237
+ dir="$(journal_dir_for "$date_arg")"
238
+ match=$(ls "$dir" 2>/dev/null | grep -E "^${seq}-" | head -1)
239
+ if [ -n "$match" ]; then cat "$dir/$match"; else warn "read: no entry $seq for $date_arg"; fi
240
+ ;;
241
+
242
+ index-path)
243
+ date_arg="$(today)"
244
+ while [ $# -gt 0 ]; do
245
+ case "$1" in --date) date_arg="${2:-$(today)}"; shift 2 ;; *) shift ;; esac
246
+ done
247
+ printf '%s/INDEX.md\n' "$(journal_dir_for "$date_arg")"
248
+ ;;
249
+
250
+ dir-path)
251
+ date_arg="$(today)"
252
+ while [ $# -gt 0 ]; do
253
+ case "$1" in --date) date_arg="${2:-$(today)}"; shift 2 ;; *) shift ;; esac
254
+ done
255
+ journal_dir_for "$date_arg"
256
+ ;;
257
+
258
+ tool-counter)
259
+ sub="${1:-}"; shift || true
260
+ session_arg=""
261
+ while [ $# -gt 0 ]; do
262
+ case "$1" in --session) session_arg="${2:-}"; shift 2 || break ;; *) shift ;; esac
263
+ done
264
+ session_key="$(resolve_session_key "$session_arg")"
265
+ dir="$(journal_dir_for "$(today)")"
266
+ mkdir -p "$dir" 2>/dev/null || true
267
+ counter="$dir/.tool-count-$session_key"
268
+ lockdir="$counter.lock"
269
+ case "$sub" in
270
+ increment)
271
+ do_increment() {
272
+ local n
273
+ n=$(counter_read_raw "$counter")
274
+ echo $((n + 1)) > "$counter" 2>/dev/null || true
275
+ }
276
+ with_counter_lock "$lockdir" do_increment
277
+ ;;
278
+ read)
279
+ printf '%d\n' "$(counter_read_raw "$counter")"
280
+ ;;
281
+ reset)
282
+ do_reset() { echo 0 > "$counter" 2>/dev/null || true; }
283
+ with_counter_lock "$lockdir" do_reset
284
+ ;;
285
+ *) warn "tool-counter: subcommand required (increment|read|reset)" ;;
286
+ esac
287
+ ;;
288
+
289
+ *)
290
+ cat >&2 <<'USAGE'
291
+ session-journal.sh: usage:
292
+ session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
293
+ session-journal.sh list [--date YYYY-MM-DD]
294
+ session-journal.sh read <NNN> [--date YYYY-MM-DD]
295
+ session-journal.sh index-path [--date YYYY-MM-DD]
296
+ session-journal.sh dir-path [--date YYYY-MM-DD]
297
+ session-journal.sh tool-counter (increment|read|reset) [--session KEY]
298
+ USAGE
299
+ exit 1
300
+ ;;
301
+ esac