@indigoai-us/hq-cli 5.108.1 → 5.108.2

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 (106) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/assets/mesh-daemon/README.md +23 -0
  3. package/assets/mesh-daemon/launchd.plist.template +33 -0
  4. package/assets/mesh-daemon/systemd.system.service.template +27 -0
  5. package/assets/mesh-daemon/systemd.user.service.template +18 -0
  6. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
  7. package/dist/commands/core.js +5 -4
  8. package/dist/commands/mesh.d.ts +15 -0
  9. package/dist/commands/mesh.js +732 -4
  10. package/dist/commands/search.d.ts +23 -0
  11. package/dist/commands/search.js +42 -1
  12. package/dist/lib/doctor/checks/work-context.d.ts +23 -0
  13. package/dist/lib/doctor/checks/work-context.js +195 -0
  14. package/dist/lib/doctor/registry.js +4 -0
  15. package/dist/lib/mesh/api.d.ts +18 -0
  16. package/dist/lib/mesh/api.js +55 -0
  17. package/dist/lib/mesh/cache.js +35 -4
  18. package/dist/lib/mesh/client.d.ts +118 -0
  19. package/dist/lib/mesh/client.js +268 -0
  20. package/dist/lib/mesh/live/backoff.d.ts +16 -0
  21. package/dist/lib/mesh/live/backoff.js +20 -0
  22. package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
  23. package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
  24. package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
  25. package/dist/lib/mesh/live/daemon/credentials.js +227 -0
  26. package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
  27. package/dist/lib/mesh/live/daemon/doctor.js +139 -0
  28. package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
  29. package/dist/lib/mesh/live/daemon/index.js +14 -0
  30. package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
  31. package/dist/lib/mesh/live/daemon/install.js +330 -0
  32. package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
  33. package/dist/lib/mesh/live/daemon/log.js +79 -0
  34. package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
  35. package/dist/lib/mesh/live/daemon/paths.js +31 -0
  36. package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
  37. package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
  38. package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
  39. package/dist/lib/mesh/live/daemon/presence.js +250 -0
  40. package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
  41. package/dist/lib/mesh/live/daemon/presign.js +65 -0
  42. package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
  43. package/dist/lib/mesh/live/daemon/run.js +400 -0
  44. package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
  45. package/dist/lib/mesh/live/daemon/state.js +57 -0
  46. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
  47. package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
  48. package/dist/lib/mesh/live/enqueue.d.ts +48 -0
  49. package/dist/lib/mesh/live/enqueue.js +100 -0
  50. package/dist/lib/mesh/live/flush.d.ts +56 -0
  51. package/dist/lib/mesh/live/flush.js +360 -0
  52. package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
  53. package/dist/lib/mesh/live/format-spool-line.js +106 -0
  54. package/dist/lib/mesh/live/index.d.ts +22 -0
  55. package/dist/lib/mesh/live/index.js +16 -0
  56. package/dist/lib/mesh/live/paths.d.ts +13 -0
  57. package/dist/lib/mesh/live/paths.js +30 -0
  58. package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
  59. package/dist/lib/mesh/live/session-events-client.js +120 -0
  60. package/dist/lib/mesh/live/session-identity.d.ts +39 -0
  61. package/dist/lib/mesh/live/session-identity.js +91 -0
  62. package/dist/lib/mesh/live/spool.d.ts +52 -0
  63. package/dist/lib/mesh/live/spool.js +193 -0
  64. package/dist/lib/mesh/live/ulid.d.ts +12 -0
  65. package/dist/lib/mesh/live/ulid.js +39 -0
  66. package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
  67. package/dist/lib/mesh/live/validate-session-event.js +128 -0
  68. package/dist/lib/search-index/index.d.ts +45 -0
  69. package/dist/lib/search-index/index.js +76 -0
  70. package/dist/lib/work-context/atomic.d.ts +18 -0
  71. package/dist/lib/work-context/atomic.js +132 -0
  72. package/dist/lib/work-context/company.d.ts +103 -0
  73. package/dist/lib/work-context/company.js +274 -0
  74. package/dist/lib/work-context/config.d.ts +59 -0
  75. package/dist/lib/work-context/config.js +172 -0
  76. package/dist/lib/work-context/contract.d.ts +81 -0
  77. package/dist/lib/work-context/contract.js +65 -0
  78. package/dist/lib/work-context/errors.d.ts +27 -0
  79. package/dist/lib/work-context/errors.js +46 -0
  80. package/dist/lib/work-context/index.d.ts +23 -0
  81. package/dist/lib/work-context/index.js +20 -0
  82. package/dist/lib/work-context/migrate.d.ts +84 -0
  83. package/dist/lib/work-context/migrate.js +287 -0
  84. package/dist/lib/work-context/organize.d.ts +130 -0
  85. package/dist/lib/work-context/organize.js +471 -0
  86. package/dist/lib/work-context/outbox.d.ts +97 -0
  87. package/dist/lib/work-context/outbox.js +357 -0
  88. package/dist/lib/work-context/paths.d.ts +14 -0
  89. package/dist/lib/work-context/paths.js +39 -0
  90. package/dist/lib/work-context/project.d.ts +78 -0
  91. package/dist/lib/work-context/project.js +247 -0
  92. package/dist/lib/work-context/reconcile.d.ts +85 -0
  93. package/dist/lib/work-context/reconcile.js +651 -0
  94. package/dist/lib/work-context/repo-remote.d.ts +43 -0
  95. package/dist/lib/work-context/repo-remote.js +200 -0
  96. package/dist/lib/work-context/state.d.ts +112 -0
  97. package/dist/lib/work-context/state.js +162 -0
  98. package/dist/main.js +40 -3
  99. package/dist/utils/cognito-session.d.ts +85 -9
  100. package/dist/utils/cognito-session.js +216 -9
  101. package/dist/utils/qmd-model-download-error.d.ts +27 -0
  102. package/dist/utils/qmd-model-download-error.js +114 -0
  103. package/dist/utils/qmd-query-document-error.d.ts +23 -0
  104. package/dist/utils/qmd-query-document-error.js +117 -0
  105. package/dist/utils/sentry-fingerprint.js +2 -0
  106. package/package.json +5 -1
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Deterministic company ownership from the enclosing git remote (US-007A).
3
+ *
4
+ * Walks up from cwd to find a .git dir/file, reads origin's URL from .git/config
5
+ * (no git subprocess), normalises to owner/name, and matches against
6
+ * companies/manifest.yaml repos. Zero or multiple matches → no signal.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { parseUserYaml } from "../../utils/user-yaml-error.js";
11
+ /**
12
+ * Normalise a git remote URL to `owner/name`.
13
+ * Handles https://github.com/owner/name(.git), git@github.com:owner/name(.git),
14
+ * and ssh://git@github.com/owner/name(.git). Returns null when unrecognised.
15
+ */
16
+ export function normalizeRemoteOwnerName(remoteUrl) {
17
+ const raw = remoteUrl.trim();
18
+ if (!raw)
19
+ return null;
20
+ let owner = "";
21
+ let name = "";
22
+ const scp = /^git@([^:]+):(.+)$/i.exec(raw);
23
+ if (scp) {
24
+ const rest = scp[2].replace(/\/+$/, "").replace(/\.git$/i, "");
25
+ const parts = rest.split("/").filter(Boolean);
26
+ if (parts.length >= 2) {
27
+ owner = parts[parts.length - 2];
28
+ name = parts[parts.length - 1];
29
+ }
30
+ }
31
+ else {
32
+ try {
33
+ const withScheme = /^[a-z+]+:\/\//i.test(raw) ? raw : `https://${raw}`;
34
+ const u = new URL(withScheme);
35
+ const parts = u.pathname
36
+ .replace(/\/+$/, "")
37
+ .replace(/\.git$/i, "")
38
+ .split("/")
39
+ .filter(Boolean);
40
+ if (parts.length >= 2) {
41
+ owner = parts[parts.length - 2];
42
+ name = parts[parts.length - 1];
43
+ }
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ if (!owner || !name)
50
+ return null;
51
+ if (!/^[A-Za-z0-9._-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(name)) {
52
+ return null;
53
+ }
54
+ return `${owner}/${name}`;
55
+ }
56
+ /** Walk up from startDir looking for a `.git` directory or file. */
57
+ export function findEnclosingGitDir(startDir) {
58
+ let cur = path.resolve(startDir);
59
+ for (;;) {
60
+ const candidate = path.join(cur, ".git");
61
+ try {
62
+ if (fs.existsSync(candidate)) {
63
+ const st = fs.lstatSync(candidate);
64
+ if (st.isDirectory())
65
+ return candidate;
66
+ if (st.isFile()) {
67
+ // Linked worktree: `.git` file contains `gitdir: <path>`
68
+ const text = fs.readFileSync(candidate, "utf8");
69
+ const m = /^\s*gitdir:\s*(.+)\s*$/m.exec(text);
70
+ if (m?.[1]) {
71
+ const gitdir = path.resolve(cur, m[1].trim());
72
+ if (fs.existsSync(gitdir))
73
+ return gitdir;
74
+ }
75
+ return null;
76
+ }
77
+ }
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ const parent = path.dirname(cur);
83
+ if (parent === cur)
84
+ return null;
85
+ cur = parent;
86
+ }
87
+ }
88
+ /**
89
+ * Read `remote.origin.url` from a `.git/config` (ini-style). No subprocess.
90
+ */
91
+ export function readOriginRemoteUrl(gitDir) {
92
+ const configPath = path.join(gitDir, "config");
93
+ if (!fs.existsSync(configPath))
94
+ return null;
95
+ let text;
96
+ try {
97
+ text = fs.readFileSync(configPath, "utf8");
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ let inOrigin = false;
103
+ for (const line of text.split(/\r?\n/)) {
104
+ const section = /^\s*\[([^\]]+)\]\s*$/.exec(line);
105
+ if (section) {
106
+ inOrigin = /^remote\s+"origin"$/i.test(section[1].trim());
107
+ continue;
108
+ }
109
+ if (!inOrigin)
110
+ continue;
111
+ const url = /^\s*url\s*=\s*(.+?)\s*$/i.exec(line);
112
+ if (url?.[1])
113
+ return url[1].trim();
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * Match normalised `owner/name` against companies/manifest repos.
119
+ * A repo entry matches if it equals owner/name (case-insensitive) or is a URL
120
+ * that normalises to the same owner/name. Exactly one company → its slug;
121
+ * zero or more than one → null.
122
+ */
123
+ export function matchCompanySlugForRepo(ownerName, companies) {
124
+ const target = ownerName.trim().toLowerCase();
125
+ if (!target || !target.includes("/"))
126
+ return null;
127
+ const matches = [];
128
+ for (const [slug, entry] of Object.entries(companies)) {
129
+ if (!entry || typeof entry !== "object")
130
+ continue;
131
+ const repos = entry.repos;
132
+ if (!Array.isArray(repos))
133
+ continue;
134
+ for (const raw of repos) {
135
+ if (typeof raw !== "string" || !raw.trim())
136
+ continue;
137
+ const trimmed = raw.trim();
138
+ let candidate = null;
139
+ if (trimmed.includes("://") ||
140
+ trimmed.startsWith("git@") ||
141
+ /@[^/]+:/.test(trimmed)) {
142
+ candidate = normalizeRemoteOwnerName(trimmed);
143
+ }
144
+ else if (trimmed.includes("/")) {
145
+ candidate = trimmed.replace(/\.git$/i, "");
146
+ }
147
+ if (candidate && candidate.toLowerCase() === target) {
148
+ matches.push(slug);
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ if (matches.length === 1)
154
+ return matches[0];
155
+ return null;
156
+ }
157
+ export function readCompaniesManifestMap(hqRoot) {
158
+ const mPath = path.join(hqRoot, "companies", "manifest.yaml");
159
+ if (!fs.existsSync(mPath))
160
+ return null;
161
+ try {
162
+ const parsed = parseUserYaml(fs.readFileSync(mPath, "utf8"), mPath);
163
+ if (!parsed ||
164
+ typeof parsed !== "object" ||
165
+ !("companies" in parsed) ||
166
+ typeof parsed.companies !== "object" ||
167
+ parsed.companies === null) {
168
+ return null;
169
+ }
170
+ return parsed.companies;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * Derive the deterministic remote-owner company slug from cwd + HQ manifest.
178
+ * Returns null when there is no unique match.
179
+ */
180
+ export function deriveRemoteOwnerSlug(opts) {
181
+ const cwd = opts.cwd?.trim() ? path.resolve(opts.cwd) : process.cwd();
182
+ const hqRoot = opts.hqRoot?.trim() ? path.resolve(opts.hqRoot) : undefined;
183
+ if (!hqRoot)
184
+ return null;
185
+ const gitDir = findEnclosingGitDir(cwd);
186
+ if (!gitDir)
187
+ return null;
188
+ const origin = readOriginRemoteUrl(gitDir);
189
+ if (!origin)
190
+ return null;
191
+ const ownerName = normalizeRemoteOwnerName(origin);
192
+ if (!ownerName)
193
+ return null;
194
+ const reader = opts.readManifest ?? readCompaniesManifestMap;
195
+ const companies = reader(hqRoot);
196
+ if (!companies)
197
+ return null;
198
+ return matchCompanySlugForRepo(ownerName, companies);
199
+ }
200
+ //# sourceMappingURL=repo-remote.js.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Per-session local state file: ~/.hq/work-context/sessions/<sid>.json
3
+ * Written only by hq mesh context. Owner 0600. Metadata-only — never prompts/tokens.
4
+ */
5
+ import type { ClassificationState } from "./contract.js";
6
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
+ export type DecisionOptionKind = "bind_existing" | "create_project" | "create_task" | "untracked" | "keep_default" | "override_evidence";
8
+ export interface SessionDecisionOption {
9
+ optionId: string;
10
+ label?: string;
11
+ kind?: DecisionOptionKind;
12
+ projectId?: string;
13
+ taskId?: string;
14
+ }
15
+ export interface SessionDecision {
16
+ decisionId: string;
17
+ options: SessionDecisionOption[];
18
+ expiresAt?: string;
19
+ /**
20
+ * When true, UserPromptSubmit injects a one-time clarification (US-007B/US-010).
21
+ * Set only after work-signal threshold; never re-armed after cancel/timeout.
22
+ */
23
+ askAfter?: boolean;
24
+ /** US-017B correction pointer when company_conflict follows an existing binding. */
25
+ correctionPath?: string;
26
+ }
27
+ /** Local-only origin of the session state writer (never sent on the wire). */
28
+ export type SessionEventSource = "hook" | "transcript";
29
+ /** Local-only harness label when network schema cannot carry it (US-018). */
30
+ export type LocalHarnessLabel = "claude-desktop" | "codex";
31
+ /** Local tracking for an in-flight or completed cross-company migrate (US-017B). */
32
+ export interface SessionMigrationRecord {
33
+ operationId: string;
34
+ digest: string;
35
+ sourceCompanyUid: string;
36
+ destinationCompanyUid: string;
37
+ destinationCompanySlug?: string;
38
+ destination?: {
39
+ projectId?: string;
40
+ taskId?: string;
41
+ };
42
+ submittedAt: string;
43
+ /** Set when the destination receipt arrives. */
44
+ receiptId?: string;
45
+ completedAt?: string;
46
+ }
47
+ export interface SessionStateFile {
48
+ contractVersion: typeof WORK_CONTEXT_CONTRACT_VERSION;
49
+ sessionId: string;
50
+ companyUid?: string;
51
+ companySlug?: string;
52
+ contextStatus: ClassificationState;
53
+ projectId?: string;
54
+ taskId?: string;
55
+ bindingEpisodeId?: string;
56
+ /** Binding / session OCC version from the last server receipt. */
57
+ version?: number;
58
+ decision?: SessionDecision;
59
+ /** Cross-company migrate tracking (pending until receiptId is set). */
60
+ migration?: SessionMigrationRecord;
61
+ /** Local consent receipt for explicit untracked. */
62
+ untrackedReceipt?: {
63
+ origin: "user";
64
+ at: string;
65
+ noBackfill: true;
66
+ };
67
+ /**
68
+ * LOCAL-ONLY. Count of file-writing tools (Edit/Write/MultiEdit/NotebookEdit/
69
+ * file-writing Bash) bumped by PostToolUse (US-010). Used for askAfter.
70
+ */
71
+ toolWrites?: number;
72
+ /** LOCAL-ONLY. First observe/register time for the 10-minute ask threshold. */
73
+ startedAt?: string;
74
+ /** LOCAL-ONLY. True once any turn has been observed for this session. */
75
+ hasTurn?: boolean;
76
+ /**
77
+ * LOCAL-ONLY. How this state was established. Hook path (US-007A/US-010) omits
78
+ * this or sets "hook". Transcript-watch fallback sets "transcript" (US-018).
79
+ * Distinct from the network SessionEvent `source` enum (`hooks` | `transcript`).
80
+ */
81
+ source?: SessionEventSource;
82
+ /**
83
+ * LOCAL-ONLY legacy label. AMEND-001 put `claude-desktop` on the network
84
+ * harness enum; transcript-watch no longer writes this.
85
+ */
86
+ localHarness?: LocalHarnessLabel;
87
+ /**
88
+ * LOCAL-ONLY. Channel-card / Atlas marker for transcript-sourced sessions
89
+ * before SESSION_STATE sync (kept after AMEND-001).
90
+ */
91
+ transcriptMarker?: "from transcript";
92
+ updatedAt: string;
93
+ }
94
+ /** Allowlisted keys that may appear in the durable state projection. */
95
+ export declare const SESSION_STATE_ALLOWLIST: Set<string>;
96
+ export declare function readSessionState(sessionId: string, root: string): SessionStateFile | null;
97
+ export declare function writeSessionState(state: SessionStateFile, root: string): string;
98
+ /** Strip any non-allowlisted fields before durable write. */
99
+ export declare function projectSessionState(state: SessionStateFile): SessionStateFile;
100
+ /**
101
+ * Merge prior local-only counters into a new state write so PostToolUse bumps
102
+ * and reconcile updates do not clobber each other.
103
+ */
104
+ export declare function mergeLocalSessionFields(next: SessionStateFile, prior: SessionStateFile | null): SessionStateFile;
105
+ /** True when a durable state file was written by the hook / reconcile path. */
106
+ export declare function isHookWrittenSessionState(state: SessionStateFile | null): boolean;
107
+ /** Authoritative prior scope usable for company precedence. */
108
+ export declare function authoritativeCompanyFromState(state: SessionStateFile | null): {
109
+ slug?: string;
110
+ uid?: string;
111
+ } | null;
112
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Per-session local state file: ~/.hq/work-context/sessions/<sid>.json
3
+ * Written only by hq mesh context. Owner 0600. Metadata-only — never prompts/tokens.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
7
+ import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
8
+ import { NotTrackingError } from "./errors.js";
9
+ import { isSafeWorkContextSegment, workContextSessionPath, workContextSessionsDir, } from "./paths.js";
10
+ /** Allowlisted keys that may appear in the durable state projection. */
11
+ export const SESSION_STATE_ALLOWLIST = new Set([
12
+ "contractVersion",
13
+ "sessionId",
14
+ "companyUid",
15
+ "companySlug",
16
+ "contextStatus",
17
+ "projectId",
18
+ "taskId",
19
+ "bindingEpisodeId",
20
+ "version",
21
+ "decision",
22
+ "migration",
23
+ "untrackedReceipt",
24
+ "toolWrites",
25
+ "startedAt",
26
+ "hasTurn",
27
+ "source",
28
+ "localHarness",
29
+ "transcriptMarker",
30
+ "updatedAt",
31
+ ]);
32
+ export function readSessionState(sessionId, root) {
33
+ if (!isSafeWorkContextSegment(sessionId))
34
+ return null;
35
+ const filePath = workContextSessionPath(sessionId, root);
36
+ if (!fs.existsSync(filePath))
37
+ return null;
38
+ try {
39
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
40
+ if (raw.sessionId !== sessionId)
41
+ return null;
42
+ return raw;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function writeSessionState(state, root) {
49
+ if (!isSafeWorkContextSegment(state.sessionId)) {
50
+ throw new NotTrackingError(`Unsafe session id for state file: ${state.sessionId}`, "NOT_TRACKING");
51
+ }
52
+ const projected = projectSessionState(state);
53
+ try {
54
+ ensureOwnerDir(workContextSessionsDir(root));
55
+ const dest = workContextSessionPath(state.sessionId, root);
56
+ atomicWriteJson(dest, projected);
57
+ return dest;
58
+ }
59
+ catch (err) {
60
+ throw new NotTrackingError(`Failed to persist session state: ${err instanceof Error ? err.message : String(err)}`, "NOT_TRACKING");
61
+ }
62
+ }
63
+ /** Strip any non-allowlisted fields before durable write. */
64
+ export function projectSessionState(state) {
65
+ const out = {
66
+ contractVersion: WORK_CONTEXT_CONTRACT_VERSION,
67
+ sessionId: state.sessionId,
68
+ contextStatus: state.contextStatus,
69
+ updatedAt: state.updatedAt,
70
+ };
71
+ if (state.companyUid)
72
+ out.companyUid = state.companyUid;
73
+ if (state.companySlug)
74
+ out.companySlug = state.companySlug;
75
+ if (state.projectId)
76
+ out.projectId = state.projectId;
77
+ if (state.taskId)
78
+ out.taskId = state.taskId;
79
+ if (state.bindingEpisodeId)
80
+ out.bindingEpisodeId = state.bindingEpisodeId;
81
+ if (typeof state.version === "number" && Number.isInteger(state.version)) {
82
+ out.version = state.version;
83
+ }
84
+ if (state.decision)
85
+ out.decision = state.decision;
86
+ if (state.migration)
87
+ out.migration = state.migration;
88
+ if (state.untrackedReceipt)
89
+ out.untrackedReceipt = state.untrackedReceipt;
90
+ if (typeof state.toolWrites === "number" && state.toolWrites >= 0) {
91
+ out.toolWrites = Math.floor(state.toolWrites);
92
+ }
93
+ if (state.startedAt)
94
+ out.startedAt = state.startedAt;
95
+ if (state.hasTurn)
96
+ out.hasTurn = true;
97
+ if (state.source)
98
+ out.source = state.source;
99
+ if (state.localHarness)
100
+ out.localHarness = state.localHarness;
101
+ if (state.transcriptMarker)
102
+ out.transcriptMarker = state.transcriptMarker;
103
+ return out;
104
+ }
105
+ /**
106
+ * Merge prior local-only counters into a new state write so PostToolUse bumps
107
+ * and reconcile updates do not clobber each other.
108
+ */
109
+ export function mergeLocalSessionFields(next, prior) {
110
+ if (!prior)
111
+ return next;
112
+ const out = { ...next };
113
+ if (out.toolWrites === undefined && typeof prior.toolWrites === "number") {
114
+ out.toolWrites = prior.toolWrites;
115
+ }
116
+ if (!out.startedAt && prior.startedAt)
117
+ out.startedAt = prior.startedAt;
118
+ if (!out.hasTurn && prior.hasTurn)
119
+ out.hasTurn = true;
120
+ // Preserve an existing one-time decision unless the caller replaces it.
121
+ if (!out.decision && prior.decision)
122
+ out.decision = prior.decision;
123
+ if (!out.untrackedReceipt && prior.untrackedReceipt) {
124
+ out.untrackedReceipt = prior.untrackedReceipt;
125
+ }
126
+ if (!out.source && prior.source)
127
+ out.source = prior.source;
128
+ if (!out.localHarness && prior.localHarness) {
129
+ out.localHarness = prior.localHarness;
130
+ }
131
+ if (!out.transcriptMarker && prior.transcriptMarker) {
132
+ out.transcriptMarker = prior.transcriptMarker;
133
+ }
134
+ if (!out.bindingEpisodeId && prior.bindingEpisodeId) {
135
+ out.bindingEpisodeId = prior.bindingEpisodeId;
136
+ }
137
+ return out;
138
+ }
139
+ /** True when a durable state file was written by the hook / reconcile path. */
140
+ export function isHookWrittenSessionState(state) {
141
+ if (!state)
142
+ return false;
143
+ return state.source !== "transcript";
144
+ }
145
+ /** Authoritative prior scope usable for company precedence. */
146
+ export function authoritativeCompanyFromState(state) {
147
+ if (!state)
148
+ return null;
149
+ const writable = new Set([
150
+ "unresolved",
151
+ "needs_project",
152
+ "needs_task",
153
+ "bound",
154
+ "migration_pending",
155
+ ]);
156
+ if (!writable.has(state.contextStatus))
157
+ return null;
158
+ if (!state.companyUid && !state.companySlug)
159
+ return null;
160
+ return { slug: state.companySlug, uid: state.companyUid };
161
+ }
162
+ //# sourceMappingURL=state.js.map
package/dist/main.js CHANGED
@@ -73,6 +73,8 @@ import { qmdLlmDisabledMessage } from "./utils/qmd-llm-disabled-error.js";
73
73
  import { qmdModuleMissingMessage } from "./utils/qmd-module-missing-error.js";
74
74
  import { qmdStoreMissingMessage } from "./utils/qmd-store-missing-error.js";
75
75
  import { qmdStoreUnopenableMessage } from "./utils/qmd-store-unopenable-error.js";
76
+ import { qmdQueryDocumentMessage } from "./utils/qmd-query-document-error.js";
77
+ import { qmdModelDownloadMessage } from "./utils/qmd-model-download-error.js";
76
78
  import { hqStateWriteErrorMessage } from "./utils/hq-state-write-error.js";
77
79
  import { isExpectedUserError } from "./utils/expected-cli-error.js";
78
80
  import { isVarlockEnvError } from "./run/env-graph-guard.js";
@@ -599,6 +601,33 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
599
601
  const storeUnopenableMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg
600
602
  ? null
601
603
  : qmdStoreUnopenableMessage(err);
604
+ // A qmd `query` (hybrid search) failure whose positional was rejected as a
605
+ // MALFORMED QUERY DOCUMENT — qmd 2.5.3 requires a lex:/vec:/hyde:/intent:/
606
+ // expand: prefix on every line of a multi-line document — is the caller's
607
+ // INPUT SHAPE, not an hq-cli defect. The common case (a fleet agent
608
+ // forwarding a whole multi-line Slack message) is normalised to one line in
609
+ // buildSearchArgs so it never reaches qmd multi-line; this branch is the
610
+ // backstop for a genuinely malformed structured document. finishRunQmd types
611
+ // it QmdQueryDocumentError; print the query-free remedy and skip capture
612
+ // (HQ-CLI-1B / Sentry 7705711476, 28 of 29 events). Scoped to the `query`
613
+ // read. Evaluated AFTER the earlier qmd typed signatures and BEFORE the
614
+ // environmental / transport / generic branches; the wordings are disjoint,
615
+ // so ordering changes no existing branch.
616
+ const queryDocumentMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg
617
+ ? null
618
+ : qmdQueryDocumentMessage(err);
619
+ // A qmd MODEL DOWNLOAD that died with ENOENT renaming its own partial
620
+ // `.ipull` file is a transient LOCAL-CACHE race (another qmd process on the
621
+ // same box finished or cleared the same download first), not an hq-cli
622
+ // defect — hq cannot coordinate qmd children other users start on the host.
623
+ // finishRunQmd types it QmdModelDownloadError (gated on BOTH the ENOENT
624
+ // rename signature AND the .ipull marker, so a durable ENOSPC/EACCES/network
625
+ // failure stays reportable); print the retry remedy and skip capture
626
+ // (HQ-CLI-1B / Sentry 7705711476, the newest event). Scoped to the reads
627
+ // that fetch a model (query/vsearch/embed).
628
+ const modelDownloadMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg
629
+ ? null
630
+ : qmdModelDownloadMessage(err);
602
631
  // An hq STATE-TREE write that failed for an environmental errno
603
632
  // (permission/quota/read-only/parent-gone) is typed at the write site as an
604
633
  // HqStateWriteError carrying the exact hq-DERIVED path it tried to write.
@@ -608,10 +637,10 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
608
637
  // remedy and skip capture. Placed with the environmental-fs family — BEFORE
609
638
  // the generic envMsg computation — so this typed carrier wins even though
610
639
  // it also carries an errno `code` environmentalFsErrorMessage could read.
611
- const stateWriteMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg
640
+ const stateWriteMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg
612
641
  ? null
613
642
  : hqStateWriteErrorMessage(err);
614
- const envMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || stateWriteMsg
643
+ const envMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || stateWriteMsg
615
644
  ? null
616
645
  : environmentalFsErrorMessage(err);
617
646
  // A LOCAL sync-state lock failure (@indigoai-us/hq-cloud's
@@ -628,7 +657,7 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
628
657
  // environmental-fs check, before network-transport — is pinned by tests.
629
658
  // The `in-process-async-holder` reason is deliberately NOT suppressed here
630
659
  // (see sync-state-lock-error.ts); it stays captured.
631
- const lockMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || stateWriteMsg || envMsg
660
+ const lockMsg = qmdMsg || collectionMsg || terminatedMsg || llmDisabledMsg || moduleMissingMsg || storeMissingMsg || storeUnopenableMsg || queryDocumentMsg || modelDownloadMsg || stateWriteMsg || envMsg
632
661
  ? null
633
662
  : syncStateLockMessage(err);
634
663
  // A raw network transport failure (undici's `TypeError: fetch failed`
@@ -648,6 +677,8 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
648
677
  moduleMissingMsg ||
649
678
  storeMissingMsg ||
650
679
  storeUnopenableMsg ||
680
+ queryDocumentMsg ||
681
+ modelDownloadMsg ||
651
682
  stateWriteMsg ||
652
683
  envMsg ||
653
684
  lockMsg
@@ -674,6 +705,12 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
674
705
  else if (storeUnopenableMsg) {
675
706
  deps.stderr.write(`hq: ${storeUnopenableMsg}\n`);
676
707
  }
708
+ else if (queryDocumentMsg) {
709
+ deps.stderr.write(`hq: ${queryDocumentMsg}\n`);
710
+ }
711
+ else if (modelDownloadMsg) {
712
+ deps.stderr.write(`hq: ${modelDownloadMsg}\n`);
713
+ }
677
714
  else if (stateWriteMsg) {
678
715
  deps.stderr.write(`hq: ${stateWriteMsg}\n`);
679
716
  }