@indigoai-us/hq-cli 5.300.0 → 5.302.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 (99) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
  3. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  4. package/dist/command-catalog.generated.d.ts +32 -3
  5. package/dist/command-catalog.generated.js +39 -3
  6. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  7. package/dist/commands/__fixtures__/access-vault.js +1 -1
  8. package/dist/commands/access.d.ts +2 -1
  9. package/dist/commands/access.js +58 -49
  10. package/dist/commands/bot.js +1 -0
  11. package/dist/commands/cloud-provision.js +51 -2
  12. package/dist/commands/core-checkpoint.js +1 -0
  13. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  14. package/dist/commands/core-company-index-scope-flag.js +6 -0
  15. package/dist/commands/core-handoff-fast.js +1 -0
  16. package/dist/commands/core-handoff.js +2 -0
  17. package/dist/commands/core-job-native-utils.d.ts +6 -0
  18. package/dist/commands/core-job-native-utils.js +80 -0
  19. package/dist/commands/core-job-native.d.ts +2 -4
  20. package/dist/commands/core-job-native.js +6 -10
  21. package/dist/commands/core-job-notify-native.d.ts +36 -0
  22. package/dist/commands/core-job-notify-native.js +359 -0
  23. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  24. package/dist/commands/core-job-remediate-native.js +374 -0
  25. package/dist/commands/core.js +26 -8
  26. package/dist/commands/deploy-comments.js +5 -2
  27. package/dist/commands/files.js +31 -14
  28. package/dist/commands/integrations-api.d.ts +6 -6
  29. package/dist/commands/integrations-api.js +4 -13
  30. package/dist/commands/integrations-connect.d.ts +3 -0
  31. package/dist/commands/integrations-connect.js +21 -2
  32. package/dist/commands/integrations-manage.js +1 -4
  33. package/dist/commands/lanes-message.js +43 -5
  34. package/dist/commands/lanes-questions.d.ts +4 -2
  35. package/dist/commands/lanes-questions.js +123 -2
  36. package/dist/commands/lanes-resume.js +25 -4
  37. package/dist/commands/lanes-shared.d.ts +15 -1
  38. package/dist/commands/lanes-watch.js +5 -1
  39. package/dist/commands/lanes.d.ts +2 -0
  40. package/dist/commands/lanes.js +92 -9
  41. package/dist/commands/meetings.js +35 -16
  42. package/dist/commands/monitor.js +17 -2
  43. package/dist/commands/report.js +5 -2
  44. package/dist/commands/rescue.d.ts +4 -0
  45. package/dist/commands/rescue.js +27 -17
  46. package/dist/commands/safe-extract.d.ts +4 -4
  47. package/dist/commands/safe-extract.js +6 -7
  48. package/dist/lib/agent-kit/fallback.d.ts +1 -0
  49. package/dist/lib/agent-kit/fallback.js +1 -0
  50. package/dist/lib/bot/daemon.d.ts +5 -1
  51. package/dist/lib/bot/daemon.js +4 -4
  52. package/dist/lib/core-utils/soft-timeout.js +1 -1
  53. package/dist/lib/core-utils/token-usage-report.js +17 -3
  54. package/dist/lib/deploy-apps.d.ts +13 -0
  55. package/dist/lib/deploy-apps.js +24 -0
  56. package/dist/lib/index-render/companies.d.ts +4 -1
  57. package/dist/lib/index-render/companies.js +66 -3
  58. package/dist/lib/index-render/index.d.ts +1 -0
  59. package/dist/lib/index-render/index.js +2 -1
  60. package/dist/lib/index-render/projects.js +82 -5
  61. package/dist/lib/index-render/shared.d.ts +1 -1
  62. package/dist/lib/index-render/shared.js +4 -2
  63. package/dist/lib/lanes/delivery.d.ts +7 -3
  64. package/dist/lib/lanes/delivery.js +18 -8
  65. package/dist/lib/lanes/index.d.ts +1 -1
  66. package/dist/lib/lanes/keep-alive.d.ts +13 -3
  67. package/dist/lib/lanes/keep-alive.js +29 -5
  68. package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
  69. package/dist/lib/lanes/outpost-dispatch.js +12 -0
  70. package/dist/lib/lanes/reconcile.d.ts +1 -0
  71. package/dist/lib/lanes/reconcile.js +358 -38
  72. package/dist/lib/lanes/spawn.d.ts +1 -0
  73. package/dist/lib/lanes/spawn.js +20 -10
  74. package/dist/lib/lanes/store.js +24 -0
  75. package/dist/lib/lanes/story-ledger.d.ts +2 -0
  76. package/dist/lib/lanes/story-ledger.js +8 -0
  77. package/dist/lib/lanes/types.d.ts +26 -4
  78. package/dist/lib/lanes/types.js +33 -1
  79. package/dist/lib/lanes/watch.d.ts +1 -0
  80. package/dist/lib/lanes/watch.js +19 -6
  81. package/dist/lib/lanes/watcher.js +139 -9
  82. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  83. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  84. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  85. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  86. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  87. package/dist/lib/mesh/live/daemon/run.js +36 -2
  88. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  89. package/dist/lib/search-index/background.d.ts +1 -0
  90. package/dist/lib/search-index/background.js +1 -0
  91. package/dist/utils/self-update.js +51 -4
  92. package/dist/utils/staged-npm-install.d.ts +16 -0
  93. package/dist/utils/staged-npm-install.js +264 -0
  94. package/dist/utils/update-command-supervisor.cjs +54 -1
  95. package/dist/utils/version-gate.d.ts +8 -0
  96. package/dist/utils/version-gate.js +83 -16
  97. package/package.json +2 -2
  98. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  99. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -0,0 +1,374 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { spawnSync } from "node:child_process";
