@indigoai-us/hq-cli 5.311.2 → 5.311.4

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.311.4] — 2026-10-02
6
+ - Preserve existing local company starter files during cloud provisioning by having the CLI request that hq-pro skip blank starter seeding; older servers are detected before company creation.
7
+
8
+ ## [5.311.3] — 2026-10-01
9
+ - Match company file access checks against the company-anchored paths printed by `hq files browse`.
10
+ - Record a placement resolved to the current host as local so the lane starts without dispatching back through Outpost pickup.
11
+
5
12
  ## [5.311.2] — 2026-10-01
6
13
  - Reduce fast-path startup work by loading network compatibility only on routes that need it.
7
14
  - Make envelope `next_check_at` an unconditional keep-alive hold and remove its completed rollout flag.
@@ -60,13 +60,31 @@ STATUS="$(jq -r '.status // empty' "$MANIFEST")"
60
60
 
61
61
  # File-ACL principals must be an email, grp_<id>, or @all — an agentUid is
62
62
  # NOT accepted by `hq files share`. For agent recipients, grants flow through
63
- # a deterministic per-agent delegation group (the /new-agent pattern):
64
- # grp_dlg-<uid tail>, containing exactly that agent.
63
+ # a deterministic per-agent delegation group keyed by the full UID digest:
64
+ # grp_dlg<first 32 SHA-256 hex characters>, containing exactly that agent.
65
+ agent_group_id() {
66
+ local principal="$1" digest
67
+ if command -v sha256sum >/dev/null 2>&1; then
68
+ digest="$(printf '%s' "$principal" | sha256sum)"
69
+ elif command -v shasum >/dev/null 2>&1; then
70
+ digest="$(printf '%s' "$principal" | shasum -a 256)"
71
+ else
72
+ die "sha256 utility is required for agent delegation groups"
73
+ fi
74
+ printf 'grp_dlg%.32s' "$digest"
75
+ }
76
+
65
77
  GRANT_PRINCIPAL="$PRINCIPAL"
66
78
  case "$PRINCIPAL" in
67
79
  agt_*)
68
80
  TAIL="$(printf '%s' "$PRINCIPAL" | tail -c 9 | tr '[:upper:]' '[:lower:]')"
69
- GRANT_PRINCIPAL="grp_dlg-$TAIL"
81
+ LEGACY_GROUP="grp_dlg-$TAIL"
82
+ NEXT_GROUP="$(agent_group_id "$PRINCIPAL")"
83
+ RECORDED_GROUP="$(jq -r '.grantPrincipal // empty' "$MANIFEST")"
84
+ case "$RECORDED_GROUP" in
85
+ "$LEGACY_GROUP"|"$NEXT_GROUP") GRANT_PRINCIPAL="$RECORDED_GROUP" ;;
86
+ *) GRANT_PRINCIPAL="$NEXT_GROUP" ;;
87
+ esac
70
88
  ;;
71
89
  esac
72
90
 
@@ -133,12 +151,20 @@ done
133
151
  # --- 1b. agent recipients: ensure the delegation group exists + contains them
134
152
 
135
153
  if [ "$GRANT_PRINCIPAL" != "$PRINCIPAL" ]; then
136
- # Both calls are idempotent-tolerated: an existing group / existing member
137
- # is fine — the ACL read-back below is the arbiter of success.
154
+ # Group creation and add are idempotent-tolerated; confirm that the complete
155
+ # member list is exactly this agent before any file ACL is shared.
138
156
  hq groups create "$GRANT_PRINCIPAL" --name "Delegation: $DISPLAY" --company "$COMPANY" \
139
157
  || echo "hq-delegate-grant: group create reported an error (may already exist) — continuing" >&2
140
158
  hq groups add "$GRANT_PRINCIPAL" "$PRINCIPAL" --company "$COMPANY" \
141
159
  || echo "hq-delegate-grant: group add reported an error (may already be a member) — continuing" >&2
160
+ GROUP_MEMBERS="$(NO_COLOR=1 FORCE_COLOR=0 hq groups members "$GRANT_PRINCIPAL" --company "$COMPANY")" \
161
+ || die "could not verify delegation group membership"
162
+ MEMBER_IDS="$(printf '%s\n' "$GROUP_MEMBERS" | awk '
163
+ { gsub(/\033\[[0-9;]*m/, "", $0) }
164
+ /^PERSON_UID([[:space:]]|$)/ { collecting = 1; next }
165
+ collecting && NF { print $1 }
166
+ ')"
167
+ [ "$MEMBER_IDS" = "$PRINCIPAL" ] || die "delegation group membership does not exactly match recipient"
142
168
  fi
143
169
 
144
170
  # --- 2+3. exact structured read-back; never infer identity from a new row ---
@@ -93,7 +93,7 @@ if [ -f "$PRD_ABS" ] && jq -e . "$PRD_ABS" >/dev/null 2>&1; then
93
93
  fi
94
94
 
95
95
  GET_LINES="$(jq -r --arg co "$COMPANY" \
96
- '.vaultPrefixes[] | "hq files get \(.prefix) --company \($co)"' "$MANIFEST")"
96
+ 'def bashq: if test("^[A-Za-z0-9_./:@+-]+$") then . else @sh end; .vaultPrefixes[] | "hq files get \(.prefix | bashq) --company \($co | bashq)"' "$MANIFEST")"
97
97
 
98
98
  REPO_IS_NULL="$(jq -r 'if .repo == null then "yes" else "no" end' "$MANIFEST")"
99
99
  REPO_SECTION=""
@@ -135,7 +135,7 @@ while [ "$i" -lt "$PREFIX_COUNT" ]; do
135
135
  if [ "$BROWSE_RC" -ne 0 ]; then
136
136
  echo " FAIL $PFX — browse errored (likely cause: the grant did not land, or the caller's session expired — re-run /hq-login and hq-delegate-grant.sh)"
137
137
  FAILED=1
138
- elif [ -z "$(printf '%s' "$BROWSE_OUT" | tr -d '[:space:]')" ]; then
138
+ elif [ -z "$(printf '%s' "$BROWSE_OUT" | tr -d '[:space:]')" ] || [ "$(printf '%s' "$BROWSE_OUT" | tr -d '[:space:]')" = "Noobjectsunderthatprefix." ]; then
139
139
  echo " FAIL $PFX — reachable but EMPTY (likely cause: the dossier was never pushed to the vault — hq sync push companies/$COMPANY/... and re-run)"
140
140
  FAILED=1
141
141
  else
@@ -156,6 +156,31 @@ done
156
156
  # Browse each distinct parent at most once, and retry an empty result a few
157
157
  # times before trusting it. (Cache dir cleaned on exit.)
158
158
  REF_BROWSE_CACHE="$(mktemp -d)"
