@indigoai-us/hq-cli 5.345.75 → 5.345.77

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 (40) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +30 -37
  3. package/dist/command-catalog.generated.d.ts +83 -3
  4. package/dist/command-catalog.generated.js +100 -3
  5. package/dist/commands/access.d.ts +1 -0
  6. package/dist/commands/access.js +17 -5
  7. package/dist/commands/bot.d.ts +12 -0
  8. package/dist/commands/bot.js +54 -2
  9. package/dist/commands/core-native-family-flag.js +1 -1
  10. package/dist/commands/files.js +7 -2
  11. package/dist/commands/members.d.ts +42 -0
  12. package/dist/commands/members.js +151 -1
  13. package/dist/commands/secrets.js +1 -1
  14. package/dist/fast-scaffold.d.ts +1 -1
  15. package/dist/fast-scaffold.js +1 -0
  16. package/dist/lib/bot/api.d.ts +11 -0
  17. package/dist/lib/bot/api.js +2 -0
  18. package/dist/lib/bot/config.js +4 -2
  19. package/dist/lib/bot/inflight.d.ts +3 -0
  20. package/dist/lib/bot/probe.d.ts +57 -0
  21. package/dist/lib/bot/probe.js +100 -0
  22. package/dist/lib/bot/run.d.ts +4 -0
  23. package/dist/lib/bot/run.js +171 -37
  24. package/dist/lib/bot/runtime/codex.d.ts +67 -1
  25. package/dist/lib/bot/runtime/codex.js +197 -3
  26. package/dist/lib/bot/runtime/grok.js +2 -2
  27. package/dist/lib/bot/runtime/index.d.ts +8 -0
  28. package/dist/lib/bot/runtime/index.js +91 -5
  29. package/dist/lib/bot/runtime/messages-stream.js +1 -1
  30. package/dist/lib/bot/runtime/types.d.ts +33 -0
  31. package/dist/lib/bot/runtime/types.js +10 -0
  32. package/dist/lib/bot/runtime-failure.d.ts +113 -0
  33. package/dist/lib/bot/runtime-failure.js +180 -0
  34. package/dist/lib/bot/runtime-sign-in.js +2 -2
  35. package/dist/lib/bot/status.d.ts +21 -0
  36. package/dist/lib/index-render/companies.js +33 -16
  37. package/dist/lib/index-render/shared.d.ts +18 -0
  38. package/dist/lib/index-render/shared.js +24 -0
  39. package/dist/lib/lanes/monitor.js +30 -15
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.77] — 2026-10-10
6
+ - Local bots no longer fail a turn when the installed Codex is too old for the
7
+ model it was asked to use (often the model a newer Codex app set in your own
8
+ Codex settings). The bot runs the same turn once more on a model your Codex
9
+ supports and notes it in `hq bot status`.
10
+ - A bot whose coding tool fails now tells you in plain words what is wrong and
11
+ the one thing to do (sign in, update, choose another model, or try again),
12
+ never the tool's raw error, and never blames the sign-in unless it is the
13
+ sign-in. The DM also carries a repair card for the HQ desktop app.
14
+ - After five model failures in ten minutes a local bot now says it is pausing,
15
+ holds its turns for ten minutes, and then answers the next message, instead
16
+ of exiting and staying down until `hq bot start`. A good turn clears the
17
+ recorded failure from `hq bot status`, which now shows a hold and a plain
18
+ problem line instead of the raw error.
19
+ - Bots created before this release: one that is still running restarts itself
20
+ onto the new hq-cli within a few minutes of the install and heals on its
21
+ first message. One that already exited after five failures on the old
22
+ release is brought back only by a login or reboot, the desktop's Start, or
23
+ `hq bot start`; after that it heals the same way.
24
+ - New `hq bot set-model <name> <model|default>` sets a bot's own model without
25
+ touching the coding tool's settings.
26
+ - New `hq bot probe (--runtime <runtime> | --bot <name>) [--model <model>]
27
+ --json` runs one tiny test turn exactly as a bot would, in an empty scratch
28
+ folder, and reports `{ ok, class, detail }`.
29
+ - Added `hq members remove <email>` to end an active membership and drop that
30
+ person's file and secret grants. `hq members revoke` still only cancels
31
+ pending invites. Guests stay active members, so company-wide grants (`@all` /
32
+ Everyone in company) still apply to them; demoting to guest does not lock
33
+ someone out. `hq access <path> --as <email>` now accepts an email and shows
34
+ that person's effective access.
35
+
36
+ ## [5.345.76] — 2026-10-10
37
+
38
+ - `hq bot` on macOS now finds the Codex CLI bundled in the ChatGPT or Codex
39
+ desktop app when `codex` is not on PATH, so a Mac with only the desktop app
40
+ can run a Codex assistant. PATH still wins.
41
+
42
+ - `hq bot create --display-name` now accepts numbers after the first letter,
43
+ so names like "R2D2" or "StefanTest123" work.
44
+ - Core delegate native commands now use the default-on kill-switch behavior when the flag snapshot or company context is unavailable. An explicit `core-native-delegate=false` override still selects the bundled shell.
45
+ - `hq core rebuild-index companies` now renders the same company INDEX.md on every machine and every day, and skips the write
46
+ when the content is unchanged (logged as `unchanged`). The header line is now `> Auto-generated.` with no date. The `repos/`
47
+ and `data/` rows show fixed labels instead of local counts, `knowledge/` always shows a subdirectory count, symlinked
48
+ children are no longer counted, and project status comes from prd.json only, not local orchestrator state.
49
+ - `hq lanes monitor-check` now recognizes a live `hq lanes watch` Monitor process when the hq
50
+ CLI is installed under a path containing spaces (for example the desktop toolchain under
51
+ `~/Library/Application Support/Indigo HQ/`). Previously those lanes stayed "uncovered" and the
52
+ Stop hook kept blocking the session.
5
53
  ## [5.345.75] — 2026-10-10
