@indigoai-us/hq-cli 5.103.12 → 5.103.13

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,25 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.13] — 2026-08-20
6
+
7
+ ### Fixed
8
+
9
+ - The bundled checkpoint stop-gate now *enforces* the user-facing reply
10
+ instead of only asking for it: a satisfying checkpoint on a work turn whose
11
+ genuine reply is under `HQ_CHECKPOINT_REPLY_MIN` non-whitespace characters
12
+ (default 80) blocks once with a dedicated reply demand, stamped per
13
+ checkpoint tool id and counted against the shared 3-consecutive-blocks loop
14
+ guard, so it can never strand a session. The reply window accepts text on
15
+ either side of the checkpoint and crosses the gate's own block feedback, so
16
+ an agent that already replied is never told to repeat itself — composing
17
+ with the reply-aware block variants — while mid-turn notes written before
18
+ the last work tool do not count. Flag exemptions are parsed robustly:
19
+ `--gate-probe` / `--idle` inside a quoted `--summary` value are data, not
20
+ flags, and a compound command ending in a checkpoint keeps owing the reply.
21
+ The `--idle` path of `hq core checkpoint` prints a conditional reminder
22
+ matching the gate's idle-only exemption.
23
+
5
24
  ## [5.103.12] — 2026-08-20
6
25
 
7
26
  ## [5.103.11] — 2026-08-20
@@ -232,65 +232,105 @@ set -uo pipefail
232
232
  end;
233
233
  def checkpoint_command:
234
234
  test("(^|[;&|(\\s])(command\\s+)?([A-Za-z_][A-Za-z0-9_]*=[^\\s]*\\s+)*hq\\s+core\\s+checkpoint(\\s|$)");
235
+ def strip_quoted:
236
+ # Remove quoted spans so flag text inside a --summary value (e.g.
237
+ # --summary "investigated --gate-probe behavior") is never mistaken for
238
+ # an active flag. Double-quoted spans honor backslash escapes. The
239
+ # u0027 escape is a single-quote character, written that way because
240
+ # this whole program lives inside a single-quoted shell string.
241
+ gsub("\"(\\\\.|[^\"\\\\])*\""; "")
242
+ | gsub("\u0027[^\u0027]*\u0027"; "");
243
+ def checkpoint_flag($flag):
244
+ strip_quoted | test("(^|\\s)" + $flag + "(\\s|$)");
245
+ def pure_checkpoint:
246
+ # The command is ONLY a checkpoint invocation (optional env-assignment
247
+ # prefix, no other command joined by a separator). A compound call like
248
+ # `git status; hq core checkpoint --idle` did other work in the same
249
+ # tool call, so its exemptions must not apply.
250
+ strip_quoted
251
+ | test("^\\s*(command\\s+)?([A-Za-z_][A-Za-z0-9_]*=[^\\s]*\\s+)*hq\\s+core\\s+checkpoint([\\s][^;&|\n]*)?$");
252
+ def gate_feedback_user:
253
+ # This gate surfaces its own Stop feedback as a synthetic user row. That
254
+ # row is a turn boundary for the checkpoint requirement, but NOT for
255
+ # deciding whether the USER has been answered: a reply the agent wrote
256
+ # before the block is already on screen and must never be demanded again
257
+ # (double-messaging).
258
+ .type == "user"
259
+ and ((.message.content? | type) == "string")
260
+ and (.message.content | startswith("Stop hook feedback:"));
235
261
  [split("\n")[] | select(length > 0) | {line: ., entry: (fromjson)}] as $rows
236
262
  | [range(0; $rows | length) | select($rows[.].entry | real_user)] as $user_indexes
237
263
  | if ($user_indexes | length) == 0 then error("no real user entry") else
238
264
  $user_indexes[-1] as $user_index
