@indigoai-us/hq-cli 5.103.8 → 5.103.9

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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.103.9] — 2026-08-20
6
+
7
+ ### Fixed
8
+
9
+ - The checkpoint stop-gate no longer causes duplicated or hidden replies
10
+ around the end-of-turn checkpoint. The gate now reads the transcript to
11
+ detect whether the agent already delivered its user-facing reply this turn
12
+ and issues the matching instruction: checkpoint-then-full-reply when no
13
+ reply exists yet, or checkpoint-and-end-without-repeating when it does. The
14
+ `hq core checkpoint` reply reminder carries the same conditional contract.
15
+
5
16
  ## [5.103.8] — 2026-08-19
6
17
 
7
18
  ### Fixed
@@ -236,14 +236,16 @@ set -uo pipefail
236
236
  | [range(0; $rows | length) | select($rows[.].entry | real_user)] as $user_indexes
237
237
  | if ($user_indexes | length) == 0 then error("no real user entry") else
238
238
  $user_indexes[-1] as $user_index
239
+ | $rows[($user_index + 1):] as $turn
239
240
  | [
240
- $rows[($user_index + 1):][]
241
- | .entry
241
+ range(0; $turn | length) as $ti
242
+ | $turn[$ti].entry
242
243
  | if .type == "assistant" then
243
244
  .message.content?
244
245
  | if type == "array" then .[]? else empty end
245
246
  | select(.type == "tool_use")
246
247
  | {
248
+ i: $ti,
247
249
  runtime: "claude",
248
250
  id: (.id? // ""),
249
251
  name: (.name? // ""),
@@ -255,6 +257,7 @@ set -uo pipefail
255
257
  ) then
256
258
  .payload
257
259
  | {
260
+ i: $ti,
258
261
  runtime: "codex",
259
262
  id: (.call_id? // .id? // ""),
260
263
  name: (.name? // ""),
@@ -264,6 +267,30 @@ set -uo pipefail
264
267
  end
265
268
  ] as $tools
266
269
  | (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
267
294
  | ([$tools[] | select((.command | checkpoint_command) | not)] | length) as $work_tool_count
268
295
  | ([
269
296
  $rows[]
@@ -280,14 +307,19 @@ set -uo pipefail
280
307
  ) then
281
308
  if $last_tool.runtime == "claude" and ($last_result_error | not) then "1"
282
309
  elif $last_tool.runtime == "codex" then "stamp"
283
- else "0"
310
+ else (if $replied then "0-replied" else "0-unreplied" end)
284
311
  end
285
312
  elif $work_tool_count == 0 then "idle"
286
- else "0"
313
+ else (if $replied then "0-replied" else "0-unreplied" end)
287
314
  end
288
315
  end
289
316
  ' 2>/dev/null)" || exit 0
290
317
  satisfied="$parsed"
318
+ replied=0
319
+ case "$satisfied" in
320
+ 0-replied) satisfied=0; replied=1 ;;
321
+ 0-unreplied) satisfied=0 ;;
322
+ esac
291
323
  case "$satisfied" in
292
324
  0|1|stamp|idle) ;;
293
325
  *) exit 0 ;;
@@ -349,7 +381,19 @@ set -uo pipefail
349
381
 
350
382
  # Built with printf rather than concatenation so the session id can appear in
351
383
  # both commands without re-splitting the message into fragments.
352
- 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. If you owe them anything — a result, a link, an answer, a status — say it in the reply as usual. The checkpoint is invisible to them and is NOT a message to them; running it 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 (this app folds any text that precedes a tool call into a collapsed sub-message — only text AFTER the last tool call renders in full): run the checkpoint FIRST, then deliver your COMPLETE user-facing reply as the final text of the turn. Every link, URL, instruction, command, and decision the user needs MUST appear in that final post-checkpoint message, restated in full even if you already wrote it earlier in the turn — earlier text is collapsed and the user will not see it.\n\n 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' "$session_id" "$session_id")"
384
+ #
385
+ # 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.
391
+ 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
+ if [ "$replied" = 1 ]; then
393
+ 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
+ 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")"
396
+ fi
353
397
 
354
398
  # Codex surfaces a blocked Stop reason as a synthetic user prompt. Preserve
355
399
  # the actionable instruction out-of-band, then use the stable marker covered
@@ -14,11 +14,14 @@ type SpawnableBackend = Exclude<Backend, "none">;
14
14
  * treating the checkpoint call as the end of the turn, leaving their actual
15
15
  * findings only in the checkpoint payload — which the user never sees. This
16
16
  * line rides the command output (the one channel guaranteed to reach the
17
- * calling agent) to force the user-facing reply. Deliberately conditional so
18
- * it composes with the Stop-gate prompt's "checkpoint first, reply last"
19
- * ordering: some hosts (the desktop app runtime) do not reliably display
20
- * assistant text that precedes a tool call, so the reply must come after
21
- * the checkpoint output as the final text of the turn. Mirrors the hq-core policies
17
+ * calling agent) to force the user-facing reply. Deliberately conditional on
18
+ * whether the reply was already delivered this turn: an unconditional
19
+ * "restate in full" made agents repeat a reply the user had already seen
20
+ * (both the CLI and the desktop app render pre-tool-call text), while an
21
+ * unconditional "no commentary after" hid the reply from agents that had not
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
22
25
  * `checkpoint-is-bookkeeping-not-user-communication` and
23
26
  * `checkpoint-is-not-the-user-report`.
24
27
  */
@@ -45,20 +45,23 @@ class CheckpointUsageError extends Error {
45
45
  * treating the checkpoint call as the end of the turn, leaving their actual
46
46
  * findings only in the checkpoint payload — which the user never sees. This
47
47
  * line rides the command output (the one channel guaranteed to reach the
48
- * calling agent) to force the user-facing reply. Deliberately conditional so
49
- * it composes with the Stop-gate prompt's "checkpoint first, reply last"
50
- * ordering: some hosts (the desktop app runtime) do not reliably display
51
- * assistant text that precedes a tool call, so the reply must come after
52
- * the checkpoint output as the final text of the turn. Mirrors the hq-core policies
48
+ * calling agent) to force the user-facing reply. Deliberately conditional on
49
+ * whether the reply was already delivered this turn: an unconditional
50
+ * "restate in full" made agents repeat a reply the user had already seen
51
+ * (both the CLI and the desktop app render pre-tool-call text), while an
52
+ * unconditional "no commentary after" hid the reply from agents that had not
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
53
56
  * `checkpoint-is-bookkeeping-not-user-communication` and
54
57
  * `checkpoint-is-not-the-user-report`.
55
58
  */
56
59
  export const CHECKPOINT_REPLY_REMINDER = "checkpoint: REMINDER — this checkpoint is invisible bookkeeping; the user never sees it " +
57
- "and it does NOT count as your reply. Now write your user-facing reply as the FINAL text " +
58
- "of the turn everything of substance from this turn (results, findings, decisions, " +
59
- "state changes, anything awaiting their input) even if you already wrote it earlier: " +
60
- "assistant text that precedes a tool call is not reliably shown to the user on all " +
61
- "hosts. Then end the turn.";
60
+ "and it does NOT count as your reply. If this turn's substance (results, findings, " +
61
+ "decisions, state changes, anything awaiting their input) has NOT yet been delivered to " +
62
+ "the user, write your user-facing reply now as the FINAL text of the turn. If you " +
63
+ "already delivered it earlier this turn, end the turn now WITHOUT repeating it the " +
64
+ "user has already seen that message, and repeating it double-messages them.";
62
65
  function printResult(line) {
63
66
  process.stdout.write(`${line}\n`);
64
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.103.8",
3
+ "version": "5.103.9",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {