@indigoai-us/hq-cli 5.300.0 → 5.302.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
  3. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  4. package/dist/command-catalog.generated.d.ts +32 -3
  5. package/dist/command-catalog.generated.js +39 -3
  6. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  7. package/dist/commands/__fixtures__/access-vault.js +1 -1
  8. package/dist/commands/access.d.ts +2 -1
  9. package/dist/commands/access.js +58 -49
  10. package/dist/commands/bot.js +1 -0
  11. package/dist/commands/cloud-provision.js +51 -2
  12. package/dist/commands/core-checkpoint.js +1 -0
  13. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  14. package/dist/commands/core-company-index-scope-flag.js +6 -0
  15. package/dist/commands/core-handoff-fast.js +1 -0
  16. package/dist/commands/core-handoff.js +2 -0
  17. package/dist/commands/core-job-native-utils.d.ts +6 -0
  18. package/dist/commands/core-job-native-utils.js +80 -0
  19. package/dist/commands/core-job-native.d.ts +2 -4
  20. package/dist/commands/core-job-native.js +6 -10
  21. package/dist/commands/core-job-notify-native.d.ts +36 -0
  22. package/dist/commands/core-job-notify-native.js +359 -0
  23. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  24. package/dist/commands/core-job-remediate-native.js +374 -0
  25. package/dist/commands/core.js +26 -8
  26. package/dist/commands/deploy-comments.js +5 -2
  27. package/dist/commands/files.js +31 -14
  28. package/dist/commands/integrations-api.d.ts +6 -6
  29. package/dist/commands/integrations-api.js +4 -13
  30. package/dist/commands/integrations-connect.d.ts +3 -0
  31. package/dist/commands/integrations-connect.js +21 -2
  32. package/dist/commands/integrations-manage.js +1 -4
  33. package/dist/commands/lanes-message.js +43 -5
  34. package/dist/commands/lanes-questions.d.ts +4 -2
  35. package/dist/commands/lanes-questions.js +123 -2
  36. package/dist/commands/lanes-resume.js +25 -4
  37. package/dist/commands/lanes-shared.d.ts +15 -1
  38. package/dist/commands/lanes-watch.js +5 -1
  39. package/dist/commands/lanes.d.ts +2 -0
  40. package/dist/commands/lanes.js +92 -9
  41. package/dist/commands/meetings.js +35 -16
  42. package/dist/commands/monitor.js +17 -2
  43. package/dist/commands/report.js +5 -2
  44. package/dist/commands/rescue.d.ts +4 -0
  45. package/dist/commands/rescue.js +27 -17
  46. package/dist/commands/safe-extract.d.ts +4 -4
  47. package/dist/commands/safe-extract.js +6 -7
  48. package/dist/lib/agent-kit/fallback.d.ts +1 -0
  49. package/dist/lib/agent-kit/fallback.js +1 -0
  50. package/dist/lib/bot/daemon.d.ts +5 -1
  51. package/dist/lib/bot/daemon.js +4 -4
  52. package/dist/lib/core-utils/soft-timeout.js +1 -1
  53. package/dist/lib/core-utils/token-usage-report.js +17 -3
  54. package/dist/lib/deploy-apps.d.ts +13 -0
  55. package/dist/lib/deploy-apps.js +24 -0
  56. package/dist/lib/index-render/companies.d.ts +4 -1
  57. package/dist/lib/index-render/companies.js +66 -3
  58. package/dist/lib/index-render/index.d.ts +1 -0
  59. package/dist/lib/index-render/index.js +2 -1
  60. package/dist/lib/index-render/projects.js +82 -5
  61. package/dist/lib/index-render/shared.d.ts +1 -1
  62. package/dist/lib/index-render/shared.js +4 -2
  63. package/dist/lib/lanes/delivery.d.ts +7 -3
  64. package/dist/lib/lanes/delivery.js +18 -8
  65. package/dist/lib/lanes/index.d.ts +1 -1
  66. package/dist/lib/lanes/keep-alive.d.ts +13 -3
  67. package/dist/lib/lanes/keep-alive.js +29 -5
  68. package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
  69. package/dist/lib/lanes/outpost-dispatch.js +12 -0
  70. package/dist/lib/lanes/reconcile.d.ts +1 -0
  71. package/dist/lib/lanes/reconcile.js +358 -38
  72. package/dist/lib/lanes/spawn.d.ts +1 -0
  73. package/dist/lib/lanes/spawn.js +20 -10
  74. package/dist/lib/lanes/store.js +24 -0
  75. package/dist/lib/lanes/story-ledger.d.ts +2 -0
  76. package/dist/lib/lanes/story-ledger.js +8 -0
  77. package/dist/lib/lanes/types.d.ts +26 -4
  78. package/dist/lib/lanes/types.js +33 -1
  79. package/dist/lib/lanes/watch.d.ts +1 -0
  80. package/dist/lib/lanes/watch.js +19 -6
  81. package/dist/lib/lanes/watcher.js +139 -9
  82. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  83. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  84. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  85. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  86. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  87. package/dist/lib/mesh/live/daemon/run.js +36 -2
  88. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  89. package/dist/lib/search-index/background.d.ts +1 -0
  90. package/dist/lib/search-index/background.js +1 -0
  91. package/dist/utils/self-update.js +51 -4
  92. package/dist/utils/staged-npm-install.d.ts +16 -0
  93. package/dist/utils/staged-npm-install.js +264 -0
  94. package/dist/utils/update-command-supervisor.cjs +54 -1
  95. package/dist/utils/version-gate.d.ts +8 -0
  96. package/dist/utils/version-gate.js +83 -16
  97. package/package.json +2 -2
  98. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  99. package/dist/lib/install-path-compatibility-flag.js +0 -12