6
54
 
7
55
  - Native delegate manifest updates now use mode 0600 on POSIX, matching the
@@ -2,11 +2,16 @@
2
2
  # rebuild-companies-index.sh — regenerate companies/{co}/INDEX.md for each company.
3
3
  #
4
4
  # Pure bash + jq. Zero Claude context. Iterates companies/*/ (skips _template,
5
- # dotfiles), summarizes top-level dirs, scans projects/*/prd.json and
6
- # workspace/orchestrator/{name}/state.json to populate the Projects table.
5
+ # dotfiles), summarizes top-level dirs, scans projects/*/prd.json to populate
6
+ # the Projects table.
7
+ #
8
+ # Output is deterministic: the same synced tree renders the same bytes on every
9
+ # machine and every day. There is no date line, nothing reads local-only state
10
+ # (repos/, data/, an embedded knowledge .git, workspace/orchestrator), and an
11
+ # unchanged file is not rewritten.
7
12
  #
8
13
  # Schema per knowledge/public/hq-core/index-md-spec.md (Company Root variant):
9
- # - "# {Company Title}" + "> Auto-generated. Updated: YYYY-MM-DD"
14
+ # - "# {Company Title}" + "> Auto-generated."
10
15
  # - Top-level Name | Description table (dirs and 1-line desc)
11
16
  # - "## Projects" table (Project | Status | Description)
12
17
  # - "## Deployments" placeholder (None for now — manifest-driven enrichment deferred)
@@ -28,8 +33,6 @@ done
28
33
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
29
34
  cd "$HQ_ROOT"
30
35
 
31
- DATE=$(date -u +%Y-%m-%d)
32
-
33
36
  # Title-case a slug: "example-co" -> "Example Co"
34
37
  titleize() {
35
38
  echo "$1" | awk -F'-' '{
@@ -47,18 +50,13 @@ describe_dir() {
47
50
  [[ -d "$path" ]] || return 0
48
51
  case "$d" in
49
52
  data)
50
- local n
51
- n=$(find "$path" -mindepth 1 -maxdepth 1 ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
52
- echo "${n} item(s)"
53
+ echo "Company data"
53
54
  ;;
54
55
  knowledge)
55
- if [[ -e "${path}/.git" ]]; then
56
- echo "Embedded git repo"
57
- else
58
- local n
59
- n=$(find "$path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
60
- echo "Inline knowledge (${n} subdirs)"
61
- fi
56
+ local n
57
+ # Trailing slash: a symlinked knowledge/ is listed through its target.
58
+ n=$(find "${path}/" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
59
+ echo "Company knowledge (${n} subdirs)"
62
60
  ;;
63
61
  settings)
64
62
  local n
@@ -81,9 +79,7 @@ describe_dir() {
81
79
  echo "${n} worker(s)"
82
80
  ;;
83
81
  repos)
84
- local n
85
- n=$(find "$path" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
86
- echo "${n} repo link(s)"
82
+ echo "Local repo links (not synced)"
87
83
  ;;
88
84
  registry)
89
85
  local n
@@ -101,22 +97,10 @@ describe_dir() {
101
97
  esac
102
98
  }
103
99
 
104
- # Resolve project status: orchestrator state.json wins, else story-pass fallback.
100
+ # Resolve project status from prd.json alone (story-pass check). The local
101
+ # workspace/orchestrator state is not synced, so it is not consulted.
105
102
  project_status() {
106
- local project="$1"
107
- local prd="$2"
108
- local state="workspace/orchestrator/${project}/state.json"
109
- if [[ -f "$state" ]]; then
110
- local raw
111
- raw=$(jq -r '.status // "active"' "$state" 2>/dev/null || echo "active")
112
- case "$raw" in
113
- completed|complete) echo "complete" ;;
114
- in_progress|active|started) echo "active" ;;
115
- archived) echo "archived" ;;
116
- *) echo "$raw" ;;
117
- esac
118
- return
119
- fi
103
+ local prd="$1"
120
104
  if [[ -f "$prd" ]]; then
