@indigoai-us/hq-cli 5.300.0 → 5.301.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
  3. package/dist/command-catalog.generated.d.ts +28 -2
  4. package/dist/command-catalog.generated.js +34 -2
  5. package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
  6. package/dist/commands/__fixtures__/access-vault.js +1 -1
  7. package/dist/commands/access.d.ts +2 -1
  8. package/dist/commands/access.js +58 -49
  9. package/dist/commands/cloud-provision.js +51 -2
  10. package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
  11. package/dist/commands/core-company-index-scope-flag.js +6 -0
  12. package/dist/commands/core-job-native-utils.d.ts +6 -0
  13. package/dist/commands/core-job-native-utils.js +80 -0
  14. package/dist/commands/core-job-native.d.ts +2 -4
  15. package/dist/commands/core-job-native.js +6 -10
  16. package/dist/commands/core-job-notify-native.d.ts +36 -0
  17. package/dist/commands/core-job-notify-native.js +359 -0
  18. package/dist/commands/core-job-remediate-native.d.ts +26 -0
  19. package/dist/commands/core-job-remediate-native.js +374 -0
  20. package/dist/commands/core.js +26 -8
  21. package/dist/commands/files.js +31 -14
  22. package/dist/commands/integrations-api.d.ts +6 -6
  23. package/dist/commands/integrations-api.js +4 -13
  24. package/dist/commands/integrations-manage.js +1 -4
  25. package/dist/commands/lanes-message.js +10 -2
  26. package/dist/commands/lanes-shared.d.ts +2 -0
  27. package/dist/commands/lanes.d.ts +2 -0
  28. package/dist/commands/lanes.js +1 -0
  29. package/dist/commands/meetings.js +35 -16
  30. package/dist/commands/safe-extract.d.ts +4 -4
  31. package/dist/commands/safe-extract.js +6 -7
  32. package/dist/lib/core-utils/token-usage-report.js +17 -3
  33. package/dist/lib/index-render/companies.d.ts +4 -1
  34. package/dist/lib/index-render/companies.js +66 -3
  35. package/dist/lib/index-render/index.d.ts +1 -0
  36. package/dist/lib/index-render/index.js +2 -1
  37. package/dist/lib/index-render/shared.d.ts +1 -1
  38. package/dist/lib/index-render/shared.js +4 -2
  39. package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
  40. package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
  41. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
  42. package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
  43. package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
  44. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
  45. package/dist/utils/self-update.js +51 -4
  46. package/dist/utils/staged-npm-install.d.ts +16 -0
  47. package/dist/utils/staged-npm-install.js +264 -0
  48. package/dist/utils/update-command-supervisor.cjs +53 -1
  49. package/dist/utils/version-gate.d.ts +8 -0
  50. package/dist/utils/version-gate.js +82 -15
  51. package/package.json +2 -2
  52. package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
  53. package/dist/lib/install-path-compatibility-flag.js +0 -12
@@ -31,6 +31,8 @@ export { emitLaneDecisionEvents };
31
31
  export interface LanesCommandDeps {
32
32
  /** Test seam for failures after a lane provider worker is started. */
33
33
  onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
34
+ /** Test seam for failures after a message is durably queued. */
35
+ onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move") => void;
34
36
  hqRoot?: () => string;
35
37
  /** Start HQ-root walk here instead of process.cwd(). Tests only. */
36
38
  cwd?: () => string;
@@ -7838,6 +7838,7 @@ export function registerLanesCommand(parent, deps = {}) {
7838
7838
  });
