@indigoai-us/hq-cli 5.311.1 → 5.311.3

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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +31 -5
  3. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  4. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +27 -2
  5. package/dist/commands/core-delegate-w1b.js +54 -7
  6. package/dist/commands/core-delegate.d.ts +1 -0
  7. package/dist/commands/core-delegate.js +1 -1
  8. package/dist/commands/files-browse.d.ts +3 -2
  9. package/dist/commands/files-browse.js +2 -7
  10. package/dist/commands/lanes-message.js +18 -7
  11. package/dist/commands/lanes-shared.d.ts +2 -0
  12. package/dist/commands/lanes-watch.js +1 -5
  13. package/dist/commands/lanes.js +10 -13
  14. package/dist/fast-scaffold.d.ts +4 -0
  15. package/dist/fast-scaffold.js +5 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/lib/flag-registry.d.ts +2 -0
  19. package/dist/lib/flag-registry.js +19 -12
  20. package/dist/lib/lanes/autopilot.js +19 -2
  21. package/dist/lib/lanes/delivery.d.ts +1 -0
  22. package/dist/lib/lanes/keep-alive.d.ts +1 -3
  23. package/dist/lib/lanes/keep-alive.js +2 -4
  24. package/dist/lib/lanes/reconcile.js +14 -36
  25. package/dist/lib/lanes/spawn.d.ts +2 -0
  26. package/dist/lib/lanes/spawn.js +35 -12
  27. package/dist/lib/lanes/types.d.ts +2 -2
  28. package/dist/lib/lanes/watch.d.ts +0 -1
  29. package/dist/lib/lanes/watch.js +4 -9
  30. package/dist/main.js +1 -1
  31. package/dist/node-network-compat-core.d.ts +12 -0
  32. package/dist/node-network-compat-core.js +41 -0
  33. package/dist/node-network-compat-fast.d.ts +7 -0
  34. package/dist/node-network-compat-fast.js +23 -0
  35. package/dist/node-network-compat.d.ts +2 -18
  36. package/dist/node-network-compat.js +4 -39
  37. package/dist/node-preflight.d.ts +7 -1
  38. package/dist/node-preflight.js +5 -5
  39. package/dist/utils/files-browse-acl.d.ts +5 -0
  40. package/dist/utils/files-browse-acl.js +11 -0
  41. package/dist/utils/hq-core-yaml.d.ts +10 -0
  42. package/dist/utils/hq-core-yaml.js +40 -0
  43. package/dist/utils/pack-contributions.d.ts +2 -9
  44. package/dist/utils/pack-contributions.js +1 -39
  45. package/dist/utils/scaffold-fast-import-guard.js +5 -1
  46. package/dist/utils/version-report.js +1 -1
  47. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.311.3] — 2026-10-01
6
+ - Match company file access checks against the company-anchored paths printed by `hq files browse`.
7
+ - Record a placement resolved to the current host as local so the lane starts without dispatching back through Outpost pickup.
8
+
9
+ ## [5.311.2] — 2026-10-01
10
+ - Reduce fast-path startup work by loading network compatibility only on routes that need it.
11
+ - Make envelope `next_check_at` an unconditional keep-alive hold and remove its completed rollout flag.
12
+ - Lead autopilot no longer asks a lead to review its own lane as a finished worker, or to relaunch a worker for a story assigned to the lead itself.
13
+ - Lead autopilot counts a story's current `lane` together with its `lanes` history, so `lanes: []` no longer hides the assigned lane.
14
+ - Keep the CLI company seed fixture byte-matched to the hq-core starter golden, including the session sentinel ignore rule.
15
+ - Record lane-watcher continuations as `watcher` resumes and label their prompt source accurately; record monitor-event and system-notice message sources distinctly in lane resume history.
16
+
5
17
  ## [5.311.1] — 2026-10-01
6
18
  - Preserve shell-compatible diagnostics when delegate pickup receives a malformed manifest.
