@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.
- package/CHANGELOG.md +11 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
- package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
- package/dist/bin/hq-auth-refresh.d.ts +5 -1
- package/dist/bin/hq-auth-refresh.js +33 -1
- package/dist/command-catalog.generated.d.ts +138 -6
- package/dist/command-catalog.generated.js +178 -6
- package/dist/command-registration-plan.d.ts +24 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
- package/dist/commands/anywhere-runtime-flag.js +22 -0
- package/dist/commands/cloud.js +23 -4
- package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
- package/dist/commands/core-derive-trigger-facts-command.js +47 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
- package/dist/commands/core-derive-trigger-facts-native.js +492 -0
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.js +39 -2
- package/dist/commands/db-provision.js +6 -1
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +10 -0
- package/dist/commands/install-global.d.ts +46 -0
- package/dist/commands/install-global.js +674 -0
- package/dist/commands/link.d.ts +30 -0
- package/dist/commands/link.js +184 -0
- package/dist/commands/mcp-status.d.ts +6 -0
- package/dist/commands/mcp-status.js +21 -4
- package/dist/commands/pkg-install.js +20 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +52 -6
- package/dist/fast-core.d.ts +2 -1
- package/dist/fast-core.js +12 -8
- package/dist/fast-scaffold.js +13 -0
- package/dist/index.js +9 -7
- package/dist/lib/cli-invite-nudge.d.ts +25 -0
- package/dist/lib/cli-invite-nudge.js +152 -0
- package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
- package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
- package/dist/lib/core-utils/timeout-guard-native.js +4 -30
- package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
- package/dist/lib/core-utils/timeout-guard.js +2 -1
- package/dist/lib/daemon/convergence.d.ts +42 -0
- package/dist/lib/daemon/convergence.js +66 -0
- package/dist/lib/daemon/heartbeat.d.ts +43 -0
- package/dist/lib/daemon/heartbeat.js +70 -0
- package/dist/lib/daemon/ops/journal.d.ts +29 -0
- package/dist/lib/daemon/ops/journal.js +141 -0
- package/dist/lib/daemon/ops/memory.d.ts +20 -0
- package/dist/lib/daemon/ops/memory.js +65 -0
- package/dist/lib/daemon/ops/mesh.d.ts +38 -0
- package/dist/lib/daemon/ops/mesh.js +70 -0
- package/dist/lib/daemon/ops/policy.d.ts +78 -0
- package/dist/lib/daemon/ops/policy.js +283 -0
- package/dist/lib/daemon/protocol.d.ts +68 -0
- package/dist/lib/daemon/protocol.js +154 -0
- package/dist/lib/daemon/run.d.ts +10 -0
- package/dist/lib/daemon/run.js +85 -3
- package/dist/lib/daemon/server.d.ts +36 -0
- package/dist/lib/daemon/server.js +306 -0
- package/dist/lib/daemon/session-registry.d.ts +30 -0
- package/dist/lib/daemon/session-registry.js +42 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
- package/dist/lib/doctor/checks/anywhere.js +182 -0
- package/dist/lib/doctor/registry.js +6 -0
- package/dist/lib/first-sync-next-step.d.ts +10 -1
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/install/claude-targets.d.ts +47 -0
- package/dist/lib/install/claude-targets.js +203 -0
- package/dist/lib/install/codex-targets.d.ts +57 -0
- package/dist/lib/install/codex-targets.js +187 -0
- package/dist/lib/mesh/live/session-seq.js +62 -9
- package/dist/lib/registry/normalize-remote.d.ts +21 -0
- package/dist/lib/registry/normalize-remote.js +68 -0
- package/dist/lib/registry/registry.d.ts +75 -0
- package/dist/lib/registry/registry.js +261 -0
- package/dist/main.js +3 -1
- package/dist/utils/cli-telemetry.d.ts +11 -0
- package/dist/utils/cli-telemetry.js +76 -0
- package/dist/utils/cognito-session.d.ts +5 -1
- package/dist/utils/cognito-session.js +8 -12
- package/dist/utils/feedback-diagnostics.d.ts +2 -0
- package/dist/utils/run-bundled-script.d.ts +7 -4
- package/dist/utils/run-bundled-script.js +47 -12
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +7 -0
- 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
|
-
|
|
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
|
|
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
|
}
|