159
+ listing_has_exact_key() {
160
+ local listing="$1" expected="$2"
161
+ printf '%s\n' "$listing" | awk -v expected="$expected" '
162
+ function exact_key(row, expected, modified, size) {
163
+ gsub(/\033\[[0-9;]*m/, "", row)
164
+ sub(/[[:space:]]+$/, "", row)
165
+ if (row !~ / +(shared-with-you|creator access — creator-only|role-bypass|personal-vault|private folder — children are creator-only)$/) return 0
166
+ sub(/ +(shared-with-you|creator access — creator-only|role-bypass|personal-vault|private folder — children are creator-only)$/, "", row)
167
+ modified = row
168
+ sub(/^.* +/, "", modified)
169
+ if (modified != "—" && modified !~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\.[0-9][0-9][0-9]Z$/) return 0
170
+ sub(/ +[^[:space:]]+$/, "", row)
171
+ sub(/[[:space:]]+$/, "", row)
172
+ size = row
173
+ sub(/^.* +/, "", size)
174
+ if (size !~ /^[0-9]+$/) return 0
175
+ sub(/ +[0-9]+$/, "", row)
176
+ sub(/[[:space:]]+$/, "", row)
177
+ return row == expected
178
+ }
179
+ NR > 2 && exact_key($0, expected) { found = 1 }
180
+ END { exit found ? 0 : 1 }
181
+ '
182
+ }
183
+
159
184
  browse_parent() { # parent -> listing (memoized per run, retried on empty)
160
185
  local parent="$1" key out attempt
161
186
  key="$REF_BROWSE_CACHE/$(printf '%s' "$parent" | tr '/@:' '___')"
@@ -187,7 +212,7 @@ if [ "$REF_COUNT" -gt 0 ]; then
187
212
  # early match, which under `set -o pipefail` surfaces printf's SIGPIPE as a
188
213
  # non-zero pipeline and FALSE-FAILS a file that is genuinely present (it bit
189
214
  # exactly the referenced files that sort early in a large parent listing).
190
- if [[ "$LISTING" == *"$BASE"* ]]; then
215
+ if listing_has_exact_key "$LISTING" "$kpath"; then
191
216
  echo " pass $REL"
192
217
  else
193
218
  echo " FAIL $REL — granted prefix is reachable but this referenced file is NOT in the vault (likely cause: it was never pushed — hq sync push $kpath --company $COMPANY and re-run)"
@@ -135,6 +135,8 @@ export interface VaultClient {
135
135
  checkSlugInMyNamespace(slug: string): Promise<{
136
136
  available: boolean;
137
137
  conflictingCompanyUid?: string;
138
+ /** Older hq-pro servers omit this field; absence means unsupported. */
139
+ skipStarterSeedSupported?: boolean;
138
140
  }>;
139
141
  /** Fetch a company entity by uid. Used to materialize the entity
140
142
  * after `checkSlugInMyNamespace` reports a same-namespace collision. */
@@ -143,6 +145,7 @@ export interface VaultClient {
143
145
  slug: string;
144
146
  name: string;
145
147
  ownerUid?: string;
148
+ skipStarterSeed?: boolean;
146
149
  }): Promise<VaultEntity>;
147
150
  /**
148
151
  * Soft-tombstone a company (`DELETE /entity/{uid}`). Owner-only. Leaves the
@@ -34,6 +34,25 @@ import * as yaml from "js-yaml";
34
34
  import { parseUserYaml } from "../utils/user-yaml-error.js";
35
35
  import { createSyncProgressRecorder, share, } from "@indigoai-us/hq-cloud";
36
36
  import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
37
+ const SHARED_COMPANY_STARTER_PATHS = [
38
+ "README.md",
39
+ "board.json",
40
+ "knowledge/README.md",
41
+ "knowledge/design-styles/packs/.gitkeep",
42
+ "people/.gitkeep",
43
+ "policies/.gitkeep",
44
+ "projects/.gitkeep",
45
+ "repos/.gitkeep",
46
+ "skills/.gitkeep",
47
+ "sources/meetings/source.yaml",
48
+ "workspace/.gitignore",
49
+ "workspace/index.jsonl",
50
+ "workspace/sessions/.gitkeep",
51
+ ];
52
+ function hasLocalSharedStarterPath(hqRoot, slug) {
53
+ const companyDir = path.join(hqRoot, "companies", slug);
54
+ return SHARED_COMPANY_STARTER_PATHS.some((relativePath) => fs.existsSync(path.join(companyDir, relativePath)));
55
+ }
37
56
  /** Custom error class so the CLI runner can map to exit codes. */
38
57
  export class ProvisionError extends Error {
39
58
  code;
@@ -333,7 +352,11 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
333
352
  const body = await safeBody(res);
334
353
  throw new ProvisionError(1, `Vault GET /entity/check-slug/me failed: ${res.status} ${res.statusText} — ${body}`);
335
354
  }
336
- return (await res.json());
355
+ const data = (await res.json());
356
+ return {
357
+ ...data,
358
+ skipStarterSeedSupported: data.skipStarterSeedSupported === true,
359
+ };
337
360
  });
338
361
  },
339
362
  async getCompanyByUid(uid) {
@@ -359,6 +382,8 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
359
382
  };
360
383
  if (input.ownerUid)
361
384
  body.ownerUid = input.ownerUid;
