@indigoai-us/hq-cli 5.345.122 → 5.345.124

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 (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +15 -0
  3. package/assets/scaffold/core/scripts/handoff-sync-prefetch.sh +121 -0
  4. package/assets/scaffold/core/scripts/handoff-sync-publish.sh +124 -0
  5. package/assets/scaffold/core/scripts/knowledge-pulse-lock.sh +27 -0
  6. package/assets/scaffold/core/scripts/lib/session-skill-catalog.sh +226 -0
  7. package/assets/scaffold/core/scripts/skill-installed.sh +43 -0
  8. package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
  9. package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
  10. package/dist/command-catalog.generated.d.ts +332 -0
  11. package/dist/command-catalog.generated.js +402 -0
  12. package/dist/command-registration-plan.d.ts +6 -0
  13. package/dist/command-registration-plan.js +1 -0
  14. package/dist/commands/core-bucket-c-handoff-sync-fast.d.ts +3 -0
  15. package/dist/commands/core-bucket-c-handoff-sync-fast.js +16 -0
  16. package/dist/commands/core-bucket-c-handoff-sync-native.d.ts +45 -0
  17. package/dist/commands/core-bucket-c-handoff-sync-native.js +300 -0
  18. package/dist/commands/core-bucket-c-small-fast.d.ts +5 -0
  19. package/dist/commands/core-bucket-c-small-fast.js +19 -0
  20. package/dist/commands/core-bucket-c-small-native.d.ts +11 -0
  21. package/dist/commands/core-bucket-c-small-native.js +197 -0
  22. package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
  23. package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
  24. package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
  25. package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
  26. package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
  27. package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
  28. package/dist/commands/core-derive-trigger-facts-native.js +4 -1
  29. package/dist/commands/core.js +37 -0
  30. package/dist/commands/deploy-comments.d.ts +2 -0
  31. package/dist/commands/deploy-comments.js +2 -2
  32. package/dist/commands/deploy-list.d.ts +22 -0
  33. package/dist/commands/deploy-list.js +153 -0
  34. package/dist/commands/lanes.js +41 -2
  35. package/dist/commands/mesh.js +457 -16
  36. package/dist/fast-scaffold.js +74 -0
  37. package/dist/lib/core-utils/session-company.d.ts +3 -0
  38. package/dist/lib/core-utils/session-company.js +78 -0
  39. package/dist/lib/lanes/quota-notice.d.ts +24 -0
  40. package/dist/lib/lanes/quota-notice.js +61 -0
  41. package/dist/lib/lanes/quota.d.ts +44 -8
  42. package/dist/lib/lanes/quota.js +187 -45
  43. package/dist/lib/lanes/reconcile.js +17 -1
  44. package/dist/lib/lanes/watcher.js +13 -1
  45. package/dist/lib/mesh/api.d.ts +2 -0
  46. package/dist/lib/mesh/api.js +3 -0
  47. package/dist/lib/mesh/client.d.ts +6 -1
  48. package/dist/lib/mesh/client.js +18 -5
  49. package/dist/lib/mesh/team-coordination.d.ts +106 -0
  50. package/dist/lib/mesh/team-coordination.js +297 -0
  51. package/dist/lib/work-context/auto-bind.d.ts +75 -0
  52. package/dist/lib/work-context/auto-bind.js +245 -0
  53. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.124] — 2026-10-14
6
+ - Lanes no longer stop every lane on an account for days after a single weekly-limit 429. Reset times such as "resets Oct 16 at 1am (America/Denver)" are now read with their time and timezone. A reset more than a day away first gets only a short hold, and the full hold applies only if the next lane after that also gets a 429. When a hold is set, the lane's senior gets a message with the provider's text and `hq lanes accounts clear-quota <provider> <account>`.
7
+
8
+ - `hq mesh claim` assigns a Board story to you and moves it to in_progress in one call.
9
+ `hq mesh story` and `hq mesh claim` accept `--force`; without it, a story held by
10
+ someone else prints who holds it and exits 2.
11
+ - `hq mesh context prd-sync` claims an unassigned story for you when it moves the
12
+ story to in_progress, and warns (without forcing) when someone else holds it.
13
+ - `hq mesh session status` shows display names, falling back to the local directory cache.
14
+ - New: `hq mesh overlap`, `hq mesh project reassign`, `hq mesh suggest`, `hq mesh roster`,
15
+ and `hq mesh context auto-bind`. On a server without these routes they print one
16
+ "not available on this server yet" line and exit 0.
17
+
18
+ ## [5.345.123] — 2026-10-14
19
+ - 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.
20
+ - `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.
21
+ - 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.
22
+ - Match shell behavior for failed claim markers and mkdir errors, skill frontmatter edge cases, and trailing company-list commas.
23
+ - Add native `hq core workflow-gate` and `hq core work-mesh-project-registration-offer` commands behind the `core-native-bucket-c` family gate, with pinned shell rollback assets.
24
+
25
+ - New `hq deploy list [query]` lists your company's hq-deploy apps with their
26
+ public URL, type and access mode. The query filters by name or subdomain;
27
+ `--company` and `--json` are supported. Errors exit non-zero.
28
+ - 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.
29
+
5
30
  ## [5.345.122] — 2026-10-13
6
31
  - `hq capacity` now renews Claude sign-ins on machines with a long lane
7
32
  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
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
5
+ FLAG_READER="$ROOT/.claude/hooks/work-mesh-project-registration-offer-flag.cjs"
6
+
7
+ die() { echo "$*" >&2; exit 1; }
8
+
9
+ resolve_cloud_uid() {
10
+ local company="$1" manifest="$ROOT/companies/manifest.yaml"
11
+ [ -f "$manifest" ] || return 0
12
+ awk -v company="$company" '
13
+ /^companies:[[:space:]]*$/ { in_companies=1; next }
14
+ in_companies && /^[^[:space:]]/ { in_companies=0 }
15
+ in_companies && /^ [^ #][^:]*:/ {
16
+ slug=$1; sub(/[[:space:]]+$/, "", slug); sub(/:$/, "", slug); in_company=(slug == company); next
17
+ }
18
+ in_company && /^ [^[:space:]#]/ { exit }
19
+ in_company && $1 == "cloud_uid:" { gsub(/["\047]/, "", $2); sub(/[[:space:]]+$/, "", $2); print $2; exit }
20
+ ' "$manifest"
21
+ }
22
+
23
+ valid_names() {
24
+ case "$1" in *[!A-Za-z0-9_-]*|"") return 1 ;; esac
25
+ case "$2" in *[!A-Za-z0-9._-]*|"") return 1 ;; esac
26
+ }
27
+
28
+ board_path() { printf '%s\n' "$ROOT/companies/$1/board.json"; }
29
+
30
+ update_prompt_state() {
31
+ local company="$1" project="$2" state="$3" board tmp
32
+ board="$(board_path "$company")"
33
+ [ -f "$board" ] || die "no company board for $company"
34
+ jq -e --arg id "$project" 'any(.projects[]?; .id == $id or .mesh_project_id == $id)' "$board" >/dev/null \
35
+ || die "no local Board project $project for $company"
36
+ tmp="$(mktemp)"
37
+ jq --arg id "$project" --arg state "$state" '
38
+ .projects |= map(if .id == $id or .mesh_project_id == $id then . + {mesh_registration_prompt_state:$state} else . end)
39
+ ' "$board" > "$tmp"
40
+ mv "$tmp" "$board"
41
+ }
42
+
43
+ check_offer() {
44
+ local company="$1" project="$2" uid board row_state enabled
45
+ valid_names "$company" "$project" || die "invalid company or project id"
46
+ uid="$(resolve_cloud_uid "$company")"
47
+ [ -n "$uid" ] || { printf 'local\n'; return 0; }
48
+ [[ "$uid" =~ ^cmp_[A-Za-z0-9]{3,128}$ ]] || { printf 'local\n'; return 0; }
49
+ board="$(board_path "$company")"
50
+ [ -f "$board" ] || { printf 'missing\n'; return 0; }
51
+ row_state="$(jq -r --arg id "$project" '
52
+ [.projects[]? | select(.id == $id or .mesh_project_id == $id)] | first |
53
+ if . == null then "missing"
54
+ elif (.threadId // "") != "" and (.channelId // "") != "" then "registered"
55
+ else (.mesh_registration_prompt_state // "offer") end
56
+ ' "$board" 2>/dev/null || printf 'missing')"
57
+ case "$row_state" in
58
+ accepted|deferred|registered|missing) printf '%s\n' "$row_state"; return 0 ;;
59
+ esac
60
+
61
+ [ -f "$FLAG_READER" ] && command -v node >/dev/null 2>&1 || { printf 'off\n'; return 0; }
62
+ enabled="$(HQ_COMPANY_UID="$uid" HQ_COMPANY_SLUG="$company" \
63
+ HQ_CLI_BIN="$(command -v hq 2>/dev/null || true)" node "$FLAG_READER" 2>/dev/null || printf 'false')"
64
+ [ "$enabled" = "true" ] && printf 'offer\n' || printf 'off\n'
65
+ }
66
+
67
+ if [ "${1:-}" = "--check" ] && [ "$#" -eq 3 ]; then
68
+ check_offer "$2" "$3"
69
+ elif [ "${1:-}" = "--accept" ] && [ "$#" -eq 3 ]; then
70
+ valid_names "$2" "$3" || die "invalid company or project id"
71
+ update_prompt_state "$2" "$3" accepted
72
+ elif [ "${1:-}" = "--defer" ] && [ "$#" -eq 3 ]; then
73
+ valid_names "$2" "$3" || die "invalid company or project id"
74
+ update_prompt_state "$2" "$3" deferred
75
+ else
76
+ die "usage: work-mesh-project-registration-offer.sh --check <company> <project-id> | --accept <company> <project-id> | --defer <company> <project-id>"
77
+ fi