@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
|
-
| $
|
|
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(
|
|
242
|
-
| $
|
|
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
|
-
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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 $
|
|
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
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
| ([
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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 $
|
|
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 $
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
#
|
|
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
|
-
|
|
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:
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
390
|
-
#
|
|
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
|
|
24
|
-
*
|
|
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
|
|
55
|
-
*
|
|
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
|
-
|
|
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);
|