package/CHANGELOG.md CHANGED
@@ -2,8 +2,50 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.302.0] — 2026-10-01
6
+
7
+ - Generate company project indexes while preserving hand-written content unless `--accept` is used.
8
+
9
+ - Follow all hq-deploy app-list pages when resolving apps for comments and report publishing.
10
+ - Let a senior message or resume an `awaiting_input` lane when its pending question is gone or cannot be reached from the senior's host. Pending questions remain answer-first, including across registered Outpost routes.
11
+ - A senior can resume or send a fallback message to an `awaiting_input` lane when its pending questions are addressed to another audience. Once the resume is accepted, those questions are cancelled on their owner; unreachable owners are reported while the lane continues.
12
+ - When a blocker escalation targets a session without a registered monitor inbox, record the reason and deliver it through the lane's existing inbox instead of retrying the failed escalation every watcher pass.
13
+ - `hq integrations disconnect` can now remove company integrations that were created outside the app catalog.
14
+ - Lead envelopes can set `next_check_at` to defer automatic keep-alive resumes
15
+ until a scheduled check, with pending work continuing to resume immediately.
16
+ - Reconcile records deduplicated keep-alive skip reasons in each lane's watcher event log.
17
+ - Add a company-scoped reconcile gate that closes local interrupted worker lanes after 24 hours when all carried project stories are done or dropped and no pending work remains.
18
+ - Skip refreshing failed-dispatch lane mirrors that never reached their Outpost owner.
19
+ - Keep lane keep-alive, retry-context, and monitor history readers moving past torn JSONL records.
20
+ - Refresh primary mirror heartbeats from fresh running Outpost reports before dead-host reconciliation.
21
+ - Report answerable awaiting-input questions as the `hq lanes show` delivery path, and retry blocker-freshness fallback messages after a failed append.
22
+ - Let `hq integrations connect` send an API key in a custom HTTP header.
23
+ - Bound `hq rescue` GitHub release and tag lookups to 30 seconds with a clear timeout error.
24
+ - hq-cli now logs live-board refresh failures and recovery while continuing to retry.
25
+ - On Windows, background jobs started by `/handoff` and search indexing no longer open console windows.
26
+
27
+ ## [5.301.0] — 2026-10-01
28
+ - `hq meetings transcript` and `hq meetings notes` stop a stalled document download after two minutes and say so, instead of waiting forever.
29
+
30
+ - Keep `hq access` from reporting a denied vault path as missing or exposing matching paths.
31
+ - Fix self-updates for npm installs whose global prefix contains spaces.
32
+ - `hq files acl companies/<slug>` and `hq files unshare companies/<slug> --with ...` now handle ACL rows at that exact company path instead of treating it as the vault root.
33
+ - Add native hq core job notify and remediate commands behind core-native-jobs, retaining bundled shell companions.
34
+ - Allow owners to disconnect custom Factory connections without installation records, and send the company UID required by the revoked-connection purge endpoint.
35
+ - Fail company provisioning when its initial sync reports per-file upload failures, while retaining the successful upload count in failure details.
36
+ - Treat plan-limited files as initial-sync failures and keep provisioning progress snapshots updated.
37
+ - Preserve story attribution after a message is durably queued by lane admission or an account move.
38
+ - The token usage report accepts `--project-dir <path>` to read one Claude activity folder instead of the default folders.
39
+
5
40
  ## [5.300.0] — 2026-09-30
41
+ - `hq files unshare --with agt_...` removes a grant made to an agent. Before, agent uids were rejected as invalid principals.
6
42
  - With the default-off hq-flags gate `files.share-empty-prefix-warning`, `hq files share` warns when a granted prefix has no files. A failed or timed-out list check does not block the grant.
43
+ - Recognize pnpm v11+ global installs and handle Windows drive-path tar archives without waiting for the asynchronous feature-flag snapshot.
44
+ - Self-update failure reports now include the npm error code and a sanitized reason.
45
+
46
+ ### Fixed
47
+
48
+ - `hq core rebuild-index companies --company <slug>` scopes the generated write to that company (behind the default-off `cli.rebuild-index-company-scope` flag), and `--help` exits without reading or writing company indexes. The unscoped run also ignores orchestrator state tied to another company's PRD.
7
49
 
8
50
  - Lane messages and resumes that switch project or story attribution now tell the provider which story is active. Reused completion artifacts no longer close the new story.
9
51
  - Make signed-out `hq sync` commands fail fast with `hq login` guidance when stdin or stdout is not a terminal, while keeping browser login in a terminal.
@@ -11,12 +11,11 @@ set -euo pipefail
11
11
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
12
  cd "$HQ_ROOT"
13
13
 
14
+ COMPANIES_DIR="companies"
14
15
  PROJECTS_DIR="personal/projects"
15
16
  OUT="${PROJECTS_DIR}/INDEX.md"
16
17
  DATE=$(date -u +%Y-%m-%d)
17
18
 
18
- [[ -d "$PROJECTS_DIR" ]] || { echo "rebuild-projects-index: ${PROJECTS_DIR}/ missing, skipping" >&2; exit 0; }
19
-
20
19
  sanitize_cell() {
21
20
  printf '%s' "$1" | tr '\n' ' ' | sed -e 's/|/\\|/g' -e 's/ */ /g' -e 's/^ *//' -e 's/ *$//'
22
21
  }
@@ -29,17 +28,23 @@ trunc() {
29
28
  project_status() {
30
29
  local project="$1"
31
30
  local prd="$2"
31
+ local fallback="${3:-—}"
32
+ local expected_prd="${4:-}"
32
33
  local state="workspace/orchestrator/${project}/state.json"
33
34
  if [[ -f "$state" ]]; then
34
- local raw
35
- raw=$(jq -r '.status // "active"' "$state" 2>/dev/null || echo "active")
36
- case "$raw" in
37
- completed|complete) echo "complete" ;;
38
- in_progress|active|started) echo "active" ;;
39
- archived) echo "archived" ;;
40
- *) echo "$raw" ;;
41
- esac
42
- return
35
+ local state_prd
36
+ state_prd=$(jq -r '.prd_path // ""' "$state" 2>/dev/null || echo "")
37
+ if [[ -z "$expected_prd" || "$state_prd" == "$expected_prd" ]]; then
38
+ local raw
39
+ raw=$(jq -r '.status // "active"' "$state" 2>/dev/null || echo "active")
40
+ case "$raw" in
41
+ completed|complete) echo "complete" ;;
42
+ in_progress|active|started) echo "active" ;;
43
+ archived) echo "archived" ;;
44
+ *) echo "$raw" ;;
45
+ esac
46
+ return
47
+ fi
43
48
  fi
44
49
  if [[ -f "$prd" ]]; then
45
50
  local total passes
@@ -50,9 +55,91 @@ project_status() {
50
55
  return
51
56
  fi
52
57
  fi
53
- echo "—"
58
+ echo "$fallback"
59
+ }
60
+
61
+ sha256_file() {
62
+ if command -v sha256sum >/dev/null 2>&1; then
63
+ sha256sum "$1" | awk '{print $1}'
64
+ else
65
+ shasum -a 256 "$1" | awk '{print $1}'
66
+ fi
54
67
  }
55
68
 
69
+ stamp_generated() {
70
+ local input="$1" output="$2" normalized hash
71
+ normalized=$(mktemp)
72
+ sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$input" > "$normalized"
73
+ hash=$(sha256_file "$normalized")
74
+ rm -f "$normalized"
75
+ awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$input" > "$output"
76
+ }
77
+
78
+ company_title() {
79
+ printf '%s\n' "$1" | awk -F'-' '{ for (i=1; i<=NF; i++) { $i = toupper(substr($i,1,1)) substr($i,2) } print }' OFS=' '
80
+ }
81
+
82
+ write_company_projects_index() {
83
+ local company="$1" company_root project_dir output tmp name prd description status
84
+ company_root="${COMPANIES_DIR}/${company}"
85
+ project_dir="${company_root}/projects"
86
+ output="${project_dir}/INDEX.md"
87
+ tmp=$(mktemp)
88
+ {
89
+ printf '# %s Projects\n\n' "$(company_title "$company")"
90
+ printf '> Auto-generated. Updated: %s\n\n' "$DATE"
91
+ echo '| Project | Status | Description |'
92
+ echo '|---------|--------|-------------|'
93
+ while IFS= read -r project; do
94
+ name=$(basename "$project")
95
+ [[ "$name" == _* ]] && continue
96
+ [[ "$name" == .* ]] && continue
97
+ prd="${project}/prd.json"
98
+ description=""
99
+ if [[ -f "$prd" ]]; then description=$(jq -r '.description // ""' "$prd" 2>/dev/null || echo ""); fi
100
+ status=$(project_status "$name" "$prd" "active" "$prd")
101
+ description=$(sanitize_cell "$description")
102
+ description=$(trunc "$description" 110)
103
+ [[ -z "$description" ]] && description="—"
104
+ printf '| `%s` | %s | %s |\n' "$name" "$status" "$description"
105
+ done < <(find "$project_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
106
+ } > "$tmp"
107
+ local existing=0 stored actual normalized
108
+ [[ -e "$output" ]] && existing=1
109
+ if [[ "$existing" == 1 ]]; then
110
+ if grep -q '<!-- hq:keep -->' "$output" || ! grep -q '^> Auto-generated. Updated: ' "$output"; then
111
+ echo "rebuild-projects-index: refused to overwrite ${output} (hand-written or kept)" >&2
112
+ rm -f "$tmp"
113
+ return 0
114
+ fi
115
+ stored=$(sed -n 's/^<!-- hq-generated-sha256: \([a-f0-9]*\) -->$/\1/p' "$output" | head -n 1)
116
+ if [[ -n "$stored" ]]; then
117
+ normalized=$(mktemp)
118
+ sed -e '/^<!-- hq-generated-sha256: [a-f0-9]* -->$/d' -e 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$output" > "$normalized"
119
+ actual=$(sha256_file "$normalized")
120
+ rm -f "$normalized"
121
+ if [[ "$stored" != "$actual" ]]; then
122
+ echo "rebuild-projects-index: refused to overwrite ${output} (generated content was edited)" >&2
123
+ rm -f "$tmp"
124
+ return 0
125
+ fi
126
+ fi
127
+ fi
128
+ stamp_generated "$tmp" "$output"
129
+ rm -f "$tmp"
130
+ echo "rebuild-projects-index: wrote ${output}" >&2
131
+ }
132
+
133
+ while IFS= read -r dir; do
134
+ co=$(basename "$dir")
135
+ [[ "$co" == _* ]] && continue
136
+ [[ "$co" == .* ]] && continue
137
+ project_dir="${COMPANIES_DIR}/${co}/projects"
138
+ [[ -d "$project_dir" ]] && write_company_projects_index "$co"
139
+ done < <(find "$COMPANIES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
140
+
141
+ [[ -d "$PROJECTS_DIR" ]] || { echo "rebuild-projects-index: ${PROJECTS_DIR}/ missing, skipping" >&2; exit 0; }
142
+
56
143
  {
57
144
  echo "# Projects"
58
145
  echo ""
@@ -5,6 +5,7 @@
5
5
  # scripts/token-usage-report.sh --last 14 # last 14 days
6
6
  # scripts/token-usage-report.sh --since 2026-05-01
7
7
  # scripts/token-usage-report.sh --json # machine-readable
8
+ # scripts/token-usage-report.sh --project-dir <path> # read one activity folder
8
9
  #
9
10
  # Effective tokens weighting (matches weekly rate-limit volume):
10
11
  # input + 5×output + 1.25×cache_creation + 0.1×cache_read
@@ -24,6 +25,14 @@ while [ $# -gt 0 ]; do
24
25
  --last) LAST_DAYS="$2"; shift 2 ;;
25
26
  --since) SINCE="$2"; shift 2 ;;
26
27
  --json) JSON_MODE=1; shift ;;
28
+ --project-dir)
29
+ if [ "$#" -lt 2 ] || [ -z "$2" ]; then
30
+ echo "token-usage-report: --project-dir requires a path" >&2
31
+ exit 1
32
+ fi
33
+ PROJECT_DIR="$2"
34
+ shift 2
35
+ ;;
27
36
  --compare-windows) shift ;; # flag, presence triggers compare mode via --before/--after
28
37
  --before) COMPARE_BEFORE="$2"; shift 2 ;;
29
38
  --after) COMPARE_AFTER="$2"; shift 2 ;;
@@ -1713,7 +1713,7 @@ export declare const COMMAND_CATALOG: readonly [{
1713
1713
  }];
1714
1714
  readonly options: readonly [{
1715
1715
  readonly flags: "--with <principal>";
1716
- readonly description: "Person uid, email address, group id, or '@all' to remove the grant";
1716
+ readonly description: "Person uid, agent uid, email address, group id, or '@all' to remove the grant";
1717
1717
  }];
1718
1718
  readonly subcommands: readonly [];
1719
1719
  }, {
@@ -3122,6 +3122,9 @@ export declare const COMMAND_CATALOG: readonly [{
3122
3122
  }, {
3123
3123
  readonly flags: "--token-stdin";
3124
3124
  readonly description: "Read the API key from stdin";
3125
+ }, {
3126
+ readonly flags: "--auth-header <name>";
3127
+ readonly description: "Send the API key in this HTTP header (implies --auth key)";
3125
3128
  }, {
3126
3129
  readonly flags: "--auth <mode>";
3127
3130
  readonly description: "Force the auth mode: none, key, or oauth (default: detect)";
@@ -5620,13 +5623,26 @@ export declare const COMMAND_CATALOG: readonly [{
5620
5623
  }];
5621
5624
  }, {
5622
5625
  readonly name: "job";
5623
- readonly description: "Run or inspect one scheduled job";
5626
+ readonly description: "Run, inspect, notify, or remediate one scheduled job";
5624
5627
  readonly aliases: readonly [];
5625
5628
  readonly hidden: false;
5626
5629
  readonly usage: "[options] [command]";
5627
5630
  readonly arguments: readonly [];
5628
5631
  readonly options: readonly [];
5629
5632
  readonly subcommands: readonly [{
5633
+ readonly name: "notify";
5634
+ readonly description: "Send a scheduled-job status alert to its owner";
5635
+ readonly aliases: readonly [];
5636
+ readonly hidden: false;
5637
+ readonly usage: "[args...]";
5638
+ readonly arguments: readonly [{
5639
+ readonly name: "args";
5640
+ readonly required: false;
5641
+ readonly variadic: true;
5642
+ }];
5643
+ readonly options: readonly [];
5644
+ readonly subcommands: readonly [];
5645
+ }, {
5630
5646
  readonly name: "probe";
5631
5647
  readonly description: "Check scheduled-job requirements and report readiness";
5632
5648
  readonly aliases: readonly [];
@@ -5639,6 +5655,19 @@ export declare const COMMAND_CATALOG: readonly [{
5639
5655
  }];
5640
5656
  readonly options: readonly [];
5641
5657
  readonly subcommands: readonly [];
5658
+ }, {
5659
+ readonly name: "remediate";
5660
+ readonly description: "Run bounded remediation for a scheduled-job failure";
5661
+ readonly aliases: readonly [];
5662
+ readonly hidden: false;
5663
+ readonly usage: "[args...]";
5664
+ readonly arguments: readonly [{
5665
+ readonly name: "args";
5666
+ readonly required: false;
5667
+ readonly variadic: true;
5668
+ }];
5669
+ readonly options: readonly [];
5670
+ readonly subcommands: readonly [];
5642
5671
  }, {
5643
5672
  readonly name: "run";
5644
5673
  readonly description: "Run a scheduled job";
@@ -8582,7 +8611,7 @@ export declare const COMMAND_CATALOG: readonly [{
8582
8611
  readonly subcommands: readonly [];
8583
8612
  }, {
8584
8613
  readonly name: "resume";
8585
- readonly description: "Resume an interrupted, done or blocked lane on its recorded provider session; recover failed lanes with the senior-only --from-failed option";
8614
+ readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option";
8586
8615
  readonly aliases: readonly [];
8587
8616
  readonly hidden: false;
8588
8617
  readonly usage: "[options] <lane-id>";
@@ -2185,7 +2185,7 @@ export const COMMAND_CATALOG = [
2185
2185
  "options": [
2186
2186
  {
2187
2187
  "flags": "--with <principal>",
2188
- "description": "Person uid, email address, group id, or '@all' to remove the grant"
2188
+ "description": "Person uid, agent uid, email address, group id, or '@all' to remove the grant"
2189
2189
  }
2190
2190
  ],
2191
2191
  "subcommands": []
@@ -4013,6 +4013,10 @@ export const COMMAND_CATALOG = [
4013
4013
  "flags": "--token-stdin",
4014
4014
  "description": "Read the API key from stdin"
4015
4015
  },
4016
+ {
4017
+ "flags": "--auth-header <name>",
4018
+ "description": "Send the API key in this HTTP header (implies --auth key)"
4019
+ },
4016
4020
  {
4017
4021
  "flags": "--auth <mode>",
4018
4022
  "description": "Force the auth mode: none, key, or oauth (default: detect)"
@@ -7246,13 +7250,29 @@ export const COMMAND_CATALOG = [
7246
7250
  },
7247
7251
  {
7248
7252
  "name": "job",
7249
- "description": "Run or inspect one scheduled job",
7253
+ "description": "Run, inspect, notify, or remediate one scheduled job",
7250
7254
  "aliases": [],
7251
7255
  "hidden": false,
7252
7256
  "usage": "[options] [command]",
7253
7257
  "arguments": [],
7254
7258
  "options": [],
7255
7259
  "subcommands": [
7260
+ {
7261
+ "name": "notify",
7262
+ "description": "Send a scheduled-job status alert to its owner",
7263
+ "aliases": [],
7264
+ "hidden": false,
7265
+ "usage": "[args...]",
7266
+ "arguments": [
7267
+ {
7268
+ "name": "args",
7269
+ "required": false,
7270
+ "variadic": true
7271
+ }
7272
+ ],
7273
+ "options": [],
7274
+ "subcommands": []
7275
+ },
7256
7276
  {
7257
7277
  "name": "probe",
7258
7278
  "description": "Check scheduled-job requirements and report readiness",
@@ -7269,6 +7289,22 @@ export const COMMAND_CATALOG = [
7269
7289
  "options": [],
7270
7290
  "subcommands": []
7271
7291
  },
7292
+ {
7293
+ "name": "remediate",
7294
+ "description": "Run bounded remediation for a scheduled-job failure",
7295
+ "aliases": [],
7296
+ "hidden": false,
7297
+ "usage": "[args...]",
7298
+ "arguments": [
7299
+ {
7300
+ "name": "args",
7301
+ "required": false,
7302
+ "variadic": true
7303
+ }
7304
+ ],
7305
+ "options": [],
7306
+ "subcommands": []
7307
+ },
7272
7308
  {
7273
7309
  "name": "run",
7274
7310
  "description": "Run a scheduled job",
@@ -11068,7 +11104,7 @@ export const COMMAND_CATALOG = [
11068
11104
  },
11069
11105
  {
11070
11106
  "name": "resume",
11071
- "description": "Resume an interrupted, done or blocked lane on its recorded provider session; recover failed lanes with the senior-only --from-failed option",
11107
+ "description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with the senior-only --from-failed option",
11072
11108
  "aliases": [],
11073
11109
  "hidden": false,
11074
11110
  "usage": "[options] <lane-id>",
@@ -82,6 +82,8 @@ export interface MakeAccessFixtureOptions {
82
82
  * without read on the prefix — the server refuses to enumerate.
83
83
  */
84
84
  listDenied?: boolean;
85
+ /** present-denied only: let a test isolate list denial from vend denial. */
86
+ vendDenied?: boolean;
85
87
  /** Deny only the ROOT (prefix "") list — the caller can read the prefix but not enumerate the bucket. */
86
88
  rootListDenied?: boolean;
87
89
  }
@@ -93,7 +93,7 @@ export function makeAccessFixture(opts) {
93
93
  if (opts.state === "missing") {
94
94
  throw attachStatus(new Error("not found (404)"), 404);
95
95
  }
96
- if (opts.state === "present-denied") {
96
+ if (opts.state === "present-denied" && opts.vendDenied !== false) {
97
97
  throw attachStatus(new Error("caller lacks read on this prefix (403)"), 403);
98
98
  }
99
99
  return FAKE_VEND;
@@ -122,7 +122,8 @@ export interface RunAccessResult {
122
122
  outcome: AccessOutcome;
123
123
  path: string;
124
124
  company: string;
125
- exists: boolean;
125
+ /** Omitted when an authorization denial leaves vault existence unknown. */
126
+ exists?: boolean;
126
127
  grantor?: AccessGrantor;
127
128
  candidates?: string[];
128
129
  steps: string[];
@@ -52,7 +52,7 @@ export function formatAccessOutcome(result) {
52
52
  lines.push(result.path);
53
53
  break;
54
54
  case "no-access":
55
- lines.push(`The file exists in the vault, but you do not have read access.`);
55
+ lines.push(`You do not have read access to this path.`);
56
56
  lines.push(result.path);
57
57
  break;
58
58
  case "pending-confirmation":
@@ -236,72 +236,81 @@ export async function runAccess(input) {
236
236
  const bucket = entity?.bucketName ?? `hq-vault-cmp-${slug}`;
237
237
  const s3 = deps.companyClient({ companyUid: deps.companyUid });
238
238
  let listDenied = false;
239
- const prefixList = await listKeys(s3, bucket, relative);
240
- const exact = prefixList.keys.find((k) => k === relative);
241
239
  let key;
242
- if (prefixList.denied) {
243
- // The server refused to enumerate — the caller has no read on this
244
- // prefix. Keep the target as the key so the request rung can still name
245
- // it for the grantor.
240
+ if (vendDenied) {
241
+ // The caller is denied at the company boundary. Do not use list results to
242
+ // resolve or disclose matching keys, even if a separate listing succeeds.
246
243
  listDenied = true;
247
244
  key = relative;
248
- steps.push("list: 403 (no-access)");
249
- }
250
- else if (exact) {
251
- key = exact;
252
- steps.push("resolve: exact key");
253
- steps.push("list: 1 hit");
245
+ steps.push("list: skipped after vend 403 (no-access)");
254
246
  }
255
247
  else {
256
- const allList = await listKeys(s3, bucket, "");
257
- const q = relative.toLowerCase();
258
- // A root-list 403 only says the caller cannot enumerate the WHOLE bucket.
259
- // The prefix list above succeeded, so fuzzy-match within it first; the
260
- // root denial is the verdict only when the readable prefix has no match.
261
- const pool = allList.denied ? prefixList.keys : allList.keys;
262
- const fuzzy = pool.filter((k) => k.toLowerCase().includes(q));
263
- if (allList.denied && fuzzy.length === 0) {
248
+ const prefixList = await listKeys(s3, bucket, relative);
249
+ const exact = prefixList.keys.find((k) => k === relative);
250
+ if (prefixList.denied) {
251
+ // The server refused to enumerate — the caller has no read on this
252
+ // prefix. Keep the target as the key so the request rung can still name
253
+ // it for the grantor.
264
254
  listDenied = true;
265
255
  key = relative;
266
256
  steps.push("list: 403 (no-access)");
267
257
  }
268
- else if (fuzzy.length === 0) {
269
- steps.push("list: 0 hits");
270
- const result = finish("never-existed", {
271
- path: anchored(relative) || input.target,
272
- company: slug,
273
- exists: false,
274
- steps,
275
- }, finishCtx);
276
- emit(input, result, log);
277
- return result;
278
- }
279
- else if (fuzzy.length === 1) {
280
- key = fuzzy[0];
281
- steps.push("resolve: fuzzy (1 hit)");
258
+ else if (exact) {
259
+ key = exact;
260
+ steps.push("resolve: exact key");
282
261
  steps.push("list: 1 hit");
283
262
  }
284
263
  else {
285
- const candidates = fuzzy.map(anchored);
286
- steps.push(`list: ${fuzzy.length} hits`);
287
- let picked = null;
288
- if (deps.pick) {
289
- picked = await deps.pick(candidates);
264
+ const allList = await listKeys(s3, bucket, "");
265
+ const q = relative.toLowerCase();
266
+ // A root-list 403 only says the caller cannot enumerate the WHOLE bucket.
267
+ // The prefix list above succeeded, so fuzzy-match within it first; the
268
+ // root denial is the verdict only when the readable prefix has no match.
269
+ const pool = allList.denied ? prefixList.keys : allList.keys;
270
+ const fuzzy = pool.filter((k) => k.toLowerCase().includes(q));
271
+ if (allList.denied && fuzzy.length === 0) {
272
+ listDenied = true;
273
+ key = relative;
274
+ steps.push("list: 403 (no-access)");
290
275
  }
291
- if (!picked) {
292
- const result = finish("ambiguous", {
293
- path: input.target,
276
+ else if (fuzzy.length === 0) {
277
+ steps.push("list: 0 hits");
278
+ const result = finish("never-existed", {
279
+ path: anchored(relative) || input.target,
294
280
  company: slug,
295
281
  exists: false,
296
- candidates,
297
282
  steps,
298
283
  }, finishCtx);
299
284
  emit(input, result, log);
300
285
  return result;
301
286
  }
302
- const stripped = normalizeTarget(picked, slug);
303
- key = stripped;
304
- steps.push("resolve: pick");
287
+ else if (fuzzy.length === 1) {
288
+ key = fuzzy[0];
289
+ steps.push("resolve: fuzzy (1 hit)");
290
+ steps.push("list: 1 hit");
291
+ }
292
+ else {
293
+ const candidates = fuzzy.map(anchored);
294
+ steps.push(`list: ${fuzzy.length} hits`);
295
+ let picked = null;
296
+ if (deps.pick) {
297
+ picked = await deps.pick(candidates);
298
+ }
299
+ if (!picked) {
300
+ const result = finish("ambiguous", {
301
+ path: input.target,
302
+ company: slug,
303
+ exists: false,
304
+ candidates,
305
+ steps,
306
+ }, finishCtx);
307
+ emit(input, result, log);
308
+ return result;
309
+ }
310
+ const stripped = normalizeTarget(picked, slug);
311
+ key = stripped;
312
+ steps.push("resolve: pick");
313
+ }
305
314
  }
306
315
  }
307
316
  const resolvedKey = key;
@@ -524,7 +533,7 @@ export async function runAccess(input) {
524
533
  const result = finish(outcome, {
525
534
  path: resolvedPath,
526
535
  company: slug,
527
- exists: true,
536
+ ...(vendDenied || listDenied ? {} : { exists: true }),
528
537
  steps,
529
538
  ...(fixed ? { fixed, localPath } : {}),
530
539
  ...(grantor ? { grantor } : {}),
@@ -541,7 +550,7 @@ function emit(input, result, log) {
541
550
  outcome: result.outcome,
542
551
  path: result.path,
543
552
  company: result.company,
544
- exists: result.exists,
553
+ ...(result.exists !== undefined ? { exists: result.exists } : {}),
545
554
  steps: result.steps,
546
555
  };
547
556
  if (result.grantor)
@@ -228,6 +228,7 @@ export function spawnDetachedRun(name, dir) {
228
228
  const fd = fs.openSync(botLogPath(dir), "a");
229
229
  const child = spawn(process.execPath, [entry, "bot", "run", name], {
230
230
  detached: true,
231
+ windowsHide: true,
231
232
  stdio: ["ignore", fd, fd],
232
233
  env: { ...process.env, HQ_NO_UPDATE_CHECK: "1" },
233
234
  });
@@ -32,7 +32,7 @@ import * as fs from "node:fs";
32
32
  import * as path from "node:path";
33
33
  import * as yaml from "js-yaml";
34
34
  import { parseUserYaml } from "../utils/user-yaml-error.js";
35
- import { share } from "@indigoai-us/hq-cloud";
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
37
  /** Custom error class so the CLI runner can map to exit codes. */
38
38
  export class ProvisionError extends Error {
@@ -421,7 +421,32 @@ async function safeBody(res) {
421
421
  }
422
422
  }
423
423
  // ── Default initial-sync runner (wraps share()) ──────────────────────────────
424
+ const INITIAL_SYNC_ERROR_FIELD_LIMIT = 240;
425
+ function truncateInitialSyncErrorField(value) {
426
+ return value.length <= INITIAL_SYNC_ERROR_FIELD_LIMIT
427
+ ? value
428
+ : `${value.slice(0, INITIAL_SYNC_ERROR_FIELD_LIMIT - 1)}…`;
429
+ }
430
+ class InitialSyncUploadFailureError extends Error {
431
+ filesUploaded;
432
+ constructor(filesUploaded, failureCount, firstFailure) {
433
+ const detail = `${truncateInitialSyncErrorField(firstFailure.path)}: ` +
434
+ truncateInitialSyncErrorField(firstFailure.message);
435
+ const failureLabel = failureCount === 1
436
+ ? "upload failure"
437
+ : "upload failures";
438
+ super(`${failureCount} ${failureLabel}; first failure: ${detail}`);
439
+ this.filesUploaded = filesUploaded;
440
+ this.name = "InitialSyncUploadFailureError";
441
+ }
442
+ }
424
443
  async function defaultRunInitialSync(args) {
444
+ let failureCount = 0;
445
+ let firstFailure;
446
+ const recordProgress = createSyncProgressRecorder({
447
+ company: args.slug,
448
+ phase: "push",
449
+ });
425
450
  const result = await share({
426
451
  paths: [companyDirPath(args.hqRoot, args.slug)],
427
452
  company: args.slug,
@@ -429,7 +454,25 @@ async function defaultRunInitialSync(args) {
429
454
  onConflict: "keep",
430
455
  vaultConfig: buildVaultConfig(args.accessToken),
431
456
  hqRoot: args.hqRoot,
457
+ progressRecorder: (event) => {
458
+ recordProgress(event);
459
+ if (event.type === "plan-limit") {
460
+ failureCount += Math.max(1, event.count ?? 0, event.paths?.length ?? 0);
461
+ firstFailure ??= {
462
+ path: event.paths?.[0] ?? "plan limit",
463
+ message: event.message ?? "plan limit refused upload",
464
+ };
465
+ return;
466
+ }
467
+ if (event.type !== "error" || event.diagnostic === true)
468
+ return;
469
+ failureCount++;
470
+ firstFailure ??= { path: event.path, message: event.message };
471
+ },
432
472
  });
473
+ if (failureCount > 0 && firstFailure) {
474
+ throw new InitialSyncUploadFailureError(result.filesUploaded, failureCount, firstFailure);
475
+ }
433
476
  return {
434
477
  filesUploaded: result.filesUploaded,
435
478
  bytesUploaded: result.bytesUploaded,
@@ -654,7 +697,13 @@ export async function provisionCompany(options) {
654
697
  created_entity: createdEntity,
655
698
  manifest_patched: true,
656
699
  config_written: true,
657
- initial_sync: { ok: false, error: msg },
700
+ initial_sync: {
701
+ ok: false,
702
+ error: msg,
703
+ ...(err instanceof InitialSyncUploadFailureError
704
+ ? { files_uploaded: err.filesUploaded }
705
+ : {}),
706
+ },
658
707
  });
659
708
  }
660
709
  }