385
+ if (input.skipStarterSeed)
386
+ body.skipStarterSeed = true;
362
387
  return withVaultRequest(url, {
363
388
  method: "POST",
364
389
  headers,
@@ -548,6 +573,7 @@ export async function provisionCompany(options) {
548
573
  // match the caller's own person UID(s) from `listMyPersonEntities`.
549
574
  const callerOwnedUids = new Set(persons.map((p) => p.uid));
550
575
  const callerIsOwner = !options.ownerUid || callerOwnedUids.has(options.ownerUid);
576
+ const hasLocalSharedStarterFiles = hasLocalSharedStarterPath(options.hqRoot, options.slug);
551
577
  let entity;
552
578
  let createdEntity = false;
553
579
  if (callerIsOwner) {
@@ -557,17 +583,24 @@ export async function provisionCompany(options) {
557
583
  entity = await vaultClient.getCompanyByUid(slugCheck.conflictingCompanyUid);
558
584
  }
559
585
  else {
586
+ if (hasLocalSharedStarterFiles && !slugCheck.skipStarterSeedSupported) {
587
+ throw new ProvisionError(1, "This hq-pro server does not support preserving local starter files during cloud provisioning; update hq-pro and retry");
588
+ }
560
589
  log(`slug available in caller's namespace — creating vault entity`);
561
590
  entity = await vaultClient.createCompanyEntity({
562
591
  slug: options.slug,
563
592
  name: options.name ?? options.slug,
564
593
  ownerUid: options.ownerUid,
594
+ ...(hasLocalSharedStarterFiles ? { skipStarterSeed: true } : {}),
565
595
  });
566
596
  createdEntity = true;
567
597
  log(`created vault entity uid=${entity.uid}`);
568
598
  }
569
599
  }
570
600
  else {
601
+ if (hasLocalSharedStarterFiles) {
602
+ throw new ProvisionError(1, "Cannot preserve local starter files when provisioning for a different owner");
603
+ }
571
604
  log(`--owner ${options.ownerUid} differs from caller's person(s); skipping namespace pre-check (server authoritatively gates per-target-namespace)`);
572
605
  entity = await vaultClient.createCompanyEntity({
573
606
  slug: options.slug,
@@ -8,6 +8,7 @@ import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
8
8
  import { describeCoreCommand } from "./core-command-catalog.js";
9
9
  import { commandExists, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
10
10
  import { resolveLiveRoot } from "../utils/hq-roots.js";
11
+ import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
11
12
  import { runBundledScript } from "../utils/run-bundled-script.js";
12
13
  const FLAG = "core-native-delegate";
13
14
  const ASSETS = {
@@ -113,6 +114,42 @@ function quoteBash(value) {
113
114
  return value;
114
115
  return `'${value.replaceAll("'", `'\\''`)}'`;
115
116
  }
117
+ function delegationGroupId(principal) {
118
+ return "grp_dlg" + createHash("sha256").update(principal).digest("hex").slice(0, 32);
119
+ }
120
+ function stripAnsiSequences(value) {
121
+ const escape = String.fromCharCode(27);
122
+ return value.replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "g"), "");
123
+ }
124
+ function groupMemberIds(output) {
125
+ const plainOutput = stripAnsiSequences(output);
126
+ const lines = plainOutput.split(/\r?\n/);
127
+ const header = lines.findIndex((line) => /^PERSON_UID(?:\s|$)/.test(line.trimStart()));
128
+ if (header < 0)
129
+ return null;
130
+ return lines.slice(header + 1).filter((line) => line.trim() !== "")
131
+ .map((line) => line.trim().split(/\s{2,}/, 1)[0] ?? "");
132
+ }
133
+ function browseContainsExactKey(listing, expected) {
134
+ const lines = listing.split(/\r?\n/);
135
+ const aclLabels = Object.values(ACL_SOURCE_DISPLAY_LABELS);
136
+ return lines.slice(2).some((line) => {
137
+ const row = stripAnsiSequences(line).trimEnd();
138
+ const acl = aclLabels.find((label) => row.endsWith(label));
139
+ if (!acl)
140
+ return false;
141
+ const beforeAcl = row.slice(0, -acl.length).trimEnd();
142
+ const modifiedMatch = /^(.*) +(\S+)$/.exec(beforeAcl);
143
+ if (!modifiedMatch)
144
+ return false;
145
+ const modified = modifiedMatch[2] ?? "";
146
+ if (modified !== "—" && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(modified))
147
+ return false;
148
+ const beforeModified = (modifiedMatch[1] ?? "").trimEnd();
149
+ const sizeMatch = /^(.*) +(\d+)$/.exec(beforeModified);
150
+ return (sizeMatch?.[1] ?? "").trimEnd() === expected;
151
+ });
152
+ }
116
153
  function parseFlags(args, allowed) {
117
154
  const values = {};
118
155
  for (let i = 0; i < args.length;) {
@@ -379,8 +416,12 @@ function runGrant(args, context) {
379
416
  if (!principal)
380
417
  return fail("grant", "manifest has no to.principal");
381
418
  let grantPrincipal = principal;
382
- if (principal.startsWith("agt_"))
383
- grantPrincipal = `grp_dlg-${principal.slice(-9).toLowerCase()}`;
419
+ if (principal.startsWith("agt_")) {
420
+ const legacyGroup = "grp_dlg-" + principal.slice(-9).toLowerCase();
421
+ const nextGroup = delegationGroupId(principal);
422
+ const recordedGroup = text(getPath(manifest, "grantPrincipal"));
423
+ grantPrincipal = recordedGroup === legacyGroup || recordedGroup === nextGroup ? recordedGroup : nextGroup;
424
+ }
384
425
  if (status !== "building" && status !== "granted")
385
426
  return fail("grant", `manifest status is '${status}' — grants run from 'building' (or re-run from 'granted'), not from there`);
386
427
  const prefixes = list(getPath(manifest, "vaultPrefixes"));
@@ -431,6 +472,13 @@ function runGrant(args, context) {
431
472
  result = runHq(["groups", "add", grantPrincipal, principal, "--company", company], context, "inherit");
432
473
  if (result.code !== 0)
433
474
  err("hq-delegate-grant: group add reported an error (may already be a member) — continuing\n");
475
+ const members = subprocess("hq", ["groups", "members", grantPrincipal, "--company", company], context.cwd, { env: { NO_COLOR: "1", FORCE_COLOR: "0" } });
476
+ if (members.code !== 0)
477
+ return fail("grant", "could not verify delegation group membership");
478
+ const memberIds = groupMemberIds(members.stdout ?? "");
479
+ if (memberIds === null || memberIds.length !== 1 || memberIds[0] !== principal) {
480
+ return fail("grant", "delegation group membership does not exactly match recipient");
481
+ }
434
482
  }
435
483
  let expectedType = "";
436
484
  let expectedId = "";
@@ -794,7 +842,7 @@ function runSend(args, context) {
794
842
  .map((entry, index) => `${index + 1}. **${text(getPath(entry, "id"))}: ${text(getPath(entry, "title"))}** — ${text(getPath(entry, "description"))}`)
795
843
  .join("\n");
796
844
  }
797
- const getLines = list(getPath(manifest, "vaultPrefixes", [])).map((entry) => `hq files get ${jqRawPath(entry, "prefix")} --company ${company}`).join("\n");
845
+ const getLines = list(getPath(manifest, "vaultPrefixes", [])).map((entry) => "hq files get " + quoteBash(jqRawPath(entry, "prefix")) + " --company " + quoteBash(company)).join("\n");
798
846
  let repoSection = "";
799
847
  const repoValue = getPath(manifest, "repo");
800
848
  if (repoValue !== null && repoValue !== undefined) {
@@ -824,7 +872,7 @@ function runSend(args, context) {
824
872
  body += `## The goal\n\n${goal}\n\n`;
825
873
  if (stateLine)
826
874
  body += `## Where things stand\n\n${stateLine}\n\n`;
827
- body += `## Pull the files (on demand — no sync)\n\nRun these from your HQ root; each materializes exactly that path:\n\n\`\`\`bash\n${getLines}\n\`\`\`\n\nStart with the project dossier: \`companies/${company}/projects/${project}/\` now contains the PRD (\`prd.json\`), a full brief (\`README.md\` / the DM's details pane), and the delegation journal.\n\n`;
875
+ body += `## Pull the files (on demand — no sync)\n\nRun these from your HQ root; each materializes exactly that path locally:\n\n\`\`\`bash\n${getLines}\n\`\`\`\n\nStart with the project dossier: \`companies/${company}/projects/${project}/\` now contains the PRD (\`prd.json\`), a full brief (\`README.md\` / the DM's details pane), and the delegation journal.\n\n`;
828
876
  if (repoSection)
829
877
  body += `${repoSection}\n\n`;
830
878
  if (secretsSection)
@@ -1024,7 +1072,7 @@ function runVerify(args, context) {
1024
1072
  out(` FAIL ${prefix} — browse errored (likely cause: the grant did not land, or the caller's session expired — re-run /hq-login and hq-delegate-grant.sh)\n`);
1025
1073
  failed = true;
1026
1074
  }
1027
- else if (!combined.replace(/\s/g, "")) {
1075
+ else if (!combined.replace(/\s/g, "") || combined.replace(/\s/g, "") === "Noobjectsunderthatprefix.") {
1028
1076
  out(` FAIL ${prefix} — reachable but EMPTY (likely cause: the dossier was never pushed to the vault — hq sync push companies/${company}/... and re-run)\n`);
1029
1077
  failed = true;
1030
1078
  }
@@ -1059,9 +1107,8 @@ function runVerify(args, context) {
1059
1107
  const relative = reference.slice(`companies/${company}/`.length);
1060
1108
  const separator = relative.lastIndexOf("/");
1061
1109
  const parent = `${separator < 0 ? "." : relative.slice(0, separator)}/`;
1062
- const base = separator < 0 ? relative : relative.slice(separator + 1);
1063
1110
  const listing = browseParent(parent);
1064
- if (listing.includes(base))
1111
+ if (browseContainsExactKey(listing, reference))
1065
1112
  out(` pass ${relative}\n`);
1066
1113
  else {
1067
1114
  out(` FAIL ${relative} — granted prefix is reachable but this referenced file is NOT in the vault (likely cause: it was never pushed — hq sync push ${reference} --company ${company} and re-run)\n`);
@@ -23,6 +23,7 @@ export declare function subprocess(command: string, args: string[], cwd: string,
23
23
  stdinFd?: number;
24
24
  stderr?: "inherit" | "ignore" | "pipe";
25
25
  stdout?: "capture" | "inherit" | "ignore";
26
+ env?: NodeJS.ProcessEnv;
26
27
  maxBuffer?: number;
27
28
  }): CommandResult;
28
29
  type JsonRecord = Record<string, unknown>;
@@ -89,7 +89,7 @@ export function commandExists(name, cwd = process.cwd(), options = {}) {
89
89
  export function subprocess(command, args, cwd, options = {}) {
90
90
  const result = spawnSync(command, args, {
91
91
  cwd,
92
- env: process.env,
92
+ env: { ...process.env, ...options.env },
93
93
  input: options.input,
94
94
  maxBuffer: options.maxBuffer,
95
95
  encoding: "buffer",
@@ -36,6 +36,9 @@
36
36
  import { Command } from "commander";
37
37
  import { ListObjectsV2Command, GetObjectCommand, type ListObjectsV2CommandOutput, type GetObjectCommandOutput } from "@aws-sdk/client-s3";
38
38
  import { VaultClient, type ExplicitGrant } from "@indigoai-us/hq-cloud";
39
+ import { type AclSource } from "../utils/files-browse-acl.js";
40
+ export { ACL_SOURCE_DISPLAY_LABELS, displayAclSource } from "../utils/files-browse-acl.js";
41
+ export type { AclSource } from "../utils/files-browse-acl.js";
39
42
  /** STS-vended credential set the browse/cat path consumes. */
40
43
  export interface BrowseCredentials {
41
44
  accessKeyId: string;
@@ -121,7 +124,6 @@ export type CompanyBrowseClientFactory = (input: {
121
124
  * - `private-folder`: this is the private folder row itself. Its children
122
125
  * are creator-only, so it never covers a child key.
123
126
  */
124
- export type AclSource = "shared-with-you" | "creator" | "role-bypass" | "personal-vault" | "private-folder";
125
127
  export interface BrowseRow {
126
128
  key: string;
127
129
  size: number;
@@ -505,5 +507,4 @@ export declare function resolveBrowseSession(): Promise<{
505
507
  export declare function registerFilesBrowseCommands(filesCmd: Command, deps?: {
506
508
  personalS3Factory?: S3ClientFactory;
507
509
  }): void;
508
- export {};
509
510
  //# sourceMappingURL=files-browse.d.ts.map
@@ -42,6 +42,8 @@ import { pipeline } from "node:stream/promises";
42
42
  import { S3Client, ListObjectsV2Command, GetObjectCommand, } from "@aws-sdk/client-s3";
43
43
  import { VaultClient, } from "@indigoai-us/hq-cloud";
44
44
  import { isPrivateFolderPattern } from "./_patterns.js";
45
+ import { displayAclSource, } from "../utils/files-browse-acl.js";
46
+ export { ACL_SOURCE_DISPLAY_LABELS, displayAclSource } from "../utils/files-browse-acl.js";
45
47
  import { DEFAULT_HQ_ROOT, DEFAULT_COGNITO, buildVaultConfig, } from "../utils/cognito-session.js";
46
48
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
47
49
  import { maxLength } from "../utils/table-width.js";
@@ -142,13 +144,6 @@ export function toBrowseGrantPattern(grantPath, slug) {
142
144
  const companyRelative = toBucketRelative(grantPath, slug);
143
145
  return companyRelative === "*" ? "" : companyRelative;
144
146
  }
145
- function displayAclSource(source) {
146
- return source === "private-folder"
147
- ? "private folder — children are creator-only"
148
- : source === "creator"
149
- ? "creator access — creator-only"
150
- : source;
151
- }
152
147
  /**
153
148
  * Bright-line guard for `--out`: refuse to write any byte beneath
154
149
  * `<hqRoot>/companies/`. We do NOT enumerate `companies/manifest.yaml`
@@ -2962,6 +2962,7 @@ async function runCreate(ctx, opts) {
2962
2962
  const config = loadLanesConfig(hqRoot);
2963
2963
  let outpostId = opts.outpost?.trim() || undefined;
2964
2964
  let placementHostOverride;
2965
+ let placementBalanceHostOverride;
2965
2966
  let autoPlacementChoice;
2966
2967
  let autoPlacementSkippedHosts = [];
2967
2968
  let placementMachineIdentity;
@@ -3013,12 +3014,14 @@ async function runCreate(ctx, opts) {
3013
3014
  if (placementToApply !== undefined) {
3014
3015
  const currentHost = laneHostFromCtx(ctx);
3015
3016
  if (placementToApply.kind === "local") {
3016
- placementHostOverride = currentHost;
3017
+ placementBalanceHostOverride = currentHost;
3018
+ placementHostOverride = { kind: "local", id: ctx.hostname() };
3017
3019
  outpostId = undefined;
3018
3020
  }
3019
3021
  else if (placementToApply.kind === "outpost") {
3020
3022
  if (currentHost.kind === "outpost" && currentHost.id === placementToApply.id) {
3021
- placementHostOverride = currentHost;
3023
+ placementBalanceHostOverride = currentHost;
3024
+ placementHostOverride = { kind: "local", id: ctx.hostname() };
3022
3025
  outpostId = undefined;
3023
3026
  }
3024
3027
  else {
@@ -3275,7 +3278,7 @@ async function runCreate(ctx, opts) {
3275
3278
  const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
3276
3279
  let accountSelection;
3277
3280
  if (provider === "claude") {
3278
- const targetHost = placementHostOverride ?? (outpostId !== undefined
3281
+ const targetHost = placementBalanceHostOverride ?? placementHostOverride ?? (outpostId !== undefined
3279
3282
  ? { kind: "outpost", id: outpostId }
3280
3283
  : laneHostFromCtx(ctx));
3281
3284
  balanceTargetHost = targetHost;
@@ -0,0 +1,5 @@
1
+ /** ACL source values and their stable labels in the hq files browse table. */
2
+ export type AclSource = "shared-with-you" | "creator" | "role-bypass" | "personal-vault" | "private-folder";
3
+ export declare const ACL_SOURCE_DISPLAY_LABELS: Readonly<Record<AclSource, string>>;
4
+ export declare function displayAclSource(source: AclSource): string;
5
+ //# sourceMappingURL=files-browse-acl.d.ts.map
@@ -0,0 +1,11 @@
1
+ export const ACL_SOURCE_DISPLAY_LABELS = {
2
+ "shared-with-you": "shared-with-you",
3
+ creator: "creator access — creator-only",
4
+ "role-bypass": "role-bypass",
5
+ "personal-vault": "personal-vault",
6
+ "private-folder": "private folder — children are creator-only",
7
+ };
8
+ export function displayAclSource(source) {
9
+ return ACL_SOURCE_DISPLAY_LABELS[source];
10
+ }
11
+ //# sourceMappingURL=files-browse-acl.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.311.2",
3
+ "version": "5.311.4",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {