@indigoai-us/hq-cli 5.86.0 → 5.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +143 -0
  3. package/assets/scaffold/core/scripts/share-worker-skill.sh +178 -0
  4. package/dist/commands/core.d.ts +29 -3
  5. package/dist/commands/core.js +121 -24
  6. package/dist/commands/index-cmd.d.ts +4 -0
  7. package/dist/commands/index-cmd.js +34 -0
  8. package/dist/lib/index-render/companies.d.ts +3 -0
  9. package/dist/lib/index-render/companies.js +57 -0
  10. package/dist/lib/index-render/company-knowledge.d.ts +3 -0
  11. package/dist/lib/index-render/company-knowledge.js +85 -0
  12. package/dist/lib/index-render/index.d.ts +5 -0
  13. package/dist/lib/index-render/index.js +43 -0
  14. package/dist/lib/index-render/orchestrator.d.ts +3 -0
  15. package/dist/lib/index-render/orchestrator.js +52 -0
  16. package/dist/lib/index-render/projects.d.ts +3 -0
  17. package/dist/lib/index-render/projects.js +33 -0
  18. package/dist/lib/index-render/public-knowledge.d.ts +3 -0
  19. package/dist/lib/index-render/public-knowledge.js +37 -0
  20. package/dist/lib/index-render/reports.d.ts +3 -0
  21. package/dist/lib/index-render/reports.js +37 -0
  22. package/dist/lib/index-render/shared.d.ts +35 -0
  23. package/dist/lib/index-render/shared.js +126 -0
  24. package/dist/lib/index-render/social-drafts.d.ts +3 -0
  25. package/dist/lib/index-render/social-drafts.js +53 -0
  26. package/dist/lib/index-render/threads.d.ts +3 -0
  27. package/dist/lib/index-render/threads.js +42 -0
  28. package/dist/lib/index-render/workers.d.ts +3 -0
  29. package/dist/lib/index-render/workers.js +49 -0
  30. package/dist/lib/search-index/background.d.ts +39 -0
  31. package/dist/lib/search-index/background.js +382 -0
  32. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.88.0]