5
+ import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
6
+ import { registerCoreJobLeaf } from "./core-job-native-utils.js";
7
+ const ASSET = "core/scripts/hq-job-remediate.sh";
8
+ const USAGE = [
9
+ "hq-job-remediate.sh — bounded self-remediation for Outpost scheduled jobs (US-006).",
10
+ "",
11
+ "Allowlist (v1):",
12
+ " - Cognito / HQ token refresh via `hq auth refresh` when available",
13
+ " - one `hq sync pull` (personal or --company)",
14
+ " - single immediate retry for transient infra (signaled via retry_recommended)",
15
+ "Always-human (never auto-looped):",
16
+ " - device re-login (Claude/Codex CLI)",
17
+ " - secret share / missing vault grants",
18
+ "",
19
+ "Usage:",
20
+ " core/scripts/hq-job-remediate.sh --failure-class <class> [--job-id <id>] \\",
21
+ " [--company <slug>] [--hq-root <dir>]",
22
+ "",
23
+ "Env / overrides (tests):",
24
+ " HQ_ROOT, HQ_JOB_REMEDIATE_AUTH_REFRESH_CMD, HQ_JOB_REMEDIATE_SYNC_PULL_CMD",
25
+ " HQ_JOB_REMEDIATE_DRY=1 — print plan only; do not execute side effects",
26
+ "",
27
+ "stdout: JSON {failure_class, attempted, actions[], retry_recommended, always_human, next_action, notes}",
28
+ "Exit: 0 always on successful classify/plan (even if nothing attempted); 2 usage.",
29
+ "",
30
+ "set -euo pipefail",
31
+ "",
32
+ ].join("\n");
33
+ function defaultCommandExists(command, env) {
34
+ if (command.includes(path.sep)) {
35
+ try {
36
+ fs.accessSync(command, fs.constants.X_OK);
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
44
+ if (!directory)
45
+ continue;
46
+ try {
47
+ fs.accessSync(path.join(directory, command), fs.constants.X_OK);
48
+ return true;
49
+ }
50
+ catch { /* Continue PATH lookup. */ }
51
+ }
52
+ return false;
53
+ }
54
+ function defaultRun(command, args, options) {
55
+ // The shell redirects both streams to the append-only log (or /dev/null). No
56
+ // timeout is set, matching its direct command invocation exactly.
57
+ let fd;
58
+ try {
59
+ fd = fs.openSync(options.logPath || os.devNull, "a");
60
+ }
61
+ catch {
62
+ // Bash reports failed redirection as a nonzero action; remediation still
63
+ // emits its classification JSON and exit status 0.
64
+ return 1;
65
+ }
66
+ try {
67
+ const result = spawnSync(command, args, {
68
+ cwd: options.cwd,
69
+ env: options.env,
70
+ stdio: ["ignore", fd, fd],
71
+ });
72
+ if (result.error) {
73
+ fs.writeSync(fd, `${result.error.message}\n`);
74
+ return 127;
75
+ }
76
+ return result.status ?? 0;
77
+ }
78
+ finally {
79
+ fs.closeSync(fd);
80
+ }
81
+ }
82
+ export function defaultJobRemediateDependencies(env = process.env, cwd = process.cwd()) {
83
+ return {
84
+ env,
85
+ cwd,
86
+ readFile: (file) => fs.readFileSync(file, "utf8"),
87
+ commandExists: (command) => defaultCommandExists(command, env),
88
+ run: defaultRun,
89
+ stdout: (text) => { process.stdout.write(text); },
90
+ stderr: (text) => { process.stderr.write(text); },
91
+ };
92
+ }
93
+ function parseArguments(argv, deps) {
94
+ const values = { hqRoot: deps.env.HQ_ROOT ?? "", failureClass: "", jobId: "", company: "", dryRun: false };
95
+ const aliases = {
96
+ "--hq-root": "hqRoot", "--failure-class": "failureClass", "--job-id": "jobId", "--company": "company",
97
+ };
98
+ for (let index = 0; index < argv.length;) {
99
+ const flag = argv[index];
100
+ if (flag === "-h" || flag === "--help") {
101
+ deps.stdout(USAGE);
102
+ return 0;
103
+ }
104
+ if (flag === "--dry-run") {
105
+ values.dryRun = true;
106
+ index += 1;
107
+ continue;
108
+ }
109
+ if (flag === "--")
110
+ break;
111
+ const key = aliases[flag];
112
+ if (key) {
113
+ const value = argv[index + 1] ?? "";
114
+ if ((key === "hqRoot" || key === "failureClass") && !value) {
115
+ deps.stderr(`hq-job-remediate: --${key === "hqRoot" ? "hq-root" : "failure-class"} requires ${key === "hqRoot" ? "a directory" : "a value"}\n`);
116
+ return 2;
117
+ }
118
+ values[key] = value;
119
+ index += 2;
120
+ continue;
121
+ }
122
+ if (flag.startsWith("-")) {
123
+ deps.stderr(`hq-job-remediate: unknown option: ${flag}\n`);
124
+ return 2;
125
+ }
126
+ deps.stderr(`hq-job-remediate: unexpected argument: ${flag}\n`);
127
+ return 2;
128
+ }
129
+ if (!values.failureClass) {
130
+ deps.stderr("hq-job-remediate: usage: hq-job-remediate.sh --failure-class <auth|secrets|timeout|agent_error|infra>\n");
131
+ return 2;
132
+ }
133
+ if (!["auth", "secrets", "timeout", "agent_error", "infra"].includes(values.failureClass)) {
134
+ deps.stderr(`hq-job-remediate: unknown failure_class: ${values.failureClass} (want auth|secrets|timeout|agent_error|infra)\n`);
135
+ return 2;
136
+ }
137
+ if (values.company && values.company !== "null" && !/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/.test(values.company)) {
138
+ deps.stderr(`hq-job-remediate: invalid --company slug '${values.company}' (want lowercase kebab/underscore)\n`);
139
+ return 2;
140
+ }
141
+ return values;
142
+ }
143
+ function canUseLiveRootForArguments(argv) {
144
+ const parsed = parseArguments(argv, {
145
+ env: {},
146
+ stdout: () => undefined,
147
+ stderr: () => undefined,
148
+ });
149
+ return typeof parsed !== "number";
150
+ }
151
+ function globRegex(segment) {
152
+ let pattern = "^";
153
+ for (let index = 0; index < segment.length; index += 1) {
154
+ const char = segment[index];
155
+ if (char === "\\" && index + 1 < segment.length) {
156
+ pattern += segment[++index].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
157
+ }
158
+ else if (char === "*")
159
+ pattern += ".*";
160
+ else if (char === "?")
161
+ pattern += ".";
162
+ else if (char === "[") {
163
+ const end = segment.indexOf("]", index + 1);
164
+ if (end < 0)
165
+ pattern += "\\[";
166
+ else {
167
+ let body = segment.slice(index + 1, end);
168
+ if (body.startsWith("!"))
169
+ body = `^${body.slice(1)}`;
170
+ pattern += `[${body.replace(/\\/g, "")} ]`.replace(" ]", "]");
171
+ index = end;
172
+ }
173
+ }
174
+ else
175
+ pattern += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
176
+ }
177
+ return new RegExp(`${pattern}$`);
178
+ }
179
+ /** Bash's unquoted array assignment splits on IFS, then expands pathname globs. */
180
+ function splitOverrideCommand(command, cwd) {
181
+ const words = command.split(/[ \t\n]+/).filter(Boolean);
182
+ const expanded = [];
183
+ for (const word of words) {
184
+ if (!/[?*[]/.test(word)) {
185
+ expanded.push(word);
186
+ continue;
187
+ }
188
+ const absolute = path.isAbsolute(word);
189
+ const parts = word.split(path.sep);
190
+ let prefixes = [absolute ? path.sep : ""];
191
+ for (const part of parts) {
192
+ if (!part)
193
+ continue;
194
+ if (!/[?*[]/.test(part)) {
195
+ prefixes = prefixes.map((prefix) => path.join(prefix || cwd, part));
196
+ continue;
197
+ }
198
+ const matcher = globRegex(part);
199
+ const next = [];
200
+ for (const prefix of prefixes) {
201
+ const directory = prefix || cwd;
202
+ let names;
203
+ try {
204
+ names = fs.readdirSync(directory);
205
+ }
206
+ catch {
207
+ continue;
208
+ }
209
+ for (const name of names) {
210
+ if (!part.startsWith(".") && name.startsWith("."))
211
+ continue;
212
+ if (matcher.test(name))
213
+ next.push(path.join(directory, name));
214
+ }
215
+ }
216
+ prefixes = next;
217
+ if (!prefixes.length)
218
+ break;
219
+ }
220
+ if (!prefixes.length)
221
+ expanded.push(word);
222
+ else
223
+ expanded.push(...prefixes.map((entry) => absolute ? entry : path.relative(cwd, entry) || "."));
224
+ }
225
+ return expanded;
226
+ }
227
+ const UNSAFE_OVERRIDE = /[;|&$`()<>]/;
228
+ export function runJobRemediate(argv, options = {}) {
229
+ const deps = {
230
+ ...defaultJobRemediateDependencies(options.dependencies?.env ?? process.env, options.dependencies?.cwd ?? process.cwd()),
231
+ ...options.dependencies,
232
+ };
233
+ if (options.hqRoot)
234
+ deps.env = { ...deps.env, HQ_ROOT: options.hqRoot };
235
+ const parsed = parseArguments(argv, deps);
236
+ if (typeof parsed === "number")
237
+ return parsed;
238
+ const hqRoot = parsed.hqRoot || options.hqRoot || deps.env.HQ_ROOT || "";
239
+ if (hqRoot)
240
+ deps.env = { ...deps.env, HQ_ROOT: hqRoot };
241
+ const dryRun = parsed.dryRun || deps.env.HQ_JOB_REMEDIATE_DRY === "1";
242
+ const actions = [];
243
+ let attempted = false;
244
+ let retryRecommended = false;
245
+ let alwaysHuman = false;
246
+ let nextAction = "";
247
+ let notes = "";
248
+ const logPath = deps.env.HQ_JOB_REMEDIATE_LOG || os.devNull;
249
+ const log = (message) => deps.stderr(`hq-job-remediate: ${message}\n`);
250
+ const emit = (code) => {
251
+ if (code === 2)
252
+ return 2;
253
+ deps.stdout(serializeJqJson({
254
+ failure_class: parsed.failureClass,
255
+ job_id: parsed.jobId,
256
+ attempted,
257
+ actions,
258
+ retry_recommended: retryRecommended,
259
+ always_human: alwaysHuman,
260
+ next_action: nextAction,
261
+ notes,
262
+ }, "compact"));
263
+ return 0;
264
+ };
265
+ const runOrDry = (label, command, args) => {
266
+ actions.push(label);
267
+ if (dryRun) {
268
+ log(`dry-run: would run: ${[command, ...args].join(" ")}`);
269
+ return true;
270
+ }
271
+ return deps.run(command, args, { cwd: deps.cwd, env: deps.env, logPath }) === 0;
272
+ };
273
+ const runOverrideOrArgv = (label, overrideOrCommand, args = []) => {
274
+ let command = overrideOrCommand;
275
+ let commandArgs = args;
276
+ if (args.length === 0 && overrideOrCommand.includes(" ")) {
277
+ if (UNSAFE_OVERRIDE.test(overrideOrCommand)) {
278
+ deps.stderr("hq-job-remediate: unsafe remediate override command rejected\n");
279
+ return 2;
280
+ }
281
+ [command, ...commandArgs] = splitOverrideCommand(overrideOrCommand, deps.cwd);
282
+ if (!command)
283
+ return true;
284
+ }
285
+ return runOrDry(label, command, commandArgs);
286
+ };
287
+ const resolveAuthRefresh = () => {
288
+ const override = deps.env.HQ_JOB_REMEDIATE_AUTH_REFRESH_CMD || "";
289
+ if (override)
290
+ return override;
291
+ if (deps.commandExists("hq"))
292
+ return "hq auth refresh";
293
+ if (deps.commandExists("hq-auth-refresh"))
294
+ return "hq-auth-refresh";
295
+ return "";
296
+ };
297
+ switch (parsed.failureClass) {
298
+ case "secrets":
299
+ alwaysHuman = true;
300
+ nextAction = "share the missing secret to this Outpost (hq secrets share KEY) then re-probe";
301
+ notes = "secret share is always-human; no self-remediation";
302
+ return emit(0);
303
+ case "auth": {
304
+ alwaysHuman = true;
305
+ nextAction = "re-login on Outpost (device code) for the job runtime CLI";
306
+ notes = "attempt Cognito refresh if available; never loop device re-login";
307
+ const command = resolveAuthRefresh();
308
+ if (command) {
309
+ attempted = true;
310
+ const result = runOverrideOrArgv("cognito_token_refresh", command);
311
+ if (result === 2)
312
+ return 2;
313
+ if (result)
314
+ notes = "cognito refresh ok; device re-login still required if CLI creds missing";
315
+ else
316
+ notes = "cognito refresh failed or unavailable; device re-login required";
317
+ }
318
+ else
319
+ notes = "no hq auth refresh on PATH; device re-login required";
320
+ return emit(0);
321
+ }
322
+ case "timeout":
323
+ nextAction = "raise timeout_seconds or split the job work";
324
+ notes = "timeout is not self-retried in v1 (avoid thrash)";
325
+ return emit(0);
326
+ case "agent_error":
327
+ nextAction = "inspect the job log and fix the prompt/skill";
328
+ notes = "agent_error is not self-remediated in v1";
329
+ return emit(0);
330
+ case "infra": {
331
+ nextAction = "retry after sync; if it persists, check Outpost disk/network";
332
+ notes = "allowlist: cognito refresh + one hq-sync pull + single retry";
333
+ const authCommand = resolveAuthRefresh();
334
+ if (authCommand) {
335
+ attempted = true;
336
+ const result = runOverrideOrArgv("cognito_token_refresh", authCommand);
337
+ if (result === 2)
338
+ return 2;
339
+ }
340
+ const syncOverride = deps.env.HQ_JOB_REMEDIATE_SYNC_PULL_CMD || "";
341
+ attempted = true;
342
+ if (syncOverride) {
343
+ const result = runOverrideOrArgv("hq_sync_pull", syncOverride);
344
+ if (result === 2)
345
+ return 2;
346
+ retryRecommended = true;
347
+ notes = result ? "sync pull ok; recommend single immediate retry" : "sync pull failed; still recommend one infra retry";
348
+ }
349
+ else if (deps.commandExists("hq")) {
350
+ const syncArgs = parsed.company && parsed.company !== "null"
351
+ ? ["sync", "pull", "--company", parsed.company, "--hq-root", hqRoot]
352
+ : ["sync", "pull", "--personal", "--hq-root", hqRoot];
353
+ const result = runOrDry("hq_sync_pull", "hq", syncArgs);
354
+ retryRecommended = true;
355
+ notes = result ? "sync pull ok; recommend single immediate retry" : "sync pull failed; still recommend one infra retry";
356
+ }
357
+ else {
358
+ actions.push("hq_sync_pull_unavailable");
359
+ retryRecommended = true;
360
+ notes = "hq sync pull unavailable; recommend single infra retry anyway";
361
+ }
362
+ return emit(0);
363
+ }
364
+ default:
365
+ // parseArguments rejects unknown classes before any remediation path.
366
+ return 2;
367
+ }
368
+ }
369
+ export function registerCoreJobRemediateNativeCommand(group, core) {
370
+ registerCoreJobLeaf(group, core, "remediate", ASSET, "Run bounded remediation for a scheduled-job failure", (argv, hqRoot) => runJobRemediate(argv, { hqRoot }), canUseLiveRootForArguments, [
371
+ "--hq-root", "--failure-class", "--job-id", "--company",
372
+ ], ["--dry-run"]);
373
+ }
374
+ //# sourceMappingURL=core-job-remediate-native.js.map
@@ -40,7 +40,7 @@ import { registerCoreJobsNativeCommand } from "./core-job-native.js";
40
40
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
41
41
  import { resolveLiveRoot } from "../utils/hq-roots.js";
42
42
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
43
- import { renderIndexTarget } from "../lib/index-render/index.js";
43
+ import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
44
44
  import { generateWorkersRegistry } from "../lib/workers-registry/index.js";
45
45
  import { scanPackages } from "../lib/scan-packages/index.js";
46
46
  import { detectStaleCorePolicyMirror } from "../lib/core-utils/detect-stale-core-policy-mirror.js";
@@ -57,6 +57,7 @@ import { timeoutGuardCommand } from "../lib/core-utils/timeout-guard.js";
57
57
  import { readSentryReportStdin, reportSentryEvent } from "../lib/core-utils/sentry-report.js";
58
58
  import { CLEAN_HANDOFF_ASSET, RESUME_LOCK_ASSET, VERIFY_DELIVERABLES_ASSET, WORKTREE_GC_ASSET, runCleanWorktreeReconcileHandoff, runResumeThreadLock, runVerifyStoryDeliverables, runWorktreeGc, } from "../lib/core-utils/worktree-resume.js";
59
59
  import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
60
+ import { coreCompanyIndexScopeEnabled } from "./core-company-index-scope-flag.js";
60
61
  import { describeCoreCommand, registeredCoreCommandListing, } from "./core-command-catalog.js";
61
62
  /**
62
63
  * These commands are now native implementations. Their retained shell assets
@@ -489,18 +490,35 @@ export function registerCoreCommands(program) {
489
490
  throw expectedUserError(`Unknown rebuild-index target "${target}". Valid targets: ${validTargets.join(", ")}.`);
490
491
  }
491
492
  const scope = core.opts();
492
- const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
493
493
  const operands = cmd.args.length > 0 ? cmd.args : args;
494
494
  const passthrough = operands.slice(1);
495
495
  const force = passthrough.includes("--accept");
496
496
  const rendererArgs = passthrough.filter((arg) => arg !== "--accept");
497
+ const isCompaniesTarget = entry.renderer === "companies";
498
+ const wantsHelp = rendererArgs.some((arg) => arg === "--help" || arg === "-h");
499
+ if (isCompaniesTarget && wantsHelp) {
500
+ renderIndexTarget(entry.renderer, { root: "", output: (message) => process.stdout.write(message) }, rendererArgs);
501
+ return;
502
+ }
503
+ const requestsCompanyScope = isCompaniesTarget && rendererArgs.some((arg) => arg === "--company" || arg.startsWith("--company="));
504
+ if (requestsCompanyScope && !coreCompanyIndexScopeEnabled()) {
505
+ throw expectedUserError("rebuild-index companies --company is disabled; enable cli.rebuild-index-company-scope for a canary.");
506
+ }
507
+ const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
497
508
  const refused = [];
498
- renderIndexTarget(entry.renderer, {
499
- root: hqRoot,
500
- log: (message) => process.stderr.write(`${message}\n`),
501
- force,
502
- refused,
503
- }, rendererArgs);
509
+ try {
510
+ renderIndexTarget(entry.renderer, {
511
+ root: hqRoot,
512
+ log: (message) => process.stderr.write(`${message}\n`),
513
+ force,
514
+ refused,
515
+ }, rendererArgs);
516
+ }
517
+ catch (error) {
518
+ if (error instanceof CompanyIndexUserError)
519
+ throw expectedUserError(error.message);
520
+ throw error;
521
+ }
504
522
  if (refused.length > 0) {
505
523
  throw expectedUserError(`rebuild-index: refused to overwrite ${refused.length} hand-edited INDEX.md file(s). Re-run with --accept to overwrite.`);
506
524
  }
@@ -1,4 +1,5 @@
1
1
  import { deployApiRequest } from "../lib/deploy-api.js";
2
+ import { listDeployApps } from "../lib/deploy-apps.js";
2
3
  import { DEFAULT_HQ_ROOT, ensureCognitoToken } from "../utils/cognito-session.js";
3
4
  import { readActiveCompanySlug } from "./skill.js";
4
5
  class DeployCommentsError extends Error {
@@ -50,8 +51,10 @@ async function deployJson(token, route, orgHeaders) {
50
51
  }
51
52
  }
52
53
  async function resolveAppId(token, appNameOrId, orgHeaders) {
53
- const result = await deployJson(token, "/api/apps", orgHeaders);
54
- const apps = Array.isArray(result.apps) ? result.apps : [];
54
+ const apps = await listDeployApps((cursor) => {
55
+ const route = cursor ? `/api/apps?cursor=${encodeURIComponent(cursor)}` : "/api/apps";
56
+ return deployJson(token, route, orgHeaders);
57
+ });
55
58
  const byId = apps.find((app) => app && typeof app.id === "string" && app.id === appNameOrId);
56
59
  const matches = byId
57
60
  ? [byId]
@@ -14,6 +14,7 @@ import { findHqRoot } from "../utils/manifest.js";
14
14
  import { maxLength } from "../utils/table-width.js";
15
15
  import { resolveCompanyFlagGate } from "../lib/flag-registry.js";
16
16
  const PERSON_UID_PATTERN = /^(prs|agt)_[A-Za-z0-9_-]+$/;
17
+ const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/;
17
18
  export const FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG = "files.share-empty-prefix-warning";
18
19
  const FILES_SHARE_EMPTY_PREFIX_CHECK_TIMEOUT_MS = 5_000;
19
20
  function collectMentions(value, previous) {
@@ -265,14 +266,15 @@ export function registerFilesCommand(program) {
265
266
  files
266
267
  .command("unshare <prefix>")
267
268
  .description("Remove a file access grant. A trailing slash targets the private-folder row exactly.")
268
- .requiredOption("--with <principal>", "Person uid, email address, group id, or '@all' to remove the grant")
269
+ .requiredOption("--with <principal>", "Person uid, agent uid, email address, group id, or '@all' to remove the grant")
269
270
  .action(async (prefix, opts) => {
270
271
  try {
271
- // Unshare MUTATES access with no preview, and it resolves the target
272
- // company from --company (not from the pasted anchor), so a mismatched
273
- // anchor would revoke a real grant in the wrong company.
272
+ // Unshare MUTATES access with no preview. Anchored content paths remain
273
+ // refused; the exact company-root ACL row is the sole exception and
274
+ // resolves its anchor against the selected company before revoking.
274
275
  const canonicalPrefix = normalizeCompanyFilePrefix(prefix, {
275
276
  mutating: true,
277
+ allowCompanyRootAclRow: true,
276
278
  ...(files.opts().company !== undefined
277
279
  ? { companySlug: files.opts().company }
278
280
  : {}),
@@ -281,11 +283,14 @@ export function registerFilesCommand(program) {
281
283
  const isAll = principal === "@all";
282
284
  const isEmail = !isAll && EMAIL_PATTERN.test(principal);
283
285
  const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
284
- const isPerson = !isAll && !isEmail && !isGroup && PERSON_UID_PATTERN.test(principal);
285
- if (!isAll && !isEmail && !isGroup && !isPerson) {
286
- console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, or a group id matching grp_<alphanumeric>`));
286
+ const isAgent = !isAll && !isEmail && !isGroup && AGENT_UID_PATTERN.test(principal);
287
+ const isPerson = !isAll && !isEmail && !isGroup && !isAgent && PERSON_UID_PATTERN.test(principal);
288
+ if (!isAll && !isEmail && !isGroup && !isPerson && !isAgent) {
289
+ console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a person uid matching prs_<alphanumeric>, an agent uid matching agt_<alphanumeric>, or a group id matching grp_<alphanumeric>`));
287
290
  process.exit(1);
288
291
  }
292
+ // The vault ACL API stores first-class agent access on its backwards-
293
+ // compatible person tuple: { granteeType: "person", granteeId: "agt_*" }.
289
294
  const granteeType = isAll
290
295
  ? "company-wide"
291
296
  : isEmail
@@ -300,8 +305,14 @@ export function registerFilesCommand(program) {
300
305
  : principal;
301
306
  const principalLabel = isAll ? "everyone in the company" : granteeId;
302
307
  const token = (await resolveVaultCredential()).token;
303
- const companySlug = files.opts().company;
304
- const companyUid = await getCompanyUid(token, companySlug);
308
+ const explicitCompanySlug = files.opts().company;
309
+ const companyRootSlug = stripRedundantCompanyScope(prefix)?.prefix === ""
310
+ ? companySlugFromAnchor(prefix)
311
+ : undefined;
312
+ const companyUid = await resolveCompanyUidForAnchoredPath(token, {
313
+ explicit: explicitCompanySlug,
314
+ anchorSlug: companyRootSlug,
315
+ });
305
316
  const res = await vaultApiFetch({
306
317
  token,
307
318
  path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
@@ -361,6 +372,7 @@ export function registerFilesCommand(program) {
361
372
  // a DIFFERENT company's ACL than the one the operator pasted.
362
373
  const explicitCompany = files.opts().company;
363
374
  const canonicalPrefix = normalizeCompanyFilePrefix(prefix, {
375
+ allowCompanyRootAclRow: true,
364
376
  ...(explicitCompany !== undefined
365
377
  ? { companySlug: explicitCompany }
366
378
  : {}),
@@ -1085,9 +1097,11 @@ export function companySlugFromAnchor(prefix, personal) {
1085
1097
  * Normalize company paths at the CLI boundary, preserving personal keys.
1086
1098
  *
1087
1099
  * `mutating` marks a route that CHANGES access and has no preview or
1088
- * confirmation step (grant, share, unshare). Those refuse an anchored path
1089
- * outright rather than guessing, because stripping the anchor changes which
1100
+ * confirmation step (grant, share, unshare). Those refuse anchored content
1101
+ * paths rather than guessing, because stripping the anchor changes which
1090
1102
  * objects the operation reaches and there is nothing downstream to catch it.
1103
+ * `allowCompanyRootAclRow` is limited to exact-root ACL inspect/revoke calls:
1104
+ * those need the original anchor to match a legacy company-root ACL row.
1091
1105
  *
1092
1106
  * `companySlug` is the company the command is actually targeting. When the
1093
1107
  * anchor names a DIFFERENT company we refuse in every mode: stripping would
@@ -1115,9 +1129,12 @@ function normalizeCompanyFilePrefix(prefix, options = {}) {
1115
1129
  `operation to '${options.companySlug}' instead. Re-run with ` +
1116
1130
  `--company ${scope.strippedSlug}, or pass the bucket-relative path.`);
1117
1131
  }
1118
- // A bare anchor strips to nothing. The ACL routes would ship that empty
1119
- // prefix to a server that answers 400, so fail here with a message that says
1120
- // what to do instead. `deferRootReject` is for callers that already have a
1132
+ if (!scope.prefix && options.allowCompanyRootAclRow) {
1133
+ return normalizeFilePrefix(prefix);
1134
+ }
1135
+ // A bare anchor strips to nothing. Content operations must fail with a
1136
+ // message that says what to do instead; exact-root ACL operations preserve
1137
+ // the original anchor above. `deferRootReject` is for callers that already have a
1121
1138
  // root guard with better copy — `runFilesDelete` says "Refusing to delete the
1122
1139
  // vault root", which beats anything this generic helper could write.
1123
1140
  if (!scope.prefix && !options.deferRootReject) {
@@ -163,15 +163,15 @@ export interface InstallInput {
163
163
  };
164
164
  }
165
165
  export declare function installIntegration(token: string, companyUid: string, input: InstallInput): Promise<InstallResult>;
166
- export declare function uninstallIntegration(token: string, companyUid: string, installationId: string): Promise<{
166
+ export declare function uninstallIntegration(token: string, companyUid: string, target: {
167
167
  installationId: string;
168
+ } | {
169
+ connectionId: string;
170
+ }): Promise<{
171
+ installationId: string | null;
168
172
  connectionId: string;
169
173
  }>;
170
- /**
171
- * Permanently remove a revoked connection tombstone. The admin purge endpoint
172
- * deliberately accepts only the connection id: it derives the company and
173
- * enforces owner authorization from the authenticated connection record.
174
- */
174
+ /** Permanently remove a revoked connection tombstone for one company. */
175
175
  export declare function purgeConnection(token: string, companyUid: string, connectionId: string): Promise<{
176
176
  connectionId: string;
177
177
  }>;
@@ -74,33 +74,24 @@ export async function installIntegration(token, companyUid, input) {
74
74
  }
75
75
  return (await res.json());
76
76
  }
77
- export async function uninstallIntegration(token, companyUid, installationId) {
77
+ export async function uninstallIntegration(token, companyUid, target) {
78
78
  const res = await vaultApiFetch({
79
79
  token,
80
80
  path: "/v1/integrations/factory/uninstall",
81
81
  method: "POST",
82
- body: { companyUid, installationId },
82
+ body: { companyUid, ...target },
83
83
  });
84
84
  if (!res.ok)
85
85
  await raiseForResponse(res, "Failed to disconnect the app");
86
86
  return (await res.json());
87
87
  }
88
- /**
89
- * Permanently remove a revoked connection tombstone. The admin purge endpoint
90
- * deliberately accepts only the connection id: it derives the company and
91
- * enforces owner authorization from the authenticated connection record.
92
- */
88
+ /** Permanently remove a revoked connection tombstone for one company. */
93
89
  export async function purgeConnection(token, companyUid, connectionId) {
94
- // Keep the company in this client's call signature with the other
95
- // connection mutations. It is resolved before the target connection so a
96
- // caller cannot use a slug from a different company, but the server's purge
97
- // contract intentionally takes only connectionId in its body.
98
- void companyUid;
99
90
  const res = await vaultApiFetch({
100
91
  token,
101
92
  path: "/v1/integrations/admin/purge",
102
93
  method: "POST",
103
- body: { connectionId },
94
+ body: { companyUid, connectionId },
104
95
  });
105
96
  if (!res.ok)
106
97
  await raiseForResponse(res, "Failed to purge the revoked connection");
@@ -30,6 +30,7 @@ interface ConnectOptions {
30
30
  displayName?: string;
31
31
  token?: string;
32
32
  tokenStdin?: boolean;
33
+ authHeader?: string;
33
34
  auth?: string;
34
35
  browser?: boolean;
35
36
  timeout?: string;
@@ -48,6 +49,8 @@ interface ConnectOptions {
48
49
  * than fall through to a default.
49
50
  */
50
51
  export declare function assertAuthMode(value: string | undefined): void;
52
+ /** RFC 7230 token characters accepted for an HTTP field name. */
53
+ export declare function assertAuthHeader(value: string | undefined): void;
51
54
  /** Single-quote a value for a copy-pasteable shell command. */
52
55
  export declare function shellQuote(value: string): string;
53
56
  /** Everything the connect verbs need to name one app to hq-pro. */