7839
7839
  const messageHandlerDeps = {
7840
7840
  onLaneWorkerStarted: deps.onLaneWorkerStarted,
7841
+ onLaneMessageQueued: deps.onLaneMessageQueued,
7841
7842
  archiveResumeMessage,
7842
7843
  assertLaneRunsHereForAdmission,
7843
7844
  assertLaneRunsHereForOperatorMutation,
@@ -3,6 +3,25 @@ import { readFile } from "node:fs/promises";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
5
5
  import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
6
+ // Two minutes gives a large transcript time to arrive over a slow connection,
7
+ // while keeping the interactive command from waiting indefinitely.
8
+ const MEETING_DOCUMENT_DOWNLOAD_TIMEOUT_MS = 120_000;
9
+ async function downloadMeetingDocument(url, readBody) {
10
+ const signal = AbortSignal.timeout(MEETING_DOCUMENT_DOWNLOAD_TIMEOUT_MS);
11
+ try {
12
+ const response = await fetch(url, { signal });
13
+ if (!response.ok)
14
+ return { response, ok: false };
15
+ return { response, ok: true, body: await readBody(response) };
16
+ }
17
+ catch (err) {
18
+ if ((signal.aborted && signal.reason instanceof DOMException && signal.reason.name === "TimeoutError") ||
19
+ (err instanceof DOMException && err.name === "TimeoutError")) {
20
+ throw new Error(`Meeting document download timed out after ${MEETING_DOCUMENT_DOWNLOAD_TIMEOUT_MS / 1000} seconds. Check your network connection and try again.`);
21
+ }
22
+ throw err;
23
+ }
24
+ }
6
25
  function formatDuration(seconds) {
7
26
  const h = Math.floor(seconds / 3600);
8
27
  const m = Math.floor((seconds % 3600) / 60);
@@ -495,12 +514,12 @@ export function registerMeetingsCommand(program) {
495
514
  console.error(chalk.red("No document available for this meeting."));
496
515
  process.exit(1);
497
516
  }
498
- const docRes = await fetch(documentUrl);
499
- if (!docRes.ok) {
500
- console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
517
+ const download = await downloadMeetingDocument(documentUrl, (response) => response.text());
518
+ if (!download.ok) {
519
+ console.error(chalk.red(`Failed to download meeting document (${download.response.status})`));
501
520
  process.exit(1);
502
521
  }
503
- const markdown = await docRes.text();
522
+ const markdown = download.body;
504
523
  if (meetings.opts().json) {
505
524
  console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
506
525
  return;
@@ -513,12 +532,12 @@ export function registerMeetingsCommand(program) {
513
532
  console.error(chalk.red("No document URL available for this meeting."));
514
533
  process.exit(1);
515
534
  }
516
- const docRes = await fetch(detail.documentUrl);
517
- if (!docRes.ok) {
518
- console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
535
+ const download = await downloadMeetingDocument(detail.documentUrl, (response) => response.json());
536
+ if (!download.ok) {
537
+ console.error(chalk.red(`Failed to download meeting document (${download.response.status})`));
519
538
  process.exit(1);
520
539
  }
521
- const doc = (await docRes.json());
540
+ const doc = download.body;
522
541
  if (meetings.opts().json) {
523
542
  console.log(JSON.stringify(doc.transcript, null, 2));
524
543
  return;
@@ -573,12 +592,12 @@ export function registerMeetingsCommand(program) {
573
592
  console.log(chalk.yellow("No notes available for this meeting."));
574
593
  return;
575
594
  }
576
- const docRes = await fetch(documentUrl);
577
- if (!docRes.ok) {
578
- console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
595
+ const download = await downloadMeetingDocument(documentUrl, (response) => response.text());
596
+ if (!download.ok) {
597
+ console.error(chalk.red(`Failed to download meeting document (${download.response.status})`));
579
598
  process.exit(1);
580
599
  }
581
- const markdown = await docRes.text();
600
+ const markdown = download.body;
582
601
  if (meetings.opts().json) {
583
602
  console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
584
603
  return;
@@ -591,12 +610,12 @@ export function registerMeetingsCommand(program) {
591
610
  console.error(chalk.red("No document URL available for this meeting."));
592
611
  process.exit(1);
593
612
  }
594
- const docRes = await fetch(detail.documentUrl);
595
- if (!docRes.ok) {
596
- console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
613
+ const download = await downloadMeetingDocument(detail.documentUrl, (response) => response.json());
614
+ if (!download.ok) {
615
+ console.error(chalk.red(`Failed to download meeting document (${download.response.status})`));
597
616
  process.exit(1);
598
617
  }
599
- const doc = (await docRes.json());
618
+ const doc = download.body;
600
619
  if (meetings.opts().json) {
601
620
  console.log(JSON.stringify(doc.notes, null, 2));
602
621
  return;
@@ -111,10 +111,10 @@ export declare function assertLinkTargetContained(packDir: string, entryAbs: str
111
111
  /**
112
112
  * List a gzip-compressed tarball's entries via `tar -tvf` (no extraction).
113
113
  * argv form, never a shell string, keeps hostile filenames out of shell syntax.
114
- * When the default-off compatibility flag is enabled, Windows drive paths use
115
- * a basename relative to the archive directory. GNU tar cannot parse `C:\\...`
116
- * as remote `host:path`, and Windows' bundled bsdtar accepts the same relative
117
- * argv without GNU-only `--force-local`.
114
+ * Windows drive paths use a basename relative to the archive directory by
115
+ * default. GNU tar cannot parse `C:\\...` as remote `host:path`, and Windows'
116
+ * bundled bsdtar accepts the same relative argv without GNU-only `--force-local`.
117
+ * Callers can still explicitly opt out with `useRelativeWindowsDrivePath: false`.
118
118
  */
119
119
  export declare function listTarball(tarballPath: string, opts?: Pick<SafeExtractOptions, 'platform' | 'useRelativeWindowsDrivePath' | 'execFileSync'>): TarEntry[];
120
120
  /**
@@ -35,7 +35,6 @@
35
35
  import { execFileSync } from 'child_process';
36
36
  import * as fs from 'fs';
37
37
  import * as path from 'path';
38
- import { installPathCompatibilityEnabled } from '../lib/install-path-compatibility-flag.js';
39
38
  export const DEFAULT_CAPS = {
40
39
  // A content pack is docs/skills/policies/small scripts. 256 MiB is already
41
40
  // generous; anything larger is almost certainly a bomb or a mistake.
@@ -218,14 +217,14 @@ function tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath)
218
217
  /**
219
218
  * List a gzip-compressed tarball's entries via `tar -tvf` (no extraction).
220
219
  * argv form, never a shell string, keeps hostile filenames out of shell syntax.
221
- * When the default-off compatibility flag is enabled, Windows drive paths use
222
- * a basename relative to the archive directory. GNU tar cannot parse `C:\\...`
223
- * as remote `host:path`, and Windows' bundled bsdtar accepts the same relative
224
- * argv without GNU-only `--force-local`.
220
+ * Windows drive paths use a basename relative to the archive directory by
221
+ * default. GNU tar cannot parse `C:\\...` as remote `host:path`, and Windows'
222
+ * bundled bsdtar accepts the same relative argv without GNU-only `--force-local`.
223
+ * Callers can still explicitly opt out with `useRelativeWindowsDrivePath: false`.
225
224
  */
226
225
  export function listTarball(tarballPath, opts = {}) {
227
226
  const platform = opts.platform ?? process.platform;
228
- const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? installPathCompatibilityEnabled();
227
+ const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? true;
229
228
  const location = tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath);
230
229
  const out = (opts.execFileSync ?? execFileSync)('tar', ['-tvf', location.archiveArg], {
231
230
  encoding: 'utf-8',
@@ -330,7 +329,7 @@ export function safeExtractTarball(tarballPath, finalDir, opts = {}) {
330
329
  }
331
330
  // Resolve once so listing and extraction use the same archive location.
332
331
  const platform = opts.platform ?? process.platform;
333
- const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? installPathCompatibilityEnabled();
332
+ const useRelativeWindowsDrivePath = opts.useRelativeWindowsDrivePath ?? true;
334
333
  const exec = opts.execFileSync ?? execFileSync;
335
334
  const location = tarballArgvLocation(tarballPath, platform, useRelativeWindowsDrivePath);
336
335
  // 1. Pre-flight against headers (no bytes written yet).
@@ -120,7 +120,7 @@ function comparison(projectDirs, before, after, minHours, json, stdout) {
120
120
  /** Print token usage totals from Claude session JSONL files. */
121
121
  export function tokenUsageReport(args = [], options = {}) {
122
122
  const { stdout, stderr } = ioFor(options);
123
- let lastDays = 7, since = "", json = false, before = "", after = "", minHours = 0;
123
+ let lastDays = 7, since = "", json = false, before = "", after = "", minHours = 0, projectDir = options.projectDir, projectDirSeen = false;
124
124
  for (let index = 0; index < args.length;) {
125
125
  const arg = args[index];
126
126
  if (arg === "--last") {
@@ -149,8 +149,22 @@ export function tokenUsageReport(args = [], options = {}) {
149
149
  minHours = Number.parseFloat(args[index + 1] ?? "");
150
150
  index += 2;
151
151
  }
152
+ else if (arg === "--project-dir") {
153
+ const value = args[index + 1];
154
+ if (!value || value.startsWith("--")) {
155
+ line(stderr, "--project-dir requires a path");
156
+ return 1;
157
+ }
158
+ if (projectDirSeen) {
159
+ line(stderr, "--project-dir may be specified only once");
160
+ return 1;
161
+ }
162
+ projectDir = value;
163
+ projectDirSeen = true;
164
+ index += 2;
165
+ }
152
166
  else if (arg === "-h" || arg === "--help") {
153
- stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
167
+ stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\n scripts/token-usage-report.sh --project-dir <path> # use one Claude activity folder\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
154
168
  return 0;
155
169
  }
156
170
  else {
@@ -158,7 +172,7 @@ export function tokenUsageReport(args = [], options = {}) {
158
172
  return 1;
159
173
  }
160
174
  }
161
- const projectDirs = resolveProjectDirs(options).filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
175
+ const projectDirs = resolveProjectDirs({ ...options, projectDir }).filter((dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory());
162
176
  if (before && after) {
163
177
  comparison(projectDirs, before, after, minHours, json, stdout);
164
178
  return 0;
@@ -1,3 +1,6 @@
1
1
  import { RenderContext, RenderResult } from "./shared.js";
2
- export declare function renderCompanies(context: RenderContext): RenderResult;
2
+ export declare class CompanyIndexUserError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function renderCompanies(context: RenderContext, args?: string[]): RenderResult;
3
6
  //# sourceMappingURL=companies.d.ts.map
@@ -1,6 +1,12 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { basename, countImmediate, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, titleize, truncate, write } from "./shared.js";
4
+ export class CompanyIndexUserError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "CompanyIndexUserError";
8
+ }
9
+ }
4
10
  function description(root, company, name) {
5
11
  const base = `companies/${company}/${name}`;
6
12
  const nonHidden = (child) => !isHidden(child);
@@ -17,9 +23,66 @@ function description(root, company, name) {
17
23
  default: return "—";
18
24
  }
19
25
  }
20
- export function renderCompanies(context) {
26
+ export function renderCompanies(context, args = []) {
27
+ if (args.some((arg) => arg === "--help" || arg === "-h")) {
28
+ (context.output ?? ((message) => process.stdout.write(message)))("Usage: hq core rebuild-index companies [--company <slug>]\n\n" +
29
+ "Regenerate company root INDEX.md files. Without --company, all companies are regenerated.\n" +
30
+ "--company <slug> regenerates only that company's index.\n" +
31
+ "Company-scoped writes require the cli.rebuild-index-company-scope flag.\n");
32
+ return { written: [] };
33
+ }
34
+ let selectedCompany;
35
+ for (let index = 0; index < args.length; index += 1) {
36
+ const arg = args[index];
37
+ if (arg === "--company") {
38
+ if (selectedCompany !== undefined)
39
+ throw new CompanyIndexUserError("rebuild-index companies: --company may be supplied once");
40
+ const value = args[index + 1];
41
+ if (!value || value.startsWith("--"))
42
+ throw new CompanyIndexUserError("rebuild-index companies: --company requires a company slug");
43
+ selectedCompany = value;
44
+ index += 1;
45
+ }
46
+ else if (arg.startsWith("--company=")) {
47
+ if (selectedCompany !== undefined)
48
+ throw new CompanyIndexUserError("rebuild-index companies: --company may be supplied once");
49
+ selectedCompany = arg.slice("--company=".length);
50
+ if (!selectedCompany)
51
+ throw new CompanyIndexUserError("rebuild-index companies: --company requires a company slug");
52
+ }
53
+ else {
54
+ throw new CompanyIndexUserError(`rebuild-index companies: unknown option ${arg}`);
55
+ }
56
+ }
21
57
  const written = [];
22
- for (const directory of immediateEntries(context.root, "companies", "dir")) {
58
+ let scopedDirectories;
59
+ if (selectedCompany === undefined) {
60
+ scopedDirectories = immediateEntries(context.root, "companies", "dir");
61
+ }
62
+ else {
63
+ if (selectedCompany === "." || selectedCompany === ".." ||
64
+ selectedCompany.includes("/") || selectedCompany.includes("\\") ||
65
+ path.basename(selectedCompany) !== selectedCompany ||
66
+ selectedCompany.startsWith("_") || isHidden(selectedCompany)) {
67
+ throw new CompanyIndexUserError(`rebuild-index companies: invalid company slug "${selectedCompany}"`);
68
+ }
69
+ const companiesRoot = path.resolve(context.root, "companies");
70
+ const selectedDirectory = path.resolve(companiesRoot, selectedCompany);
71
+ let selectedStat;
72
+ try {
73
+ selectedStat = fs.lstatSync(selectedDirectory);
74
+ }
75
+ catch (cause) {
76
+ if (cause.code !== "ENOENT")
77
+ throw cause;
78
+ throw new CompanyIndexUserError(`rebuild-index companies: no company directory named "${selectedCompany}"`);
79
+ }
80
+ if (!selectedStat.isDirectory()) {
81
+ throw new CompanyIndexUserError(`rebuild-index companies: no company directory named "${selectedCompany}"`);
82
+ }
83
+ scopedDirectories = [selectedDirectory];
84
+ }
85
+ for (const directory of scopedDirectories) {
23
86
  const company = basename(directory);
24
87
  if (company.startsWith("_") || isHidden(company))
25
88
  continue;
@@ -40,7 +103,7 @@ export function renderCompanies(context) {
40
103
  const parsed = readJson(prd);
41
104
  let text = typeof parsed?.description === "string" ? parsed.description : "";
42
105
  text = truncate(sanitize(text), 110) || "—";
43
- lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active")} | ${text} |`);
106
+ lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active", prd)} | ${text} |`);
44
107
  }
45
108
  }
46
109
  else
@@ -2,4 +2,5 @@ import { RenderContext, RenderResult } from "./shared.js";
2
2
  export type IndexTarget = "all" | "companies" | "company-knowledge" | "orchestrator" | "projects" | "public-knowledge" | "reports" | "social-drafts" | "threads" | "workers";
3
3
  export declare function renderIndexTarget(target: IndexTarget, context: RenderContext, args?: string[]): RenderResult;
4
4
  export { atomicWrite, comparePath, immediateEntries, type RenderContext, type RenderResult } from "./shared.js";
5
+ export { CompanyIndexUserError } from "./companies.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -10,7 +10,7 @@ import { renderWorkers } from "./workers.js";
10
10
  const allOrder = ["threads", "orchestrator", "companies", "projects", "company-knowledge", "public-knowledge", "workers", "reports", "social-drafts"];
11
11
  export function renderIndexTarget(target, context, args = []) {
12
12
  switch (target) {
13
- case "companies": return renderCompanies(context);
13
+ case "companies": return renderCompanies(context, args);
14
14
  case "company-knowledge": return renderCompanyKnowledge(context);
15
15
  case "orchestrator": return renderOrchestrator(context);
16
16
  case "projects": return renderProjects(context);
@@ -40,4 +40,5 @@ export function renderIndexTarget(target, context, args = []) {
40
40
  }
41
41
  }
42
42
  export { atomicWrite, comparePath, immediateEntries } from "./shared.js";
43
+ export { CompanyIndexUserError } from "./companies.js";
43
44
  //# sourceMappingURL=index.js.map
@@ -39,7 +39,7 @@ export declare function hasUnclosedKeepMarker(content: string): boolean;
39
39
  export declare function looksAutoGenerated(content: string): boolean;
40
40
  export declare function generatedStamp(content: string): string | undefined;
41
41
  export declare function log(context: RenderContext, message: string): void;
42
- export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string): string;
42
+ export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string, expectedStatePrdPath?: string): string;
43
43
  export declare function basename(file: string): string;
44
44
  export declare function isHidden(name: string): boolean;
45
45
  export declare function mtime(file: string): number;
@@ -134,9 +134,11 @@ export function generatedStamp(content) {
134
134
  return content.match(/^<!-- hq-generated-sha256: ([a-f0-9]{64}) -->/m)?.[1];
135
135
  }
136
136
  export function log(context, message) { context.log?.(message); }
137
- export function projectStatus(root, project, prdPath, fallback) {
137
+ export function projectStatus(root, project, prdPath, fallback, expectedStatePrdPath) {
138
138
  const state = readJson(at(root, `workspace/orchestrator/${project}/state.json`));
139
- if (state) {
139
+ const stateMatchesPrd = expectedStatePrdPath === undefined || (typeof state?.prd_path === "string" &&
140
+ path.resolve(root, state.prd_path) === path.resolve(root, expectedStatePrdPath));
141
+ if (state && stateMatchesPrd) {
140
142
  const raw = typeof state.status === "string" ? state.status : "active";
141
143
  if (["completed", "complete"].includes(raw))
142
144
  return "complete";
@@ -0,0 +1,7 @@
1
+ import type { MqttClientEventCallbacks } from "mqtt";
2
+ /** Event callbacks shared by the daemon's two MQTT client seams. */
3
+ export type MeshMqttEventMap = Pick<MqttClientEventCallbacks, "connect" | "error" | "close" | "message">;
4
+ export interface MeshMqttEventClient {
5
+ on<Event extends keyof MeshMqttEventMap>(event: Event, handler: MeshMqttEventMap[Event]): unknown;
6
+ }
7
+ //# sourceMappingURL=mqtt-events.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mqtt-events.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mqtt-events.test-d.d.ts.map
@@ -0,0 +1,10 @@
1
+ // @ts-expect-error unsupported event names are not part of mqtt's event map
2
+ presence.on("conect", () => { });
3
+ // @ts-expect-error MQTT message handlers receive a string topic
4
+ presence.on("message", (topic) => void topic);
5
+ // @ts-expect-error unsupported event names are not part of mqtt's event map
6
+ workCache.on("conect", () => { });
7
+ // @ts-expect-error MQTT message handlers receive a string topic
8
+ workCache.on("message", (topic) => void topic);
9
+ export {};
10
+ //# sourceMappingURL=mqtt-events.test-d.js.map
@@ -12,6 +12,7 @@
12
12
  * - Server credential refusals use a long retry (default 10m ±20%)
13
13
  */
14
14
  import { type IClientOptions, type MqttClient } from "mqtt";
15
+ import type { MeshMqttEventClient } from "./mqtt-events.js";
15
16
  import { type Contract3Bundle, type CredentialsFetcher, type TimerHost } from "./credentials.js";
16
17
  export type MqttConnectionState = "idle" | "connecting" | "connected" | "reconnecting" | "closed";
17
18
  /**
@@ -49,8 +50,7 @@ export interface PresencePayload {
49
50
  at: string;
50
51
  }
51
52
  /** Structural mqtt client surface used by PresenceClient (test seam). */
52
- export interface MeshMqttClientLike {
53
- on(event: string, handler: (...args: any[]) => void): unknown;
53
+ export interface MeshMqttClientLike extends MeshMqttEventClient {
54
54
  publish(topic: string, message: string | Buffer, opts: {
55
55
  qos: 0 | 1 | 2;
56
56
  retain?: boolean;
@@ -3,9 +3,9 @@
3
3
  * Owns live-cache.json writes. Presence publish stays on PresenceClient.
4
4
  */
5
5
  import type { IClientOptions } from "mqtt";
6
+ import type { MeshMqttEventClient } from "./mqtt-events.js";
6
7
  import { type PersonalCredentialsFetcher } from "./credentials.js";
7
- export interface WorkCacheMqtt {
8
- on(event: string, handler: (...args: any[]) => void): unknown;
8
+ export interface WorkCacheMqtt extends MeshMqttEventClient {
9
9
  subscribe(topic: string, opts: {
10
10
  qos: 0 | 1;
11
11
  }, cb?: (err?: Error | null) => void): unknown;
@@ -748,7 +748,7 @@ async function updateAndReexec(argv, flavor, known, deps) {
748
748
  console.error(chalk.dim(` Or repair the install: hq rescue`));
749
749
  }
750
750
  console.error(chalk.dim(` Continuing the ${flavor.noun} on ${current}.`));
751
- await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result.code, env, flavor.noun === "rescue");
751
+ await captureSelfUpdateOutcome("update-failed", current, latestValid, install.manager, result, env, flavor.noun === "rescue");
752
752
  return { action: "update-failed", latest };
753
753
  }
754
754
  if (installSnapshot)
@@ -801,10 +801,52 @@ async function updateAndReexec(argv, flavor, known, deps) {
801
801
  console.error(chalk.dim(`✓ hq-cli updated to ${latest}.`));
802
802
  return { action: "reexec", reexecStatus: status, latest };
803
803
  }
804
- const SELF_UPDATE_ERROR_CLASSES = new Set(["MODULE_NOT_FOUND", "EACCES", "EEXIST"]);
805
- async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, code, env, flushBeforeExit) {
804
+ const SELF_UPDATE_ERROR_CLASSES = new Set([
805
+ "MODULE_NOT_FOUND",
806
+ "ENOENT",
807
+ "EACCES",
808
+ "EEXIST",
809
+ "E404",
810
+ "EAI_AGAIN",
811
+ "ECONNREFUSED",
812
+ "ECONNRESET",
813
+ "ENOTFOUND",
814
+ "EPERM",
815
+ "ETARGET",
816
+ "ETIMEDOUT",
817
+ ]);
818
+ function selfUpdateErrorReason(code, failureKind) {
819
+ switch (code) {
820
+ case "MODULE_NOT_FOUND":
821
+ return "module_missing";
822
+ case "ENOENT":
823
+ return "program_not_found";
824
+ case "EACCES":
825
+ case "EPERM":
826
+ return "permission_denied";
827
+ case "EEXIST":
828
+ return "file_collision";
829
+ case "E404":
830
+ return "npm_package_not_found";
831
+ case "ETARGET":
832
+ return "npm_target_unavailable";
833
+ case "EAI_AGAIN":
834
+ case "ECONNREFUSED":
835
+ case "ECONNRESET":
836
+ case "ENOTFOUND":
837
+ return "registry_network_error";
838
+ case "ETIMEDOUT":
839
+ return "timed_out";
840
+ default:
841
+ if (failureKind === "package_manager_exit")
842
+ return "package_manager_exit";
843
+ return code ? "other_error" : "unknown";
844
+ }
845
+ }
846
+ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, installFlavor, failure, env, flushBeforeExit) {
806
847
  if (isCliTelemetryDisabled(env))
807
848
  return;
849
+ const code = failure?.diagnosticCode ?? failure?.code;
808
850
  const error = new Error(action === "update-failed" ? "Self-update failed" : "Self-update ineffective");
809
851
  // This event needs no stack: frames would expose the machine's install path.
810
852
  error.stack = undefined;
@@ -814,7 +856,12 @@ async function captureSelfUpdateOutcome(action, fromVersion, targetVersion, inst
814
856
  from_version: fromVersion,
815
857
  target_version: targetVersion,
816
858
  install_flavor: installFlavor,
817
- error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code) ? code : "other",
859
+ error_class: code && SELF_UPDATE_ERROR_CLASSES.has(code)
860
+ ? code
861
+ : failure?.failureKind === "package_manager_exit"
862
+ ? "package_manager_exit"
863
+ : "other",
864
+ error_reason: selfUpdateErrorReason(code, failure?.failureKind),
818
865
  },
819
866
  });
820
867
  if (flushBeforeExit) {
@@ -0,0 +1,16 @@
1
+ export type StagedInstallResult = {
2
+ ok: boolean;
3
+ detail?: string;
4
+ code?: string;
5
+ };
6
+ export type StagedInstallRunner = (command: string, args: string[], env?: NodeJS.ProcessEnv) => StagedInstallResult;
7
+ /**
8
+ * Build hq-cli in a space-free temporary npm prefix, then replace the package
9
+ * and its executable links under the real prefix only after npm succeeds.
10
+ * Existing files are held beside their destinations until activation completes,
11
+ * so failed installs and failed activation restore the prior working tree.
12
+ */
13
+ export declare function installNpmPackageStaged(prefix: string, packageSpec: string, runner: StagedInstallRunner, platform?: NodeJS.Platform, activate?: typeof activateStagedNpmPackage): StagedInstallResult;
14
+ /** Atomically replace a package already built under a temporary npm prefix. */
15
+ export declare function activateStagedNpmPackage(prefix: string, stagePrefix: string, platform?: NodeJS.Platform): StagedInstallResult;
16
+ //# sourceMappingURL=staged-npm-install.d.ts.map