@indigoai-us/hq-cli 5.108.0 → 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.
- package/CHANGELOG.md +79 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/cloud.d.ts +6 -0
- package/dist/commands/cloud.js +26 -3
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- package/package.json +5 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Mesh Live — session-event validator (US-001).
|
|
3
|
+
*
|
|
4
|
+
* Uses Ajv draft 2020-12 against the verbatim schema copy. Prohibited content
|
|
5
|
+
* classes are rejected (additionalProperties: false). Error messages never
|
|
6
|
+
* echo prohibited marker values (redaction assertion surface).
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
13
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
export const SESSION_EVENT_SCHEMA_PATH = join(HERE, "session-event.schema.json");
|
|
15
|
+
/** Expected SHA-256 of the canonical schema bytes (hq-pro + hq-cli must match). */
|
|
16
|
+
export const SESSION_EVENT_SCHEMA_SHA256 = "be300120b4793a5c6f1e713bf9f72b2fd22f6ac082bef8eedfa507b684b09ccb";
|
|
17
|
+
export const PROHIBITED_CONTENT_CLASSES = [
|
|
18
|
+
"prompts",
|
|
19
|
+
"model_output",
|
|
20
|
+
"transcripts",
|
|
21
|
+
"message_bodies",
|
|
22
|
+
"tokens",
|
|
23
|
+
"credentials",
|
|
24
|
+
];
|
|
25
|
+
/** Fixture field name → prohibited class. */
|
|
26
|
+
export const PROHIBITED_FIELD_BY_CLASS = {
|
|
27
|
+
prompts: "prompt",
|
|
28
|
+
model_output: "modelOutput",
|
|
29
|
+
transcripts: "transcript",
|
|
30
|
+
message_bodies: "messageBody",
|
|
31
|
+
tokens: "token",
|
|
32
|
+
credentials: "credentials",
|
|
33
|
+
};
|
|
34
|
+
const REDACT_MARKERS = [
|
|
35
|
+
"REDACT_MARKER_PROMPT_",
|
|
36
|
+
"REDACT_MARKER_MODEL_OUTPUT_",
|
|
37
|
+
"REDACT_MARKER_TRANSCRIPT_",
|
|
38
|
+
"REDACT_MARKER_MESSAGE_BODY_",
|
|
39
|
+
"REDACT_MARKER_TOKEN_",
|
|
40
|
+
"REDACT_MARKER_CREDENTIALS_",
|
|
41
|
+
];
|
|
42
|
+
let cachedValidate;
|
|
43
|
+
function loadSchema() {
|
|
44
|
+
return JSON.parse(readFileSync(SESSION_EVENT_SCHEMA_PATH, "utf8"));
|
|
45
|
+
}
|
|
46
|
+
export function getSessionEventValidator() {
|
|
47
|
+
if (cachedValidate)
|
|
48
|
+
return cachedValidate;
|
|
49
|
+
const ajv = new Ajv2020({
|
|
50
|
+
allErrors: true,
|
|
51
|
+
strict: false,
|
|
52
|
+
validateFormats: false,
|
|
53
|
+
});
|
|
54
|
+
cachedValidate = ajv.compile(loadSchema());
|
|
55
|
+
return cachedValidate;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Redact known prohibited markers from Ajv error text so diagnostics never
|
|
59
|
+
* leak prompt/token/credential material.
|
|
60
|
+
*/
|
|
61
|
+
export function redactValidationMessage(message) {
|
|
62
|
+
let out = message;
|
|
63
|
+
for (const marker of REDACT_MARKERS) {
|
|
64
|
+
if (out.includes(marker)) {
|
|
65
|
+
out = out.split(marker)[0] + "[REDACTED]";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Also strip any bearer-shaped or op:// fragments that slipped through.
|
|
69
|
+
out = out.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]");
|
|
70
|
+
out = out.replace(/op:\/\/[^\s"]+/g, "op://[REDACTED]");
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function formatErrors(errors) {
|
|
74
|
+
if (!errors || errors.length === 0)
|
|
75
|
+
return ["validation_failed"];
|
|
76
|
+
return errors.map((e) => {
|
|
77
|
+
const path = e.instancePath || "/";
|
|
78
|
+
const msg = e.message ?? "invalid";
|
|
79
|
+
return redactValidationMessage(`${path}: ${msg}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function validateSessionEvent(candidate) {
|
|
83
|
+
const validate = getSessionEventValidator();
|
|
84
|
+
const ok = validate(candidate);
|
|
85
|
+
if (ok)
|
|
86
|
+
return { ok: true, errors: [] };
|
|
87
|
+
return { ok: false, errors: formatErrors(validate.errors) };
|
|
88
|
+
}
|
|
89
|
+
export function sha256File(path) {
|
|
90
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
91
|
+
}
|
|
92
|
+
export function sha256SessionEventSchema() {
|
|
93
|
+
return sha256File(SESSION_EVENT_SCHEMA_PATH);
|
|
94
|
+
}
|
|
95
|
+
const HQ_PRO_SCHEMA_RELATIVE = "src/vault-service/work-mesh/live/session-event.schema.json";
|
|
96
|
+
/** Walk up from `start` until a directory containing package.json is found. */
|
|
97
|
+
function findRepoRoot(start) {
|
|
98
|
+
let dir = start;
|
|
99
|
+
for (;;) {
|
|
100
|
+
if (existsSync(join(dir, "package.json")))
|
|
101
|
+
return dir;
|
|
102
|
+
const parent = dirname(dir);
|
|
103
|
+
if (parent === dir)
|
|
104
|
+
return null;
|
|
105
|
+
dir = parent;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the hq-pro canonical schema path when available.
|
|
110
|
+
*
|
|
111
|
+
* Order: `HQ_PRO_SCHEMA_PATH` env override (absolute path to the hq-pro
|
|
112
|
+
* schema file) → sibling checkout
|
|
113
|
+
* `../hq-pro/src/vault-service/work-mesh/live/session-event.schema.json`
|
|
114
|
+
* from the hq-cli repo root → null.
|
|
115
|
+
* Shipped code must not hardcode machine-specific worktree paths.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveHqProSchemaPath() {
|
|
118
|
+
const envPath = process.env.HQ_PRO_SCHEMA_PATH?.trim();
|
|
119
|
+
if (envPath) {
|
|
120
|
+
return isAbsolute(envPath) ? envPath : resolve(envPath);
|
|
121
|
+
}
|
|
122
|
+
const repoRoot = findRepoRoot(HERE);
|
|
123
|
+
if (!repoRoot)
|
|
124
|
+
return null;
|
|
125
|
+
const sibling = resolve(repoRoot, "..", "hq-pro", HQ_PRO_SCHEMA_RELATIVE);
|
|
126
|
+
return existsSync(sibling) ? sibling : null;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=validate-session-event.js.map
|
|
@@ -200,6 +200,51 @@ export declare class QmdStoreUnopenableError extends QmdExitError {
|
|
|
200
200
|
name: string;
|
|
201
201
|
constructor(message: string, args: string[], status: number | null, stdout: string, stderr: string, storeDir?: string | undefined, probeReason?: string | undefined);
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* qmd's `query` subcommand (hybrid search) rejected the positional as a
|
|
205
|
+
* MALFORMED QUERY DOCUMENT: qmd 2.5.3 parses a multi-line positional as a query
|
|
206
|
+
* document and requires EVERY non-empty line to carry a lex:/vec:/hyde:/intent:/
|
|
207
|
+
* expand: prefix (dist/cli/qmd.js:2157-2217), throwing otherwise. That is the
|
|
208
|
+
* caller's INPUT SHAPE — a multi-line free-text query, most often a fleet agent
|
|
209
|
+
* forwarding a whole Slack message — not a bug HQ can fix in code.
|
|
210
|
+
*
|
|
211
|
+
* HQ-CLI-1B (Sentry indigo-d0/hq-cli 7705711476, 28 of 29 events): `hq search
|
|
212
|
+
* --mode hybrid` mapped to `qmd query` and handed the caller's multi-line query
|
|
213
|
+
* through unchanged; qmd exited 1 with `Line 1 is missing a lex:/vec:/hyde:/
|
|
214
|
+
* intent: prefix. …`. With no classifier the generic template raised a plain
|
|
215
|
+
* QmdExitError that reached the boundary's final else and was captured.
|
|
216
|
+
* buildSearchArgs now normalises free text to a single line so the common case
|
|
217
|
+
* never reaches qmd multi-line; this typed subclass lets the boundary classify a
|
|
218
|
+
* genuinely malformed structured document as the caller's input and stop
|
|
219
|
+
* fingerprinting on the query text: it is raised only from qmd's OWN captured
|
|
220
|
+
* streams and its message names the SUBCOMMAND only.
|
|
221
|
+
*/
|
|
222
|
+
export declare class QmdQueryDocumentError extends QmdExitError {
|
|
223
|
+
name: string;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A qmd MODEL DOWNLOAD did not finish: qmd crashed with ENOENT while renaming its
|
|
227
|
+
* own partial `.ipull` download file into place. The partial file was gone by the
|
|
228
|
+
* time the rename ran — the signature of ANOTHER qmd process on the same box
|
|
229
|
+
* finishing (or clearing) the same download first. That is a transient, retryable
|
|
230
|
+
* LOCAL-CACHE race, not a bug HQ can fix in code: hq cannot coordinate qmd
|
|
231
|
+
* children other users start on the same host, and reaching into qmd's model
|
|
232
|
+
* cache would be hq owning upstream state.
|
|
233
|
+
*
|
|
234
|
+
* HQ-CLI-1B (Sentry indigo-d0/hq-cli 7705711476, the newest event, on release
|
|
235
|
+
* 5.107.1): `qmd query` reached its reranking stage and its reranker-model
|
|
236
|
+
* downloader died uncaught with `[Error: ENOENT: no such file or directory,
|
|
237
|
+
* rename '<cache>/…-reranker-….gguf.ipull' -> '<cache>/…-reranker-….gguf']`.
|
|
238
|
+
* With no classifier it took the identical fall-through to capture as the
|
|
239
|
+
* query-document crash — and because both share the (QmdExitError, qmd:query,
|
|
240
|
+
* exit:1) fingerprint, a query-document-only fix would leave this issue alive.
|
|
241
|
+
* This typed subclass lets the boundary classify it and stop the capture: it is
|
|
242
|
+
* raised only from qmd's OWN captured streams and its message names the
|
|
243
|
+
* SUBCOMMAND only.
|
|
244
|
+
*/
|
|
245
|
+
export declare class QmdModelDownloadError extends QmdExitError {
|
|
246
|
+
name: string;
|
|
247
|
+
}
|
|
203
248
|
export type ResolveQmdBinOptions = {
|
|
204
249
|
env?: Record<string, string | undefined>;
|
|
205
250
|
isExecutable?: (candidate: string) => boolean;
|
|
@@ -4,8 +4,10 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import * as os from 'node:os';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { isQmdModelDownloadError } from '../../utils/qmd-model-download-error.js';
|
|
7
8
|
import { isQmdModuleMissingError } from '../../utils/qmd-module-missing-error.js';
|
|
8
9
|
import { isQmdNativeBindingError } from '../../utils/qmd-native-binding-error.js';
|
|
10
|
+
import { isQmdQueryDocumentError } from '../../utils/qmd-query-document-error.js';
|
|
9
11
|
import { isQmdStoreMissingError } from '../../utils/qmd-store-missing-error.js';
|
|
10
12
|
import { isQmdStoreUnopenableError } from '../../utils/qmd-store-unopenable-error.js';
|
|
11
13
|
import { redactErrorText } from '../../utils/redact-error-text.js';
|
|
@@ -189,6 +191,51 @@ export class QmdStoreUnopenableError extends QmdExitError {
|
|
|
189
191
|
this.probeReason = probeReason;
|
|
190
192
|
}
|
|
191
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* qmd's `query` subcommand (hybrid search) rejected the positional as a
|
|
196
|
+
* MALFORMED QUERY DOCUMENT: qmd 2.5.3 parses a multi-line positional as a query
|
|
197
|
+
* document and requires EVERY non-empty line to carry a lex:/vec:/hyde:/intent:/
|
|
198
|
+
* expand: prefix (dist/cli/qmd.js:2157-2217), throwing otherwise. That is the
|
|
199
|
+
* caller's INPUT SHAPE — a multi-line free-text query, most often a fleet agent
|
|
200
|
+
* forwarding a whole Slack message — not a bug HQ can fix in code.
|
|
201
|
+
*
|
|
202
|
+
* HQ-CLI-1B (Sentry indigo-d0/hq-cli 7705711476, 28 of 29 events): `hq search
|
|
203
|
+
* --mode hybrid` mapped to `qmd query` and handed the caller's multi-line query
|
|
204
|
+
* through unchanged; qmd exited 1 with `Line 1 is missing a lex:/vec:/hyde:/
|
|
205
|
+
* intent: prefix. …`. With no classifier the generic template raised a plain
|
|
206
|
+
* QmdExitError that reached the boundary's final else and was captured.
|
|
207
|
+
* buildSearchArgs now normalises free text to a single line so the common case
|
|
208
|
+
* never reaches qmd multi-line; this typed subclass lets the boundary classify a
|
|
209
|
+
* genuinely malformed structured document as the caller's input and stop
|
|
210
|
+
* fingerprinting on the query text: it is raised only from qmd's OWN captured
|
|
211
|
+
* streams and its message names the SUBCOMMAND only.
|
|
212
|
+
*/
|
|
213
|
+
export class QmdQueryDocumentError extends QmdExitError {
|
|
214
|
+
name = 'QmdQueryDocumentError';
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A qmd MODEL DOWNLOAD did not finish: qmd crashed with ENOENT while renaming its
|
|
218
|
+
* own partial `.ipull` download file into place. The partial file was gone by the
|
|
219
|
+
* time the rename ran — the signature of ANOTHER qmd process on the same box
|
|
220
|
+
* finishing (or clearing) the same download first. That is a transient, retryable
|
|
221
|
+
* LOCAL-CACHE race, not a bug HQ can fix in code: hq cannot coordinate qmd
|
|
222
|
+
* children other users start on the same host, and reaching into qmd's model
|
|
223
|
+
* cache would be hq owning upstream state.
|
|
224
|
+
*
|
|
225
|
+
* HQ-CLI-1B (Sentry indigo-d0/hq-cli 7705711476, the newest event, on release
|
|
226
|
+
* 5.107.1): `qmd query` reached its reranking stage and its reranker-model
|
|
227
|
+
* downloader died uncaught with `[Error: ENOENT: no such file or directory,
|
|
228
|
+
* rename '<cache>/…-reranker-….gguf.ipull' -> '<cache>/…-reranker-….gguf']`.
|
|
229
|
+
* With no classifier it took the identical fall-through to capture as the
|
|
230
|
+
* query-document crash — and because both share the (QmdExitError, qmd:query,
|
|
231
|
+
* exit:1) fingerprint, a query-document-only fix would leave this issue alive.
|
|
232
|
+
* This typed subclass lets the boundary classify it and stop the capture: it is
|
|
233
|
+
* raised only from qmd's OWN captured streams and its message names the
|
|
234
|
+
* SUBCOMMAND only.
|
|
235
|
+
*/
|
|
236
|
+
export class QmdModelDownloadError extends QmdExitError {
|
|
237
|
+
name = 'QmdModelDownloadError';
|
|
238
|
+
}
|
|
192
239
|
function isExecutable(candidate) {
|
|
193
240
|
try {
|
|
194
241
|
fs.accessSync(candidate, fs.constants.X_OK);
|
|
@@ -1036,6 +1083,35 @@ function finishRunQmd(result, bin, args) {
|
|
|
1036
1083
|
const ensure = lastQmdStoreEnsure;
|
|
1037
1084
|
throw new QmdStoreUnopenableError(`qmd ${subcommand} could not run: its local search store could not be opened`, args, normalized.status, normalized.stdout, normalized.stderr, ensure?.dir, ensure && !ensure.writable ? ensure.reason : undefined);
|
|
1038
1085
|
}
|
|
1086
|
+
// qmd's `query` subcommand rejected the positional as a MALFORMED QUERY
|
|
1087
|
+
// DOCUMENT: qmd 2.5.3 parses a multi-line positional as a document and requires
|
|
1088
|
+
// every non-empty line to carry a lex:/vec:/hyde:/intent:/expand: prefix, so a
|
|
1089
|
+
// multi-line free-text query (a fleet agent forwarding a whole Slack message)
|
|
1090
|
+
// exits 1 with a syntax diagnostic. That is the caller's INPUT SHAPE, not an
|
|
1091
|
+
// hq-cli defect. Read qmd's OWN captured streams only, never the synthesized
|
|
1092
|
+
// message, so a user query that merely contains the wording can never trip it.
|
|
1093
|
+
// hq-cli side: buildSearchArgs now normalises free text to one line so this no
|
|
1094
|
+
// longer fires for the common case; this branch types a genuinely malformed
|
|
1095
|
+
// structured document so the boundary prints a query-free remedy and skips
|
|
1096
|
+
// capture. Placed AFTER the store checks and BEFORE the collection-missing
|
|
1097
|
+
// regex; the wordings are disjoint (that regex needs collection/qmd:// adjacent
|
|
1098
|
+
// to a not-found token, absent here), so no existing branch changes behaviour
|
|
1099
|
+
// (HQ-CLI-1B / Sentry 7705711476).
|
|
1100
|
+
if (isQmdQueryDocumentError({ stderr: normalized.stderr, stdout: normalized.stdout })) {
|
|
1101
|
+
throw new QmdQueryDocumentError(`qmd ${subcommand} could not run: the query was read as a malformed query document`, args, normalized.status, normalized.stdout, normalized.stderr);
|
|
1102
|
+
}
|
|
1103
|
+
// A qmd MODEL DOWNLOAD died with ENOENT renaming its own partial `.ipull` file
|
|
1104
|
+
// — the signature of another qmd process on the same box finishing or clearing
|
|
1105
|
+
// the same download first. That is a transient, retryable LOCAL-CACHE race, not
|
|
1106
|
+
// an hq-cli defect (hq cannot coordinate qmd children other users start on the
|
|
1107
|
+
// host). Require BOTH the ENOENT-rename signature AND the `.ipull` marker so a
|
|
1108
|
+
// durable failure (ENOSPC/EACCES/network) keeps its reportable path. Read qmd's
|
|
1109
|
+
// OWN captured streams only. Placed with the query-document check, before the
|
|
1110
|
+
// collection-missing regex; the signatures are disjoint, so no existing branch
|
|
1111
|
+
// changes behaviour (HQ-CLI-1B / Sentry 7705711476, the newest event).
|
|
1112
|
+
if (isQmdModelDownloadError({ stderr: normalized.stderr, stdout: normalized.stdout })) {
|
|
1113
|
+
throw new QmdModelDownloadError(`qmd ${subcommand} could not run: a model download did not finish (its partial file disappeared)`, args, normalized.status, normalized.stdout, normalized.stderr);
|
|
1114
|
+
}
|
|
1039
1115
|
if (/(?:collection|qmd:\/\/).*(?:not found|does not exist|unknown)|(?:not found|does not exist).*collection/i.test(classifyText)) {
|
|
1040
1116
|
throw new QmdCollectionMissingError(message, args, normalized.status, normalized.stdout, normalized.stderr);
|
|
1041
1117
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic owner-only file writes with symlink and path guards for work-context.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Resolve symlinks on an existing path; for a missing leaf, resolve the parent
|
|
6
|
+
* and re-append the basename so we never rename onto a symlink.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveRealTarget(target: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Ensure parent dir exists as a real directory (not a symlink), mode 0700.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureOwnerDir(dir: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Atomically replace target with data at mode 0600 (same-dir tmp + rename).
|
|
15
|
+
*/
|
|
16
|
+
export declare function atomicWriteFile(target: string, data: string | Buffer): void;
|
|
17
|
+
export declare function atomicWriteJson(target: string, value: unknown): void;
|
|
18
|
+
//# sourceMappingURL=atomic.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic owner-only file writes with symlink and path guards for work-context.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { UnsafeConfigPathError } from "./errors.js";
|
|
7
|
+
function fsyncDir(dir) {
|
|
8
|
+
try {
|
|
9
|
+
const fd = fs.openSync(dir, "r");
|
|
10
|
+
try {
|
|
11
|
+
fs.fsyncSync(fd);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
fs.closeSync(fd);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Best-effort: some platforms reject directory fsync.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve symlinks on an existing path; for a missing leaf, resolve the parent
|
|
23
|
+
* and re-append the basename so we never rename onto a symlink.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveRealTarget(target) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.realpathSync(target);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Leaf may not exist yet.
|
|
31
|
+
}
|
|
32
|
+
let cur = path.dirname(target);
|
|
33
|
+
const base = path.basename(target);
|
|
34
|
+
for (let i = 0; i < 40; i += 1) {
|
|
35
|
+
try {
|
|
36
|
+
const lst = fs.lstatSync(cur);
|
|
37
|
+
if (lst.isSymbolicLink()) {
|
|
38
|
+
throw new UnsafeConfigPathError(`Refusing write through symlink: ${cur}`);
|
|
39
|
+
}
|
|
40
|
+
const real = fs.realpathSync(cur);
|
|
41
|
+
return path.join(real, base);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err instanceof UnsafeConfigPathError)
|
|
45
|
+
throw err;
|
|
46
|
+
const parent = path.dirname(cur);
|
|
47
|
+
if (parent === cur)
|
|
48
|
+
break;
|
|
49
|
+
cur = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Ensure parent dir exists as a real directory (not a symlink), mode 0700.
|
|
56
|
+
*/
|
|
57
|
+
export function ensureOwnerDir(dir) {
|
|
58
|
+
const resolved = (() => {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(dir)) {
|
|
61
|
+
const lst = fs.lstatSync(dir);
|
|
62
|
+
if (lst.isSymbolicLink()) {
|
|
63
|
+
throw new UnsafeConfigPathError(`Refusing directory that is a symlink: ${dir}`);
|
|
64
|
+
}
|
|
65
|
+
return fs.realpathSync(dir);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err instanceof UnsafeConfigPathError)
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
return dir;
|
|
73
|
+
})();
|
|
74
|
+
fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
75
|
+
try {
|
|
76
|
+
fs.chmodSync(resolved, 0o700);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore chmod failures on exotic FS
|
|
80
|
+
}
|
|
81
|
+
const lst = fs.lstatSync(resolved);
|
|
82
|
+
if (lst.isSymbolicLink()) {
|
|
83
|
+
throw new UnsafeConfigPathError(`Refusing directory that is a symlink: ${resolved}`);
|
|
84
|
+
}
|
|
85
|
+
return fs.realpathSync(resolved);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Atomically replace target with data at mode 0600 (same-dir tmp + rename).
|
|
89
|
+
*/
|
|
90
|
+
export function atomicWriteFile(target, data) {
|
|
91
|
+
const realTarget = resolveRealTarget(target);
|
|
92
|
+
const dir = ensureOwnerDir(path.dirname(realTarget));
|
|
93
|
+
const finalPath = path.join(dir, path.basename(realTarget));
|
|
94
|
+
const tmp = path.join(dir, `.${path.basename(finalPath)}.tmp.${process.pid}.${Date.now()}`);
|
|
95
|
+
let fd;
|
|
96
|
+
try {
|
|
97
|
+
fd = fs.openSync(tmp, "wx", 0o600);
|
|
98
|
+
fs.writeSync(fd, typeof data === "string" ? Buffer.from(data, "utf8") : data);
|
|
99
|
+
fs.fsyncSync(fd);
|
|
100
|
+
fs.closeSync(fd);
|
|
101
|
+
fd = undefined;
|
|
102
|
+
fs.renameSync(tmp, finalPath);
|
|
103
|
+
fsyncDir(dir);
|
|
104
|
+
try {
|
|
105
|
+
fs.chmodSync(finalPath, 0o600);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (fd !== undefined) {
|
|
113
|
+
try {
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* already closed */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(tmp);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* temp may not exist */
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function atomicWriteJson(target, value) {
|
|
130
|
+
atomicWriteFile(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=atomic.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company precedence for work-context reconcile (US-007A).
|
|
3
|
+
*
|
|
4
|
+
* Order:
|
|
5
|
+
* 1. trusted explicit (HQ_SPAWN_COMPANY env or observation.trustedContext/explicit)
|
|
6
|
+
* 2. existing authoritative session scope
|
|
7
|
+
* 3. session meta.yaml company_slug (/startwork)
|
|
8
|
+
* 4. agent-box identity file (HQ_AGENT_IDENTITY_FILE → companyUid; source
|
|
9
|
+
* trusted_explicit — never "default company mode"; below HQ_SPAWN_COMPANY,
|
|
10
|
+
* above device default)
|
|
11
|
+
* 5. enabled device default
|
|
12
|
+
* 6. exactly one deterministic mapping (cwd under companies/{slug}/ or repo remote)
|
|
13
|
+
*
|
|
14
|
+
* Prompt-text matching is never used.
|
|
15
|
+
* Device default + conflicting deterministic evidence → company_conflict.
|
|
16
|
+
*/
|
|
17
|
+
import type { TrustedExplicitContext } from "./contract.js";
|
|
18
|
+
import { type SessionStateFile } from "./state.js";
|
|
19
|
+
export type CompanyResolutionSource = "trusted_explicit" | "existing_scope" | "session_meta" | "device_default" | "deterministic_cwd" | "deterministic_remote";
|
|
20
|
+
export interface ResolvedCompany {
|
|
21
|
+
slug?: string;
|
|
22
|
+
uid?: string;
|
|
23
|
+
source: CompanyResolutionSource;
|
|
24
|
+
}
|
|
25
|
+
/** Pointer to the US-017B correction client (never auto-switches). */
|
|
26
|
+
export declare const COMPANY_CORRECTION_PATH_PREFIX = "hq mesh context correct --session";
|
|
27
|
+
export declare function companyCorrectionPath(sessionId: string): string;
|
|
28
|
+
export type CompanyResolution = {
|
|
29
|
+
status: "resolved";
|
|
30
|
+
company: ResolvedCompany;
|
|
31
|
+
} | {
|
|
32
|
+
status: "needs_company";
|
|
33
|
+
} | {
|
|
34
|
+
status: "company_conflict";
|
|
35
|
+
defaultCompany: ResolvedCompany;
|
|
36
|
+
evidenceCompany: ResolvedCompany;
|
|
37
|
+
/** Present when a prior binding exists and evidence names another company. */
|
|
38
|
+
correctionPath?: string;
|
|
39
|
+
/** True when conflict is after an existing binding (cannot auto-switch). */
|
|
40
|
+
afterBinding?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export interface CompanyResolveInput {
|
|
43
|
+
root: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
/** Trusted explicit from observation (trustedContext or explicit). */
|
|
47
|
+
trusted?: TrustedExplicitContext & {
|
|
48
|
+
companySlug?: string;
|
|
49
|
+
};
|
|
50
|
+
cwd?: string;
|
|
51
|
+
hqRoot?: string;
|
|
52
|
+
/** Optional preloaded state (tests). */
|
|
53
|
+
existingState?: SessionStateFile | null;
|
|
54
|
+
/** Optional injected meta company_slug. */
|
|
55
|
+
metaCompanySlug?: string | null;
|
|
56
|
+
/** Optional injected deterministic remote ownership slug. */
|
|
57
|
+
remoteOwnerSlug?: string | null;
|
|
58
|
+
}
|
|
59
|
+
/** Env naming the on-box identity.json (fleet agent boxes). */
|
|
60
|
+
export declare const HQ_AGENT_IDENTITY_FILE_ENV = "HQ_AGENT_IDENTITY_FILE";
|
|
61
|
+
/**
|
|
62
|
+
* Read `companyUid` from the agent identity file when
|
|
63
|
+
* `HQ_AGENT_IDENTITY_FILE` is set. Missing/unreadable/malformed → undefined
|
|
64
|
+
* (never throws; never logs file contents).
|
|
65
|
+
*/
|
|
66
|
+
export declare function readAgentIdentityCompanyUid(env?: NodeJS.ProcessEnv): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Compare companies: UIDs when both sides have them; otherwise normalized
|
|
69
|
+
* slugs when both have slugs. Never treat uid+slug vs slug-only as a
|
|
70
|
+
* conflict when the slugs match (verified scope under companies/{slug}/).
|
|
71
|
+
*/
|
|
72
|
+
export declare function sameCompany(a: {
|
|
73
|
+
slug?: string;
|
|
74
|
+
uid?: string;
|
|
75
|
+
}, b: {
|
|
76
|
+
slug?: string;
|
|
77
|
+
uid?: string;
|
|
78
|
+
}): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Extract company slug from a cwd path containing `companies/{slug}/`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function companySlugFromCwd(cwd: string | undefined, hqRoot?: string): string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Extract project id from cwd under `companies/{slug}/projects/{project}/`
|
|
85
|
+
* (US-007B deterministic mapping). Never invents a project.
|
|
86
|
+
*/
|
|
87
|
+
export declare function projectIdFromCwd(cwd: string | undefined, hqRoot?: string): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Read company_slug from workspace/sessions/<sid>/meta.yaml (best-effort, no yaml dep).
|
|
90
|
+
*/
|
|
91
|
+
export declare function readMetaCompanySlug(sessionId: string, hqRoot: string | undefined): string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Deterministic mapping: exactly one of cwd companies/{slug} or remote owner.
|
|
94
|
+
* Multiple distinct signals that disagree → no deterministic mapping (needs_company
|
|
95
|
+
* unless a higher-precedence source exists). Matching signals collapse to one.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveDeterministicCompany(input: {
|
|
98
|
+
cwd?: string;
|
|
99
|
+
hqRoot?: string;
|
|
100
|
+
remoteOwnerSlug?: string | null;
|
|
101
|
+
}): ResolvedCompany | null;
|
|
102
|
+
export declare function resolveCompany(input: CompanyResolveInput): CompanyResolution;
|
|
103
|
+
//# sourceMappingURL=company.d.ts.map
|