@indigoai-us/hq-cli 5.345.122 → 5.345.123
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/README.md +15 -0
- package/assets/scaffold/core/scripts/handoff-sync-prefetch.sh +121 -0
- package/assets/scaffold/core/scripts/handoff-sync-publish.sh +124 -0
- package/assets/scaffold/core/scripts/knowledge-pulse-lock.sh +27 -0
- package/assets/scaffold/core/scripts/lib/session-skill-catalog.sh +226 -0
- package/assets/scaffold/core/scripts/skill-installed.sh +43 -0
- package/dist/command-catalog.generated.d.ts +93 -0
- package/dist/command-catalog.generated.js +111 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core-bucket-c-handoff-sync-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-handoff-sync-fast.js +16 -0
- package/dist/commands/core-bucket-c-handoff-sync-native.d.ts +45 -0
- package/dist/commands/core-bucket-c-handoff-sync-native.js +300 -0
- package/dist/commands/core-bucket-c-small-fast.d.ts +5 -0
- package/dist/commands/core-bucket-c-small-fast.js +19 -0
- package/dist/commands/core-bucket-c-small-native.d.ts +11 -0
- package/dist/commands/core-bucket-c-small-native.js +197 -0
- package/dist/commands/core-derive-trigger-facts-native.js +4 -1
- package/dist/commands/core.js +25 -0
- package/dist/commands/deploy-comments.d.ts +2 -0
- package/dist/commands/deploy-comments.js +2 -2
- package/dist/commands/deploy-list.d.ts +22 -0
- package/dist/commands/deploy-list.js +153 -0
- package/dist/commands/lanes.js +41 -2
- package/dist/fast-scaffold.js +48 -0
- package/dist/lib/core-utils/session-company.d.ts +3 -0
- package/dist/lib/core-utils/session-company.js +78 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.123] — 2026-10-14
|
|
6
|
+
- Add native `hq core handoff-sync-prefetch` and `hq core handoff-sync-publish` commands behind the `core-native-bucket-c` family gate, with pinned shell rollback assets.
|
|
7
|
+
- `hq lanes create` without `--provider` or a worker-profile provider now uses the engine of the orchestrator session instead of always picking Codex: the senior lane's provider, then the senior session's `conduct_engine`, then `HQ_HARNESS`, Codex/Grok runtime variables, or `CLAUDECODE=1`. When none of these is known it exits with `provider_required` and asks for `--provider`. `provider_source` stays `default` for these lanes.
|
|
8
|
+
- Add native `hq core knowledge-pulse-lock` and `hq core skill-installed` commands behind the existing `core-native-bucket-c` family gate, with pinned shell rollback assets.
|
|
9
|
+
- Match shell behavior for failed claim markers and mkdir errors, skill frontmatter edge cases, and trailing company-list commas.
|
|
10
|
+
|
|
11
|
+
- New `hq deploy list [query]` lists your company's hq-deploy apps with their
|
|
12
|
+
public URL, type and access mode. The query filters by name or subdomain;
|
|
13
|
+
`--company` and `--json` are supported. Errors exit non-zero.
|
|
14
|
+
- Fix native `derive-trigger-facts` to derive `shared_branch` from the resolved live root, matching the bundled shell when `--hq-root` differs from the caller cwd.
|
|
15
|
+
|
|
5
16
|
## [5.345.122] — 2026-10-13
|
|
6
17
|
- `hq capacity` now renews Claude sign-ins on machines with a long lane
|
|
7
18
|
history. Its safety check read the lane list with a 1 MB limit, so on busy
|
package/README.md
CHANGED
|
@@ -44,6 +44,21 @@ Live transcripts are provisional. Spoken content is untrusted data and must
|
|
|
44
44
|
never be treated as instructions. Only the recorder and meeting attendees can
|
|
45
45
|
read them.
|
|
46
46
|
|
|
47
|
+
## Deploy apps
|
|
48
|
+
|
|
49
|
+
List the hq-deploy apps in your company. Each line shows the app name, public
|
|
50
|
+
URL, type, access mode and last activity time (updated, last visit or created,
|
|
51
|
+
whichever the API returns). The optional `query` filters case-insensitively by
|
|
52
|
+
substring across name, subdomain and title. `--company` defaults to the active
|
|
53
|
+
company. Use `--json` for machine-readable output.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
hq deploy list [query] [--company <slug>] [--json]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
An empty unfiltered list prints a hint instead of claiming the org has no apps.
|
|
60
|
+
Auth, network and unknown-company errors exit non-zero.
|
|
61
|
+
|
|
47
62
|
## Deploy comments
|
|
48
63
|
|
|
49
64
|
List comments left on an app in your current hq-deploy organization. Use the
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# handoff-sync-prefetch.sh - pull the latest session continuity pointer before
|
|
3
|
+
# a fresh session reads it.
|
|
4
|
+
#
|
|
5
|
+
# `/startwork` and `/resumework` both read `workspace/threads/handoff.json`
|
|
6
|
+
# (and the thread file it references) to decide what to resume. On a second
|
|
7
|
+
# device those files only exist after a vault pull has landed, so a naive
|
|
8
|
+
# resume right after sign-in sees stale state. This helper runs that pull
|
|
9
|
+
# BEFORE the skill reads the pointer.
|
|
10
|
+
#
|
|
11
|
+
# Fail-soft:
|
|
12
|
+
# - no-op when the `hq` CLI is missing
|
|
13
|
+
# - no-op when offline or not logged in
|
|
14
|
+
# - timeboxed (default 25s)
|
|
15
|
+
# - never fails the caller: a successful or failed pull are both exit 0;
|
|
16
|
+
# the caller falls through to the local pointer either way.
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# handoff-sync-prefetch.sh [--hq-root PATH] [--timeout SECONDS] [--log PATH]
|
|
20
|
+
#
|
|
21
|
+
# Prints one line of status on stdout for the caller to echo (or ignore):
|
|
22
|
+
# `ok` - pull completed within the timeout
|
|
23
|
+
# `missing-cli` - `hq` is not on PATH; nothing was attempted
|
|
24
|
+
# `skipped` - CLI is present but the pull refused to start (no login,
|
|
25
|
+
# locked, etc.); local pointer is still used
|
|
26
|
+
# `timed-out` - pull exceeded the timeout and was killed
|
|
27
|
+
# `error:<rc>` - pull returned a non-zero exit code
|
|
28
|
+
#
|
|
29
|
+
# Exits 0 in every case so a shell `|| true` is unnecessary at call sites.
|
|
30
|
+
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
HQ_ROOT=""
|
|
34
|
+
TIMEOUT="${HQ_HANDOFF_PREFETCH_TIMEOUT:-25}"
|
|
35
|
+
LOG="/tmp/handoff-sync-prefetch.log"
|
|
36
|
+
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--hq-root) HQ_ROOT="$2"; shift 2 ;;
|
|
40
|
+
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
41
|
+
--log) LOG="$2"; shift 2 ;;
|
|
42
|
+
-h|--help)
|
|
43
|
+
sed -n '1,/^set -euo pipefail/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
|
|
44
|
+
exit 0 ;;
|
|
45
|
+
*) echo "handoff-sync-prefetch: unknown arg: $1" >&2; exit 2 ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
if ! command -v hq >/dev/null 2>&1; then
|
|
50
|
+
echo "missing-cli"
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ -z "$HQ_ROOT" ]]; then
|
|
55
|
+
HQ_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
|
|
59
|
+
: >>"$LOG"
|
|
60
|
+
printf '\n--- handoff-sync-prefetch %s hq_root=%s timeout=%ss ---\n' \
|
|
61
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HQ_ROOT" "$TIMEOUT" >>"$LOG"
|
|
62
|
+
|
|
63
|
+
# Portable timeout: do not depend on coreutils `timeout` (missing on stock
|
|
64
|
+
# macOS, missing in some Git Bash installs). Run the child in the background,
|
|
65
|
+
# start a watchdog that polls until the deadline, and kill the child on expiry.
|
|
66
|
+
# Exit code 124 is reserved to mean "timed out" so the caller branch is
|
|
67
|
+
# reachable regardless of which path ran.
|
|
68
|
+
run_with_timeout() {
|
|
69
|
+
local secs="$1"; shift
|
|
70
|
+
local flag="/tmp/handoff-sync-prefetch.timeout.$$.$RANDOM"
|
|
71
|
+
rm -f "$flag"
|
|
72
|
+
"$@" >>"$LOG" 2>&1 &
|
|
73
|
+
local child=$!
|
|
74
|
+
(
|
|
75
|
+
local waited=0
|
|
76
|
+
while [ "$waited" -lt "$secs" ]; do
|
|
77
|
+
sleep 1
|
|
78
|
+
kill -0 "$child" 2>/dev/null || exit 0
|
|
79
|
+
waited=$((waited + 1))
|
|
80
|
+
done
|
|
81
|
+
: >"$flag"
|
|
82
|
+
kill -TERM "$child" 2>/dev/null || true
|
|
83
|
+
sleep 1
|
|
84
|
+
if kill -0 "$child" 2>/dev/null; then
|
|
85
|
+
kill -KILL "$child" 2>/dev/null || true
|
|
86
|
+
fi
|
|
87
|
+
) >/dev/null 2>&1 &
|
|
88
|
+
local watchdog=$!
|
|
89
|
+
local rc=0
|
|
90
|
+
wait "$child" 2>/dev/null || rc=$?
|
|
91
|
+
kill "$watchdog" 2>/dev/null || true
|
|
92
|
+
wait "$watchdog" 2>/dev/null || true
|
|
93
|
+
if [ -f "$flag" ]; then
|
|
94
|
+
rm -f "$flag"
|
|
95
|
+
return 124
|
|
96
|
+
fi
|
|
97
|
+
rm -f "$flag"
|
|
98
|
+
return "$rc"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
rc=0
|
|
102
|
+
run_with_timeout "$TIMEOUT" \
|
|
103
|
+
hq sync pull --personal --hq-root "$HQ_ROOT" --lock-timeout 0 \
|
|
104
|
+
|| rc=$?
|
|
105
|
+
printf 'exit=%s\n' "$rc" >>"$LOG"
|
|
106
|
+
|
|
107
|
+
case "$rc" in
|
|
108
|
+
0) echo "ok" ;;
|
|
109
|
+
124) echo "timed-out" ;;
|
|
110
|
+
*)
|
|
111
|
+
# Distinguish "could not even start" (no login / lock refused) from a real
|
|
112
|
+
# error so operator logs don't scream when the user simply isn't signed in
|
|
113
|
+
# yet. `hq sync pull` returns 1 for both; the log file carries the detail.
|
|
114
|
+
if grep -qE 'not logged in|no cached session|refused immediately|requires login' "$LOG" 2>/dev/null; then
|
|
115
|
+
echo "skipped"
|
|
116
|
+
else
|
|
117
|
+
echo "error:${rc}"
|
|
118
|
+
fi
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
exit 0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# handoff-sync-publish.sh - push the session continuity pointer to the personal vault.
|
|
3
|
+
#
|
|
4
|
+
# Cross-device handoff depends on `workspace/threads/handoff.json` and the
|
|
5
|
+
# single thread file it points to reaching the vault before a fresh session on
|
|
6
|
+
# another machine runs `/startwork` or `/resumework`. The workspace tree is
|
|
7
|
+
# otherwise machine-local (`PERSONAL_VAULT_EXCLUDED_TOP_LEVEL`), so without a
|
|
8
|
+
# push run between the two sessions machine B sees stale state or no handoff
|
|
9
|
+
# at all. See hq-cloud-sync `computeContinuityPointerPaths`.
|
|
10
|
+
#
|
|
11
|
+
# This helper is intentionally minimal and fail-soft:
|
|
12
|
+
# - no-op when the `hq` CLI is missing (fresh install, agent box without CLI)
|
|
13
|
+
# - no-op when offline or not logged in (vault push will fail; we never
|
|
14
|
+
# block /handoff on cloud reachability)
|
|
15
|
+
# - timeboxed (default 25s) so a slow network cannot stall session end
|
|
16
|
+
# - detached by default; the handoff-finalize payload is already emitted
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# handoff-sync-publish.sh [--hq-root PATH] [--timeout SECONDS]
|
|
20
|
+
# [--sync] [--log PATH]
|
|
21
|
+
#
|
|
22
|
+
# --sync Run synchronously in the foreground and return the exit code.
|
|
23
|
+
# Default behaviour forks a backgrounded worker and prints its PID.
|
|
24
|
+
# --timeout Wall-clock bound in seconds; the push is killed on expiry.
|
|
25
|
+
# --log Append combined stdout/stderr to this path (default: `/tmp/handoff-sync-publish.log`).
|
|
26
|
+
#
|
|
27
|
+
# Exits 0 on dispatch (background mode) or on push success (sync mode).
|
|
28
|
+
# A sync-mode failure returns the underlying `hq` exit, or 124 on timeout.
|
|
29
|
+
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
HQ_ROOT=""
|
|
33
|
+
TIMEOUT="${HQ_HANDOFF_PUBLISH_TIMEOUT:-25}"
|
|
34
|
+
MODE="background"
|
|
35
|
+
LOG="/tmp/handoff-sync-publish.log"
|
|
36
|
+
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--hq-root) HQ_ROOT="$2"; shift 2 ;;
|
|
40
|
+
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
41
|
+
--sync) MODE="sync"; shift ;;
|
|
42
|
+
--log) LOG="$2"; shift 2 ;;
|
|
43
|
+
-h|--help)
|
|
44
|
+
sed -n '1,/^set -euo pipefail/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//'
|
|
45
|
+
exit 0 ;;
|
|
46
|
+
*) echo "handoff-sync-publish: unknown arg: $1" >&2; exit 2 ;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if ! command -v hq >/dev/null 2>&1; then
|
|
51
|
+
# Fresh install / agent box without the CLI. Nothing to publish from here.
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [[ -z "$HQ_ROOT" ]]; then
|
|
56
|
+
HQ_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Portable timeout: do not depend on coreutils `timeout` (missing on stock
|
|
60
|
+
# macOS, missing in some Git Bash installs). Return 124 on timeout so the
|
|
61
|
+
# caller can distinguish wall-clock expiry from a real `hq` error.
|
|
62
|
+
run_with_timeout() {
|
|
63
|
+
local secs="$1"; shift
|
|
64
|
+
local flag="/tmp/handoff-sync-publish.timeout.$$.$RANDOM"
|
|
65
|
+
rm -f "$flag"
|
|
66
|
+
"$@" >>"$LOG" 2>&1 &
|
|
67
|
+
local child=$!
|
|
68
|
+
(
|
|
69
|
+
local waited=0
|
|
70
|
+
while [ "$waited" -lt "$secs" ]; do
|
|
71
|
+
sleep 1
|
|
72
|
+
kill -0 "$child" 2>/dev/null || exit 0
|
|
73
|
+
waited=$((waited + 1))
|
|
74
|
+
done
|
|
75
|
+
: >"$flag"
|
|
76
|
+
kill -TERM "$child" 2>/dev/null || true
|
|
77
|
+
sleep 1
|
|
78
|
+
if kill -0 "$child" 2>/dev/null; then
|
|
79
|
+
kill -KILL "$child" 2>/dev/null || true
|
|
80
|
+
fi
|
|
81
|
+
) >/dev/null 2>&1 &
|
|
82
|
+
local watchdog=$!
|
|
83
|
+
local rc=0
|
|
84
|
+
wait "$child" 2>/dev/null || rc=$?
|
|
85
|
+
kill "$watchdog" 2>/dev/null || true
|
|
86
|
+
wait "$watchdog" 2>/dev/null || true
|
|
87
|
+
if [ -f "$flag" ]; then
|
|
88
|
+
rm -f "$flag"
|
|
89
|
+
return 124
|
|
90
|
+
fi
|
|
91
|
+
rm -f "$flag"
|
|
92
|
+
return "$rc"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
run_publish() {
|
|
96
|
+
# `hq sync push --personal` is the canonical publish path for the session
|
|
97
|
+
# continuity pointer (the vault scope that carves out handoff.json + the
|
|
98
|
+
# referenced thread file). A `--lock-timeout 0` keeps the push from queueing
|
|
99
|
+
# behind a long-running sync pass; the operator can rerun later.
|
|
100
|
+
run_with_timeout "$TIMEOUT" \
|
|
101
|
+
hq sync push --personal --hq-root "$HQ_ROOT" --lock-timeout 0 \
|
|
102
|
+
--message "handoff continuity pointer"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
|
|
106
|
+
: >>"$LOG"
|
|
107
|
+
printf '\n--- handoff-sync-publish %s hq_root=%s mode=%s timeout=%ss ---\n' \
|
|
108
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HQ_ROOT" "$MODE" "$TIMEOUT" >>"$LOG"
|
|
109
|
+
|
|
110
|
+
if [[ "$MODE" == "sync" ]]; then
|
|
111
|
+
rc=0; run_publish || rc=$?
|
|
112
|
+
printf 'exit=%s\n' "$rc" >>"$LOG"
|
|
113
|
+
exit "$rc"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# background: detach cleanly so handoff-finalize can return immediately.
|
|
117
|
+
(
|
|
118
|
+
rc=0
|
|
119
|
+
run_publish || rc=$?
|
|
120
|
+
printf 'exit=%s\n' "$rc" >>"$LOG"
|
|
121
|
+
) </dev/null >>"$LOG" 2>&1 &
|
|
122
|
+
disown || true
|
|
123
|
+
printf '%s\n' "$!"
|
|
124
|
+
exit 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ "$#" -ne 4 || "$1" != "claim" ]]; then
|
|
5
|
+
echo "Usage: knowledge-pulse-lock.sh claim <claim-root> <company-slug> <YYYY-MM-DD>" >&2
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
claim_root="$2"
|
|
10
|
+
company_slug="$3"
|
|
11
|
+
claim_date="$4"
|
|
12
|
+
|
|
13
|
+
if [[ -z "$claim_root" || ! "$company_slug" =~ ^[a-z0-9][a-z0-9-]*$ || ! "$claim_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
|
14
|
+
echo "knowledge-pulse-lock: invalid claim root, company slug, or date" >&2
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
mkdir -p "$claim_root"
|
|
19
|
+
marker="$claim_root/$company_slug-$claim_date.claimed"
|
|
20
|
+
if mkdir "$marker" 2>/dev/null; then
|
|
21
|
+
printf 'claimed\n'
|
|
22
|
+
elif [[ -d "$marker" ]]; then
|
|
23
|
+
printf 'already-claimed\n'
|
|
24
|
+
else
|
|
25
|
+
echo "knowledge-pulse-lock: could not create claim marker" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# session-skill-catalog.sh — bounded company-scoped skill catalog for system.txt.
|
|
4
|
+
#
|
|
5
|
+
# Enumerates SKILL.md frontmatter (name + description) from:
|
|
6
|
+
# 1. companies/<companySlug>/skills/*/SKILL.md (wins on name collision)
|
|
7
|
+
# 2. .claude/skills/*/SKILL.md
|
|
8
|
+
# 3. core/packages/*/skills/*/SKILL.md
|
|
9
|
+
#
|
|
10
|
+
# Cross-company skills are never listed. Catalog is name + one-line description
|
|
11
|
+
# only (never full bodies). Bounded by HQ_SESSION_SKILL_CATALOG_MAX_BYTES
|
|
12
|
+
# (default 32768). Exports:
|
|
13
|
+
# SESSION_SKILLS_AVAILABLE
|
|
14
|
+
# SESSION_SKILL_CATALOG_TSV — name<TAB>path<TAB>origin per line
|
|
15
|
+
# SESSION_SKILL_CATALOG_BODY
|
|
16
|
+
# SESSION_SKILL_CATALOG_RENDERED_BYTES
|
|
17
|
+
|
|
18
|
+
# session_skill_read_frontmatter <skill.md>
|
|
19
|
+
# Prints "name<TAB>description" from YAML frontmatter (first --- block).
|
|
20
|
+
session_skill_read_frontmatter() {
|
|
21
|
+
local file="${1:-}"
|
|
22
|
+
[ -f "$file" ] || return 1
|
|
23
|
+
awk '
|
|
24
|
+
BEGIN { in_fm=0; name=""; desc=""; in_desc_block=0; desc_indent=0 }
|
|
25
|
+
/^---[ \t]*$/ {
|
|
26
|
+
if (in_fm == 0) { in_fm=1; next }
|
|
27
|
+
if (name != "") {
|
|
28
|
+
gsub(/[ \t\r\n]+/, " ", desc)
|
|
29
|
+
gsub(/^ | $/, "", desc)
|
|
30
|
+
print name "\t" desc
|
|
31
|
+
}
|
|
32
|
+
exit
|
|
33
|
+
}
|
|
34
|
+
in_fm && in_desc_block {
|
|
35
|
+
if ($0 ~ /^[ \t]*$/) {
|
|
36
|
+
desc=desc " "
|
|
37
|
+
next
|
|
38
|
+
}
|
|
39
|
+
indent=match($0, /[^ \t]/) - 1
|
|
40
|
+
if (indent > desc_indent) {
|
|
41
|
+
desc=desc " " substr($0, indent + 1)
|
|
42
|
+
next
|
|
43
|
+
}
|
|
44
|
+
in_desc_block=0
|
|
45
|
+
}
|
|
46
|
+
in_fm && /^name:[ \t]*/ {
|
|
47
|
+
s=$0; sub(/^name:[ \t]*/,"",s)
|
|
48
|
+
gsub(/^["'"'"']|["'"'"']$/,"",s)
|
|
49
|
+
name=s; next
|
|
50
|
+
}
|
|
51
|
+
in_fm && /^description:[ \t]*/ {
|
|
52
|
+
s=$0; sub(/^description:[ \t]*/,"",s)
|
|
53
|
+
if (s ~ /^[|>][-+]?[ \t]*$/) {
|
|
54
|
+
in_desc_block=1
|
|
55
|
+
desc_indent=match($0, /[^ \t]/) - 1
|
|
56
|
+
next
|
|
57
|
+
}
|
|
58
|
+
gsub(/^["'"'"']|["'"'"']$/,"",s)
|
|
59
|
+
desc=s; next
|
|
60
|
+
}
|
|
61
|
+
' "$file"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# session_skill_catalog_build <root> <companySlug>
|
|
65
|
+
# Builds internal catalog state. Prints skillsAvailable on stdout.
|
|
66
|
+
session_skill_catalog_build() {
|
|
67
|
+
local root="${1:-}" company="${2:-}"
|
|
68
|
+
local max_bytes skill_dir f fm n d line lb name_line name_bytes
|
|
69
|
+
local full_line full_bytes remaining_name_bytes indicator indicator_bytes
|
|
70
|
+
local count=0 rendered_bytes=0 name_only_bytes=0
|
|
71
|
+
local tsv_file render_file list_file catalog_file
|
|
72
|
+
|
|
73
|
+
SESSION_SKILLS_AVAILABLE=0
|
|
74
|
+
SESSION_SKILL_CATALOG_TSV=""
|
|
75
|
+
SESSION_SKILL_CATALOG_RENDERED_BYTES=0
|
|
76
|
+
SESSION_SKILL_CATALOG_BODY=""
|
|
77
|
+
|
|
78
|
+
[ -n "$root" ] && [ -n "$company" ] || {
|
|
79
|
+
echo 0
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
max_bytes="${HQ_SESSION_SKILL_CATALOG_MAX_BYTES:-32768}"
|
|
84
|
+
case "$max_bytes" in ''|*[!0-9]*) max_bytes=32768 ;; esac
|
|
85
|
+
|
|
86
|
+
tsv_file="$(mktemp)"
|
|
87
|
+
render_file="$(mktemp)"
|
|
88
|
+
list_file="$(mktemp)"
|
|
89
|
+
catalog_file="$(mktemp)"
|
|
90
|
+
: > "$tsv_file"
|
|
91
|
+
: > "$render_file"
|
|
92
|
+
: > "$list_file"
|
|
93
|
+
: > "$catalog_file"
|
|
94
|
+
|
|
95
|
+
# Build ordered candidate list: company first, then root, then packages.
|
|
96
|
+
# Format per line: origin<TAB>path
|
|
97
|
+
local -a company_slugs=()
|
|
98
|
+
IFS=, read -r -a company_slugs <<< "$company"
|
|
99
|
+
for company in "${company_slugs[@]}"; do
|
|
100
|
+
[ -n "$company" ] || continue
|
|
101
|
+
if [ -d "$root/companies/$company/skills" ]; then
|
|
102
|
+
for skill_dir in "$root/companies/$company/skills"/*/; do
|
|
103
|
+
[ -d "$skill_dir" ] || continue
|
|
104
|
+
f="${skill_dir}SKILL.md"
|
|
105
|
+
[ -f "$f" ] && printf 'company\t%s\n' "$f" >> "$list_file"
|
|
106
|
+
done
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
if [ -d "$root/.claude/skills" ]; then
|
|
110
|
+
for skill_dir in "$root/.claude/skills"/*/; do
|
|
111
|
+
[ -d "$skill_dir" ] || continue
|
|
112
|
+
case "$(basename "$skill_dir")" in
|
|
113
|
+
_*|README*) continue ;;
|
|
114
|
+
esac
|
|
115
|
+
f="${skill_dir}SKILL.md"
|
|
116
|
+
[ -f "$f" ] && printf 'root\t%s\n' "$f" >> "$list_file"
|
|
117
|
+
done
|
|
118
|
+
fi
|
|
119
|
+
if [ -d "$root/core/packages" ]; then
|
|
120
|
+
for skill_dir in "$root/core/packages"/*/skills/*/; do
|
|
121
|
+
[ -d "$skill_dir" ] || continue
|
|
122
|
+
f="${skill_dir}SKILL.md"
|
|
123
|
+
[ -f "$f" ] && printf 'package\t%s\n' "$f" >> "$list_file"
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# First name wins (company enumerated first → shadowing).
|
|
128
|
+
local origin names_blob=""
|
|
129
|
+
names_blob=""
|
|
130
|
+
while IFS=$'\t' read -r origin f || [ -n "${origin:-}" ]; do
|
|
131
|
+
[ -z "${origin:-}" ] && continue
|
|
132
|
+
[ -f "$f" ] || continue
|
|
133
|
+
fm="$(session_skill_read_frontmatter "$f")" || continue
|
|
134
|
+
[ -n "$fm" ] || continue
|
|
135
|
+
n="${fm%%$'\t'*}"
|
|
136
|
+
d="${fm#*$'\t'}"
|
|
137
|
+
[ -n "$n" ] || continue
|
|
138
|
+
case $'\n'"$names_blob"$'\n' in
|
|
139
|
+
*$'\n'"$n"$'\n'*) continue ;;
|
|
140
|
+
esac
|
|
141
|
+
names_blob="${names_blob}${n}"$'\n'
|
|
142
|
+
printf '%s\t%s\t%s\n' "$n" "$f" "$origin" >> "$tsv_file"
|
|
143
|
+
printf '%s\t%s\n' "$n" "$d" >> "$catalog_file"
|
|
144
|
+
count=$((count + 1))
|
|
145
|
+
done < "$list_file"
|
|
146
|
+
|
|
147
|
+
# Names are the catalog's discovery contract. Calculate their minimum total
|
|
148
|
+
# first, then spend only surplus bytes on descriptions. This prevents early
|
|
149
|
+
# verbose entries from silently crowding later skill names out of the prompt.
|
|
150
|
+
while IFS=$'\t' read -r n d || [ -n "${n:-}" ]; do
|
|
151
|
+
[ -n "${n:-}" ] || continue
|
|
152
|
+
name_line="- /$n"
|
|
153
|
+
name_bytes="$(printf '%s\n' "$name_line" | wc -c | tr -d '[:space:]')"
|
|
154
|
+
case "$name_bytes" in ''|*[!0-9]*) name_bytes=0 ;; esac
|
|
155
|
+
name_only_bytes=$((name_only_bytes + name_bytes))
|
|
156
|
+
done < "$catalog_file"
|
|
157
|
+
|
|
158
|
+
if [ "$name_only_bytes" -le "$max_bytes" ]; then
|
|
159
|
+
remaining_name_bytes="$name_only_bytes"
|
|
160
|
+
while IFS=$'\t' read -r n d || [ -n "${n:-}" ]; do
|
|
161
|
+
[ -n "${n:-}" ] || continue
|
|
162
|
+
name_line="- /$n"
|
|
163
|
+
name_bytes="$(printf '%s\n' "$name_line" | wc -c | tr -d '[:space:]')"
|
|
164
|
+
case "$name_bytes" in ''|*[!0-9]*) name_bytes=0 ;; esac
|
|
165
|
+
remaining_name_bytes=$((remaining_name_bytes - name_bytes))
|
|
166
|
+
line="$name_line"
|
|
167
|
+
lb="$name_bytes"
|
|
168
|
+
if [ -n "${d:-}" ]; then
|
|
169
|
+
full_line="$name_line — $d"
|
|
170
|
+
full_bytes="$(printf '%s\n' "$full_line" | wc -c | tr -d '[:space:]')"
|
|
171
|
+
case "$full_bytes" in ''|*[!0-9]*) full_bytes="$name_bytes" ;; esac
|
|
172
|
+
if [ $((rendered_bytes + full_bytes + remaining_name_bytes)) -le "$max_bytes" ]; then
|
|
173
|
+
line="$full_line"
|
|
174
|
+
lb="$full_bytes"
|
|
175
|
+
fi
|
|
176
|
+
fi
|
|
177
|
+
printf '%s\n' "$line" >> "$render_file"
|
|
178
|
+
rendered_bytes=$((rendered_bytes + lb))
|
|
179
|
+
done < "$catalog_file"
|
|
180
|
+
else
|
|
181
|
+
indicator="- (catalog truncated)"
|
|
182
|
+
indicator_bytes="$(printf '%s\n' "$indicator" | wc -c | tr -d '[:space:]')"
|
|
183
|
+
case "$indicator_bytes" in ''|*[!0-9]*) indicator_bytes=0 ;; esac
|
|
184
|
+
while IFS=$'\t' read -r n d || [ -n "${n:-}" ]; do
|
|
185
|
+
[ -n "${n:-}" ] || continue
|
|
186
|
+
name_line="- /$n"
|
|
187
|
+
name_bytes="$(printf '%s\n' "$name_line" | wc -c | tr -d '[:space:]')"
|
|
188
|
+
case "$name_bytes" in ''|*[!0-9]*) name_bytes=0 ;; esac
|
|
189
|
+
if [ $((rendered_bytes + name_bytes + indicator_bytes)) -le "$max_bytes" ]; then
|
|
190
|
+
printf '%s\n' "$name_line" >> "$render_file"
|
|
191
|
+
rendered_bytes=$((rendered_bytes + name_bytes))
|
|
192
|
+
fi
|
|
193
|
+
done < "$catalog_file"
|
|
194
|
+
if [ $((rendered_bytes + indicator_bytes)) -le "$max_bytes" ]; then
|
|
195
|
+
printf '%s\n' "$indicator" >> "$render_file"
|
|
196
|
+
rendered_bytes=$((rendered_bytes + indicator_bytes))
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
SESSION_SKILLS_AVAILABLE="$count"
|
|
201
|
+
SESSION_SKILL_CATALOG_TSV="$(cat "$tsv_file")"
|
|
202
|
+
SESSION_SKILL_CATALOG_RENDERED_BYTES="$rendered_bytes"
|
|
203
|
+
SESSION_SKILL_CATALOG_BODY="$(cat "$render_file")"
|
|
204
|
+
|
|
205
|
+
rm -f "$tsv_file" "$render_file" "$list_file" "$catalog_file"
|
|
206
|
+
echo "$count"
|
|
207
|
+
return 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# session_skill_catalog_append <runDir> <companySlug> <compactFlag>
|
|
211
|
+
session_skill_catalog_append() {
|
|
212
|
+
local run_dir="${1:-}" company="${2:-}" compact="${3:-0}"
|
|
213
|
+
local system_txt="$run_dir/system.txt"
|
|
214
|
+
[ -f "$system_txt" ] || return 0
|
|
215
|
+
{
|
|
216
|
+
printf '<!-- hq-section: skill-catalog -->\n'
|
|
217
|
+
if [ "$compact" = "true" ]; then
|
|
218
|
+
printf 'Read the relevant SKILL.md directly when a task calls for an HQ skill. Search only .claude/skills/*/SKILL.md, companies/%s/skills/*/SKILL.md, and core/packages/*/skills/*/SKILL.md. Do not inspect another company directory.\n' "$company"
|
|
219
|
+
elif [ -n "${SESSION_SKILL_CATALOG_BODY:-}" ]; then
|
|
220
|
+
printf '%s\n' "$SESSION_SKILL_CATALOG_BODY"
|
|
221
|
+
else
|
|
222
|
+
printf '(no skills available)\n'
|
|
223
|
+
fi
|
|
224
|
+
} >> "$system_txt"
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Report whether a skill is in the active session's company-scoped catalog.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
skill="${1:-}"
|
|
7
|
+
company="${2:-${HQ_ACTIVE_COMPANY:-}}"
|
|
8
|
+
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
9
|
+
resolver="$HQ_ROOT/core/scripts/lib/session-skill-catalog.sh"
|
|
10
|
+
|
|
11
|
+
if [[ -z "$company" ]]; then
|
|
12
|
+
company="$(bash "$HQ_ROOT/core/scripts/hq-session.sh" get company_slugs 2>/dev/null || true)"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [[ ! "$skill" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
if [[ -z "$company" ]]; then
|
|
19
|
+
# The catalog requires a company to enumerate skills. This impossible
|
|
20
|
+
# company slug selects the same root and package skills without a tenant.
|
|
21
|
+
company="__hq_unbound_session__"
|
|
22
|
+
else
|
|
23
|
+
IFS=, read -r -a companies <<< "$company"
|
|
24
|
+
for slug in "${companies[@]}"; do
|
|
25
|
+
[[ "$slug" =~ ^[a-z][a-z0-9_-]*$ ]] || exit 1
|
|
26
|
+
done
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ ! -r "$resolver" ]]; then
|
|
30
|
+
printf 'skill availability resolver is missing: %s\n' "$resolver" >&2
|
|
31
|
+
exit 2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Use the same ordered, company-scoped catalog as session skill discovery.
|
|
35
|
+
# shellcheck source=core/scripts/lib/session-skill-catalog.sh
|
|
36
|
+
source "$resolver"
|
|
37
|
+
session_skill_catalog_build "$HQ_ROOT" "$company" >/dev/null
|
|
38
|
+
|
|
39
|
+
while IFS=$'\t' read -r catalog_skill catalog_path _; do
|
|
40
|
+
[[ "$catalog_skill" == "$skill" && -f "$catalog_path" ]] && exit 0
|
|
41
|
+
done <<< "${SESSION_SKILL_CATALOG_TSV:-}"
|
|
42
|
+
|
|
43
|
+
exit 1
|