121
105
  local total passes
122
106
  total=$(jq '[.userStories[]?] | length' "$prd" 2>/dev/null || echo 0)
@@ -148,6 +132,7 @@ trunc() {
148
132
  write_company_index() {
149
133
  local co="$1"
150
134
  local out="companies/${co}/INDEX.md"
135
+ local tmp="companies/${co}/.INDEX.md.$$.tmp"
151
136
  local title
152
137
  title=$(titleize "$co")
153
138
 
@@ -155,7 +140,7 @@ write_company_index() {
155
140
  {
156
141
  echo "# ${title}"
157
142
  echo ""
158
- echo "> Auto-generated. Updated: ${DATE}"
143
+ echo "> Auto-generated."
159
144
  echo ""
160
145
  echo "| Name | Description |"
161
146
  echo "|------|-------------|"
@@ -187,7 +172,7 @@ write_company_index() {
187
172
  desc=$(jq -r '.description // ""' "$prd" 2>/dev/null || echo "")
188
173
  fi
189
174
  local status
190
- status=$(project_status "$name" "$prd")
175
+ status=$(project_status "$prd")
191
176
  desc=$(sanitize_cell "$desc")
192
177
  desc=$(trunc "$desc" 110)
193
178
  [[ -z "$desc" ]] && desc="—"
@@ -201,9 +186,17 @@ write_company_index() {
201
186
  echo "## Deployments"
202
187
  echo ""
203
188
  echo "_See \`companies/manifest.yaml\` for deployment targets._"
204
- } > "$out"
189
+ } > "$tmp"
205
190
 
206
- echo "rebuild-companies-index: wrote ${out}" >&2
191
+ # Rewrite only when the content changed, so an unchanged index keeps its
192
+ # mtime and is not re-uploaded by HQ Sync.
193
+ if [[ -f "$out" ]] && cmp -s "$tmp" "$out"; then
194
+ rm -f "$tmp"
195
+ echo "rebuild-companies-index: unchanged ${out}" >&2
196
+ else
197
+ mv -f "$tmp" "$out"
198
+ echo "rebuild-companies-index: wrote ${out}" >&2
199
+ fi
207
200
  }
208
201
 
209
202
  COUNT=0
@@ -2880,8 +2880,8 @@ export declare const COMMAND_CATALOG: readonly [{
2880
2880
  readonly description: "Company slug";
2881
2881
  readonly hidden: false;
2882
2882
  }, {
2883
- readonly flags: "--as <person-uid>";
2884
- readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
2883
+ readonly flags: "--as <person>";
2884
+ readonly description: "Check a person's access (email, prs_, or agt_ uid; self or owner/admin)";
2885
2885
  readonly hidden: false;
2886
2886
  }, {
2887
2887
  readonly flags: "--json";
@@ -3164,9 +3164,26 @@ export declare const COMMAND_CATALOG: readonly [{
3164
3164
  readonly hidden: false;
3165
3165
  }];
3166
3166
  readonly subcommands: readonly [];
3167
+ }, {
3168
+ readonly name: "remove";
3169
+ readonly description: "Remove an active member. Ends membership so company-wide grants no longer apply, and drops that person's file and secret grants. Accepts an email, prs_ personUid, agt_ agentUid, or full membership key.";
3170
+ readonly aliases: readonly [];
3171
+ readonly hidden: false;
3172
+ readonly helpOption: true;
3173
+ readonly helpCommand: false;
3174
+ readonly usage: "[options] <target>";
3175
+ readonly helpText: readonly [];
3176
+ readonly arguments: readonly [{
3177
+ readonly name: "target";
3178
+ readonly required: true;
3179
+ readonly variadic: false;
3180
+ readonly description: "";
3181
+ }];
3182
+ readonly options: readonly [];
3183
+ readonly subcommands: readonly [];
3167
3184
  }, {
3168
3185
  readonly name: "revoke";
3169
- readonly description: "Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.";
3186
+ readonly description: "Revoke a pending invite. For an active member, use `hq members remove` instead. Accepts an email, personUid, full membershipKey, or legacy inviteToken.";
3170
3187
  readonly aliases: readonly [];
3171
3188
  readonly hidden: false;
3172
3189
  readonly helpOption: true;
@@ -15644,6 +15661,69 @@ export declare const COMMAND_CATALOG: readonly [{
15644
15661
  readonly hidden: false;
15645
15662
  }];
15646
15663
  readonly subcommands: readonly [];
15664
+ }, {
15665
+ readonly name: "set-model";
15666
+ readonly description: "Set the model this bot asks its runtime for (stored in the bot's own settings; the runtime's own config is never changed), or default to clear it";
15667
+ readonly aliases: readonly [];
15668
+ readonly hidden: false;
15669
+ readonly helpOption: true;
15670
+ readonly helpCommand: false;
15671
+ readonly usage: "[options] <name> <model>";
15672
+ readonly helpText: readonly [];
15673
+ readonly arguments: readonly [{
15674
+ readonly name: "name";
15675
+ readonly required: true;
15676
+ readonly variadic: false;
15677
+ readonly description: "";
15678
+ }, {
15679
+ readonly name: "model";
15680
+ readonly required: true;
15681
+ readonly variadic: false;
15682
+ readonly description: "";
15683
+ }];
15684
+ readonly options: readonly [{
15685
+ readonly flags: "--json";
15686
+ readonly description: "Print machine-readable JSON";
15687
+ readonly hidden: false;
15688
+ }];
15689
+ readonly subcommands: readonly [];
15690
+ }, {
15691
+ readonly name: "probe";
15692
+ readonly description: "Run one minimal test turn with a runtime, model and thinking level exactly as a bot would, and say whether a bot set up that way can answer. Costs one tiny model turn on your own login; runs in an empty scratch folder, so no HQ settings or hooks load";
15693
+ readonly aliases: readonly [];
15694
+ readonly hidden: false;
15695
+ readonly helpOption: true;
15696
+ readonly helpCommand: false;
15697
+ readonly usage: "[options]";
15698
+ readonly helpText: readonly [];
15699
+ readonly arguments: readonly [];
15700
+ readonly options: readonly [{
15701
+ readonly flags: "--runtime <runtime>";
15702
+ readonly description: "Model CLI to check: claude|codex|grok (required unless --bot is given)";
15703
+ readonly hidden: false;
15704
+ }, {
15705
+ readonly flags: "--bot <name>";
15706
+ readonly description: "Check with this bot's saved runtime, model and thinking level (--runtime/--model/--effort override them)";
15707
+ readonly hidden: false;
15708
+ }, {
15709
+ readonly flags: "--model <model>";
15710
+ readonly description: "Model id to check, or default for the runtime's own default";
15711
+ readonly hidden: false;
15712
+ }, {
15713
+ readonly flags: "--effort <level>";
15714
+ readonly description: "Thinking level to check";
15715
+ readonly hidden: false;
15716
+ }, {
15717
+ readonly flags: "--timeout <seconds>";
15718
+ readonly description: "Give up after this many seconds";
15719
+ readonly hidden: false;
15720
+ readonly defaultValue: "120";
15721
+ }, {
15722
+ readonly flags: "--json";
15723
+ readonly description: "Print { ok, class, detail, ... } as JSON";
15724
+ readonly hidden: false;
15725
+ }];
15726
+ readonly subcommands: readonly [];
15647
15727
  }, {
15648
15728
  readonly name: "intro";
15649
15729
  readonly description: "Have the bot re-send its introduction DM to you";
@@ -3473,8 +3473,8 @@ export const COMMAND_CATALOG = [
3473
3473
  "hidden": false
3474
3474
  },
3475
3475
  {
3476
- "flags": "--as <person-uid>",
3477
- "description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
3476
+ "flags": "--as <person>",
3477
+ "description": "Check a person's access (email, prs_, or agt_ uid; self or owner/admin)",
3478
3478
  "hidden": false
3479
3479
  },
3480
3480
  {
@@ -3825,9 +3825,29 @@ export const COMMAND_CATALOG = [
3825
3825
  ],
3826
3826
  "subcommands": []
3827
3827
  },
3828
+ {
3829
+ "name": "remove",
3830
+ "description": "Remove an active member. Ends membership so company-wide grants no longer apply, and drops that person's file and secret grants. Accepts an email, prs_ personUid, agt_ agentUid, or full membership key.",
3831
+ "aliases": [],
3832
+ "hidden": false,
3833
+ "helpOption": true,
3834
+ "helpCommand": false,
3835
+ "usage": "[options] <target>",
3836
+ "helpText": [],
3837
+ "arguments": [
3838
+ {
3839
+ "name": "target",
3840
+ "required": true,
3841
+ "variadic": false,
3842
+ "description": ""
3843
+ }
3844
+ ],
3845
+ "options": [],
3846
+ "subcommands": []
3847
+ },
3828
3848
  {
3829
3849
  "name": "revoke",
3830
- "description": "Revoke a pending invite. Accepts an email, personUid, full membershipKey, or legacy inviteToken.",
3850
+ "description": "Revoke a pending invite. For an active member, use `hq members remove` instead. Accepts an email, personUid, full membershipKey, or legacy inviteToken.",
3831
3851
  "aliases": [],
3832
3852
  "hidden": false,
3833
3853
  "helpOption": true,
@@ -19052,6 +19072,83 @@ export const COMMAND_CATALOG = [
19052
19072
  ],
19053
19073
  "subcommands": []
19054
19074
  },
19075
+ {
19076
+ "name": "set-model",
19077
+ "description": "Set the model this bot asks its runtime for (stored in the bot's own settings; the runtime's own config is never changed), or default to clear it",
19078
+ "aliases": [],
19079
+ "hidden": false,
19080
+ "helpOption": true,
19081
+ "helpCommand": false,
19082
+ "usage": "[options] <name> <model>",
19083
+ "helpText": [],
19084
+ "arguments": [
19085
+ {
19086
+ "name": "name",
19087
+ "required": true,
19088
+ "variadic": false,
19089
+ "description": ""
19090
+ },
19091
+ {
19092
+ "name": "model",
19093
+ "required": true,
19094
+ "variadic": false,
19095
+ "description": ""
19096
+ }
19097
+ ],
19098
+ "options": [
19099
+ {
19100
+ "flags": "--json",
19101
+ "description": "Print machine-readable JSON",
19102
+ "hidden": false
19103
+ }
19104
+ ],
19105
+ "subcommands": []
19106
+ },
19107
+ {
19108
+ "name": "probe",
19109
+ "description": "Run one minimal test turn with a runtime, model and thinking level exactly as a bot would, and say whether a bot set up that way can answer. Costs one tiny model turn on your own login; runs in an empty scratch folder, so no HQ settings or hooks load",
19110
+ "aliases": [],
19111
+ "hidden": false,
19112
+ "helpOption": true,
19113
+ "helpCommand": false,
19114
+ "usage": "[options]",
19115
+ "helpText": [],
19116
+ "arguments": [],
19117
+ "options": [
19118
+ {
19119
+ "flags": "--runtime <runtime>",
19120
+ "description": "Model CLI to check: claude|codex|grok (required unless --bot is given)",
19121
+ "hidden": false
19122
+ },
19123
+ {
19124
+ "flags": "--bot <name>",
19125
+ "description": "Check with this bot's saved runtime, model and thinking level (--runtime/--model/--effort override them)",
19126
+ "hidden": false
19127
+ },
19128
+ {
19129
+ "flags": "--model <model>",
19130
+ "description": "Model id to check, or default for the runtime's own default",
19131
+ "hidden": false
19132
+ },
19133
+ {
19134
+ "flags": "--effort <level>",
19135
+ "description": "Thinking level to check",
19136
+ "hidden": false
19137
+ },
19138
+ {
19139
+ "flags": "--timeout <seconds>",
19140
+ "description": "Give up after this many seconds",
19141
+ "hidden": false,
19142
+ "defaultValue": "120"
19143
+ },
19144
+ {
19145
+ "flags": "--json",
19146
+ "description": "Print { ok, class, detail, ... } as JSON",
19147
+ "hidden": false
19148
+ }
19149
+ ],
19150
+ "subcommands": []
19151
+ },
19055
19152
  {
19056
19153
  "name": "intro",
19057
19154
  "description": "Have the bot re-send its introduction DM to you",
@@ -158,6 +158,7 @@ export declare function resolveAccessCompany(target: string, opts?: {
158
158
  company?: string;
159
159
  hqRoot?: string;
160
160
  }): string | undefined;
161
+ export declare function resolveAccessAsPersonUid(token: string, companyUid: string, person: string): Promise<string>;
161
162
  export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
162
163
  company?: string;
163
164
  hqRoot?: string;
@@ -641,7 +641,7 @@ export function registerAccessCommand(program) {
641
641
  .command("access <path-or-query>")
642
642
  .description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
643
643
  .option("--company <slug>", "Company slug")
644
- .option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
644
+ .option("--as <person>", "Check a person's access (email, prs_, or agt_ uid; self or owner/admin)")
645
645
  .option("--json", "Print a single JSON object")
646
646
  .option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
647
647
  .option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
@@ -680,11 +680,22 @@ export function resolveAccessCompany(target, opts) {
680
680
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
681
681
  return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
682
682
  }
683
+ export async function resolveAccessAsPersonUid(token, companyUid, person) {
684
+ const trimmed = person.trim();
685
+ if (/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(trimmed))
686
+ return trimmed;
687
+ const email = trimmed.toLowerCase();
688
+ if (!/^[^\s]+@[^\s]+$/.test(email)) {
689
+ throw new Error("Pass an email, or the person's exact prs_ or agt_ UID, to --as.");
690
+ }
691
+ const members = await listActiveMembers(token, companyUid);
692
+ const member = members.find((candidate) => candidate.personEmail?.trim().toLowerCase() === email);
693
+ if (member?.personUid)
694
+ return member.personUid;
695
+ throw new Error(`No active member has email '${email}' in this company. Run \`hq members list\`.`);
696
+ }
683
697
  export async function runAccessForPerson(rawPath, personUid, opts) {
684
- const uid = personUid.trim();
685
- if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
686
- throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
687
- }
698
+ const requested = personUid.trim();
688
699
  const pathSlug = slugFromTarget(rawPath);
689
700
  if (opts?.company && pathSlug && opts.company !== pathSlug) {
690
701
  throw new Error("The --company value does not match the company in the path.");
@@ -698,6 +709,7 @@ export async function runAccessForPerson(rawPath, personUid, opts) {
698
709
  }
699
710
  const { token } = await resolveBrowseSession();
700
711
  const companyUid = await getCompanyUid(token, company);
712
+ const uid = await resolveAccessAsPersonUid(token, companyUid, requested);
701
713
  const response = await vaultApiFetch({
702
714
  token,
703
715
  path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { type RuntimeSignInIssue } from "../lib/bot/runtime-sign-in.js";
3
+ import { probeBotRuntime, type RuntimeProbeResult } from "../lib/bot/probe.js";
3
4
  import { type BotLocalConfig, type MyLocalBot, type BotKind, type BotMemoryMode, type BotNotRunnableReason, type BotConfig, type BotRuntimeId, type LaunchctlExec } from "../lib/bot/index.js";
4
5
  /** Real launchctl; every failure is a non-ok result, never a throw. */
5
6
  export declare const defaultLaunchctl: LaunchctlExec;
@@ -134,6 +135,17 @@ export declare function runBotSet(nameArg: string, opts: {
134
135
  effort?: string;
135
136
  json?: boolean;
136
137
  }): Promise<void>;
138
+ /** `hq bot probe`: exit code 0 when the runtime answered, 1 when it did not. */
139
+ export declare function runBotProbe(opts: {
140
+ runtime?: string;
141
+ bot?: string;
142
+ model?: string;
143
+ effort?: string;
144
+ timeout?: string;
145
+ json?: boolean;
146
+ }, deps?: {
147
+ probe?: typeof probeBotRuntime;
148
+ }): Promise<RuntimeProbeResult>;
137
149
  /**
138
150
  * The one bot the caller means. Matches the local folder name, the cloud slug,
139
151
  * the display name, or the agt_ uid; an ambiguous name is refused rather than
@@ -49,6 +49,8 @@ import { daemonPaths } from "../lib/daemon/paths.js";
49
49
  import { launchdLabel } from "../lib/bot/paths.js";
50
50
  import { isAncestorProcess } from "../lib/bot/self-process.js";
51
51
  import { runtimeDisplayName } from "../lib/bot/runtime-sign-in.js";
52
+ import { classifyRuntimeFailure, runtimeFailureReason } from "../lib/bot/runtime-failure.js";
53
+ import { PROBE_DEFAULT_TIMEOUT_MS, probeBotRuntime } from "../lib/bot/probe.js";
52
54
  import { BotApi, BotApiError, BOT_RESTORE_DEFAULTS, buildBotLocalConfig, describeBotLocalConfig, orphanBotRunMessage, parseBotLocalConfig, removeOrphanBotDaemon, botDaemonStatus, botDir, botCredsPath, botLogPath, botsRoot, botTokenStateDir, botWorkerRelDir, buildBotDaemonPaths, createBotLogger, deleteBotCreds, installBotDaemon, repairBotDaemonIfStale, healBotDaemonNodeFromJob, introDmText, isBotMemoryMode, botLocalMemoryDir, botMemoryMode, resolveBotMemoryDir, validateBotIntro, validateBotDisplayName, validateBotKickoff, validateBotEffort, validateBotModel, effectiveBotEffort, BOT_EFFORT_LEVELS, DEFAULT_BOT_EFFORT, BOT_MEMORY_MODES, BOT_KINDS, isBotKind, effectiveBotKind, effectiveBotCompanies, normalizeBotCompanies, SETUP_BOT_WORKER_ID, isBotRuntimeId, localNameForRemoteBot, remoteBotRunnability, isPidAlive, listBotWorkerOptions, patchBotConfig, readBotConfig, readBotCredsIdentity, readBotStatus, resolveBotWorker, runBot, runtimeFor, scaffoldBotMemory, scaffoldBotWorker, startBotDaemon, stopBotDaemon, uninstallBotDaemon, validateBotName, writeBotConfig, writeBotCreds, BOT_RUNTIMES, BOT_INTRO_MAX_CHARS, BOT_DISPLAY_NAME_MAX_CHARS, BOT_KICKOFF_MAX_CHARS, detectBotPlatform, } from "../lib/bot/index.js";
53
55
  function collectRepeated(value, previous) {
54
56
  return [...previous, value];
@@ -994,8 +996,17 @@ async function runBotStatus(nameArg, opts) {
994
996
  const tool = runtimeDisplayName(status.runtimeSignIn.runtime);
995
997
  console.log(` ${chalk.yellow(`${tool} needs to sign in again`)} — sign in from HQ, or run \`${status.runtimeSignIn.runtime} ${status.runtimeSignIn.runtime === "claude" ? "auth login" : "login"}\`, then \`hq bot restart ${name}\``);
996
998
  }
997
- if (status.lastError)
998
- console.log(` last error: ${chalk.yellow(status.lastError)}`);
999
+ if (status.modelHold && new Date(status.modelHold.until).getTime() > Date.now()) {
1000
+ console.log(` ${chalk.yellow(`holding until ${status.modelHold.until}`)} after repeated model failures; the next message after that runs again`);
1001
+ }
1002
+ if (status.runtimeModelFallback) {
1003
+ console.log(` model: ${chalk.yellow(`running on ${status.runtimeModelFallback.to}`)} because ${row.runtime} here is too old for ${status.runtimeModelFallback.from || "its configured model"}; update ${row.runtime} to use it`);
1004
+ }
1005
+ // The raw runtime text stays in the log (hq bot logs) and the JSON output.
1006
+ if (status.lastError) {
1007
+ const cls = classifyRuntimeFailure(status.lastError);
1008
+ console.log(` last problem: ${chalk.yellow(`${cls}: ${runtimeFailureReason(cls, row.runtime)}`)} ${chalk.dim(`(details: hq bot logs ${name})`)}`);
1009
+ }
999
1010
  }
1000
1011
  console.log(chalk.dim(` state dir: ${dir}`));
1001
1012
  }
@@ -1278,6 +1289,32 @@ export async function runBotSet(nameArg, opts) {
1278
1289
  }
1279
1290
  console.log(chalk.green(`${name} now thinks with ${result.model ?? "the default model"} at ${result.effort} effort (from its next message).`));
1280
1291
  }
1292
+ /** `hq bot probe`: exit code 0 when the runtime answered, 1 when it did not. */
1293
+ export async function runBotProbe(opts, deps = {}) {
1294
+ const saved = opts.bot ? requireBot(opts.bot).config : undefined;
1295
+ const runtimeArg = opts.runtime ?? saved?.runtime;
1296
+ if (!isBotRuntimeId(runtimeArg)) {
1297
+ throw Object.assign(new Error(opts.runtime === undefined && !opts.bot ? `Pass --runtime <${BOT_RUNTIMES.join("|")}> or --bot <name>.` : `--runtime must be one of ${BOT_RUNTIMES.join(", ")}`), { expected: true });
1298
+ }
1299
+ const runtime = runtimeArg;
1300
+ const model = opts.model !== undefined ? validateBotModel(opts.model) : saved?.model;
1301
+ const effort = opts.effort !== undefined ? validateBotEffort(runtime, opts.effort) : saved?.effort;
1302
+ const seconds = Number(opts.timeout ?? PROBE_DEFAULT_TIMEOUT_MS / 1000);
1303
+ const timeoutMs = Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : PROBE_DEFAULT_TIMEOUT_MS;
1304
+ const result = await (deps.probe ?? probeBotRuntime)({
1305
+ runtime,
1306
+ ...(model ? { model } : {}),
1307
+ ...(effort ? { effort } : {}),
1308
+ timeoutMs,
1309
+ });
1310
+ if (!result.ok)
1311
+ process.exitCode = 1;
1312
+ if (opts.json)
1313
+ console.log(JSON.stringify(result, null, 2));
1314
+ else
1315
+ console.log(result.ok ? chalk.green(result.detail) : chalk.red(result.detail));
1316
+ return result;
1317
+ }
1281
1318
  async function runBotIntro(nameArg) {
1282
1319
  const { name, dir, config } = requireBot(nameArg);
1283
1320
  if (hasPromotionHold(dir))
@@ -1938,6 +1975,21 @@ export function registerBotCommand(program) {
1938
1975
  .option("--effort <level>", `Thinking level (claude ${BOT_EFFORT_LEVELS.claude.join("|")}; codex ${BOT_EFFORT_LEVELS.codex.join("|")}; grok ${BOT_EFFORT_LEVELS.grok.join("|")}), or default (${DEFAULT_BOT_EFFORT})`)
1939
1976
  .option("--json", "Print machine-readable JSON")
1940
1977
  .action((name, opts) => wrap(() => runBotSet(name, opts))());
1978
+ bot
1979
+ .command("set-model <name> <model>")
1980
+ .description("Set the model this bot asks its runtime for (stored in the bot's own settings; the runtime's own config is never changed), or default to clear it")
1981
+ .option("--json", "Print machine-readable JSON")
1982
+ .action((name, model, opts) => wrap(() => runBotSet(name, { model, ...(opts.json ? { json: true } : {}) }))());
1983
+ bot
1984
+ .command("probe")
1985
+ .description("Run one minimal test turn with a runtime, model and thinking level exactly as a bot would, and say whether a bot set up that way can answer. Costs one tiny model turn on your own login; runs in an empty scratch folder, so no HQ settings or hooks load")
1986
+ .option("--runtime <runtime>", `Model CLI to check: ${BOT_RUNTIMES.join("|")} (required unless --bot is given)`)
1987
+ .option("--bot <name>", "Check with this bot's saved runtime, model and thinking level (--runtime/--model/--effort override them)")
1988
+ .option("--model <model>", "Model id to check, or default for the runtime's own default")
1989
+ .option("--effort <level>", "Thinking level to check")
1990
+ .option("--timeout <seconds>", "Give up after this many seconds", String(PROBE_DEFAULT_TIMEOUT_MS / 1000))
1991
+ .option("--json", "Print { ok, class, detail, ... } as JSON")
1992
+ .action((opts) => wrap(async () => { await runBotProbe(opts); })());
1941
1993
  bot
1942
1994
  .command("intro <name>")
1943
1995
  .description("Have the bot re-send its introduction DM to you")
@@ -13,7 +13,7 @@ export async function coreNativeFamilyEnabled(flagKey) {
13
13
  export const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
14
14
  export const CORE_NATIVE_ONTOLOGY_FLAG = "core-native-ontology";
15
15
  export function coreNativeDelegateEnabled() {
16
- return import("../lib/flag-registry.js").then(({ resolveProcessFlagGate }) => resolveProcessFlagGate(CORE_NATIVE_DELEGATE_FLAG, { fallback: false }, () => false));
16
+ return coreNativeFamilyEnabled(CORE_NATIVE_DELEGATE_FLAG);
17
17
  }
18
18
  export function coreNativeOntologyEnabled() {
19
19
  return coreNativeFamilyEnabled(CORE_NATIVE_ONTOLOGY_FLAG);
@@ -474,8 +474,9 @@ export function registerFilesCommand(program) {
474
474
  // store `granteeId === ""` on the wire, but a blank cell is confusing
475
475
  // in tabular output, so we render a human-readable phrase instead.
476
476
  function displayGrantee(e) {
477
- if (e.granteeType === "company-wide")
478
- return "Everyone in company";
477
+ if (e.granteeType === "company-wide") {
478
+ return "Everyone in company (including guests)";
479
+ }
479
480
  return e.granteeId;
480
481
  }
481
482
  function printEntryTable(rows, showSource) {
@@ -531,6 +532,10 @@ export function registerFilesCommand(program) {
531
532
  console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
532
533
  printEntryTable(tree.children, true);
533
534
  }
535
+ const allEntries = [...tree.direct, ...tree.inherited, ...tree.children];
536
+ if (allEntries.some((entry) => entry.granteeType === "company-wide")) {
537
+ console.log(chalk.gray("\nCompany-wide grants apply to every active member, including guests. Demoting someone to guest does not exclude them. Use hq members remove to end membership, or hq access <path> --as <email> to see effective access."));
538
+ }
534
539
  }
535
540
  catch (err) {
536
541
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));