@indigoai-us/hq-cli 5.98.2 → 5.99.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 +52 -0
- package/assets/scaffold/core/scripts/checkpoint-stop-gate.sh +347 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +557 -0
- package/assets/scaffold/core/scripts/hq-session.sh +251 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +96 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +52 -0
- package/dist/commands/core.js +25 -5
- package/dist/commands/doctor.d.ts +97 -0
- package/dist/commands/doctor.js +228 -0
- package/dist/commands/scaffold-fast.d.ts +41 -0
- package/dist/commands/scaffold-fast.js +57 -0
- package/dist/fast-core.d.ts +16 -0
- package/dist/fast-core.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -1
- package/dist/lib/doctor/__testing__/fake-hq-tree.d.ts +194 -0
- package/dist/lib/doctor/__testing__/fake-hq-tree.js +357 -0
- package/dist/lib/doctor/allowed-divergence.d.ts +72 -0
- package/dist/lib/doctor/allowed-divergence.js +134 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +55 -0
- package/dist/lib/doctor/checks/claude-wiring.js +524 -0
- package/dist/lib/doctor/checks/codex-wiring.d.ts +45 -0
- package/dist/lib/doctor/checks/codex-wiring.js +376 -0
- package/dist/lib/doctor/checks/grok-wiring.d.ts +35 -0
- package/dist/lib/doctor/checks/grok-wiring.js +186 -0
- package/dist/lib/doctor/checks/runtime-probe.d.ts +101 -0
- package/dist/lib/doctor/checks/runtime-probe.js +335 -0
- package/dist/lib/doctor/compat.d.ts +85 -0
- package/dist/lib/doctor/compat.js +102 -0
- package/dist/lib/doctor/deep/classify.d.ts +61 -0
- package/dist/lib/doctor/deep/classify.js +75 -0
- package/dist/lib/doctor/deep/effects.d.ts +107 -0
- package/dist/lib/doctor/deep/effects.js +229 -0
- package/dist/lib/doctor/deep/executor.d.ts +112 -0
- package/dist/lib/doctor/deep/executor.js +369 -0
- package/dist/lib/doctor/deep/parity.d.ts +129 -0
- package/dist/lib/doctor/deep/parity.js +355 -0
- package/dist/lib/doctor/deep/sandbox.d.ts +190 -0
- package/dist/lib/doctor/deep/sandbox.js +572 -0
- package/dist/lib/doctor/fix/apply.d.ts +119 -0
- package/dist/lib/doctor/fix/apply.js +352 -0
- package/dist/lib/doctor/fix/backup.d.ts +40 -0
- package/dist/lib/doctor/fix/backup.js +64 -0
- package/dist/lib/doctor/fix/remediation.d.ts +71 -0
- package/dist/lib/doctor/fix/remediation.js +103 -0
- package/dist/lib/doctor/fixtures/discover.d.ts +96 -0
- package/dist/lib/doctor/fixtures/discover.js +287 -0
- package/dist/lib/doctor/fixtures/schema.d.ts +171 -0
- package/dist/lib/doctor/fixtures/schema.js +248 -0
- package/dist/lib/doctor/hook-gate-profiles.d.ts +55 -0
- package/dist/lib/doctor/hook-gate-profiles.js +107 -0
- package/dist/lib/doctor/json-output.d.ts +90 -0
- package/dist/lib/doctor/json-output.js +76 -0
- package/dist/lib/doctor/payload-shapes.d.ts +170 -0
- package/dist/lib/doctor/payload-shapes.js +275 -0
- package/dist/lib/doctor/platform.d.ts +244 -0
- package/dist/lib/doctor/platform.js +490 -0
- package/dist/lib/doctor/registry.d.ts +49 -0
- package/dist/lib/doctor/registry.js +176 -0
- package/dist/lib/doctor/report.d.ts +87 -0
- package/dist/lib/doctor/report.js +164 -0
- package/dist/lib/doctor/types.d.ts +87 -0
- package/dist/lib/doctor/types.js +29 -0
- package/dist/main.js +6 -0
- package/dist/utils/hook-trust.d.ts +10 -13
- package/dist/utils/hook-trust.js +148 -27
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +1 -1
- package/dist/utils/version-gate.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# hq-session.sh — read/write the current session's metadata.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# core/scripts/hq-session.sh current # print current session_id (or empty)
|
|
7
|
+
# core/scripts/hq-session.sh path # print path to current meta.yaml
|
|
8
|
+
# core/scripts/hq-session.sh get <key> # read a key from meta.yaml
|
|
9
|
+
# core/scripts/hq-session.sh set <key> <value> # set/replace a top-level key
|
|
10
|
+
#
|
|
11
|
+
# Reserved value:
|
|
12
|
+
# set company_slug personal # bind the session to no-company (personal) work
|
|
13
|
+
#
|
|
14
|
+
# Global option (before the subcommand):
|
|
15
|
+
# --session-id <id> operate on this session instead of the resolved one
|
|
16
|
+
#
|
|
17
|
+
# Session bootstrapping is owned by .claude/hooks/master-hook.sh, which
|
|
18
|
+
# writes workspace/sessions/.current and ensures
|
|
19
|
+
# workspace/sessions/<session_id>/meta.yaml exists on every hook event.
|
|
20
|
+
#
|
|
21
|
+
# "Current session" is resolved by core/scripts/lib/session-id.sh: the session id
|
|
22
|
+
# exported into this process wins, and workspace/sessions/.current is only a
|
|
23
|
+
# fallback. .current is a single global pointer rewritten by every hook event, so
|
|
24
|
+
# with concurrent sessions it can name someone else's session — trusting it made
|
|
25
|
+
# `set company_slug` write to the wrong session's meta.yaml while the calling
|
|
26
|
+
# session stayed unbound and blocked by the scope guard.
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
# The DATA tree (sessions, companies, hooks) is the live HQ root. When this
|
|
32
|
+
# script is dispatched as `hq core hq-session` from inside the hq-cli package,
|
|
33
|
+
# its own location is NOT that tree — the CLI injects the live root via
|
|
34
|
+
# HQ_ROOT / CLAUDE_PROJECT_DIR. Prefer those; fall back to the self-relative
|
|
35
|
+
# root so a loose-file (in-tree) invocation keeps resolving exactly as before.
|
|
36
|
+
REPO_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)}}"
|
|
37
|
+
SESSIONS_DIR="$REPO_ROOT/workspace/sessions"
|
|
38
|
+
# Libs are sourced from THIS script's own directory, so a bundled dispatch loads
|
|
39
|
+
# the bundled copies and a loose-file invocation loads the tree's copies.
|
|
40
|
+
LIB_DIR="$SCRIPT_DIR/lib"
|
|
41
|
+
# shellcheck source=lib/session-scope-capability.sh
|
|
42
|
+
. "$LIB_DIR/session-scope-capability.sh"
|
|
43
|
+
# shellcheck source=lib/session-id.sh
|
|
44
|
+
. "$LIB_DIR/session-id.sh"
|
|
45
|
+
|
|
46
|
+
# Set by the --session-id global option; empty means "resolve it".
|
|
47
|
+
SESSION_ID_OVERRIDE=""
|
|
48
|
+
|
|
49
|
+
usage() {
|
|
50
|
+
sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
current_id() {
|
|
54
|
+
if [ -n "$SESSION_ID_OVERRIDE" ]; then
|
|
55
|
+
printf '%s' "$SESSION_ID_OVERRIDE"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
session_id_resolve "$REPO_ROOT"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
current_meta() {
|
|
62
|
+
local id
|
|
63
|
+
id="$(current_id)"
|
|
64
|
+
[ -n "$id" ] || return 0
|
|
65
|
+
printf '%s/%s/meta.yaml' "$SESSIONS_DIR" "$id"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
cmd_current() {
|
|
69
|
+
current_id
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cmd_path() {
|
|
73
|
+
current_meta
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cmd_get() {
|
|
77
|
+
local key="${1:-}"
|
|
78
|
+
[ -n "$key" ] || { echo "usage: hq-session.sh get <key>" >&2; exit 1; }
|
|
79
|
+
local meta
|
|
80
|
+
meta="$(current_meta)"
|
|
81
|
+
[ -n "$meta" ] && [ -f "$meta" ] || return 0
|
|
82
|
+
awk -v k="$key" '
|
|
83
|
+
$1 == k":" {
|
|
84
|
+
sub(/^[^:]+:[[:space:]]*/, "")
|
|
85
|
+
gsub(/^"|"$/, "")
|
|
86
|
+
print
|
|
87
|
+
exit
|
|
88
|
+
}
|
|
89
|
+
' "$meta"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
cmd_set() {
|
|
93
|
+
local key="${1:-}" value="${2:-}"
|
|
94
|
+
[ -n "$key" ] && [ $# -ge 2 ] || { echo "usage: hq-session.sh set <key> <value>" >&2; exit 1; }
|
|
95
|
+
|
|
96
|
+
# Reject an invalid company_slug BEFORE writing anything. Otherwise a bad value
|
|
97
|
+
# lands in meta.yaml and only fails later at session_scope_mint, leaving
|
|
98
|
+
# corrupt state that the scope gate would read as a (bogus) binding. Mirrors
|
|
99
|
+
# session_scope_mint's own character-set rule; `personal` is a valid slug.
|
|
100
|
+
if [ "$key" = "company_slug" ] && [ -n "$value" ]; then
|
|
101
|
+
case "$value" in
|
|
102
|
+
*[!a-z0-9_-]*)
|
|
103
|
+
echo "hq-session: invalid company_slug: '$value' (allowed characters: a-z 0-9 - _)" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
local id meta
|
|
110
|
+
id="$(current_id)"
|
|
111
|
+
if [ -z "$id" ]; then
|
|
112
|
+
echo "hq-session: no current session — no session id in the environment (HQ_SESSION_ID, CLAUDE_CODE_SESSION_ID, CLAUDE_SESSION_ID, CODEX_SESSION_ID, CODEX_THREAD_ID) and workspace/sessions/.current is missing or invalid. Is master-hook installed? Pass --session-id <id> to target a session explicitly." >&2
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
meta="$SESSIONS_DIR/$id/meta.yaml"
|
|
116
|
+
mkdir -p "$(dirname "$meta")"
|
|
117
|
+
# Seed the same header master-hook.sh writes, so binding a session the hook
|
|
118
|
+
# has not bootstrapped yet still produces a well-formed record.
|
|
119
|
+
if [ ! -f "$meta" ]; then
|
|
120
|
+
printf 'session_id: %s\nstarted_at: "%s"\n' \
|
|
121
|
+
"$id" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$meta"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Capture the prior value so we only surface policies when it actually changes.
|
|
125
|
+
local prev=""
|
|
126
|
+
prev="$(cmd_get "$key" 2>/dev/null || true)"
|
|
127
|
+
|
|
128
|
+
local tmp
|
|
129
|
+
tmp="$(mktemp)"
|
|
130
|
+
awk -v k="$key" -v v="$value" '
|
|
131
|
+
BEGIN { found = 0 }
|
|
132
|
+
$1 == k":" { print k": " v; found = 1; next }
|
|
133
|
+
{ print }
|
|
134
|
+
END { if (!found) print k": " v }
|
|
135
|
+
' "$meta" > "$tmp"
|
|
136
|
+
mv "$tmp" "$meta"
|
|
137
|
+
|
|
138
|
+
# When a company is bound (or rebound) mid-session, surface that company's
|
|
139
|
+
# hard-enforcement policies into this Bash tool result. SessionStart only
|
|
140
|
+
# injects company policies for the company known *at start*; binding a
|
|
141
|
+
# company afterward (this path) otherwise surfaces nothing, so an agent can
|
|
142
|
+
# do company infra/deploy/credential work blind to hard rules. This closes
|
|
143
|
+
# that gap. Emits policy text only — never secrets.
|
|
144
|
+
if [ "$key" = "company_slug" ] && [ -n "$value" ] && [ "$value" != "$prev" ]; then
|
|
145
|
+
session_scope_mint "$REPO_ROOT" "$id" "$value" || {
|
|
146
|
+
echo "hq-session: failed to mint scope-capability for session $id" >&2
|
|
147
|
+
exit 1
|
|
148
|
+
}
|
|
149
|
+
# `personal` is the reserved scope for work that belongs to no company. It
|
|
150
|
+
# binds the session — satisfying the checkpoint company gate and scoping the
|
|
151
|
+
# authorizer to personal/ — but it has no tenant directory, so there are no
|
|
152
|
+
# company hard-policies to surface and nothing to register with the
|
|
153
|
+
# company-scoped Work Mesh. Everything else is a real company bind.
|
|
154
|
+
if [ "$value" != "personal" ]; then
|
|
155
|
+
emit_company_hard_policies "$value"
|
|
156
|
+
# Fire-and-forget: register this company bind with the Work Mesh (US-003).
|
|
157
|
+
# Fully silent (all output → the hook's own bounded log) and guarded so it
|
|
158
|
+
# can neither fail cmd_set under `set -euo pipefail` nor delay its return.
|
|
159
|
+
spawn_work_mesh_register "$id" || true
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Spawn the client-side Work Mesh registration hook, detached, for a mid-session
|
|
165
|
+
# company bind. No-ops silently when the hook or jq is absent (e.g. sandboxed
|
|
166
|
+
# tests copy only hq-session.sh). Never blocks: the hook itself is fire-and-forget
|
|
167
|
+
# and this backgrounds even its fast foreground path off cmd_set's return path.
|
|
168
|
+
spawn_work_mesh_register() {
|
|
169
|
+
local sid="$1"
|
|
170
|
+
local hook="$REPO_ROOT/core/hooks/work-mesh-register.sh"
|
|
171
|
+
local logf="$REPO_ROOT/workspace/logs/work-mesh-hook.log"
|
|
172
|
+
[ -x "$hook" ] || return 0
|
|
173
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
174
|
+
local ev
|
|
175
|
+
ev="$(jq -nc --arg sid "$sid" --arg cwd "${PWD:-}" '{session_id:$sid,cwd:$cwd}' 2>/dev/null)" || return 0
|
|
176
|
+
[ -n "$ev" ] || return 0
|
|
177
|
+
mkdir -p "$(dirname "$logf")" 2>/dev/null || true
|
|
178
|
+
HQ_ROOT="$REPO_ROOT" nohup bash -c 'printf "%s" "$1" | "$2" company_slug' _ "$ev" "$hook" \
|
|
179
|
+
>>"$logf" 2>&1 </dev/null &
|
|
180
|
+
disown 2>/dev/null || true
|
|
181
|
+
return 0
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Print a company's hard-enforcement policies, read directly from the policy
|
|
185
|
+
# files (the pre-built digest was retired — the when/on trigger hook is now the
|
|
186
|
+
# sole policy-surfacing path). Emits one `- [hard] **slug**: rule` line each.
|
|
187
|
+
emit_company_hard_policies() {
|
|
188
|
+
local co="$1"
|
|
189
|
+
local dir="$REPO_ROOT/companies/$co/policies"
|
|
190
|
+
[ -d "$dir" ] || return 0
|
|
191
|
+
# Guard the glob: with no matching files "$dir"/*.md expands literally, awk
|
|
192
|
+
# exits nonzero, and under `set -e` that would abort an otherwise fine bind.
|
|
193
|
+
local have=0 f
|
|
194
|
+
for f in "$dir"/*.md; do
|
|
195
|
+
[ -e "$f" ] && { have=1; break; }
|
|
196
|
+
done
|
|
197
|
+
[ "$have" = 1 ] || return 0
|
|
198
|
+
local lines
|
|
199
|
+
lines="$(awk '
|
|
200
|
+
function bn(p, n,a,b){ n=split(p,a,"/"); b=a[n]; sub(/\.md$/,"",b); return b }
|
|
201
|
+
function flush(){ if(enf=="hard" && rule!=""){ if(id=="")id=bn(fn); printf "- [hard] **%s**: %s\n", id, rule } }
|
|
202
|
+
FNR==1 { if(seen) flush(); d=0;id="";enf="";rule="";rsec=0;rcap=0;fn=FILENAME;seen=1 }
|
|
203
|
+
/^---[ \t]*$/ { d++; next }
|
|
204
|
+
d==1 && /^id:/ { s=$0; sub(/^id:[ \t]*/,"",s); gsub(/^["'"'"']|["'"'"']$/,"",s); id=s; next }
|
|
205
|
+
d==1 && /^enforcement:/ { s=$0; sub(/^enforcement:[ \t]*/,"",s); gsub(/[ \t]/,"",s); enf=s; next }
|
|
206
|
+
d>=2 && /^## Rule[ \t]*$/ { rsec=1; next }
|
|
207
|
+
d>=2 && rsec && /^## / { rsec=0 }
|
|
208
|
+
d>=2 && rsec && !rcap && NF { line=$0; gsub(/\*\*/,"",line); if(length(line)>160)line=substr(line,1,157)"..."; rule=line; rcap=1 }
|
|
209
|
+
END { if(seen) flush() }
|
|
210
|
+
' "$dir"/*.md 2>/dev/null)"
|
|
211
|
+
[ -z "$lines" ] && return 0
|
|
212
|
+
printf '\n<company-policy-digest co="%s">\n' "$co"
|
|
213
|
+
printf '# %s hard-enforcement policies (auto-surfaced on company bind)\n' "$co"
|
|
214
|
+
printf '> Company context just bound mid-session. These HARD rules now apply.\n'
|
|
215
|
+
printf '> Full text: `companies/%s/policies/{slug}.md` (or `qmd get -c %s {slug}`).\n\n' "$co" "$co"
|
|
216
|
+
printf '%s\n' "$lines"
|
|
217
|
+
printf '</company-policy-digest>\n'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
while [ $# -gt 0 ]; do
|
|
221
|
+
case "${1:-}" in
|
|
222
|
+
--session-id)
|
|
223
|
+
SESSION_ID_OVERRIDE="${2:-}"
|
|
224
|
+
if ! session_id_is_valid "$SESSION_ID_OVERRIDE"; then
|
|
225
|
+
echo "hq-session: invalid --session-id: '${SESSION_ID_OVERRIDE}'" >&2
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
shift 2
|
|
229
|
+
;;
|
|
230
|
+
--session-id=*)
|
|
231
|
+
SESSION_ID_OVERRIDE="${1#--session-id=}"
|
|
232
|
+
if ! session_id_is_valid "$SESSION_ID_OVERRIDE"; then
|
|
233
|
+
echo "hq-session: invalid --session-id: '${SESSION_ID_OVERRIDE}'" >&2
|
|
234
|
+
exit 1
|
|
235
|
+
fi
|
|
236
|
+
shift
|
|
237
|
+
;;
|
|
238
|
+
*) break ;;
|
|
239
|
+
esac
|
|
240
|
+
done
|
|
241
|
+
|
|
242
|
+
sub="${1:-}"
|
|
243
|
+
shift || true
|
|
244
|
+
case "$sub" in
|
|
245
|
+
current) cmd_current "$@" ;;
|
|
246
|
+
path) cmd_path "$@" ;;
|
|
247
|
+
get) cmd_get "$@" ;;
|
|
248
|
+
set) cmd_set "$@" ;;
|
|
249
|
+
""|-h|--help|help) usage ;;
|
|
250
|
+
*) echo "hq-session: unknown subcommand '$sub'" >&2; usage >&2; exit 1 ;;
|
|
251
|
+
esac
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# session-id.sh — resolve the session id the CURRENT process belongs to.
|
|
4
|
+
#
|
|
5
|
+
# Sourced by core/scripts/hq-session.sh and .claude/hooks/mandatory-scope-authorizer.sh.
|
|
6
|
+
# Never execute directly.
|
|
7
|
+
#
|
|
8
|
+
# WHY THIS EXISTS
|
|
9
|
+
#
|
|
10
|
+
# workspace/sessions/.current is a single, global, last-writer-wins pointer
|
|
11
|
+
# maintained by .claude/hooks/master-hook.sh: every hook event overwrites it with
|
|
12
|
+
# the session id from that event's payload. With two sessions alive at once — an
|
|
13
|
+
# interactive session plus a scheduled/agent run, or two terminals — .current
|
|
14
|
+
# names whichever session fired a hook most recently, NOT the session the running
|
|
15
|
+
# command belongs to.
|
|
16
|
+
#
|
|
17
|
+
# That matters because the enforcement side does not use .current. The scope
|
|
18
|
+
# guard (.claude/hooks/mandatory-scope-authorizer.sh) reads the authoritative
|
|
19
|
+
# session id out of the hook payload. So a helper that resolved "this session"
|
|
20
|
+
# through .current could write company_slug into a DIFFERENT session's record,
|
|
21
|
+
# report success, and leave the calling session unbound — the guard keeps
|
|
22
|
+
# blocking, and a foreign session silently acquires a company binding it never
|
|
23
|
+
# asked for.
|
|
24
|
+
#
|
|
25
|
+
# The session id is also exported into every child process by the host, so an
|
|
26
|
+
# environment variable always describes the session that spawned this command.
|
|
27
|
+
# Prefer it, and keep .current only as a fallback for invocations that have no
|
|
28
|
+
# session environment at all (cron, CI, a bare shell).
|
|
29
|
+
#
|
|
30
|
+
# RESOLUTION ORDER (first syntactically valid, non-empty value wins)
|
|
31
|
+
# 1. HQ_SESSION_ID — explicit caller/test override
|
|
32
|
+
# 2. CLAUDE_CODE_SESSION_ID — Claude Code CLI and app
|
|
33
|
+
# 3. CLAUDE_SESSION_ID — Claude Code (alternate/legacy)
|
|
34
|
+
# 4. CODEX_SESSION_ID — Codex
|
|
35
|
+
# 5. CODEX_THREAD_ID — Codex, thread-scoped runs
|
|
36
|
+
# 6. workspace/sessions/.current — no session env; last-writer-wins fallback
|
|
37
|
+
#
|
|
38
|
+
# The env precedence list matches .claude/skills/_shared/journal.sh so every
|
|
39
|
+
# per-session artifact (journal, session meta, scope capability) keys off the
|
|
40
|
+
# same identity.
|
|
41
|
+
|
|
42
|
+
# session_id_is_valid <id>
|
|
43
|
+
# Session ids become a path segment under workspace/sessions/, so restrict them
|
|
44
|
+
# to a conservative charset and reject the traversal spellings outright.
|
|
45
|
+
session_id_is_valid() {
|
|
46
|
+
local id="${1:-}"
|
|
47
|
+
[ -n "$id" ] || return 1
|
|
48
|
+
case "$id" in
|
|
49
|
+
.|..) return 1 ;;
|
|
50
|
+
*[!A-Za-z0-9._-]*) return 1 ;;
|
|
51
|
+
esac
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# session_id_from_env
|
|
56
|
+
# Print the first valid session id found in the environment, or nothing.
|
|
57
|
+
# An unset, empty, or malformed variable is skipped rather than fatal, so a
|
|
58
|
+
# stray value in one host cannot wedge every helper.
|
|
59
|
+
session_id_from_env() {
|
|
60
|
+
local var val
|
|
61
|
+
for var in HQ_SESSION_ID CLAUDE_CODE_SESSION_ID CLAUDE_SESSION_ID \
|
|
62
|
+
CODEX_SESSION_ID CODEX_THREAD_ID; do
|
|
63
|
+
val="${!var:-}"
|
|
64
|
+
val="$(printf '%s' "$val" | tr -d '[:space:]')"
|
|
65
|
+
if session_id_is_valid "$val"; then
|
|
66
|
+
printf '%s' "$val"
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# session_id_from_current_file <root>
|
|
74
|
+
# Print the id in workspace/sessions/.current, or nothing when absent/invalid.
|
|
75
|
+
session_id_from_current_file() {
|
|
76
|
+
local root="${1:-}" file val
|
|
77
|
+
[ -n "$root" ] || return 0
|
|
78
|
+
file="$root/workspace/sessions/.current"
|
|
79
|
+
[ -f "$file" ] || return 0
|
|
80
|
+
val="$(tr -d '[:space:]' < "$file" 2>/dev/null || true)"
|
|
81
|
+
session_id_is_valid "$val" && printf '%s' "$val"
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# session_id_resolve <root>
|
|
86
|
+
# Print this process's session id: environment first, .current as fallback.
|
|
87
|
+
# Prints nothing (exit 0) when neither source yields a valid id — callers that
|
|
88
|
+
# need one must fail loudly themselves.
|
|
89
|
+
session_id_resolve() {
|
|
90
|
+
local root="${1:-}" id
|
|
91
|
+
id="$(session_id_from_env)"
|
|
92
|
+
if [ -z "$id" ]; then
|
|
93
|
+
id="$(session_id_from_current_file "$root")"
|
|
94
|
+
fi
|
|
95
|
+
printf '%s' "$id"
|
|
96
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# session-scope-capability.sh — mint/read workspace/sessions/<id>/scope-capability.json
|
|
4
|
+
#
|
|
5
|
+
# Sourced by core/scripts/hq-session.sh and .claude/hooks/mandatory-scope-authorizer.sh.
|
|
6
|
+
# Never execute directly.
|
|
7
|
+
|
|
8
|
+
# session_scope_capability_path <root> <session_id>
|
|
9
|
+
# Print the absolute path to scope-capability.json for a session.
|
|
10
|
+
session_scope_capability_path() {
|
|
11
|
+
local root="${1:-}" sid="${2:-}"
|
|
12
|
+
[ -n "$root" ] && [ -n "$sid" ] || return 1
|
|
13
|
+
printf '%s/workspace/sessions/%s/scope-capability.json' "$root" "$sid"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# session_scope_read <root> <session_id>
|
|
17
|
+
# Print company_slug from scope-capability.json, or empty when absent/invalid.
|
|
18
|
+
session_scope_read() {
|
|
19
|
+
local root="${1:-}" sid="${2:-}" cap
|
|
20
|
+
cap="$(session_scope_capability_path "$root" "$sid")" || return 0
|
|
21
|
+
[ -f "$cap" ] || return 0
|
|
22
|
+
jq -r '.company_slug // empty' "$cap" 2>/dev/null || true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# session_scope_mint <root> <session_id> <company_slug>
|
|
26
|
+
# Write scope-capability.json for the session. Returns 0 on success.
|
|
27
|
+
session_scope_mint() {
|
|
28
|
+
local root="${1:-}" sid="${2:-}" slug="${3:-}"
|
|
29
|
+
[ -n "$root" ] && [ -n "$sid" ] && [ -n "$slug" ] || return 1
|
|
30
|
+
|
|
31
|
+
case "$slug" in
|
|
32
|
+
''|*[!a-z0-9_-]*)
|
|
33
|
+
echo "session-scope-capability: invalid company_slug: $slug" >&2
|
|
34
|
+
return 1
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
local cap dir tmp minted_at
|
|
39
|
+
cap="$(session_scope_capability_path "$root" "$sid")" || return 1
|
|
40
|
+
dir="$(dirname "$cap")"
|
|
41
|
+
mkdir -p "$dir" || return 1
|
|
42
|
+
|
|
43
|
+
minted_at="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S'Z')"
|
|
44
|
+
tmp="$(mktemp)"
|
|
45
|
+
jq -n \
|
|
46
|
+
--arg sid "$sid" \
|
|
47
|
+
--arg slug "$slug" \
|
|
48
|
+
--arg minted_at "$minted_at" \
|
|
49
|
+
'{session_id: $sid, company_slug: $slug, minted_at: $minted_at}' >"$tmp" \
|
|
50
|
+
|| { rm -f "$tmp"; return 1; }
|
|
51
|
+
mv "$tmp" "$cap"
|
|
52
|
+
}
|
package/dist/commands/core.js
CHANGED
|
@@ -164,11 +164,24 @@ export const REBUILD_INDEX_TARGETS = [
|
|
|
164
164
|
* would make the forwarders harder to audit against this table.
|
|
165
165
|
*/
|
|
166
166
|
export const SCAFFOLD_COMMANDS = [
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
167
|
+
// Session/harness plumbing relocated out of the HQ tree so a single published
|
|
168
|
+
// CLI owns it. Both are `root: "live"`: they read and write the live HQ tree
|
|
169
|
+
// (workspace/sessions, companies/, workspace/orchestrator/hook-state), so the
|
|
170
|
+
// CLI resolves and injects the live root, exactly as their in-tree originals
|
|
171
|
+
// derived it. Their loose-file copies remain as thin fallbacks for a tree
|
|
172
|
+
// whose installed CLI predates this command.
|
|
173
|
+
{
|
|
174
|
+
name: "checkpoint-stop-gate",
|
|
175
|
+
asset: "core/scripts/checkpoint-stop-gate.sh",
|
|
176
|
+
root: "live",
|
|
177
|
+
summary: "Stop-hook checkpoint + company-scope gate (reads the hook payload on stdin)",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "hq-session",
|
|
181
|
+
asset: "core/scripts/hq-session.sh",
|
|
182
|
+
root: "live",
|
|
183
|
+
summary: "Read/write the current session's meta.yaml (get/set/current/path)",
|
|
184
|
+
},
|
|
172
185
|
];
|
|
173
186
|
/**
|
|
174
187
|
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
@@ -176,6 +189,13 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
176
189
|
* direct `hq core` command.
|
|
177
190
|
*/
|
|
178
191
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
192
|
+
// Shared shell libs sourced by the two relocated scripts. Never dispatched on
|
|
193
|
+
// their own (so no `hq core` command), but they MUST ship in the tarball, or
|
|
194
|
+
// the bundled hq-session / checkpoint-stop-gate die at `source` time on a
|
|
195
|
+
// user's box. `root` is inert here — they are sourced, not run.
|
|
196
|
+
{ asset: "core/scripts/hook-lib.sh", root: "live", summary: "Shared shell primitives sourced by the checkpoint gate" },
|
|
197
|
+
{ asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
|
|
198
|
+
{ asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
|
|
179
199
|
{ asset: "core/scripts/archive-old-threads.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
180
200
|
{ asset: "core/scripts/codex-skill-bridge.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
181
201
|
{ asset: "core/scripts/qmd-reindex-after-sync.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq doctor` — verify HQ's hook guardrails are wired and firing.
|
|
3
|
+
*
|
|
4
|
+
* US-002 builds the command skeleton and the extensible check registry:
|
|
5
|
+
* - HQ root resolution walks up from the working directory looking for the
|
|
6
|
+
* marker directories that identify an HQ tree, and exposes the resolved root
|
|
7
|
+
* to every check.
|
|
8
|
+
* - Running outside any HQ tree exits non-zero with a message naming exactly
|
|
9
|
+
* what it looked for — never a throw, never a false PASS.
|
|
10
|
+
* - The command performs no network calls and needs no authentication; it is
|
|
11
|
+
* purely a function of the on-disk shape of the tree.
|
|
12
|
+
*
|
|
13
|
+
* US-015 adds reporting, `--json`, and the exit-code contract: the exit code is
|
|
14
|
+
* 0 unless some result is FAIL or UNKNOWN (WARN/UNTESTED/NA/KNOWN-DEFECT never
|
|
15
|
+
* fail); `--verbose` also prints PASS results; `--json` emits the versioned,
|
|
16
|
+
* machine-readable document; and text output carries no ANSI when stdout is not
|
|
17
|
+
* a TTY. The rendering and contract live in ../lib/doctor/report.ts and
|
|
18
|
+
* ../lib/doctor/json-output.ts; this command wires them to the CLI.
|
|
19
|
+
*
|
|
20
|
+
* The detected platform is a placeholder (UNKNOWN_PLATFORM) until host platform
|
|
21
|
+
* detection (US-003) lands and injects the real value through `platform`.
|
|
22
|
+
*/
|
|
23
|
+
import { Command } from "commander";
|
|
24
|
+
import { type DoctorRegistry } from "../lib/doctor/registry.js";
|
|
25
|
+
import type { FamilyRun } from "../lib/doctor/types.js";
|
|
26
|
+
import { type DoctorPlatform } from "../lib/doctor/report.js";
|
|
27
|
+
/**
|
|
28
|
+
* Marker directories that identify an HQ tree root. A directory is an HQ root
|
|
29
|
+
* only when it contains ALL of these. `.claude` + `core` are present in both
|
|
30
|
+
* the real HQ tree and the synthetic fixture trees the doctor is tested
|
|
31
|
+
* against, and are absent from an arbitrary directory — so requiring both
|
|
32
|
+
* avoids a false positive on a random repo that merely carries a `.claude/`
|
|
33
|
+
* folder.
|
|
34
|
+
*/
|
|
35
|
+
export declare const HQ_ROOT_MARKERS: readonly [".claude", "core"];
|
|
36
|
+
/**
|
|
37
|
+
* Walk up from `startDir` (default: the current working directory) looking for
|
|
38
|
+
* the nearest ancestor that contains every {@link HQ_ROOT_MARKERS} entry as a
|
|
39
|
+
* directory. Returns the resolved (realpath'd) root, or `null` when no ancestor
|
|
40
|
+
* qualifies — i.e. the caller is not inside an HQ tree.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveHqRoot(startDir?: string): string | null;
|
|
43
|
+
/** Options for {@link runDoctor}. All are injectable so the run is unit-testable. */
|
|
44
|
+
export interface RunDoctorOptions {
|
|
45
|
+
/** Directory to resolve the HQ root from. Default: `process.cwd()`. */
|
|
46
|
+
cwd?: string;
|
|
47
|
+
/** Explicit HQ root, bypassing resolution. Takes precedence over `cwd`. */
|
|
48
|
+
root?: string;
|
|
49
|
+
/** Registry to run. Default: {@link createDefaultRegistry}. */
|
|
50
|
+
registry?: DoctorRegistry;
|
|
51
|
+
/** stdout sink. Default: `process.stdout.write`. */
|
|
52
|
+
stdout?: (chunk: string) => void;
|
|
53
|
+
/** stderr sink. Default: `process.stderr.write`. */
|
|
54
|
+
stderr?: (chunk: string) => void;
|
|
55
|
+
/** Emit the machine-readable JSON document instead of text. Default: false. */
|
|
56
|
+
json?: boolean;
|
|
57
|
+
/** In text mode, also print every PASS result. Default: false. */
|
|
58
|
+
verbose?: boolean;
|
|
59
|
+
/** In text mode, emit ANSI colour. Default: false (no escape sequences). */
|
|
60
|
+
color?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The detected platform. Defaults to {@link UNKNOWN_PLATFORM}; host platform
|
|
63
|
+
* detection (US-003) injects the real value here. It is exposed to every check
|
|
64
|
+
* so host-specific checks (the US-006 runtime probe) can key off it.
|
|
65
|
+
*/
|
|
66
|
+
platform?: DoctorPlatform;
|
|
67
|
+
/**
|
|
68
|
+
* Session id to scope the runtime probe's ledger check to (US-006
|
|
69
|
+
* `--session-id`), so an older session's ledger cannot be mistaken for the
|
|
70
|
+
* current runtime.
|
|
71
|
+
*/
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Additionally execute pure-guard fixtures through the real gate under all
|
|
75
|
+
* three profiles, in a sandbox (US-008 `--deep-test`). Off by default: the
|
|
76
|
+
* default registry never fires a hook, and this branch is the ONLY path that
|
|
77
|
+
* does — so a plain `hq doctor` stays purely a function of the on-disk shape.
|
|
78
|
+
*/
|
|
79
|
+
deepTest?: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** The outcome of a doctor run, returned rather than thrown so it is testable. */
|
|
82
|
+
export interface RunDoctorResult {
|
|
83
|
+
/** Process exit code: 0 inside a tree, 1 when no HQ tree was found. */
|
|
84
|
+
exitCode: number;
|
|
85
|
+
/** The resolved HQ root, or null when resolution failed. */
|
|
86
|
+
hqRoot: string | null;
|
|
87
|
+
/** Grouped per-family results (empty when resolution failed). */
|
|
88
|
+
families: FamilyRun[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the HQ root, run every registered check family against it, and render
|
|
92
|
+
* a plain-text summary. Makes no network calls and needs no authentication.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
|
|
95
|
+
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
96
|
+
export declare function registerDoctorCommand(program: Command): void;
|
|
97
|
+
//# sourceMappingURL=doctor.d.ts.map
|