239
- | $rows[($user_index + 1):] as $turn
265
+ | ([$user_indexes[] | select(($rows[.].entry | gate_feedback_user) | not)]
266
+ | if length > 0 then .[-1] else $user_indexes[0] end) as $genuine_index
240
267
  | [
241
- range(0; $turn | length) as $ti
242
- | $turn[$ti].entry
268
+ range($genuine_index + 1; $rows | length) as $ri
269
+ | $rows[$ri].entry
243
270
  | if .type == "assistant" then
244
271
  .message.content?
245
272
  | if type == "array" then .[]? else empty end
246
- | select(.type == "tool_use")
247
- | {
248
- i: $ti,
249
- runtime: "claude",
250
- id: (.id? // ""),
251
- name: (.name? // ""),
252
- command: (.input.command? // "")
253
- }
273
+ | if .type == "tool_use" then
274
+ {
275
+ row: $ri,
276
+ kind: "tool",
277
+ runtime: "claude",
278
+ id: (.id? // ""),
279
+ name: (.name? // ""),
280
+ command: (.input.command? // "")
281
+ }
282
+ elif .type == "text" then
283
+ # Whitespace-stripped so blank filler can never pass for a
284
+ # reply.
285
+ {row: $ri, kind: "text",
286
+ chars: ((.text? // "") | gsub("\\s+"; "") | length)}
287
+ else empty
288
+ end
254
289
  elif (
255
290
  .type == "response_item"
256
291
  and (.payload.type? == "custom_tool_call" or .payload.type? == "function_call")
257
292
  ) then
258
293
  .payload
259
294
  | {
260
- i: $ti,
295
+ row: $ri,
296
+ kind: "tool",
261
297
  runtime: "codex",
262
298
  id: (.call_id? // .id? // ""),
263
299
  name: (.name? // ""),
264
300
  command: codex_command
265
301
  }
302
+ elif (
303
+ .type == "response_item"
304
+ and .payload.type? == "message"
305
+ and .payload.role? == "assistant"
306
+ and ((.payload.content? | type) == "array")
307
+ ) then
308
+ {row: $ri, kind: "text",
309
+ chars: ([.payload.content[]? | (.text? // "")] | join("")
310
+ | gsub("\\s+"; "") | length)}
266
311
  else empty
267
312
  end
268
- ] as $tools
313
+ ] as $events
314
+ | [$events[] | select(.row > $user_index)] as $seg_events
315
+ | [$seg_events[] | select(.kind == "tool")] as $tools
269
316
  | (if ($tools | length) > 0 then $tools[-1] else {} end) as $last_tool
270
- # A user-visible assistant reply strictly AFTER the turn-s last tool call
271
- # means the user has already been answered, so the block reason must
272
- # forbid a restatement instead of demanding one. Text earlier in the turn
273
- # (status notes between tool calls) does not count as the reply.
274
- | (if ($tools | length) > 0 then $tools[-1].i else -1 end) as $last_tool_i
275
- | ([
276
- $turn[($last_tool_i + 1):][]
277
- | .entry
278
- | if .type == "assistant" then
279
- .message.content?
280
- | if type == "string" then .
281
- elif type == "array" then ([.[]? | select(.type == "text") | (.text? // "")] | join(""))
282
- else ""
283
- end
284
- elif (
285
- .type == "response_item"
286
- and .payload.type? == "message"
287
- and .payload.role? == "assistant"
288
- and ((.payload.content? | type) == "array")
289
- ) then
290
- ([.payload.content[]? | (.text? // "")] | join(""))
291
- else ""
292
- end
293
- ] | join("") | test("\\S")) as $replied
317
+ # Reply visible after THIS segment last tool call: chooses which block
318
+ # variant an unsatisfied turn gets (write your reply vs do not repeat
319
+ # it). Mid-turn notes between tool calls do not count.
320
+ | ([range(0; $seg_events | length) | select($seg_events[.].kind == "tool")]
321
+ | if length > 0 then .[-1] else -1 end) as $seg_last_tool_index
322
+ | ([$seg_events[($seg_last_tool_index + 1):][] | select(.kind == "text") | .chars]
323
+ | add // 0) as $seg_reply_chars
324
+ # Reply owed by the WHOLE genuine turn: all assistant text after its
325
+ # last non-checkpoint tool call, crossing this gate feedback
326
+ # boundaries. An agent that replied and then checkpointed (in either
327
+ # order across a block) is never told to write the reply again.
328
+ | ([range(0; $events | length)
329
+ | select(($events[.].kind == "tool")
330
+ and (($events[.].command | checkpoint_command) | not))]
331
+ | if length > 0 then .[-1] else -1 end) as $last_work_index
332
+ | ([$events[($last_work_index + 1):][] | select(.kind == "text") | .chars]
333
+ | add // 0) as $reply_chars
294
334
  | ([$tools[] | select((.command | checkpoint_command) | not)] | length) as $work_tool_count
295
335
  | ([
296
336
  $rows[]
@@ -305,18 +345,50 @@ set -uo pipefail
305
345
  ($tools | length) > 0
306
346
  and ($last_tool.command | checkpoint_command)
307
347
  ) then
308
- if $last_tool.runtime == "claude" and ($last_result_error | not) then "1"
348
+ if $last_tool.runtime == "claude" and ($last_result_error | not) then
349
+ # A reply is owed after a real (--summary) checkpoint always, and
350
+ # after an --idle checkpoint only when the turn ran other tools
351
+ # (a read-only investigation still owes its findings). A
352
+ # --gate-probe is pure bookkeeping and never owes one. Both
353
+ # exemptions apply only to a PURE checkpoint invocation, with the
354
+ # flags read outside quoted text — a compound command did other
355
+ # work in the same call, and flag text inside a --summary value
356
+ # is data, not a flag.
357
+ (
358
+ (($last_tool.command | pure_checkpoint) | not)
359
+ or (
360
+ (($last_tool.command | checkpoint_flag("--gate-probe")) | not)
361
+ and (
362
+ (($last_tool.command | checkpoint_flag("--idle")) | not)
363
+ or ($work_tool_count > 0)
364
+ )
365
+ )
366
+ ) as $reply_required
367
+ | "1\t\($last_tool.id)\t\($reply_chars)\t\(if $reply_required then 1 else 0 end)"
309
368
  elif $last_tool.runtime == "codex" then "stamp"
310
- else (if $replied then "0-replied" else "0-unreplied" end)
369
+ else (if $seg_reply_chars > 0 then "0-replied" else "0-unreplied" end)
311
370
  end
312
371
  elif $work_tool_count == 0 then "idle"
313
- else (if $replied then "0-replied" else "0-unreplied" end)
372
+ else (if $seg_reply_chars > 0 then "0-replied" else "0-unreplied" end)
314
373
  end
315
374
  end
316
375
  ' 2>/dev/null)" || exit 0
376
+ # A Claude success verdict carries the satisfying checkpoint's tool id, the
377
+ # count of non-whitespace assistant text characters delivered after the
378
+ # genuine turn's last work tool (its user-facing reply, wherever it sits
379
+ # relative to the checkpoint), and whether this checkpoint shape owes the
380
+ # user a reply at all. An unsatisfied verdict says whether a reply is
381
+ # already visible, so the block message never demands a duplicate.
317
382
  satisfied="$parsed"
318
383
  replied=0
319
- case "$satisfied" in
384
+ reply_tool_id=""
385
+ reply_chars=""
386
+ reply_required=""
387
+ gate_tab="$(printf '\t')"
388
+ case "$parsed" in
389
+ "1${gate_tab}"*)
390
+ IFS="$gate_tab" read -r satisfied reply_tool_id reply_chars reply_required <<<"$parsed"
391
+ ;;
320
392
  0-replied) satisfied=0; replied=1 ;;
321
393
  0-unreplied) satisfied=0 ;;
322
394
  esac
@@ -350,49 +422,95 @@ set -uo pipefail
350
422
  # there is nothing to record. Demanding one anyway turns every conversational
351
423
  # reply — and every background-notification wake-up — into an --idle round
352
424
  # trip that writes no state and only costs a turn.
425
+ # Consecutive-block counter, shared by every block this gate can emit (the
426
+ # missing-checkpoint demand and the reply demand alike). A session whose
427
+ # checkpoint command keeps FAILING (a broken or mid-self-update `hq` binary)
428
+ # would otherwise be re-prompted on every Stop, forever. Past the cap the
429
+ # gate fails open per this file's never-strand-a-session doctrine; any Stop
430
+ # that ends in an allow resets it.
431
+ block_count_file="$state_dir/checkpoint-block-count-$session_key"
432
+ block_count="$(tr -d '\r\n' <"$block_count_file" 2>/dev/null || true)"
433
+ case "$block_count" in
434
+ ''|*[!0-9]*) block_count=0 ;;
435
+ esac
436
+ bump_block_count() {
437
+ block_count_tmp="$block_count_file.$$"
438
+ if (umask 077 && printf '%s' "$((block_count + 1))" >"$block_count_tmp" && mv -f "$block_count_tmp" "$block_count_file"); then
439
+ :
440
+ else
441
+ rm -f "$block_count_tmp" 2>/dev/null || true
442
+ fi
443
+ }
444
+
353
445
  if [ "$satisfied" = "1" ] || [ "$satisfied" = "idle" ]; then
354
446
  if [ "$runtime" = "codex" ]; then
355
447
  rm -f "$state_dir/codex-checkpoint-reprompt-$session_key" 2>/dev/null || true
356
448
  fi
357
- rm -f "$state_dir/checkpoint-block-count-$session_key" 2>/dev/null || true
449
+
450
+ # Reply-owed enforcement (Claude runtime only). The checkpoint payload
451
+ # goes to the background sibling, never the user. A genuine turn that did
452
+ # work and reaches a satisfying checkpoint with (next to) no assistant
453
+ # text after its last WORK tool call is a turn whose user-facing reply is
454
+ # missing, however rich the checkpoint flags are: block once and demand
455
+ # the reply. The window deliberately crosses this gate's own feedback
456
+ # boundaries and accepts a reply on either side of the checkpoint, so an
457
+ # agent that replied and then checkpointed is never told to repeat itself
458
+ # (double-messaging is the failure the reply-aware block variants exist
459
+ # to prevent). Evidence (transcript audit 2026-08-19, 1471 stop-gate
460
+ # checkpoint turns): 2.7% ended with no reply anywhere while the substance
461
+ # lived only in the checkpoint payload. The nudge fires at most once per
462
+ # checkpoint tool id (stamped in hook-state), so a legitimately terse turn
463
+ # costs exactly one extra Stop round-trip and the gate can never loop.
464
+ # Codex is excluded: its Stop feedback is a synthetic re-prompt with its
465
+ # own delivery contract. HQ_CHECKPOINT_REPLY_MIN overrides the minimum
466
+ # non-whitespace character threshold; 0 disables the enforcement.
467
+ if [ "$satisfied" = "1" ] && [ "$runtime" = "claude" ] \
468
+ && [ "$reply_required" = "1" ] && [ -n "$reply_tool_id" ]; then
469
+ reply_min="${HQ_CHECKPOINT_REPLY_MIN:-80}"
470
+ case "$reply_min" in ''|*[!0-9]*) reply_min=80 ;; esac
471
+ case "$reply_chars" in ''|*[!0-9]*) reply_chars=0 ;; esac
472
+ if [ "$reply_min" -gt 0 ] && [ "$reply_chars" -lt "$reply_min" ] && [ "$block_count" -lt 3 ]; then
473
+ nudge_file="$state_dir/checkpoint-reply-nudge-$session_key"
474
+ nudge_prev="$(cat "$nudge_file" 2>/dev/null || true)"
475
+ if [ "$nudge_prev" != "$reply_tool_id" ]; then
476
+ nudge_tmp="$nudge_file.$$"
477
+ if (umask 077 && printf '%s' "$reply_tool_id" >"$nudge_tmp" && mv -f "$nudge_tmp" "$nudge_file"); then
478
+ bump_block_count
479
+ reply_reason='Checkpoint recorded — but the turn cannot end yet: the user has not received a reply this turn. The checkpoint payload went to a background maintenance agent; the user sees NONE of it, and nothing you have written since your last real action reads as a reply to them. Deliver your complete reply to the user now, as ordinary text: every result, finding, link, decision, and anything awaiting their input, in plain language. Do not run another checkpoint and do not call any other tool — end the turn with text only.'
480
+ printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reply_reason" | hq_json_encode)"
481
+ exit 0
482
+ fi
483
+ rm -f "$nudge_tmp" 2>/dev/null || true
484
+ fi
485
+ fi
486
+ fi
487
+ # This Stop ends in an allow: the session is not stuck, so the
488
+ # consecutive-block counter starts over.
489
+ rm -f "$block_count_file" 2>/dev/null || true
358
490
  exit 0
359
491
  fi
360
492
 
361
- # Loop guard: a session whose checkpoint command keeps FAILING (a broken or
362
- # mid-self-update `hq` binary) would otherwise be re-prompted on every Stop,
363
- # forever — block → retry checkpoint → error → block again. Cap consecutive
364
- # blocks per session; past the cap, fail open per this file's
365
- # never-strand-a-session doctrine. Any successful checkpoint or idle turn
366
- # resets the counter above.
367
- block_count_file="$state_dir/checkpoint-block-count-$session_key"
368
- block_count="$(tr -d '\r\n' <"$block_count_file" 2>/dev/null || true)"
369
- case "$block_count" in
370
- ''|*[!0-9]*) block_count=0 ;;
371
- esac
493
+ # Loop guard for the missing-checkpoint demand below: cap consecutive blocks
494
+ # per session, then fail open (see the counter's comment above).
372
495
  if [ "$block_count" -ge 3 ]; then
373
496
  exit 0
374
497
  fi
375
- block_count_tmp="$block_count_file.$$"
376
- if (umask 077 && printf '%s' "$((block_count + 1))" >"$block_count_tmp" && mv -f "$block_count_tmp" "$block_count_file"); then
377
- :
378
- else
379
- rm -f "$block_count_tmp" 2>/dev/null || true
380
- fi
498
+ bump_block_count
381
499
 
382
500
  # Built with printf rather than concatenation so the session id can appear in
383
501
  # both commands without re-splitting the message into fragments.
384
502
  #
385
503
  # Two variants, chosen from the transcript rather than left to the agent's
386
- # judgment: the historical single instruction either demanded a full
387
- # restatement (double-messaging hosts that render pre-tool text the CLI and
388
- # the desktop app both do) or forbade post-checkpoint text (hiding the reply
389
- # when the agent had not yet written one). The transcript already tells us
390
- # which case we are in, so say exactly one thing.
504
+ # judgment: a single instruction either demanded a full restatement
505
+ # (double-messaging hosts that render pre-tool text) or forbade
506
+ # post-checkpoint text (hiding the reply when the agent had not yet written
507
+ # one). The transcript already tells us which case we are in, so say exactly
508
+ # one thing.
391
509
  flags_spec=' hq core checkpoint --session-id %s --trigger stop-gate --summary "<what changed, in one line>" [--file <path>] [--decision "<choice and why>"] [--learning "<reusable rule>"] [--next "<outstanding step>"]\n\nOnly --summary is required, and the repeatable flags are what the sibling uses to enrich the record, distil policies and update the indexes — a bare summary gives it almost nothing to work with. Write them as machine record, not prose for the user, and pass each one that genuinely applies:\n --file every path you created or modified this turn\n --decision a choice you made that a reader would otherwise have to reverse-engineer\n --learning a rule that changes how someone acts next time, not a restatement of what just happened\n --next work that is genuinely still outstanding\nOmit a flag rather than padding it: an empty or invented learning is worse than none.\n\nIf this turn only read or inspected things and changed no state, the correct call instead is:\n\n hq core checkpoint --session-id %s --idle'
392
510
  if [ "$replied" = 1 ]; then
393
511
  reason="$(printf 'This turn changed something, so it needs an end-of-turn checkpoint before it can end.\n\nYour user-facing reply is ALREADY delivered — the message you just wrote is visible to the user. Do NOT send it again, in full or summarized: repeating it double-messages the user, which is exactly the bug this gate guards against.\n\nTHE SIBLING (a background maintenance agent) reads only the checkpoint payload, never your chat reply — anything it needs must go into the flags.\n\nRun the checkpoint now as the FINAL action of the turn and end the turn immediately after it, adding no further text:\n\n'"$flags_spec" "$session_id" "$session_id")"
394
512
  else
395
- reason="$(printf 'This turn changed something, so it needs an end-of-turn checkpoint. Two different audiences are involved — do not conflate them:\n\n1. THE USER reads your normal chat reply. You have not sent one yet this turn — everything you owe them (results, links, answers, status, decisions) must go into it. The checkpoint is invisible to them and does NOT count as having replied.\n2. THE SIBLING (a background maintenance agent) reads the checkpoint payload. It never sees your chat reply, so anything it needs must go into the flags.\n\nORDER: run the checkpoint FIRST, then deliver your complete user-facing reply as the FINAL text of the turn — final-position text is the one placement every host renders in full. Every link, URL, instruction, command, and decision the user needs must appear in that final message.\n\n'"$flags_spec" "$session_id" "$session_id")"
513
+ reason="$(printf 'This turn changed something, so it needs an end-of-turn checkpoint. Two different audiences are involved — do not conflate them:\n\n1. THE USER reads your normal chat reply. You have not sent one yet this turn — everything you owe them (results, links, answers, status, decisions) must go into it. The checkpoint is invisible to them and does NOT count as having replied.\n2. THE SIBLING (a background maintenance agent) reads the checkpoint payload. It never sees your chat reply, so anything it needs must go into the flags.\n\nORDER: run the checkpoint FIRST, then deliver your complete user-facing reply as the FINAL text of the turn — final-position text is the one placement every host renders in full. Every link, URL, instruction, command, and decision the user needs must appear in that final message. The gate enforces this: a turn that ends without a user-facing reply is blocked until the reply is delivered.\n\n'"$flags_spec" "$session_id" "$session_id")"
396
514
  fi
397
515
 
398
516
  # Codex surfaces a blocked Stop reason as a synthetic user prompt. Preserve
@@ -20,12 +20,21 @@ type SpawnableBackend = Exclude<Backend, "none">;
20
20
  * (both the CLI and the desktop app render pre-tool-call text), while an
21
21
  * unconditional "no commentary after" hid the reply from agents that had not
22
22
  * yet written one. The Stop gate makes the same distinction from the
23
- * transcript; this line covers proactive checkpoints the gate never sees.
24
- * Mirrors the hq-core policies
23
+ * transcript and backstops this reminder mechanically: a satisfying
24
+ * checkpoint on a work turn whose genuine reply is still missing is blocked
25
+ * once with a dedicated reply demand. This line covers proactive checkpoints
26
+ * the gate never sees. Mirrors the hq-core policies
25
27
  * `checkpoint-is-bookkeeping-not-user-communication` and
26
28
  * `checkpoint-is-not-the-user-report`.
27
29
  */
28
30
  export declare const CHECKPOINT_REPLY_REMINDER: string;
31
+ /**
32
+ * The --idle variant: an idle-only turn (nothing ran, nothing to report) is
33
+ * exempt at the Stop gate, so this text must never order a reply into
34
+ * existence — but an --idle after read-only work is NOT exempt (the gate
35
+ * demands the findings), so the conditional half still points there.
36
+ */
37
+ export declare const CHECKPOINT_IDLE_REPLY_REMINDER: string;
29
38
  /**
30
39
  * Kept in TypeScript rather than in a bundled asset: it is an instruction to
31
40
  * a locally-installed agent, not a scaffold script that should be packaged.
@@ -51,8 +51,10 @@ class CheckpointUsageError extends Error {
51
51
  * (both the CLI and the desktop app render pre-tool-call text), while an
52
52
  * unconditional "no commentary after" hid the reply from agents that had not
53
53
  * yet written one. The Stop gate makes the same distinction from the
54
- * transcript; this line covers proactive checkpoints the gate never sees.
55
- * Mirrors the hq-core policies
54
+ * transcript and backstops this reminder mechanically: a satisfying
55
+ * checkpoint on a work turn whose genuine reply is still missing is blocked
56
+ * once with a dedicated reply demand. This line covers proactive checkpoints
57
+ * the gate never sees. Mirrors the hq-core policies
56
58
  * `checkpoint-is-bookkeeping-not-user-communication` and
57
59
  * `checkpoint-is-not-the-user-report`.
58
60
  */
@@ -62,6 +64,15 @@ export const CHECKPOINT_REPLY_REMINDER = "checkpoint: REMINDER — this checkpoi
62
64
  "the user, write your user-facing reply now as the FINAL text of the turn. If you " +
63
65
  "already delivered it earlier this turn, end the turn now WITHOUT repeating it — the " +
64
66
  "user has already seen that message, and repeating it double-messages them.";
67
+ /**
68
+ * The --idle variant: an idle-only turn (nothing ran, nothing to report) is
69
+ * exempt at the Stop gate, so this text must never order a reply into
70
+ * existence — but an --idle after read-only work is NOT exempt (the gate
71
+ * demands the findings), so the conditional half still points there.
72
+ */
73
+ export const CHECKPOINT_IDLE_REPLY_REMINDER = "checkpoint: idle recorded — invisible bookkeeping; the user sees none of it. If this " +
74
+ "turn surfaced findings or answers the user has not been told, deliver them now as the " +
75
+ "turn's final text. If the turn genuinely produced nothing to report, end it.";
65
76
  function printResult(line) {
66
77
  process.stdout.write(`${line}\n`);
67
78
  }
@@ -824,8 +835,10 @@ function runCheckpoint(options, command, group) {
824
835
  writeStamps(liveRoot, resolveSessionId(options, command, readPayload(options.payload)));
825
836
  printResult("checkpoint: idle (nothing to record)");
826
837
  // An idle turn persisted nothing, but a read-only turn can still have
827
- // produced substantive findings the user has not been told about.
828
- printResult(CHECKPOINT_REPLY_REMINDER);
838
+ // produced substantive findings the user has not been told about. The
839
+ // conditional idle variant matches the Stop gate, which exempts an
840
+ // idle-only turn from the reply demand.
841
+ printResult(CHECKPOINT_IDLE_REPLY_REMINDER);
829
842
  return;
830
843
  }
831
844
  const payload = readPayload(options.payload);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.12",
3
+ "version": "5.103.13",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {