@indigoai-us/hq-cli 5.300.0 → 5.301.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 (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  3. package/dist/command-catalog.generated.d.ts +28 -2
  4. package/dist/command-catalog.generated.js +34 -2
  5. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  6. package/dist/commands/__fixtures__/access-vault.js +1 -1
  7. package/dist/commands/access.d.ts +2 -1
  8. package/dist/commands/access.js +58 -49
  9. package/dist/commands/cloud-provision.js +51 -2
  10. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  11. package/dist/commands/core-company-index-scope-flag.js +6 -0
  12. package/dist/commands/core-job-native-utils.d.ts +6 -0
  13. package/dist/commands/core-job-native-utils.js +80 -0
  14. package/dist/commands/core-job-native.d.ts +2 -4
  15. package/dist/commands/core-job-native.js +6 -10
  16. package/dist/commands/core-job-notify-native.d.ts +36 -0
  17. package/dist/commands/core-job-notify-native.js +359 -0
  18. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  19. package/dist/commands/core-job-remediate-native.js +374 -0
  20. package/dist/commands/core.js +26 -8
  21. package/dist/commands/files.js +31 -14
  22. package/dist/commands/integrations-api.d.ts +6 -6
  23. package/dist/commands/integrations-api.js +4 -13
  24. package/dist/commands/integrations-manage.js +1 -4
  25. package/dist/commands/lanes-message.js +10 -2
  26. package/dist/commands/lanes-shared.d.ts +2 -0
  27. package/dist/commands/lanes.d.ts +2 -0
  28. package/dist/commands/lanes.js +1 -0
  29. package/dist/commands/meetings.js +35 -16
  30. package/dist/commands/safe-extract.d.ts +4 -4
  31. package/dist/commands/safe-extract.js +6 -7
  32. package/dist/lib/core-utils/token-usage-report.js +17 -3
  33. package/dist/lib/index-render/companies.d.ts +4 -1
  34. package/dist/lib/index-render/companies.js +66 -3
  35. package/dist/lib/index-render/index.d.ts +1 -0
  36. package/dist/lib/index-render/index.js +2 -1
  37. package/dist/lib/index-render/shared.d.ts +1 -1
  38. package/dist/lib/index-render/shared.js +4 -2
  39. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  40. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  41. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  42. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  43. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  45. package/dist/utils/self-update.js +51 -4
  46. package/dist/utils/staged-npm-install.d.ts +16 -0
  47. package/dist/utils/staged-npm-install.js +264 -0
  48. package/dist/utils/update-command-supervisor.cjs +53 -1
  49. package/dist/utils/version-gate.d.ts +8 -0
  50. package/dist/utils/version-gate.js +82 -15
  51. package/package.json +2 -2
  52. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  53. 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
  }
@@ -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");
@@ -610,9 +610,6 @@ export function registerManageCommands(integrations) {
610
610
  const companyUid = await getCompanyUid(token, opts.company);
611
611
  const connection = await resolveConnection(token, companyUid, app, opts);
612
612
  const installationId = connection.installation?.id;
613
- if (!installationId) {
614
- throw new IntegrationsCliError(`${connectionLabel(connection)} was not installed through the app catalog, so it cannot be disconnected from here.`, { expected: true });
615
- }
616
613
  if (!opts.yes) {
617
614
  const label = connectionLabel(connection);
618
615
  // Warning + prompt go to stderr: with --json, stdout must stay parseable,
@@ -625,7 +622,7 @@ export function registerManageCommands(integrations) {
625
622
  throw new IntegrationsCliError("Not disconnected. Re-run with --yes if you are sure.", { expected: true });
626
623
  }
627
624
  }
628
- const result = await uninstallIntegration(token, companyUid, installationId);
625
+ const result = await uninstallIntegration(token, companyUid, installationId ? { installationId } : { connectionId: connection.id });
629
626
  if (opts.json) {
630
627
  printJson(result);
631
628
  return;
@@ -488,6 +488,8 @@ export function createLaneMessageHandlers(deps) {
488
488
  });
489
489
  lane = move.lane;
490
490
  if (move.queued) {
491
+ storySwitchDeliveryStarted = true;
492
+ deps.onLaneMessageQueued?.(laneId, "account_move");
491
493
  archiveResumeMessage(hqRoot, laneId, dropboxMessage);
492
494
  }
493
495
  }
@@ -513,8 +515,11 @@ export function createLaneMessageHandlers(deps) {
513
515
  accountSelection: selection,
514
516
  });
515
517
  lane = move.lane;
516
- if (move.queued)
518
+ if (move.queued) {
519
+ storySwitchDeliveryStarted = true;
520
+ deps.onLaneMessageQueued?.(laneId, "account_move");
517
521
  archiveResumeMessage(hqRoot, laneId, dropboxMessage);
522
+ }
518
523
  }
519
524
  else {
520
525
  lane = persistClaudeAccountSelection(hqRoot, lane, selection);
@@ -565,7 +570,7 @@ export function createLaneMessageHandlers(deps) {
565
570
  }
566
571
  archiveResumeMessage(hqRoot, laneId, dropboxMessage);
567
572
  queuedOnAdmission = true;
568
- return casUpdateLane(hqRoot, laneId, (current) => {
573
+ const queued = casUpdateLane(hqRoot, laneId, (current) => {
569
574
  if (current.version !== lane.version || current.state !== lane.state) {
570
575
  throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
571
576
  lane_id: laneId,
@@ -592,6 +597,9 @@ export function createLaneMessageHandlers(deps) {
592
597
  },
593
598
  };
594
599
  });
600
+ storySwitchDeliveryStarted = true;
601
+ deps.onLaneMessageQueued?.(laneId, "admission");
602
+ return queued;
595
603
  }
596
604
  const started = spawnLaneWorker(hqRoot, lane, {
597
605
  env: ctx.env,
@@ -146,6 +146,8 @@ export interface OutpostLaneDispatchBehavior {
146
146
  export interface LaneMessageHandlerDeps {
147
147
  /** Test seam invoked after a provider worker has been started. */
148
148
  onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
149
+ /** Test seam invoked after a message has been durably queued for a lane. */
150
+ onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move") => void;
149
151
  archiveResumeMessage(hqRoot: string, laneId: string, message: {
150
152
  message_id: string;
151
153
  [key: string]: unknown;