@indigoai-us/hq-cli 5.322.0 → 5.324.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
  3. package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
  4. package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
  5. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  6. package/dist/bin/hq-auth-refresh.js +33 -1
  7. package/dist/command-catalog.generated.d.ts +138 -6
  8. package/dist/command-catalog.generated.js +178 -6
  9. package/dist/command-registration-plan.d.ts +24 -0
  10. package/dist/command-registration-plan.js +4 -0
  11. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  12. package/dist/commands/anywhere-runtime-flag.js +22 -0
  13. package/dist/commands/cloud.js +23 -4
  14. package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
  15. package/dist/commands/core-derive-trigger-facts-command.js +47 -0
  16. package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
  17. package/dist/commands/core-derive-trigger-facts-native.js +492 -0
  18. package/dist/commands/core.js +6 -0
  19. package/dist/commands/daemon.js +39 -2
  20. package/dist/commands/db-provision.js +6 -1
  21. package/dist/commands/doctor.js +8 -4
  22. package/dist/commands/feedback.d.ts +2 -0
  23. package/dist/commands/feedback.js +10 -0
  24. package/dist/commands/install-global.d.ts +46 -0
  25. package/dist/commands/install-global.js +674 -0
  26. package/dist/commands/link.d.ts +30 -0
  27. package/dist/commands/link.js +184 -0
  28. package/dist/commands/mcp-status.d.ts +6 -0
  29. package/dist/commands/mcp-status.js +21 -4
  30. package/dist/commands/pkg-install.js +20 -1
  31. package/dist/commands/update.d.ts +19 -0
  32. package/dist/commands/update.js +52 -6
  33. package/dist/fast-core.d.ts +2 -1
  34. package/dist/fast-core.js +12 -8
  35. package/dist/fast-scaffold.js +13 -0
  36. package/dist/index.js +9 -7
  37. package/dist/lib/cli-invite-nudge.d.ts +25 -0
  38. package/dist/lib/cli-invite-nudge.js +152 -0
  39. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  40. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  41. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  42. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  43. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  44. package/dist/lib/core-utils/timeout-guard.js +2 -1
  45. package/dist/lib/daemon/convergence.d.ts +42 -0
  46. package/dist/lib/daemon/convergence.js +66 -0
  47. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  48. package/dist/lib/daemon/heartbeat.js +70 -0
  49. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  50. package/dist/lib/daemon/ops/journal.js +141 -0
  51. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  52. package/dist/lib/daemon/ops/memory.js +65 -0
  53. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  54. package/dist/lib/daemon/ops/mesh.js +70 -0
  55. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  56. package/dist/lib/daemon/ops/policy.js +283 -0
  57. package/dist/lib/daemon/protocol.d.ts +68 -0
  58. package/dist/lib/daemon/protocol.js +154 -0
  59. package/dist/lib/daemon/run.d.ts +10 -0
  60. package/dist/lib/daemon/run.js +85 -3
  61. package/dist/lib/daemon/server.d.ts +36 -0
  62. package/dist/lib/daemon/server.js +306 -0
  63. package/dist/lib/daemon/session-registry.d.ts +30 -0
  64. package/dist/lib/daemon/session-registry.js +42 -0
  65. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  66. package/dist/lib/doctor/checks/anywhere.js +182 -0
  67. package/dist/lib/doctor/registry.js +6 -0
  68. package/dist/lib/first-sync-next-step.d.ts +10 -1
  69. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  70. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  71. package/dist/lib/install/claude-targets.d.ts +47 -0
  72. package/dist/lib/install/claude-targets.js +203 -0
  73. package/dist/lib/install/codex-targets.d.ts +57 -0
  74. package/dist/lib/install/codex-targets.js +187 -0
  75. package/dist/lib/mesh/live/session-seq.js +62 -9
  76. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  77. package/dist/lib/registry/normalize-remote.js +68 -0
  78. package/dist/lib/registry/registry.d.ts +75 -0
  79. package/dist/lib/registry/registry.js +261 -0
  80. package/dist/main.js +3 -1
  81. package/dist/utils/cli-telemetry.d.ts +11 -0
  82. package/dist/utils/cli-telemetry.js +76 -0
  83. package/dist/utils/cognito-session.d.ts +5 -1
  84. package/dist/utils/cognito-session.js +8 -12
  85. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  86. package/dist/utils/run-bundled-script.d.ts +7 -4
  87. package/dist/utils/run-bundled-script.js +47 -12
  88. package/dist/utils/version-check.d.ts +2 -0
  89. package/dist/utils/version-check.js +7 -0
  90. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,7 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.324.0] — 2026-10-02
6
+ - Keep the bundled shell runner out of native fast-route startup and defer timeout-guard flag snapshot imports until a timeout would block.
7
+ - Reuse the parsed fast-route entry when dispatching the selected command.
8
+ - Add native `hq core derive-trigger-facts` on the fast route behind the default-off `core-native-hook-helpers` flag.
9
+ - Feedback reports can include the current cached hq-cli release when the default-off `feedback.latest-version` flag is enabled.
10
+ - Added flag-gated global Anywhere installation, company linking, and hqd daemon commands.
11
+ - Added flag-gated Anywhere daemon operations, doctor checks, and update convergence.
12
+ - `hq-auth-refresh` accepts `--min-validity-seconds` or `HQ_AUTH_MIN_VALIDITY_SECONDS`; values above 1800 seconds use the resolver's 1800-second cache margin.
13
+ - `hq db provision` now says remote databases are not available yet, instead of printing the server adapter error.
14
+
5
15
  ## [5.322.0] — 2026-10-02
16
+ - Add an opt-in, default-off CLI prompt for solo companies to invite a teammate after a work-bearing sync.
6
17
 
7
18
  - Retried idempotent vault API GETs once when API Gateway returned its generic Lambda-invoke 504 response without a function request ID.
8
19
 
@@ -0,0 +1,263 @@
1
+ #!/bin/bash
2
+ # derive-trigger-facts.sh — build the fact set a policy `when:` expression is
3
+ # evaluated against, for a given hook event.
4
+ #
5
+ # Usage:
6
+ # <hook-json on stdin> | derive-trigger-facts.sh <EVENT> [--with-assistant-intent]
7
+ # EVENT in {PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent}
8
+ # -> prints a space-separated, de-duplicated fact set to stdout.
9
+ # With --with-assistant-intent (PreToolUse and UserPromptSubmit only), prints
10
+ # the primary and AssistantIntent fact sets as two newline-delimited records.
11
+ #
12
+ # Facts = event tokens + best-effort static facts (company / repo / shared_branch),
13
+ # EXCEPT AssistantIntent, which is AI-message tokens only (no static facts).
14
+ #
15
+ # Tokens are OPEN — there is NO curated vocabulary. The fact set for a text
16
+ # event is EVERY word token in that text (lowercased, letter-led, length >= 2),
17
+ # so a policy `when:` can key on any word that naturally appears when it is
18
+ # relevant (`refactor`, `monitor`, `docker`, `linear`, ...) without the engine
19
+ # having to know it in advance. On top of the literal words, a few NON-LITERAL
20
+ # / structured facts are derived (a word that is not itself present in the text):
21
+ # secret + apikey <- credential locations and key-shaped strings
22
+ # completed <- clear completion messages in tool output or assistant text
23
+ # shared_branch <- a shared branch name (main/master/staging/production/release/)
24
+ # <basename>+.ext <- a file reference (see Filename tokens below)
25
+ # /command <- a slash-command mention (see Slash-command tokens below)
26
+ # run_in_background <- PreToolUse Bash with tool_input.run_in_background: true
27
+ # Per-event source text:
28
+ # PreToolUse Bash -> the command. (`gh pr create` -> the word `pr`, etc.)
29
+ # PreToolUse other -> lowercased tool name (Glob->glob, Grep->grep, ...).
30
+ # UserPromptSubmit -> the prompt text.
31
+ # PostToolUse -> the tool OUTPUT (tool_response) text.
32
+ # AssistantIntent -> assistant message text emitted since the last user turn
33
+ # in transcript_path, AND NOTHING ELSE (no command/prompt
34
+ # tokens, no static facts). The dedicated AI-message
35
+ # channel; raw PreToolUse / UserPromptSubmit fact sets do
36
+ # NOT include look-back.
37
+ #
38
+ # Filename tokens: any file reference in the text emits a literal basename token
39
+ # and a `.ext` token (the eval-trigger grammar allows dots and slashes). So a
40
+ # policy keys on the file directly. `.claude/settings.json` -> `settings.json` +
41
+ # `.json`; `.mcp.json` -> `.mcp.json` + `.json`; `shot.png` -> `shot.png` + `.png`.
42
+ # This is what lets file-scoped policies fire from AssistantIntent (the AI naming
43
+ # the file it is about to edit/read) without the hook seeing the non-Bash
44
+ # Edit/Read tool call itself.
45
+ #
46
+ # Slash-command tokens: a `/command` mentioned in the text emits a `/command`
47
+ # token (`/brainstorm`, `/deep-plan`), so a slash-command-scoped policy fires
48
+ # when the command is invoked or referenced in a prompt.
49
+ #
50
+ # Pure read-only. Requires jq for JSON parsing (falls back to best-effort sed).
51
+
52
+ set -euo pipefail
53
+
54
+ EVENT="${1:-}"
55
+ WITH_ASSISTANT_INTENT=0
56
+ [ "${2:-}" = "--with-assistant-intent" ] && WITH_ASSISTANT_INTENT=1
57
+
58
+ # Bash can read the hook pipe directly without the separate `cat` process. An
59
+ # empty pipe retains the old best-effort empty-object behavior.
60
+ STDIN_JSON="$(</dev/stdin)" || STDIN_JSON='{}'
61
+ [ -n "$STDIN_JSON" ] || STDIN_JSON='{}'
62
+ JQ="$(command -v jq || true)"
63
+
64
+ # HQ root — used only to resolve the session's meta.yaml for the company fact
65
+ # (US-004). Prefer an explicit HQ_ROOT / CLAUDE_PROJECT_DIR; otherwise walk up
66
+ # from this script (core/scripts/derive-trigger-facts.sh -> ../.. == HQ root).
67
+ SCRIPT_PATH="${BASH_SOURCE[0]}"
68
+ case "$SCRIPT_PATH" in
69
+ */*) SCRIPT_DIR="${SCRIPT_PATH%/*}" ;;
70
+ *) SCRIPT_DIR="." ;;
71
+ esac
72
+ SCRIPT_DIR="$(cd "$SCRIPT_DIR" && pwd)"
73
+ HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)}}"
74
+ TRIGGER_FACT_TEXT_AWK="$SCRIPT_DIR/lib/trigger-fact-text.awk"
75
+ if [ ! -r "$TRIGGER_FACT_TEXT_AWK" ]; then
76
+ printf '%s\n' 'ERROR: derive-trigger-facts: missing core/scripts/lib/trigger-fact-text.awk' >&2
77
+ exit 1
78
+ fi
79
+ TRANSCRIPT_TAIL_LIB="$SCRIPT_DIR/lib/transcript-tail.sh"
80
+ if [ ! -r "$TRANSCRIPT_TAIL_LIB" ]; then
81
+ printf '%s\n' 'ERROR: derive-trigger-facts: missing core/scripts/lib/transcript-tail.sh' >&2
82
+ exit 1
83
+ fi
84
+ . "$TRANSCRIPT_TAIL_LIB"
85
+
86
+ # Parse the hook payload once. The old per-field jget() helper launched jq for
87
+ # every scalar, then the policy hook launched this script twice for a single
88
+ # PreToolUse/UserPromptSubmit fire. NUL-delimited records preserve embedded
89
+ # newlines in commands and prompts; JSON strings containing NUL were already
90
+ # unsupported by bash command substitution on the old path.
91
+ JSON_FIELDS=()
92
+ if [ -n "$JQ" ]; then
93
+ while IFS= read -r -d '' json_field; do
94
+ JSON_FIELDS+=("$json_field")
95
+ done < <(
96
+ printf '%s' "$STDIN_JSON" | "$JQ" -rj '
97
+ def text:
98
+ if . == null then ""
99
+ elif type == "string" then .
100
+ else tostring
101
+ end;
102
+ [
103
+ (.tool_name // ""),
104
+ (.tool_input.command // ""),
105
+ (.tool_input.run_in_background // ""),
106
+ (if .tool_response == null then ""
107
+ elif (.tool_response | type) == "string" then .tool_response
108
+ else (.tool_response | tostring)
109
+ end),
110
+ (.prompt // ""),
111
+ (.cwd // ""),
112
+ (.session_id // ""),
113
+ (.transcript_path // "")
114
+ ] | .[] | text + "\u0000"
115
+ ' 2>/dev/null
116
+ )
117
+ fi
118
+
119
+ TOOL="${JSON_FIELDS[0]:-}"
120
+ COMMAND="${JSON_FIELDS[1]:-}"
121
+ RUN_IN_BACKGROUND="${JSON_FIELDS[2]:-}"
122
+ TOOL_RESPONSE="${JSON_FIELDS[3]:-}"
123
+ PROMPT="${JSON_FIELDS[4]:-}"
124
+ PAYLOAD_CWD="${JSON_FIELDS[5]:-}"
125
+ PAYLOAD_SESSION_ID="${JSON_FIELDS[6]:-}"
126
+ TRANSCRIPT_PATH="${JSON_FIELDS[7]:-}"
127
+
128
+ # match_keywords <text> — emit (newline-separated) the OPEN fact set for <text>:
129
+ # every word token present (case-insensitive), plus the non-literal derived
130
+ # tokens (secret / shared_branch) and structured filename / slash-command tokens.
131
+ match_keywords() {
132
+ printf '%s\n' "$1" | awk -v mode=all -f "$TRIGGER_FACT_TEXT_AWK"
133
+ }
134
+
135
+ # --- company: a SESSION-IDENTITY fact, resolved once per payload (US-004) ---
136
+ # Company is not look-back content; it identifies the active tenant. It is
137
+ # derived with the SAME precedence as inject-policy-on-trigger.sh DIRS —
138
+ # HQ_POLICY_COMPANY env override > cwd companies/<slug> > session-meta
139
+ # company_slug (workspace/sessions/$session_id/meta.yaml).
140
+ # A paired primary/AssistantIntent derivation shares this resolution, but each
141
+ # resulting fact set still receives the same `company` token it would alone.
142
+ CWD="$PAYLOAD_CWD"; [ -z "$CWD" ] && CWD="${CLAUDE_PROJECT_DIR:-$PWD}"
143
+ SESSION_ID="$PAYLOAD_SESSION_ID"
144
+ co_scope=""
145
+ if [ -n "${HQ_POLICY_COMPANY:-}" ]; then
146
+ co_scope="$HQ_POLICY_COMPANY"
147
+ else
148
+ case "$CWD" in
149
+ *companies/*)
150
+ co_path="${CWD##*companies/}"
151
+ co_scope="${co_path%%/*}"
152
+ ;;
153
+ esac
154
+ if [ -z "$co_scope" ] && [ -n "$SESSION_ID" ]; then
155
+ # Read the session's own meta.yaml directly (awk idiom from
156
+ # master-hook.sh:119); READ ONLY, never via hq-session.sh get.
157
+ META="$HQ_ROOT/workspace/sessions/$SESSION_ID/meta.yaml"
158
+ [ -f "$META" ] && co_scope="$(awk '$1 == "company_slug:" { sub(/^[^:]+:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit }' "$META")"
159
+ fi
160
+ fi
161
+ COMPANY_FACT=""
162
+ [ -n "$co_scope" ] && COMPANY_FACT="company"
163
+
164
+ FACTS=""
165
+ FACTS_OUTPUT=""
166
+ add() { FACTS="$FACTS $*"; }
167
+
168
+ # Preserve the existing splitting and first-seen ordering but do it in one awk
169
+ # process instead of awk | tr | sed. `FACTS` only contains space/newline token
170
+ # boundaries from the existing match_keywords contract.
171
+ normalize_facts() {
172
+ FACTS_OUTPUT="$(printf '%s\n' $FACTS | awk -v mode=normalize -v branch="$branch" -f "$TRIGGER_FACT_TEXT_AWK")"
173
+ }
174
+
175
+ # derive_event <event> sets FACTS_OUTPUT. It does not write stdout so paired
176
+ # mode can run both channels in this one process without a second hook launch.
177
+ derive_event() {
178
+ local event="$1" lookback_text branch=""
179
+ FACTS=""
180
+
181
+ # `always` is present in every fact set so `when: always` is the canonical
182
+ # "no condition" expression (used by SessionStart-introduced advisory policies).
183
+ add always
184
+
185
+ case "$event" in
186
+ PreToolUse|PostToolUse)
187
+ case "$TOOL" in
188
+ Bash)
189
+ if [ "$event" = "PreToolUse" ]; then
190
+ add "$(match_keywords "$COMMAND")"
191
+ # structured: a harness-tracked background task (`run_in_background:
192
+ # true`) -> `run_in_background`, so a policy can tell a backgrounded
193
+ # poll loop from the same command run in the foreground.
194
+ [ "$RUN_IN_BACKGROUND" = "true" ] && add run_in_background
195
+ else
196
+ # PostToolUse: derive from the tool OUTPUT, not the input command.
197
+ add "$(match_keywords "$TOOL_RESPONSE")"
198
+ fi
199
+ ;;
200
+ "" ) : ;;
201
+ * )
202
+ # non-Bash tool -> lowercased tool name token (PreToolUse mainly)
203
+ if [ "$event" = "PreToolUse" ]; then
204
+ add "$(printf '%s' "$TOOL" | tr '[:upper:]' '[:lower:]')"
205
+ else
206
+ add "$(match_keywords "$TOOL_RESPONSE")"
207
+ fi
208
+ ;;
209
+ esac
210
+ ;;
211
+ UserPromptSubmit)
212
+ add "$(match_keywords "$PROMPT")"
213
+ ;;
214
+ esac
215
+
216
+ [ -n "$COMPANY_FACT" ] && add "$COMPANY_FACT"
217
+
218
+ # The dedicated channel for "what the assistant said it would do" — assistant
219
+ # text emitted since the last user turn in transcript_path. No command/prompt
220
+ # tokens are mixed in; the raw PreToolUse/UserPromptSubmit fact sets deliberately
221
+ # exclude this look-back so the two channels stay crisp. (Company, above, is the
222
+ # one session-identity fact shared across channels.)
223
+ if [ "$event" = "AssistantIntent" ]; then
224
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -n "$JQ" ]; then
225
+ lookback_text="$("$JQ" -nr '
226
+ # Real Claude Code transcript: each line is {type, message:{role,content}}
227
+ # where assistant text lives at .message.content[] | select(.type=="text")
228
+ # | .text. Fall back to a flat top-level .content (string) for simple
229
+ # fixtures. (Mirrors enforce-capability-link-render.sh / capture-estimates.sh.)
230
+ reduce inputs as $e ("";
231
+ ($e.type // "") as $ty
232
+ | (($e.message.content // $e.content) as $c
233
+ | if ($c|type)=="array" then ([$c[]? | select(.type=="text") | .text] | join(" "))
234
+ elif ($c|type)=="string" then $c
235
+ else "" end) as $txt
236
+ | if $ty == "user" then ""
237
+ elif $ty == "assistant" then . + (if length > 0 then " " else "" end) + $txt
238
+ else . end
239
+ )
240
+ ' < <(hq_transcript_tail "$TRANSCRIPT_PATH") 2>/dev/null)"
241
+ [ -n "$lookback_text" ] && add "$(match_keywords "$lookback_text")"
242
+ fi
243
+ else
244
+ # Best-effort static session facts (repo / shared_branch), primary events
245
+ # only. AssistantIntent is facts-from-AI-only except for `company` above.
246
+ case "$CWD" in
247
+ *repos/public/*|*repos/private/*) add repo ;;
248
+ esac
249
+ branch="$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
250
+ fi
251
+
252
+ normalize_facts
253
+ }
254
+
255
+ derive_event "$EVENT"
256
+ PRIMARY_FACTS="$FACTS_OUTPUT"
257
+ if [ "$WITH_ASSISTANT_INTENT" = "1" ] \
258
+ && { [ "$EVENT" = "PreToolUse" ] || [ "$EVENT" = "UserPromptSubmit" ]; }; then
259
+ derive_event AssistantIntent
260
+ printf '%s\n%s\n' "$PRIMARY_FACTS" "$FACTS_OUTPUT"
261
+ else
262
+ printf '%s\n' "$PRIMARY_FACTS"
263
+ fi
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # Shared bounded reads for JSONL transcripts on hot hook paths.
3
+
4
+ HQ_TRANSCRIPT_TAIL_MAX_BYTES=1048576
5
+
6
+ # hq_transcript_size <path>
7
+ # Report the file's exact byte size from filesystem metadata without reading
8
+ # its contents.
9
+ hq_transcript_size() {
10
+ [ "$#" -eq 1 ] || return 2
11
+ local path="$1" size=""
12
+ size="$(stat -L -c %s "$path" 2>/dev/null || true)"
13
+ case "$size" in
14
+ ''|*[!0-9]*)
15
+ size="$(stat -L -f %z "$path" 2>/dev/null || true)"
16
+ ;;
17
+ esac
18
+ case "$size" in
19
+ ''|*[!0-9]*) return 1 ;;
20
+ esac
21
+ printf '%s\n' "$size"
22
+ }
23
+
24
+ # hq_transcript_tail <path> [max_bytes]
25
+ # Keep a complete-line suffix of a JSONL transcript. The extra byte is a
26
+ # boundary sentinel: when the tail starts mid-record, sed drops that partial
27
+ # first line. If the byte window contains no complete line because the newest
28
+ # record is oversized, return only that final record.
29
+ hq_transcript_tail() {
30
+ [ "$#" -ge 1 ] && [ "$#" -le 2 ] || return 2
31
+ local path="$1" max_bytes="${2:-$HQ_TRANSCRIPT_TAIL_MAX_BYTES}" size
32
+ case "$max_bytes" in
33
+ ''|*[!0-9]*) return 2 ;;
34
+ esac
35
+ [ "$max_bytes" -gt 0 ] || return 2
36
+ size="$(hq_transcript_size "$path")" || {
37
+ printf '%s\n' "hq_transcript_tail: cannot read transcript size: $path" >&2
38
+ return 1
39
+ }
40
+ if [ "$size" -le "$((max_bytes + 1))" ]; then
41
+ cat "$path"
42
+ return $?
43
+ fi
44
+ local bounded_tail
45
+ bounded_tail="$(tail -c "$((max_bytes + 1))" "$path" | sed '1d'; printf '.')"
46
+ bounded_tail="${bounded_tail%.}"
47
+ if [ -n "$bounded_tail" ]; then
48
+ printf '%s' "$bounded_tail"
49
+ else
50
+ # The newest record itself may be larger than the byte window. Keep that
51
+ # complete record so safety and capture hooks can still inspect it.
52
+ tail -n 1 "$path"
53
+ fi
54
+ }
55
+
56
+ # hq_transcript_tail_with_latest_assistant <path> [max_bytes] [scan_cap_bytes]
57
+ # Return the normal bounded suffix plus the latest complete assistant record
58
+ # when later metadata records have pushed it outside that suffix. The fallback
59
+ # scan is bounded; status 3 means the latest assistant could not be verified
60
+ # within the cap and security-sensitive callers must fail closed.
61
+ hq_transcript_tail_with_latest_assistant() {
62
+ [ "$#" -ge 1 ] && [ "$#" -le 3 ] || return 2
63
+ local path="$1" max_bytes="${2:-$HQ_TRANSCRIPT_TAIL_MAX_BYTES}"
64
+ local scan_cap="${3:-16777216}" size normal_tail assistant_record scan_tail
65
+ case "$scan_cap" in ''|*[!0-9]*) return 2 ;; esac
66
+ [ "$scan_cap" -gt 0 ] || return 2
67
+
68
+ normal_tail="$(hq_transcript_tail "$path" "$max_bytes")" || return $?
69
+ assistant_record="$(printf '%s\n' "$normal_tail" | jq -Rrc 'fromjson? | select(.type == "assistant")' | tail -n 1)"
70
+ if [ -n "$assistant_record" ]; then
71
+ [ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
72
+ return 0
73
+ fi
74
+
75
+ size="$(hq_transcript_size "$path")" || {
76
+ printf '%s\n' "hq_transcript_tail_with_latest_assistant: cannot read transcript size: $path" >&2
77
+ return 1
78
+ }
79
+ if [ "$size" -le "$scan_cap" ]; then
80
+ scan_tail="$(cat "$path"; printf '.')"
81
+ else
82
+ scan_tail="$(tail -c "$((scan_cap + 1))" "$path" | sed '1d'; printf '.')"
83
+ fi
84
+ scan_tail="${scan_tail%.}"
85
+ assistant_record="$(printf '%s\n' "$scan_tail" | jq -Rrc 'fromjson? | select(.type == "assistant")' | tail -n 1)"
86
+ if [ -n "$assistant_record" ]; then
87
+ printf '%s\n' "$assistant_record"
88
+ [ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
89
+ return 0
90
+ fi
91
+
92
+ if [ "$size" -gt "$scan_cap" ]; then
93
+ printf '%s\n' "hq_transcript_tail_with_latest_assistant: latest assistant record exceeds the ${scan_cap}-byte verification window" >&2
94
+ return 3
95
+ fi
96
+
97
+ [ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
98
+ }
@@ -0,0 +1,86 @@
1
+ # Shared source for non-literal text facts derived by derive-trigger-facts.sh
2
+ # and the adapter policy-vocabulary prefilter. Keep every text-derived fact
3
+ # here so the prefilter cannot drift from the real policy trigger path.
4
+ function hq_is_shared_branch(t) {
5
+ return t ~ /(^|[^a-z0-9_])(main|master|staging|production)([^a-z0-9_]|$)/ || t ~ /release\//
6
+ }
7
+
8
+ function hq_emit_derived_text_facts(t) {
9
+ if (t ~ /(^|[^a-z0-9_])aws_profile/ || t ~ /op:\/\/|\.env([^a-z0-9]|$)/) print "secret"
10
+ if (hq_is_shared_branch(t)) print "shared_branch"
11
+ if (t ~ /(^|[^a-z0-9])sk-[a-z0-9]/ \
12
+ || t ~ /(^|[^a-z0-9])(gh[opsur]_|github_pat_)[a-z0-9_]/ \
13
+ || t ~ /(^|[^a-z0-9])akia[a-z0-9][a-z0-9]/ \
14
+ || t ~ /(^|[^a-z0-9])xox[bpsa]-[a-z0-9]/ \
15
+ || t ~ /(^|[^a-z0-9])glpat-[a-z0-9]/ \
16
+ || t ~ /-----begin[a-z -]*private key/ \
17
+ || t ~ /(^|[^a-z0-9])bearer[ ][a-z0-9._-][a-z0-9._-][a-z0-9._-]/) {
18
+ print "apikey"
19
+ print "secret"
20
+ }
21
+ if (t ~ /successfully (merged|deployed|pushed|published|created)/ \
22
+ || t ~ /(deployment|deploy|build|release) (complete|completed|succeeded|ready)/ \
23
+ || t ~ /merged pull request/ \
24
+ || t ~ /pull request #?[0-9]+ .* merged/) print "completed"
25
+ }
26
+
27
+ {
28
+ if (NR > 1) text = text "\n"
29
+ text = text $0
30
+ }
31
+
32
+ END {
33
+ t = tolower(text)
34
+ if (mode == "derived") {
35
+ hq_emit_derived_text_facts(t)
36
+ if (hq_is_shared_branch(tolower(branch))) print "shared_branch"
37
+ exit
38
+ }
39
+ if (mode == "normalize") {
40
+ rest = t
41
+ while (match(rest, /[^[:space:]]+/)) {
42
+ fact = substr(rest, RSTART, RLENGTH)
43
+ rest = substr(rest, RSTART + RLENGTH)
44
+ if (!seen[fact]++) {
45
+ if (out != "") out = out " "
46
+ out = out fact
47
+ }
48
+ }
49
+ if (hq_is_shared_branch(tolower(branch)) && !seen["shared_branch"]++) {
50
+ if (out != "") out = out " "
51
+ out = out "shared_branch"
52
+ }
53
+ printf "%s", out
54
+ exit
55
+ }
56
+
57
+ # Open tokenization: every word token in the text becomes a fact. Tokens are
58
+ # letter-led and length >= 2; underscores and internal hyphens are retained.
59
+ tw = t
60
+ while (match(tw, /[a-z][a-z0-9_-]+/)) {
61
+ print substr(tw, RSTART, RLENGTH)
62
+ tw = substr(tw, RSTART + RLENGTH)
63
+ }
64
+ hq_emit_derived_text_facts(t)
65
+
66
+ # File references emit their basename and extension as facts.
67
+ tmp = t
68
+ while (match(tmp, "\\.?[a-z0-9_][a-z0-9_./-]*\\.[a-z][a-z0-9]+")) {
69
+ fn = substr(tmp, RSTART, RLENGTH)
70
+ tmp = substr(tmp, RSTART + RLENGTH)
71
+ bn = fn
72
+ sub(/.*\//, "", bn)
73
+ ext = bn
74
+ sub(/.*\./, "", ext)
75
+ print "." ext
76
+ print bn
77
+ }
78
+
79
+ # Slash-command mentions emit /command facts; paths such as repos/public do not.
80
+ tmp2 = " " t
81
+ while (match(tmp2, " /[a-z][a-z0-9-]*")) {
82
+ sc = substr(tmp2, RSTART + 1, RLENGTH - 1)
83
+ tmp2 = substr(tmp2, RSTART + RLENGTH)
84
+ print sc
85
+ }
86
+ }
@@ -14,6 +14,10 @@ import "../node-preflight.js";
14
14
  import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
15
15
  import "../node-network-compat.js";
16
16
  import type { refreshCachedSession } from "../utils/cognito-session.js";
17
- export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
17
+ type RefreshSession = (options?: {
18
+ minValiditySeconds?: number;
19
+ }) => ReturnType<typeof refreshCachedSession>;
20
+ export declare function runAuthRefresh(refreshSession?: RefreshSession): Promise<number>;
18
21
  export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
22
+ export {};
19
23
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -17,12 +17,44 @@ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
17
17
  import "../node-network-compat.js";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import path from "node:path";
20
+ const MIN_VALIDITY_FLAG = "--min-validity-seconds";
21
+ function parseMinimumValiditySeconds(argv, env) {
22
+ let raw;
23
+ let fromFlag = false;
24
+ for (let index = 0; index < argv.length; index += 1) {
25
+ const arg = argv[index];
26
+ if (arg === MIN_VALIDITY_FLAG) {
27
+ raw = argv[index + 1];
28
+ fromFlag = true;
29
+ if (!raw || raw.startsWith("--")) {
30
+ throw new Error(`${MIN_VALIDITY_FLAG} requires a non-negative integer`);
31
+ }
32
+ break;
33
+ }
34
+ if (arg.startsWith(`${MIN_VALIDITY_FLAG}=`)) {
35
+ raw = arg.slice(MIN_VALIDITY_FLAG.length + 1);
36
+ fromFlag = true;
37
+ break;
38
+ }
39
+ }
40
+ if (!fromFlag)
41
+ raw = env.HQ_AUTH_MIN_VALIDITY_SECONDS || undefined;
42
+ if (raw === undefined)
43
+ return undefined;
44
+ const value = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
45
+ if (!Number.isSafeInteger(value)) {
46
+ throw new Error("minimum token validity must be a non-negative integer");
47
+ }
48
+ // Values above the provider's cache margin are clamped by cognito-session.
49
+ return value;
50
+ }
20
51
  export async function runAuthRefresh(refreshSession) {
21
52
  const { finishSentrySession, Sentry } = await import("../sentry.js");
22
53
  const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
23
54
  let exitCode = 1;
24
55
  try {
25
- const result = await (refreshSession ?? refresh)();
56
+ const minValiditySeconds = parseMinimumValiditySeconds(process.argv.slice(2), process.env);
57
+ const result = await (refreshSession ?? refresh)(minValiditySeconds === undefined ? {} : { minValiditySeconds });
26
58
  if (result.refreshed) {
27
59
  exitCode = 0;
28
60
  }