6
+
7
+ ### Changed
8
+
9
+ - Replaced the ten bundled `rebuild-*-index.sh` assets behind
10
+ `hq core rebuild-index <target>` with native, tested TypeScript renderers.
11
+ The command surface and scaffold-only assets remain unchanged, while rebuilds
12
+ no longer depend on bundled scaffold assets being present. (#311)
13
+
14
+ ## [5.87.0]
15
+
16
+ ### Added
17
+
18
+ - Added the hidden `hq core worker` subgroup for worker-scoped skill
19
+ maintenance: `hq core worker lint` fails when a skill shared by multiple
20
+ workers is duplicated by copy instead of single-sourced (or a shared-skill
21
+ symlink dangles), and `hq core worker share` migrates duplicated copies onto
22
+ one canonical file plus relative symlinks (dry-run by default; refuses
23
+ cross-scope paths and never silently clobbers a drifted copy). (#309)
24
+ - Added single-flight background reindex. (#308)
25
+
5
26
  ## [5.86.0]
6
27
 
7
28
  ### Added
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # lint-shared-worker-skills.sh — fail if a worker-scoped skill is duplicated by
3
+ # copy instead of being single-sourced through the shared-skills convention.
4
+ #
5
+ # THE DRIFT PROBLEM this guards against: a skill used by several workers used to
6
+ # be physically COPIED into each worker's skills/ directory. Editing one copy
7
+ # left the others stale, so the copies drifted apart over time (e.g. a shared
8
+ # e2e-testing skill splitting into two divergent versions). The fix is to keep
9
+ # ONE canonical file under a scope-appropriate `_shared-skills/` store and point
10
+ # each sharing worker's skill entry at it with a relative symlink — see
11
+ # core/knowledge/public/hq-core/shared-worker-skills.md.
12
+ #
13
+ # This linter makes that convention enforceable. It flags two failure shapes:
14
+ #
15
+ # DUPLICATE — two or more NON-symlink skill files, in any worker under the
16
+ # scanned roots, whose byte content is identical. Identical bytes
17
+ # across two real files is exactly an un-single-sourced copy: the
18
+ # moment someone edits one, they drift. (Symlinks that resolve to
19
+ # a shared canonical are single-sourced and are NOT flagged, even
20
+ # though their resolved content matches the canonical.)
21
+ # BROKEN — a skill entry that is a symlink whose target does not resolve.
22
+ #
23
+ # It deliberately does NOT flag two same-NAMED skills whose content differs
24
+ # (e.g. an API-level vs a browser-level e2e skill): distinct content means they
25
+ # are distinct skills that merely share a filename, not a drifted share.
26
+ #
27
+ # Usage: lint-shared-worker-skills.sh [root ...]
28
+ # Default roots: core/workers core/packages (the SHIPPED hq-core scope —
29
+ # company `_shared-skills/` stores live under companies/<co>/ and are linted
30
+ # per-tenant, not here).
31
+ #
32
+ # Exit 0 + "OK:" line when clean; exit 1 + a report naming every offending group
33
+ # when not. Matches the loud-and-specific style of lint-skill-script-refs.sh.
34
+
35
+ set -euo pipefail
36
+
37
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
38
+ cd "$repo_root"
39
+
40
+ roots=("$@")
41
+ if [[ ${#roots[@]} -eq 0 ]]; then
42
+ roots=(core/workers core/packages)
43
+ fi
44
+
45
+ # Content hash of a file, portable across Linux and macOS (neither guaranteed to
46
+ # ship the other's tool): md5sum (GNU) → md5 (BSD/macOS) → shasum (Perl, both).
47
+ content_hash() {
48
+ if command -v md5sum >/dev/null 2>&1; then
49
+ md5sum "$1" | cut -d' ' -f1
50
+ elif command -v md5 >/dev/null 2>&1; then
51
+ md5 -q "$1"
52
+ else
53
+ shasum "$1" | cut -d' ' -f1
54
+ fi
55
+ }
56
+
57
+ # Collect skill entries: regular files AND symlinks named *.md under any
58
+ # .../skills/... path. -type l must be matched explicitly — a symlink is not a
59
+ # -type f, so a skills symlink would otherwise be invisible to the scan.
60
+ mapfile -t entries < <(
61
+ find "${roots[@]}" \( -type f -o -type l \) -path '*/skills/*.md' 2>/dev/null | sort
62
+ )
63
+
64
+ # Empty is clean — and guards `"${entries[@]}"` under `set -u` on bash 3.2
65
+ # (macOS), which errors on an empty array expansion.
66
+ if [[ ${#entries[@]} -eq 0 ]]; then
67
+ echo "OK: worker-scoped skills are single-sourced (0 skill entries under: ${roots[*]})"
68
+ exit 0
69
+ fi
70
+
71
+ broken=()
72
+ # Parallel arrays keyed by content hash: hash_keys[i] is a hash, and
73
+ # hash_regfiles[i] is a newline-joined list of the NON-symlink files with that
74
+ # hash. Bash 3.2 (macOS) has no associative arrays in a portable-guaranteed way,
75
+ # so a linear scan over parallel arrays keeps this runnable everywhere HQ runs.
76
+ hash_keys=()
77
+ hash_regfiles=()
78
+
79
+ hash_index() { # echo the index of $1 in hash_keys, or -1
80
+ local want="$1" i
81
+ for i in "${!hash_keys[@]}"; do
82
+ if [[ "${hash_keys[$i]}" == "$want" ]]; then
83
+ echo "$i"; return 0
84
+ fi
85
+ done
86
+ echo "-1"
87
+ }
88
+
89
+ for entry in "${entries[@]}"; do
90
+ if [[ -L "$entry" ]]; then
91
+ # Symlink: single-sourced by design. Only a DANGLING one is a problem.
92
+ if [[ ! -e "$entry" ]]; then
93
+ broken+=("$entry")
94
+ fi
95
+ continue
96
+ fi
97
+ # Regular file: hash its bytes and bucket it. Two regular files sharing a
98
+ # bucket are two copies of the same skill — the drift hazard.
99
+ h="$(content_hash "$entry")"
100
+ idx="$(hash_index "$h")"
101
+ if [[ "$idx" == "-1" ]]; then
102
+ hash_keys+=("$h")
103
+ hash_regfiles+=("$entry")
104
+ else
105
+ hash_regfiles[$idx]="${hash_regfiles[$idx]}"$'\n'"$entry"
106
+ fi
107
+ done
108
+
109
+ findings=0
110
+
111
+ for i in "${!hash_keys[@]}"; do
112
+ group="${hash_regfiles[$i]}"
113
+ count="$(printf '%s\n' "$group" | grep -c .)"
114
+ if [[ "$count" -ge 2 ]]; then
115
+ if [[ $findings -eq 0 ]]; then
116
+ echo "lint-shared-worker-skills: FAIL — duplicated worker skills (single-source these via _shared-skills/ + relative symlinks):" >&2
117
+ fi
118
+ findings=$((findings + 1))
119
+ echo " DUPLICATE (identical content, ${count} copies — pick one canonical and symlink the rest):" >&2
120
+ printf ' %s\n' "$group" >&2
121
+ fi
122
+ done
123
+
124
+ if [[ ${#broken[@]} -gt 0 ]]; then
125
+ if [[ $findings -eq 0 ]]; then
126
+ echo "lint-shared-worker-skills: FAIL — broken shared-skill symlink(s):" >&2
127
+ fi
128
+ findings=$((findings + ${#broken[@]}))
129
+ echo " BROKEN (symlink target does not resolve):" >&2
130
+ printf ' %s\n' "${broken[@]}" >&2
131
+ fi
132
+
133
+ if [[ $findings -gt 0 ]]; then
134
+ echo "" >&2
135
+ echo " Fix: keep ONE canonical file in the narrowest-scope _shared-skills/ store" >&2
136
+ echo " (core/workers/_shared-skills, core/packages/<pack>/workers/_shared-skills," >&2
137
+ echo " or companies/<co>/workers/_shared-skills) and replace each duplicate with a" >&2
138
+ echo " relative symlink. Helper: hq core worker share" >&2
139
+ echo " Doc: core/knowledge/public/hq-core/shared-worker-skills.md" >&2
140
+ exit 1
141
+ fi
142
+
143
+ echo "OK: worker-scoped skills are single-sourced (${#entries[@]} skill entries scanned under: ${roots[*]})"
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bash
2
+ # share-worker-skill.sh — convert duplicated worker-scoped skill copies into a
3
+ # single canonical file plus relative symlinks, the migration step for the
4
+ # shared-worker-skills convention (core/knowledge/public/hq-core/shared-worker-skills.md).
5
+ #
6
+ # This performs the ONLY mechanical move the design needs: it never edits any
7
+ # worker.yaml (the skill name still resolves to the same path — only the file
8
+ # type changes from regular file to symlink), and it never touches a skill
9
+ # resolver. `/run` reads {worker}/skills/{skill}.md and follows the symlink
10
+ # transparently, so the change is purely file-layout.
11
+ #
12
+ # Usage:
13
+ # share-worker-skill.sh <scope-dir> <skill-name> <worker-skill-path> [<worker-skill-path> ...]
14
+ #
15
+ # <scope-dir> The narrowest scope that contains EVERY sharing worker,
16
+ # e.g. core/workers, core/packages/<pack>/workers, or
17
+ # companies/<co>/workers. The canonical file is created at
18
+ # <scope-dir>/_shared-skills/<skill-name>.md.
19
+ # <skill-name> Bare skill name (no .md), e.g. e2e-testing.
20
+ # <worker-skill-path> Each worker's current copy, e.g.
21
+ # core/workers/foo/skills/e2e-testing.md
22
+ #
23
+ # Safety rails:
24
+ # * Every worker path MUST live under <scope-dir> (no cross-scope symlinks,
25
+ # which would break on pack install / hq-sync). Refuses otherwise.
26
+ # * If no canonical exists yet, the FIRST worker path is promoted to canonical.
27
+ # * A worker copy whose content DIFFERS from the canonical is a DRIFTED copy:
28
+ # the script refuses and tells you to reconcile by hand first (the human
29
+ # decision the design reserves — never a silent overwrite). Re-run after
30
+ # reconciling, or pass --force to accept the canonical for that copy.
31
+ # * Idempotent: a path already symlinked to the canonical is left alone.
32
+ # * DRY-RUN BY DEFAULT. Pass --apply to make changes.
33
+
34
+ set -euo pipefail
35
+
36
+ APPLY=0
37
+ FORCE=0
38
+ args=()
39
+ for a in "$@"; do
40
+ case "$a" in
41
+ --apply) APPLY=1 ;;
42
+ --force) FORCE=1 ;;
43
+ -*) echo "unknown flag: $a" >&2; exit 2 ;;
44
+ *) args+=("$a") ;;
45
+ esac
46
+ done
47
+
48
+ if [[ ${#args[@]} -lt 3 ]]; then
49
+ echo "usage: $0 [--apply] [--force] <scope-dir> <skill-name> <worker-skill-path>..." >&2
50
+ exit 2
51
+ fi
52
+
53
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
54
+ cd "$repo_root"
55
+
56
+ scope_dir="${args[0]%/}"
57
+ skill_name="${args[1]}"
58
+ worker_paths=("${args[@]:2}")
59
+
60
+ if [[ ! "$skill_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
61
+ echo "error: skill-name '$skill_name' is not a bare skill name (no slashes, no .md)" >&2
62
+ exit 2
63
+ fi
64
+ if [[ ! -d "$scope_dir" ]]; then
65
+ echo "error: scope dir does not exist: $scope_dir" >&2
66
+ exit 2
67
+ fi
68
+
69
+ canonical="$scope_dir/_shared-skills/$skill_name.md"
70
+
71
+ note() { printf '%s\n' "$*"; }
72
+ would() { if [[ $APPLY -eq 1 ]]; then note " did: $*"; else note " would: $*"; fi; }
73
+
74
+ # Normalize a path to repo-relative without requiring GNU realpath (macOS-safe).
75
+ rel_under() { # rel_under <path> <dir> → 0 if <path> is inside <dir>
76
+ local p="${1#./}" d="${2#./}"
77
+ [[ "$p" == "$d/"* ]]
78
+ }
79
+
80
+ # rel_path <from-dir> <to-file> → relative path from <from-dir> to <to-file>,
81
+ # computed purely from the two repo-relative strings (both already normalized,
82
+ # no `..` segments, no symlinks in the literal path). Portable: no python3, no
83
+ # GNU realpath --relative-to (macOS ships neither).
84
+ rel_path() {
85
+ local from="${1#./}" to="${2#./}"
86
+ local -a fa ta
87
+ IFS='/' read -r -a fa <<< "$from"
88
+ IFS='/' read -r -a ta <<< "$to"
89
+ local i=0
90
+ while [[ $i -lt ${#fa[@]} && $i -lt ${#ta[@]} && "${fa[$i]}" == "${ta[$i]}" ]]; do
91
+ i=$((i + 1))
92
+ done
93
+ local up="" j
94
+ for (( j=i; j<${#fa[@]}; j++ )); do up="../$up"; done
95
+ local down="" k
96
+ for (( k=i; k<${#ta[@]}; k++ )); do down="$down${ta[$k]}/"; done
97
+ down="${down%/}"
98
+ printf '%s%s' "$up" "$down"
99
+ }
100
+
101
+ # Every worker path must be inside the scope dir.
102
+ for wp in "${worker_paths[@]}"; do
103
+ wp="${wp#./}"
104
+ if ! rel_under "$wp" "$scope_dir"; then
105
+ echo "error: worker path '$wp' is not under scope dir '$scope_dir'" >&2
106
+ echo " canonical must live in the narrowest scope covering all sharers;" >&2
107
+ echo " cross-scope symlinks are disallowed. Aborting (no changes made)." >&2
108
+ exit 2
109
+ fi
110
+ done
111
+
112
+ note "Canonical: $canonical"
113
+ note "Scope: $scope_dir"
114
+ [[ $APPLY -eq 1 ]] || note "(dry run — pass --apply to make changes)"
115
+
116
+ # 1) Establish the canonical file.
117
+ if [[ -e "$canonical" && ! -L "$canonical" ]]; then
118
+ note "canonical already exists (regular file) — reusing it"
119
+ else
120
+ # Promote the first worker path that is a real (non-symlink) file.
121
+ seed=""
122
+ for wp in "${worker_paths[@]}"; do
123
+ if [[ -f "$wp" && ! -L "$wp" ]]; then seed="$wp"; break; fi
124
+ done
125
+ if [[ -z "$seed" ]]; then
126
+ echo "error: no canonical exists and no worker path is a regular file to seed it from" >&2
127
+ exit 2
128
+ fi
129
+ would "mkdir -p $scope_dir/_shared-skills"
130
+ would "git mv $seed $canonical (promote first copy to canonical)"
131
+ if [[ $APPLY -eq 1 ]]; then
132
+ mkdir -p "$scope_dir/_shared-skills"
133
+ git mv "$seed" "$canonical" 2>/dev/null || mv "$seed" "$canonical"
134
+ fi
135
+ fi
136
+
137
+ # 2) Point each worker path at the canonical via a relative symlink.
138
+ for wp in "${worker_paths[@]}"; do
139
+ wp="${wp#./}"
140
+ # Already the canonical file itself (post-promotion) → link it too so every
141
+ # worker slot is a symlink and the canonical lives only under _shared-skills.
142
+ target_dir="$(dirname "$wp")"
143
+ # Relative path from the worker's skills dir to the canonical (portable).
144
+ rel="$(rel_path "$target_dir" "$canonical")"
145
+
146
+ if [[ -L "$wp" ]]; then
147
+ cur="$(readlink "$wp")"
148
+ if [[ "$cur" == "$rel" ]]; then
149
+ note "ok: $wp already links to canonical"
150
+ continue
151
+ fi
152
+ would "relink $wp -> $rel (was: $cur)"
153
+ if [[ $APPLY -eq 1 ]]; then ln -sfn "$rel" "$wp"; fi
154
+ continue
155
+ fi
156
+
157
+ if [[ -e "$wp" ]]; then
158
+ # Regular file present — must match canonical or be reconciled first.
159
+ if cmp -s "$wp" "$canonical"; then
160
+ would "replace identical copy $wp with symlink -> $rel"
161
+ if [[ $APPLY -eq 1 ]]; then rm -f "$wp"; ln -s "$rel" "$wp"; fi
162
+ else
163
+ if [[ $FORCE -eq 1 ]]; then
164
+ would "FORCE replace DRIFTED copy $wp with symlink -> $rel (content discarded)"
165
+ if [[ $APPLY -eq 1 ]]; then rm -f "$wp"; ln -s "$rel" "$wp"; fi
166
+ else
167
+ echo " DRIFT: $wp differs from canonical — reconcile by hand, then re-run" >&2
168
+ echo " (diff $wp $canonical), or pass --force to accept canonical." >&2
169
+ exit 3
170
+ fi
171
+ fi
172
+ else
173
+ would "create symlink $wp -> $rel"
174
+ if [[ $APPLY -eq 1 ]]; then mkdir -p "$target_dir"; ln -s "$rel" "$wp"; fi
175
+ fi
176
+ done
177
+
178
+ note "done."
@@ -29,6 +29,7 @@
29
29
  * `ScaffoldRoot`.
30
30
  */
31
31
  import { Command } from "commander";
32
+ import { type IndexTarget } from "../lib/index-render/index.js";
32
33
  /**
33
34
  * Which tree a scaffold script operates on, mirroring how the ORIGINAL script
34
35
  * found its root before it moved. Getting this wrong is not cosmetic: injecting a
@@ -64,14 +65,33 @@ export type ScaffoldCommand = ScaffoldAsset & {
64
65
  /** Direct subcommand name — `hq core <name>`. */
65
66
  name: string;
66
67
  };
67
- export type RebuildIndexTarget = ScaffoldAsset & {
68
+ export type RebuildIndexTarget = {
68
69
  /** Rebuild target — `hq core rebuild-index <target>`. */
69
70
  target: string;
71
+ /** Native renderer identifier. */
72
+ renderer: IndexTarget;
73
+ /** One-line summary, shown in `hq core --help`. */
74
+ summary: string;
75
+ };
76
+ export type WorkerSubcommand = ScaffoldAsset & {
77
+ /** Subcommand name under the `worker` group — `hq core worker <name>`. */
78
+ name: string;
70
79
  };
80
+ /**
81
+ * The `hq core worker <name>` subgroup — worker-scoped skill maintenance.
82
+ *
83
+ * Both are cold, explicitly-invoked operations (a linter and a one-shot
84
+ * migration), which the scaffold-vs-cli-code-ownership policy permits in the
85
+ * CLI. They are `root: "cwd"` because each derives its root from the caller
86
+ * (`git rev-parse --show-toplevel || pwd`): the CLI must inject nothing, or it
87
+ * would retarget the lint at the CLI's own checkout instead of the HQ tree the
88
+ * operator is standing in. See core/knowledge/public/hq-core/shared-worker-skills.md.
89
+ */
90
+ export declare const WORKER_SUBCOMMANDS: WorkerSubcommand[];
71
91
  /**
72
92
  * The ten index rebuild targets. The command registration below intentionally
73
93
  * loops over this table for lookup, so adding a target never requires another
74
- * action handler and every target retains its own bundled asset.
94
+ * action handler and every target retains its own native renderer.
75
95
  */
76
96
  export declare const REBUILD_INDEX_TARGETS: RebuildIndexTarget[];
77
97
  /**
@@ -83,7 +103,13 @@ export declare const REBUILD_INDEX_TARGETS: RebuildIndexTarget[];
83
103
  * would make the forwarders harder to audit against this table.
84
104
  */
85
105
  export declare const SCAFFOLD_COMMANDS: ScaffoldCommand[];
86
- /** Every bundled asset, used by packaging and interpreter tests. */
106
+ /**
107
+ * Bundled solely for HQ-root scaffolds, which still need these as loose files.
108
+ * `rebuild-index` dispatches them natively since this change, so none has a
109
+ * direct `hq core` command.
110
+ */
111
+ export declare const SCAFFOLD_ONLY_ASSETS: ScaffoldAsset[];
112
+ /** Every claimed bundled asset, used by packaging and interpreter tests. */
87
113
  export declare const SCAFFOLD_ASSETS: ScaffoldAsset[];
88
114
  export declare function registerCoreCommands(program: Command): Command;
89
115
  export {};
@@ -32,70 +32,85 @@ import { Option } from "commander";
32
32
  import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
33
33
  import { resolveLiveRoot } from "../utils/hq-roots.js";
34
34
  import { runBundledScript } from "../utils/run-bundled-script.js";
35
+ import { renderIndexTarget } from "../lib/index-render/index.js";
36
+ /**
37
+ * The `hq core worker <name>` subgroup — worker-scoped skill maintenance.
38
+ *
39
+ * Both are cold, explicitly-invoked operations (a linter and a one-shot
40
+ * migration), which the scaffold-vs-cli-code-ownership policy permits in the
41
+ * CLI. They are `root: "cwd"` because each derives its root from the caller
42
+ * (`git rev-parse --show-toplevel || pwd`): the CLI must inject nothing, or it
43
+ * would retarget the lint at the CLI's own checkout instead of the HQ tree the
44
+ * operator is standing in. See core/knowledge/public/hq-core/shared-worker-skills.md.
45
+ */
46
+ export const WORKER_SUBCOMMANDS = [
47
+ {
48
+ name: "lint",
49
+ asset: "core/scripts/lint-shared-worker-skills.sh",
50
+ root: "cwd",
51
+ summary: "Fail on worker-scoped skills duplicated by copy instead of single-sourced",
52
+ },
53
+ {
54
+ name: "share",
55
+ asset: "core/scripts/share-worker-skill.sh",
56
+ root: "cwd",
57
+ summary: "Migrate duplicated worker skills onto one canonical file + relative symlinks",
58
+ },
59
+ ];
35
60
  /**
36
61
  * The ten index rebuild targets. The command registration below intentionally
37
62
  * loops over this table for lookup, so adding a target never requires another
38
- * action handler and every target retains its own bundled asset.
63
+ * action handler and every target retains its own native renderer.
39
64
  */
40
65
  export const REBUILD_INDEX_TARGETS = [
41
66
  {
42
67
  target: "all",
43
- asset: "core/scripts/rebuild-all-indexes.sh",
44
- root: "live",
68
+ renderer: "all",
45
69
  summary: "Regenerate every INDEX.md across the HQ tree",
46
70
  },
47
71
  {
48
72
  target: "companies",
49
- asset: "core/scripts/rebuild-companies-index.sh",
50
- root: "live",
73
+ renderer: "companies",
51
74
  summary: "Regenerate companies/INDEX.md",
52
75
  },
53
76
  {
54
77
  target: "company-knowledge",
55
- asset: "core/scripts/rebuild-company-knowledge-index.sh",
56
- root: "live",
78
+ renderer: "company-knowledge",
57
79
  summary: "Regenerate a company's knowledge INDEX.md",
58
80
  },
59
81
  {
60
82
  target: "orchestrator",
61
- asset: "core/scripts/rebuild-orchestrator-index.sh",
62
- root: "live",
83
+ renderer: "orchestrator",
63
84
  summary: "Regenerate the orchestrator workspace INDEX.md",
64
85
  },
65
86
  {
66
87
  target: "projects",
67
- asset: "core/scripts/rebuild-projects-index.sh",
68
- root: "live",
88
+ renderer: "projects",
69
89
  summary: "Regenerate the projects INDEX.md",
70
90
  },
71
91
  {
72
92
  target: "public-knowledge",
73
- asset: "core/scripts/rebuild-public-knowledge-index.sh",
74
- root: "live",
93
+ renderer: "public-knowledge",
75
94
  summary: "Regenerate core/knowledge/public/INDEX.md",
76
95
  },
77
96
  {
78
97
  target: "reports",
79
- asset: "core/scripts/rebuild-reports-index.sh",
80
- root: "live",
98
+ renderer: "reports",
81
99
  summary: "Regenerate the reports INDEX.md",
82
100
  },
83
101
  {
84
102
  target: "social-drafts",
85
- asset: "core/scripts/rebuild-social-drafts-index.sh",
86
- root: "live",
103
+ renderer: "social-drafts",
87
104
  summary: "Regenerate workspace/social-drafts/INDEX.md",
88
105
  },
89
106
  {
90
107
  target: "threads",
91
- asset: "core/scripts/rebuild-threads-index.sh",
92
- root: "live",
108
+ renderer: "threads",
93
109
  summary: "Regenerate the session-threads INDEX.md",
94
110
  },
95
111
  {
96
112
  target: "workers",
97
- asset: "core/scripts/rebuild-workers-index.sh",
98
- root: "live",
113
+ renderer: "workers",
99
114
  summary: "Regenerate the workers INDEX.md",
100
115
  },
101
116
  ];
@@ -177,10 +192,68 @@ export const SCAFFOLD_COMMANDS = [
177
192
  summary: "Create a git worktree under workspace/worktrees/",
178
193
  },
179
194
  ];
180
- /** Every bundled asset, used by packaging and interpreter tests. */
195
+ /**
196
+ * Bundled solely for HQ-root scaffolds, which still need these as loose files.
197
+ * `rebuild-index` dispatches them natively since this change, so none has a
198
+ * direct `hq core` command.
199
+ */
200
+ export const SCAFFOLD_ONLY_ASSETS = [
201
+ {
202
+ asset: "core/scripts/rebuild-all-indexes.sh",
203
+ root: "cwd",
204
+ summary: "Scaffold-only native rebuild-index companion",
205
+ },
206
+ {
207
+ asset: "core/scripts/rebuild-companies-index.sh",
208
+ root: "cwd",
209
+ summary: "Scaffold-only native rebuild-index companion",
210
+ },
211
+ {
212
+ asset: "core/scripts/rebuild-company-knowledge-index.sh",
213
+ root: "cwd",
214
+ summary: "Scaffold-only native rebuild-index companion",
215
+ },
216
+ {
217
+ asset: "core/scripts/rebuild-orchestrator-index.sh",
218
+ root: "cwd",
219
+ summary: "Scaffold-only native rebuild-index companion",
220
+ },
221
+ {
222
+ asset: "core/scripts/rebuild-projects-index.sh",
223
+ root: "cwd",
224
+ summary: "Scaffold-only native rebuild-index companion",
225
+ },
226
+ {
227
+ asset: "core/scripts/rebuild-public-knowledge-index.sh",
228
+ root: "cwd",
229
+ summary: "Scaffold-only native rebuild-index companion",
230
+ },
231
+ {
232
+ asset: "core/scripts/rebuild-reports-index.sh",
233
+ root: "cwd",
234
+ summary: "Scaffold-only native rebuild-index companion",
235
+ },
236
+ {
237
+ asset: "core/scripts/rebuild-social-drafts-index.sh",
238
+ root: "cwd",
239
+ summary: "Scaffold-only native rebuild-index companion",
240
+ },
241
+ {
242
+ asset: "core/scripts/rebuild-threads-index.sh",
243
+ root: "cwd",
244
+ summary: "Scaffold-only native rebuild-index companion",
245
+ },
246
+ {
247
+ asset: "core/scripts/rebuild-workers-index.sh",
248
+ root: "cwd",
249
+ summary: "Scaffold-only native rebuild-index companion",
250
+ },
251
+ ];
252
+ /** Every claimed bundled asset, used by packaging and interpreter tests. */
181
253
  export const SCAFFOLD_ASSETS = [
182
- ...REBUILD_INDEX_TARGETS,
183
254
  ...SCAFFOLD_COMMANDS,
255
+ ...SCAFFOLD_ONLY_ASSETS,
256
+ ...WORKER_SUBCOMMANDS,
184
257
  ];
185
258
  /**
186
259
  * Resolve the tree an entry runs against, and the cwd to run it in.
@@ -237,6 +310,27 @@ export function registerCoreCommands(program) {
237
310
  // This group primarily hosts manifest-driven bundled assets, but it also
238
311
  // hosts native TypeScript plumbing when a scaffold contract needs it.
239
312
  registerCoreCheckpointCommand(core);
313
+ // `hq core worker <name>` — a nested subgroup for worker-scoped skill
314
+ // maintenance. Nested (rather than flat `hq core worker-<name>`) so the two
315
+ // related operations read as one family.
316
+ const worker = core
317
+ .command("worker")
318
+ .description("Worker-scoped skill maintenance (lint, share)");
319
+ for (const entry of WORKER_SUBCOMMANDS) {
320
+ worker
321
+ .command(entry.name)
322
+ .description(entry.summary)
323
+ // Pure passthrough: the wrapped script owns its own argument grammar.
324
+ .allowUnknownOption()
325
+ .allowExcessArguments()
326
+ .helpOption(false)
327
+ .argument("[args...]", "arguments passed through to the script")
328
+ .action((args = [], _opts, cmd) => {
329
+ const scope = core.opts();
330
+ const operands = cmd.args.length > 0 ? cmd.args : args;
331
+ runEntry(entry, scope, operands);
332
+ });
333
+ }
240
334
  const runCommand = (entry, args = [], cmd, target) => {
241
335
  const scope = core.opts();
242
336
  // `cmd.args` is the authoritative operand list: with
@@ -261,7 +355,10 @@ export function registerCoreCommands(program) {
261
355
  if (!entry) {
262
356
  throw expectedUserError(`Unknown rebuild-index target "${target}". Valid targets: ${validTargets.join(", ")}.`);
263
357
  }
264
- runCommand(entry, args, cmd, target);
358
+ const scope = core.opts();
359
+ const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
360
+ const operands = cmd.args.length > 0 ? cmd.args : args;
361
+ renderIndexTarget(entry.renderer, { root: hqRoot, log: (message) => process.stderr.write(`${message}\n`) }, operands.slice(1));
265
362
  });
266
363
  for (const entry of SCAFFOLD_COMMANDS) {
267
364
  core
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { type RunQmdOptions, type SearchCollection, type QmdProcessResult } from '../lib/search-index/index.js';
3
+ import { type BackgroundDependencies, type BackgroundResult, type BackgroundStatus } from '../lib/search-index/background.js';
3
4
  export type SearchIndexDependencies = {
4
5
  reconcileCollections: (hqRoot: string) => unknown;
5
6
  deriveCollections: (hqRoot: string) => SearchCollection[];
@@ -7,6 +8,9 @@ export type SearchIndexDependencies = {
7
8
  resolveQmdBin: () => string;
8
9
  resolveQmdVersion: () => string | undefined;
9
10
  runQmd: (args: string[], options?: RunQmdOptions) => QmdProcessResult;
11
+ runBackgroundLauncher?: (dependencies: BackgroundDependencies) => BackgroundResult;
12
+ runBackgroundWorker?: (dependencies: BackgroundDependencies) => BackgroundResult;
13
+ backgroundStatus?: (dependencies: BackgroundDependencies) => BackgroundStatus;
10
14
  };
11
15
  /** Incrementally update qmd, embedding only when an operator explicitly asks. */
12
16
  export declare function syncSearchIndex(hqRoot: string, embed: boolean, dependencies?: SearchIndexDependencies): void;