7
19
  - Print URLs returned by `hq integrations call` without redacting their paths or query values. Other integration JSON output keeps its existing URL redaction.
@@ -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)"
@@ -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`
@@ -448,6 +448,11 @@ export function createLaneMessageHandlers(deps) {
448
448
  text: promptedText,
449
449
  });
450
450
  const deliveryText = framedMonitorEvent ?? promptedText;
451
+ const messageHistoryTrigger = opts.monitorEvent !== undefined
452
+ ? "monitor_event"
453
+ : opts.messageSource ?? "message";
454
+ const isMonitorEvent = messageHistoryTrigger === "monitor_event";
455
+ const isSystemNotice = messageHistoryTrigger === "system_notice";
451
456
  const dropboxMessage = {
452
457
  at,
453
458
  message_id: messageId,
@@ -562,9 +567,9 @@ export function createLaneMessageHandlers(deps) {
562
567
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
563
568
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
564
569
  pending: {
565
- trigger: "message",
570
+ trigger: messageHistoryTrigger,
566
571
  text: deliveryText,
567
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
572
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
568
573
  message_id: messageId,
569
574
  compact: opts.compact !== false,
570
575
  },
@@ -589,9 +594,9 @@ export function createLaneMessageHandlers(deps) {
589
594
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
590
595
  ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
591
596
  pending: {
592
- trigger: "message",
597
+ trigger: messageHistoryTrigger,
593
598
  text: deliveryText,
594
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
599
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
595
600
  message_id: messageId,
596
601
  compact: opts.compact !== false,
597
602
  },
@@ -674,10 +679,10 @@ export function createLaneMessageHandlers(deps) {
674
679
  queued_at: at,
675
680
  pending_resume: {
676
681
  session_id: sessionId,
677
- trigger: "message",
682
+ trigger: messageHistoryTrigger,
678
683
  caller: ctx.env.HQ_LANE_ID?.trim() || "user",
679
684
  text: deliveryText,
680
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
685
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
681
686
  message_id: messageId,
682
687
  compact: opts.compact !== false,
683
688
  at,
@@ -708,7 +713,8 @@ export function createLaneMessageHandlers(deps) {
708
713
  kind: "message",
709
714
  text: deliveryText,
710
715
  message_id: messageId,
711
- ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
716
+ ...(isMonitorEvent ? { monitor_event: true } : {}),
717
+ ...(isSystemNotice ? { system_notice: true } : {}),
712
718
  },
713
719
  compact: opts.compact !== false,
714
720
  effort: resumeEffort ?? recordedResumeEffort(lane),
@@ -868,6 +874,11 @@ export function createLaneMessageHandlers(deps) {
868
874
  try {
869
875
  await runMessage(ctx, laneId, {
870
876
  text: formatDropboxMessages(messages),
877
+ messageSource: messages.some((message) => message.kind === "monitor_event")
878
+ ? "monitor_event"
879
+ : messages.some((message) => message.kind === "system_notice")
880
+ ? "system_notice"
881
+ : undefined,
871
882
  compact: false,
872
883
  silent: true,
873
884
  onAccepted: () => { accepted = true; },
@@ -52,6 +52,8 @@ export interface LaneMessageOptions {
52
52
  description: string;
53
53
  sequence: number;
54
54
  };
55
+ /** Source kind for a message recovered from the lane dropbox. */
56
+ messageSource?: "monitor_event" | "system_notice";
55
57
  /** Internal recovery messages do not emit a second command response. */
56
58
  silent?: boolean;
57
59
  /** Internal recovery claim is safe to archive once this callback runs. */
@@ -1,5 +1,5 @@
1
1
  import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
2
- import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG } from "../lib/lanes/keep-alive.js";
2
+ import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
3
3
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
4
4
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
5
5
  import { isLanesError, LanesError } from "../lib/lanes/errors.js";
@@ -50,10 +50,6 @@ export function createLaneWatchHandlers(deps) {
50
50
  company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
51
51
  env: ctx.env,
52
52
  }),
53
- keepAliveNextCheckAtEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
54
- company: resolveLaneCompany(hqRoot, lane).company ?? undefined,
55
- env: ctx.env,
56
- }),
57
53
  readAccountStatuses: async (lanes) => {
58
54
  if (lanes.length === 0)
59
55
  return [];
@@ -61,7 +61,7 @@ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STA
61
61
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
62
62
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
63
63
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
64
- import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
64
+ import { doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
65
65
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
66
66
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
67
67
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -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;
@@ -5159,17 +5162,11 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane) {
5159
5162
  const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
5160
5163
  const keepAliveCompany = resolveLaneCompany(hqRoot, storedLane);
5161
5164
  const now = ctx.now().getTime();
5162
- const nextCheckAtEnabled = storedLane.state === "done" && storedLane.keep_alive === true
5163
- ? await readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, {
5164
- company: keepAliveCompany.company ?? undefined,
5165
- env: ctx.env,
5166
- })
5167
- : false;
5168
- const hasPendingWork = storedLane.keep_alive_standing === true || nextCheckAtEnabled
5165
+ const hasPendingWork = storedLane.state === "done" && storedLane.keep_alive === true
5169
5166
  ? hasPendingKeepAliveWork(hqRoot, storedLane)
5170
5167
  : false;
5171
5168
  const doneStateVerdict = storedLane.state === "done" && storedLane.keep_alive === true
5172
- ? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete, next_check_at: storedLane.last_envelope?.next_check_at }, storedLane.keep_alive_standing === true && hasPendingWork, { nextCheckAtEnabled, hasPendingWork, nowMs: now })
5169
+ ? doneKeepAliveVerdict(storedLane, { decision: storedLane.last_envelope?.decision, story_complete: storedLane.story_complete, next_check_at: storedLane.last_envelope?.next_check_at }, storedLane.keep_alive_standing === true && hasPendingWork, { hasPendingWork, nowMs: now })
5173
5170
  : null;
5174
5171
  let managerStatusTarget;
5175
5172
  let managerStatusGateError;
@@ -5191,7 +5188,7 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane) {
5191
5188
  now: ctx.now().getTime(),
5192
5189
  });
5193
5190
  const laneIsSenior = allLanes.some((item) => item.senior.kind === "lane" && item.senior.id === laneId);
5194
- const requestedNextCheckMs = nextCheckAtEnabled && !hasPendingWork
5191
+ const requestedNextCheckMs = storedLane.state === "done" && storedLane.keep_alive === true && !hasPendingWork
5195
5192
  ? keepAliveNextCheckAtMs(storedLane.last_envelope?.next_check_at, now, Date.parse(storedLane.last_envelope?.at ?? storedLane.updated_at))
5196
5193
  : undefined;
5197
5194
  const storedNextEligibleMs = Date.parse(storedLane.keep_alive_next_eligible_at ?? "");
@@ -16,6 +16,8 @@ export type FastScaffoldScript = {
16
16
  /** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
17
17
  asset: string;
18
18
  root?: "live";
19
+ /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
20
+ requiresNodeNetworkCompatibility?: boolean;
19
21
  } | {
20
22
  /** Command path, joined with spaces, e.g. `policy frontmatter`. */
21
23
  name: string;
@@ -38,6 +40,8 @@ export type FastScaffoldScript = {
38
40
  shellHqRoot?: "none" | "live" | "reconcile-default";
39
41
  /** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
40
42
  readsStdin?: boolean;
43
+ /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
44
+ requiresNodeNetworkCompatibility?: boolean;
41
45
  };
42
46
  /** The relocated scripts that qualify for the entrypoint fast path. */
43
47
  export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
@@ -115,6 +115,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
115
115
  flagKey: "hq-core-native-fast-route",
116
116
  familyFlagKey: "core-native-handoff",
117
117
  asset: "core/scripts/worktree-gc.sh",
118
+ requiresNodeNetworkCompatibility: true,
118
119
  root: "live",
119
120
  shellCwd: "live",
120
121
  shellHqRoot: "live",
@@ -127,6 +128,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
127
128
  flagKey: "hq-core-native-fast-route",
128
129
  familyFlagKey: "core-native-handoff",
129
130
  asset: "core/scripts/clean-worktree-reconcile-handoff.sh",
131
+ requiresNodeNetworkCompatibility: true,
130
132
  root: "cwd",
131
133
  shellCwd: "caller",
132
134
  shellHqRoot: "none",
@@ -139,6 +141,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
139
141
  flagKey: "hq-core-native-fast-route",
140
142
  familyFlagKey: "core-native-handoff",
141
143
  asset: "core/scripts/resume-thread-lock.sh",
144
+ requiresNodeNetworkCompatibility: true,
142
145
  root: "live",
143
146
  shellCwd: "live",
144
147
  shellHqRoot: "live",
@@ -151,6 +154,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
151
154
  flagKey: "hq-core-native-fast-route",
152
155
  familyFlagKey: "core-native-handoff",
153
156
  asset: "core/scripts/verify-story-deliverables.sh",
157
+ requiresNodeNetworkCompatibility: true,
154
158
  root: "live",
155
159
  shellCwd: "live",
156
160
  shellHqRoot: "live",
@@ -163,6 +167,7 @@ export const FAST_SCAFFOLD_SCRIPTS = [
163
167
  flagKey: "hq-core-native-fast-route",
164
168
  familyFlagKey: "core-native-jobs",
165
169
  asset: `core/scripts/${name === "reconcile" ? "outpost-jobs-reconcile" : `jobs-${name}`}.sh`,
170
+ requiresNodeNetworkCompatibility: true,
166
171
  root: name === "reconcile" ? "passthrough" : "cwd",
167
172
  shellCwd: "caller",
168
173
  shellHqRoot: name === "reconcile" ? "reconcile-default" : "none",
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./node-preflight.js";
3
- import "./node-network-compat.js";
3
+ import "./node-network-compat-fast.js";
4
4
  import { isVersionRequest } from "./version-request.js";
5
5
  import { isFastCoreRequest } from "./fast-scaffold.js";
6
6
  export declare const __test__: {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // MUST be first: guard the Node version before any dependency that needs a
3
3
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
4
4
  import "./node-preflight.js";
5
- import "./node-network-compat.js";
5
+ import "./node-network-compat-fast.js";
6
6
  import { isVersionRequest, wantsLabelledVersionReport } from "./version-request.js";
7
7
  // Dependency-light: a pure parser + table, no command graph. Safe to load on
8
8
  // every path (including --version) without reintroducing the heavy startup.
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type WorkerOptions } from "node:worker_threads";
9
9
  import { createFlagClient, type FlagClient, type FlagLookupOptions, type FlagSnapshot } from "@indigoai-us/hq-flags-client";
10
+ type FlagRegistrySentry = Pick<typeof import("@sentry/node"), "captureException" | "withScope">;
10
11
  export { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
11
12
  export type { ProcessFlagCompanyContext } from "./process-flag-company-context.js";
12
13
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
@@ -20,6 +21,7 @@ interface WorkerLike {
20
21
  export interface FlagRegistryDependencies {
21
22
  endpoint?: string;
22
23
  createWorker?: (filename: string | URL, options: WorkerOptions) => WorkerLike;
24
+ sentry?: FlagRegistrySentry;
23
25
  /** Test seam for exercising the no-source fail-open path. */
24
26
  workerSource?: string;
25
27
  captureWorkerFailure?: (error: Error, tags: Record<string, string>) => void;