@indigoai-us/hq-cli 5.307.0 → 5.309.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.
- package/CHANGELOG.md +11 -0
- package/assets/scaffold/core/scripts/resolve-company.sh +199 -0
- package/dist/command-catalog.generated.d.ts +13 -0
- package/dist/command-catalog.generated.js +16 -0
- package/dist/commands/core-resolve-company-command.d.ts +4 -0
- package/dist/commands/core-resolve-company-command.js +42 -0
- package/dist/commands/core-resolve-company-native.d.ts +28 -0
- package/dist/commands/core-resolve-company-native.js +408 -0
- package/dist/commands/core.js +3 -0
- package/dist/commands/lanes-checks.d.ts +7 -1
- package/dist/commands/lanes-checks.js +68 -18
- package/dist/commands/lanes-report.js +5 -2
- package/dist/fast-scaffold.js +12 -0
- package/dist/lib/doctor/checks/codex-wiring.d.ts +0 -4
- package/dist/lib/doctor/checks/codex-wiring.js +2 -2
- package/dist/lib/doctor/checks/runtime-health.d.ts +0 -17
- package/dist/lib/doctor/checks/runtime-health.js +4 -4
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +0 -1
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +1 -1
- package/dist/lib/doctor/compat.d.ts +0 -16
- package/dist/lib/doctor/compat.js +3 -3
- package/dist/lib/doctor/platform.d.ts +0 -26
- package/dist/lib/doctor/platform.js +5 -5
- package/dist/lib/lanes/autopilot.js +122 -102
- package/dist/lib/onboarding/errors.d.ts +2 -1
- package/dist/lib/onboarding/errors.js +1 -1
- package/dist/lib/onboarding/index.d.ts +2 -6
- package/dist/lib/onboarding/index.js +2 -5
- package/dist/lib/search-index/embed-lock.d.ts +0 -7
- package/dist/lib/search-index/embed-lock.js +1 -1
- package/dist/lib/search-index/load-gate.d.ts +0 -8
- package/dist/lib/search-index/load-gate.js +3 -3
- package/dist/lib/search-index/max-doc-bytes.d.ts +2 -3
- package/dist/lib/search-index/max-doc-bytes.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/** Native port of resolve-company.sh. This module stays on the fast import path. */
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
6
|
+
export const RESOLVE_COMPANY_HELP = [
|
|
7
|
+
"hq-core: public",
|
|
8
|
+
"resolve-company.sh — resolve the active company for skills and routing.",
|
|
9
|
+
"",
|
|
10
|
+
"Resolution order (first hit wins): explicit manifest slug in the prompt,",
|
|
11
|
+
"session company_slug, enabled device default, then none. Prompt matching is",
|
|
12
|
+
"whole-token only; free text never guesses a company.",
|
|
13
|
+
"",
|
|
14
|
+
"Output: {\"company\":\"<slug>\",\"source\":\"prompt|session|device_default|none\"}",
|
|
15
|
+
"Always exits 0 and never writes state.",
|
|
16
|
+
"",
|
|
17
|
+
"set -uo pipefail",
|
|
18
|
+
"",
|
|
19
|
+
"ROOT=\"\"",
|
|
20
|
+
].join("\n") + "\n";
|
|
21
|
+
const EMPTY_ENV = {};
|
|
22
|
+
const keepSlug = (slug) => slug !== "" && slug !== "_template" && slug !== "companies" && slug !== "unaffiliated_repos";
|
|
23
|
+
function errorCode(error) {
|
|
24
|
+
return error && typeof error === "object" && "code" in error && typeof error.code === "string"
|
|
25
|
+
? error.code
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
function readFile(filePath, fileSystem) {
|
|
29
|
+
try {
|
|
30
|
+
return fileSystem.readFileSync(filePath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const code = errorCode(error);
|
|
34
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP")
|
|
35
|
+
return undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readManifest(filePath, fileSystem) {
|
|
40
|
+
try {
|
|
41
|
+
return fileSystem.readFileSync(filePath, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const code = errorCode(error);
|
|
45
|
+
if (code === "EACCES") {
|
|
46
|
+
return {
|
|
47
|
+
code: 0,
|
|
48
|
+
stdout: Buffer.from('{"company":"","source":"none"}\n'),
|
|
49
|
+
stderr: Buffer.from("awk: fatal: cannot open file `" + filePath + "' for reading: Permission denied\n"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP")
|
|
53
|
+
return output("", "none");
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isFile(filePath, fileSystem) {
|
|
58
|
+
try {
|
|
59
|
+
return fileSystem.statSync(filePath).isFile();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = errorCode(error);
|
|
63
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP")
|
|
64
|
+
return false;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseManifestSlugs(text) {
|
|
69
|
+
const slugs = new Set();
|
|
70
|
+
let wrapped = false;
|
|
71
|
+
for (const rawLine of text.split("\n")) {
|
|
72
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
73
|
+
if (/^companies:[ \t]*$/.test(line)) {
|
|
74
|
+
wrapped = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (wrapped && /^[^ \t][^:]*:[ \t]*$/.test(line))
|
|
78
|
+
wrapped = false;
|
|
79
|
+
if (wrapped) {
|
|
80
|
+
const match = line.match(/^ {2}([A-Za-z][A-Za-z0-9_-]*):/);
|
|
81
|
+
if (match && keepSlug(match[1]))
|
|
82
|
+
slugs.add(match[1]);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):/);
|
|
86
|
+
if (match && keepSlug(match[1]))
|
|
87
|
+
slugs.add(match[1]);
|
|
88
|
+
}
|
|
89
|
+
return [...slugs];
|
|
90
|
+
}
|
|
91
|
+
function promptSlug(prompt, slugs) {
|
|
92
|
+
if (!prompt)
|
|
93
|
+
return "";
|
|
94
|
+
const asciiLower = (value) => value.replace(/[A-Z]/g, (letter) => String.fromCharCode(letter.charCodeAt(0) + 32));
|
|
95
|
+
const normalized = asciiLower(prompt).replace(/[^a-z0-9_-]/g, " ");
|
|
96
|
+
const positions = new Map();
|
|
97
|
+
normalized.split(/[ \t]+/).forEach((token, index) => {
|
|
98
|
+
if (token && !positions.has(token))
|
|
99
|
+
positions.set(token, index + 1);
|
|
100
|
+
});
|
|
101
|
+
const matches = new Map();
|
|
102
|
+
for (const slug of slugs) {
|
|
103
|
+
const key = asciiLower(slug);
|
|
104
|
+
if (!positions.has(key))
|
|
105
|
+
continue;
|
|
106
|
+
matches.set(key, [...(matches.get(key) ?? []), slug]);
|
|
107
|
+
}
|
|
108
|
+
let bestSlug = "";
|
|
109
|
+
let bestLength = 0;
|
|
110
|
+
let bestPosition = 0;
|
|
111
|
+
for (const [key, candidates] of matches) {
|
|
112
|
+
if (candidates.length !== 1)
|
|
113
|
+
continue;
|
|
114
|
+
const slug = candidates[0];
|
|
115
|
+
const length = slug.length;
|
|
116
|
+
const position = positions.get(key);
|
|
117
|
+
if (length > bestLength || (length === bestLength && position < bestPosition)) {
|
|
118
|
+
bestSlug = slug;
|
|
119
|
+
bestLength = length;
|
|
120
|
+
bestPosition = position;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return bestSlug;
|
|
124
|
+
}
|
|
125
|
+
function validSessionId(value) {
|
|
126
|
+
return value !== "" && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
|
|
127
|
+
}
|
|
128
|
+
function sessionId(root, env, fileSystem) {
|
|
129
|
+
for (const name of ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"]) {
|
|
130
|
+
const value = (env[name] ?? "").replace(/[ \t\n\v\f\r]/g, "");
|
|
131
|
+
if (validSessionId(value))
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
const current = readFile(path.join(root, "workspace/sessions/.current"), fileSystem)?.replace(/[ \t\n\v\f\r]/g, "") ?? "";
|
|
135
|
+
return validSessionId(current) ? current : "";
|
|
136
|
+
}
|
|
137
|
+
function sessionCompany(root, env, fileSystem) {
|
|
138
|
+
const helper = path.join(root, "core/scripts/hq-session.sh");
|
|
139
|
+
try {
|
|
140
|
+
fileSystem.accessSync(helper, fs.constants.X_OK);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const code = errorCode(error);
|
|
144
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP")
|
|
145
|
+
return "";
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
// A directory may be executable according to access(2), but `bash directory`
|
|
149
|
+
// fails without output, just as hq-session.sh does here.
|
|
150
|
+
if (!isFile(helper, fileSystem))
|
|
151
|
+
return "";
|
|
152
|
+
const id = sessionId(root, env, fileSystem);
|
|
153
|
+
if (!id)
|
|
154
|
+
return "";
|
|
155
|
+
const meta = readFile(path.join(root, "workspace/sessions", id, "meta.yaml"), fileSystem);
|
|
156
|
+
if (meta === undefined)
|
|
157
|
+
return "";
|
|
158
|
+
let value = "";
|
|
159
|
+
for (const line of meta.split("\n")) {
|
|
160
|
+
const firstField = line.replace(/^[ \t]+/, "").split(/[ \t]+/, 1)[0];
|
|
161
|
+
if (firstField !== "company_slug:")
|
|
162
|
+
continue;
|
|
163
|
+
value = line.replace(/^[^:]+:[ \t]*/, "").replace(/^"|"$/g, "");
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
// The shell's command substitution strips trailing newlines, then tr removes
|
|
167
|
+
// all remaining whitespace and double quotes from the helper's output.
|
|
168
|
+
return value.replace(/[ \t\n\v\f\r"]/g, "");
|
|
169
|
+
}
|
|
170
|
+
function fleetIdentityPresent(rootPrefix, env, fileSystem) {
|
|
171
|
+
if (env.HQ_AGENT_WORKDIR || env.HQ_AGENT_COMPANY_DIR || env.HQ_AGENT_IDENTITY_FILE)
|
|
172
|
+
return true;
|
|
173
|
+
const paths = [
|
|
174
|
+
"/etc/hq-agent/identity.json",
|
|
175
|
+
"/etc/hq-agent/machine-creds.json",
|
|
176
|
+
rootPrefix ? `${rootPrefix.replace(/\/$/, "")}/var/lib/hq-agent/identity.json` : "/var/lib/hq-agent/identity.json",
|
|
177
|
+
"/var/lib/hq-agent/machine-creds.json",
|
|
178
|
+
];
|
|
179
|
+
return paths.some((filePath) => isFile(filePath, fileSystem));
|
|
180
|
+
}
|
|
181
|
+
function parseJson(text) {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(text);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function record(value) {
|
|
190
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
191
|
+
? value
|
|
192
|
+
: undefined;
|
|
193
|
+
}
|
|
194
|
+
function normalizeShellValue(value) {
|
|
195
|
+
return value.replace(/[ \t\n\v\f\r"]/g, "");
|
|
196
|
+
}
|
|
197
|
+
function defaultSlugFromJson(text, knownSlugs) {
|
|
198
|
+
const root = record(parseJson(text));
|
|
199
|
+
if (!root)
|
|
200
|
+
return "";
|
|
201
|
+
const candidate = root.defaultCompany;
|
|
202
|
+
const preference = candidate === null || candidate === undefined || candidate === false
|
|
203
|
+
? root
|
|
204
|
+
: record(candidate);
|
|
205
|
+
if (!preference)
|
|
206
|
+
return "";
|
|
207
|
+
if (preference.enabled !== true || (preference.source ?? "") === "disabled" || (preference.needsChoice ?? false) !== false)
|
|
208
|
+
return "";
|
|
209
|
+
if (typeof preference.slug !== "string")
|
|
210
|
+
return "";
|
|
211
|
+
const slug = normalizeShellValue(preference.slug);
|
|
212
|
+
return knownSlugs.has(slug) ? slug : "";
|
|
213
|
+
}
|
|
214
|
+
function validPreflight(text, slug, session, operation) {
|
|
215
|
+
const value = record(parseJson(text));
|
|
216
|
+
return Boolean(value && value.contractVersion === 1 && value.sessionId === session &&
|
|
217
|
+
value.clientOperationId === operation &&
|
|
218
|
+
typeof value.kind === "string" && ["queued", "needs_project", "needs_task", "bound"].includes(value.kind) &&
|
|
219
|
+
typeof value.classification === "string" && ["needs_project", "needs_task", "bound"].includes(value.classification) &&
|
|
220
|
+
typeof value.delivery === "string" && ["queued", "acked"].includes(value.delivery) && value.lifecycle === "open" &&
|
|
221
|
+
value.companySlug === slug &&
|
|
222
|
+
typeof value.companyUid === "string" && value.companyUid.length > 0);
|
|
223
|
+
}
|
|
224
|
+
function commandOnPath(name, env) {
|
|
225
|
+
const pathValue = env.PATH ?? "";
|
|
226
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
227
|
+
const candidate = path.join(directory || ".", name);
|
|
228
|
+
try {
|
|
229
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// `command -v` simply advances to the next PATH entry.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function runHq(args, env, timeoutMs, cwd) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
let settled = false;
|
|
241
|
+
let stdout = "";
|
|
242
|
+
let child;
|
|
243
|
+
let timeoutKill;
|
|
244
|
+
let timedOut = false;
|
|
245
|
+
const signalGroup = (signal) => {
|
|
246
|
+
if (!child?.pid)
|
|
247
|
+
return;
|
|
248
|
+
try {
|
|
249
|
+
process.kill(-child.pid, signal);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (errorCode(error) !== "ESRCH")
|
|
253
|
+
finish({ code: 1, stdout });
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const finish = (result) => {
|
|
257
|
+
if (settled)
|
|
258
|
+
return;
|
|
259
|
+
settled = true;
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
if (timeoutKill)
|
|
262
|
+
clearTimeout(timeoutKill);
|
|
263
|
+
resolve(result);
|
|
264
|
+
};
|
|
265
|
+
const timer = setTimeout(() => {
|
|
266
|
+
timedOut = true;
|
|
267
|
+
if (child?.pid) {
|
|
268
|
+
signalGroup("SIGTERM");
|
|
269
|
+
// The shell wrapper returns shortly after its 2 second deadline. Do
|
|
270
|
+
// not wait for inherited stdio handles after killing the process group.
|
|
271
|
+
timeoutKill = setTimeout(() => {
|
|
272
|
+
signalGroup("SIGKILL");
|
|
273
|
+
finish({ code: 124, stdout });
|
|
274
|
+
}, 250);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
finish({ code: 124, stdout });
|
|
278
|
+
}
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
try {
|
|
281
|
+
child = spawn("hq", args, { cwd, detached: true, stdio: ["ignore", "pipe", "ignore"], env });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
finish({ code: 127, stdout });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
child.stdout?.setEncoding("utf8");
|
|
288
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
289
|
+
child.once("error", () => finish({ code: 127, stdout }));
|
|
290
|
+
child.once("exit", (code, signal) => {
|
|
291
|
+
signalGroup("SIGTERM");
|
|
292
|
+
finish({ code: timedOut ? 124 : typeof code === "number" ? code : signal ? 128 : 1, stdout });
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function output(company, source) {
|
|
297
|
+
return { code: 0, stdout: Buffer.from(`{"company":"${company}","source":"${source}"}\n`), stderr: Buffer.alloc(0) };
|
|
298
|
+
}
|
|
299
|
+
function parseArgs(args) {
|
|
300
|
+
let root = "";
|
|
301
|
+
let prompt = "";
|
|
302
|
+
let havePrompt = false;
|
|
303
|
+
for (let index = 0; index < args.length;) {
|
|
304
|
+
const arg = args[index];
|
|
305
|
+
if (arg === "--root") {
|
|
306
|
+
root = args[index + 1] ?? "";
|
|
307
|
+
index += Math.min(2, args.length - index);
|
|
308
|
+
}
|
|
309
|
+
else if (arg === "--prompt") {
|
|
310
|
+
prompt = args[index + 1] ?? "";
|
|
311
|
+
havePrompt = true;
|
|
312
|
+
index += Math.min(2, args.length - index);
|
|
313
|
+
}
|
|
314
|
+
else if (arg === "--help" || arg === "-h") {
|
|
315
|
+
return { root, prompt, havePrompt, help: true };
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
index += 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { root, prompt, havePrompt, help: false };
|
|
322
|
+
}
|
|
323
|
+
async function readStdin() {
|
|
324
|
+
const chunks = [];
|
|
325
|
+
for await (const chunk of process.stdin)
|
|
326
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
327
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
328
|
+
}
|
|
329
|
+
export async function resolveCompany(args, dependencies) {
|
|
330
|
+
const env = dependencies.env ?? process.env;
|
|
331
|
+
const fileSystem = dependencies.fs ?? fs;
|
|
332
|
+
const parsed = parseArgs(args);
|
|
333
|
+
if (parsed.help)
|
|
334
|
+
return { code: 0, stdout: Buffer.from(RESOLVE_COMPANY_HELP), stderr: Buffer.alloc(0) };
|
|
335
|
+
let prompt = parsed.prompt;
|
|
336
|
+
if (!parsed.havePrompt && dependencies.stdinIsTty !== true) {
|
|
337
|
+
prompt = dependencies.stdin ?? await readStdin();
|
|
338
|
+
}
|
|
339
|
+
const root = parsed.root || dependencies.hqRoot || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR || dependencies.cwd;
|
|
340
|
+
const manifest = path.join(root, "companies/manifest.yaml");
|
|
341
|
+
if (!isFile(manifest, fileSystem))
|
|
342
|
+
return output("", "none");
|
|
343
|
+
const manifestResult = readManifest(manifest, fileSystem);
|
|
344
|
+
if (typeof manifestResult !== "string")
|
|
345
|
+
return manifestResult;
|
|
346
|
+
const manifestText = manifestResult;
|
|
347
|
+
const slugs = parseManifestSlugs(manifestText);
|
|
348
|
+
if (slugs.length === 0)
|
|
349
|
+
return output("", "none");
|
|
350
|
+
const knownSlugs = new Set(slugs);
|
|
351
|
+
const promptMatch = promptSlug(prompt, slugs);
|
|
352
|
+
if (promptMatch)
|
|
353
|
+
return output(promptMatch, "prompt");
|
|
354
|
+
const sessionMatch = sessionCompany(root, env, fileSystem);
|
|
355
|
+
if (sessionMatch && knownSlugs.has(sessionMatch))
|
|
356
|
+
return output(sessionMatch, "session");
|
|
357
|
+
if (fleetIdentityPresent(env.HQ_AGENT_ROOT_PREFIX ?? "", env, fileSystem))
|
|
358
|
+
return output("", "none");
|
|
359
|
+
// jq is intentionally no longer needed. The native route is always hosted by
|
|
360
|
+
// Node; the old shell route required `node`, `hq`, and `jq` on PATH.
|
|
361
|
+
if (!commandOnPath("hq", env))
|
|
362
|
+
return output("", "none");
|
|
363
|
+
const hqEnv = { ...EMPTY_ENV, ...env };
|
|
364
|
+
if (dependencies.hqRoot) {
|
|
365
|
+
hqEnv.HQ_ROOT = dependencies.hqRoot;
|
|
366
|
+
hqEnv.CLAUDE_PROJECT_DIR = dependencies.hqRoot;
|
|
367
|
+
}
|
|
368
|
+
const invoke = dependencies.spawnHq ?? runHq;
|
|
369
|
+
const defaultResponse = await invoke(["mesh", "context", "default", "get", "--json"], hqEnv, 2_000, root);
|
|
370
|
+
const defaultSlug = defaultSlugFromJson(defaultResponse.stdout, knownSlugs);
|
|
371
|
+
if (!defaultSlug)
|
|
372
|
+
return output("", "none");
|
|
373
|
+
const pid = dependencies.pid ?? process.pid;
|
|
374
|
+
const random = dependencies.random ?? Math.random;
|
|
375
|
+
const session = `resolve-default-${pid}-${Math.floor(random() * 32768)}`;
|
|
376
|
+
const operation = `resolve-default-op-${pid}-${Math.floor(random() * 32768)}`;
|
|
377
|
+
const observation = JSON.stringify({
|
|
378
|
+
contractVersion: 1,
|
|
379
|
+
clientOperationId: operation,
|
|
380
|
+
identity: { sessionId: session, harness: "hq-core", adapterVersion: "1" },
|
|
381
|
+
cwd: root,
|
|
382
|
+
hqRoot: root,
|
|
383
|
+
}).replaceAll("\u007f", "\\u007f");
|
|
384
|
+
const preflight = await invoke([
|
|
385
|
+
"mesh", "context", "reconcile", "--observation-json", observation, "--machine", "--offline",
|
|
386
|
+
], hqEnv, 2_000, root);
|
|
387
|
+
if (preflight.code === 0 && validPreflight(preflight.stdout, defaultSlug, session, operation)) {
|
|
388
|
+
return output(defaultSlug, "device_default");
|
|
389
|
+
}
|
|
390
|
+
return output("", "none");
|
|
391
|
+
}
|
|
392
|
+
export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandName, _hqRootArg, stdinReader = readStdin) {
|
|
393
|
+
const parsed = parseArgs(args);
|
|
394
|
+
const actualStdin = parsed.havePrompt || process.stdin.isTTY === true
|
|
395
|
+
? stdin
|
|
396
|
+
: stdin || await stdinReader();
|
|
397
|
+
const result = await resolveCompany(args, {
|
|
398
|
+
cwd,
|
|
399
|
+
hqRoot,
|
|
400
|
+
stdin: actualStdin,
|
|
401
|
+
stdinIsTty: process.stdin.isTTY === true,
|
|
402
|
+
});
|
|
403
|
+
process.stdout.write(result.stdout);
|
|
404
|
+
if (result.stderr.length)
|
|
405
|
+
process.stderr.write(result.stderr);
|
|
406
|
+
return result.code;
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=core-resolve-company-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -37,6 +37,7 @@ import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
|
37
37
|
import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
38
38
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
39
39
|
import { registerCoreJobsNativeCommand } from "./core-job-native.js";
|
|
40
|
+
import { registerCoreResolveCompanyNativeCommand } from "./core-resolve-company-command.js";
|
|
40
41
|
import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
|
|
41
42
|
import { registerCoreAuditLogNative } from "./core-audit-log-native.js";
|
|
42
43
|
import { registerCoreGitPackExtensionCheckNative } from "./core-git-pack-extension-check-native.js";
|
|
@@ -218,6 +219,7 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
218
219
|
* direct `hq core` command.
|
|
219
220
|
*/
|
|
220
221
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
222
|
+
{ asset: "core/scripts/resolve-company.sh", root: "live", summary: "Rollback companion for native resolve-company" },
|
|
221
223
|
{ asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
|
|
222
224
|
{ asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
|
|
223
225
|
{ asset: "core/scripts/outpost-jobs-reconcile.sh", root: "live", summary: "Rollback companion for native jobs reconcile" },
|
|
@@ -365,6 +367,7 @@ export function registerCoreCommands(program) {
|
|
|
365
367
|
registerCoreKnowledgePrefsCommand(core);
|
|
366
368
|
registerCorePolicyCommand(core);
|
|
367
369
|
registerCoreJobsNativeCommand(core);
|
|
370
|
+
registerCoreResolveCompanyNativeCommand(core);
|
|
368
371
|
registerCoreAuditLogNative(core);
|
|
369
372
|
registerCoreGitPackExtensionCheckNative(core);
|
|
370
373
|
registerCorePinNative(core);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
import { type TrackerCheckTeam } from "../lib/lanes/tracker-checks.js";
|
|
2
3
|
import { type TrackerKind, type TrackerRecord } from "../lib/lanes/tracker-store.js";
|
|
3
|
-
import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
4
|
+
import type { LaneRecord, LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
4
5
|
export interface LaneChecksDeps {
|
|
5
6
|
hqRoot: () => string;
|
|
6
7
|
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
@@ -14,7 +15,11 @@ export interface ProjectMapping {
|
|
|
14
15
|
team: string;
|
|
15
16
|
leadLaneId?: string;
|
|
16
17
|
statusPath: string;
|
|
18
|
+
storyPrefix?: string;
|
|
19
|
+
excludePrefix?: string;
|
|
17
20
|
}
|
|
21
|
+
export declare function mappingIncludesStory(mapping: ProjectMapping, storyId: string): boolean;
|
|
22
|
+
export declare function ambiguousProjectMappings(mappings: readonly ProjectMapping[]): Map<string, string[]>;
|
|
18
23
|
export interface ProjectReadResult {
|
|
19
24
|
stories: Array<Record<string, unknown>>;
|
|
20
25
|
mtime: number;
|
|
@@ -28,6 +33,7 @@ export interface ProjectReadResult {
|
|
|
28
33
|
export declare function loadProjectMappings(root: string, company: string, requestedProject?: string): Map<string, ProjectMapping>;
|
|
29
34
|
export declare function readProject(root: string, company: string, prdPath: string, tolerantStories?: boolean): ProjectReadResult;
|
|
30
35
|
export declare function loadCompanyQuestionRecords(root: string, company: LaneCompanyRef, projectId?: string): QuestionRecord[];
|
|
36
|
+
export declare function readTeamInputs(root: string, company: LaneCompanyRef, projects: readonly ProjectMapping[], lanes: readonly LaneRecord[]): TrackerCheckTeam[];
|
|
31
37
|
export declare function pathForTrackerRecord(projectDirectory: string, kind: TrackerKind, id: string): string;
|
|
32
38
|
export declare function listTrackerRecords(projectDirectory: string, kind: TrackerKind): TrackerRecord[];
|
|
33
39
|
export interface TolerantTrackerReadIssue {
|
|
@@ -12,6 +12,37 @@ import { DEFAULT_ORPHAN_USER_IDLE_HOURS, findOrphanLanes, parseOrphanMinAge, par
|
|
|
12
12
|
import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
|
|
13
13
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
14
14
|
import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
|
|
15
|
+
export function mappingIncludesStory(mapping, storyId) {
|
|
16
|
+
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
17
|
+
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
18
|
+
}
|
|
19
|
+
function mappingsOverlap(left, right) {
|
|
20
|
+
const includes = [left.storyPrefix, right.storyPrefix].filter((value) => value !== undefined);
|
|
21
|
+
if (includes.length === 2 && !includes[0].startsWith(includes[1]) && !includes[1].startsWith(includes[0]))
|
|
22
|
+
return false;
|
|
23
|
+
const commonPrefix = includes.sort((a, b) => b.length - a.length)[0] ?? "";
|
|
24
|
+
return ![left.excludePrefix, right.excludePrefix].some((excluded) => excluded !== undefined && commonPrefix.startsWith(excluded));
|
|
25
|
+
}
|
|
26
|
+
export function ambiguousProjectMappings(mappings) {
|
|
27
|
+
const byProject = new Map();
|
|
28
|
+
for (const mapping of mappings)
|
|
29
|
+
byProject.set(mapping.projectId, [...(byProject.get(mapping.projectId) ?? []), mapping]);
|
|
30
|
+
const ambiguous = new Map();
|
|
31
|
+
for (const [projectId, rows] of byProject) {
|
|
32
|
+
const conflicts = new Set();
|
|
33
|
+
for (let i = 0; i < rows.length; i++) {
|
|
34
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
35
|
+
if (mappingsOverlap(rows[i], rows[j])) {
|
|
36
|
+
conflicts.add(rows[i].team);
|
|
37
|
+
conflicts.add(rows[j].team);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (conflicts.size > 0)
|
|
42
|
+
ambiguous.set(projectId, [...conflicts].sort());
|
|
43
|
+
}
|
|
44
|
+
return ambiguous;
|
|
45
|
+
}
|
|
15
46
|
function fail(error, message, details = {}) {
|
|
16
47
|
throw new LanesError(error, message, details);
|
|
17
48
|
}
|
|
@@ -165,16 +196,26 @@ export function loadProjectMappings(root, company, requestedProject) {
|
|
|
165
196
|
const team = validSegment(entry.team, "team");
|
|
166
197
|
const leadLaneId = validSegment(entry.lane, "lead lane id");
|
|
167
198
|
const statusPath = checkCompanyPath(root, company, entry.status, registryPath);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
const parsePrefix = (field) => {
|
|
200
|
+
const value = entry[field];
|
|
201
|
+
if (value === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
204
|
+
return fail("tracker_checks_mapping_invalid", `${field} must be a non-empty string`, { path: registryPath, index, field });
|
|
205
|
+
}
|
|
206
|
+
return value.trim();
|
|
207
|
+
};
|
|
208
|
+
const storyPrefix = parsePrefix("story_prefix");
|
|
209
|
+
const excludePrefix = parsePrefix("exclude_prefix");
|
|
210
|
+
const key = `${projectId}:${team}:${index}`;
|
|
211
|
+
mappings.set(key, { projectId, team, leadLaneId, statusPath, ...(storyPrefix === undefined ? {} : { storyPrefix }), ...(excludePrefix === undefined ? {} : { excludePrefix }) });
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
const projectsRoot = path.join(root, "companies", company, "projects");
|
|
174
215
|
const knownProjectIds = new Set(projectDirectories(root, company));
|
|
175
216
|
for (const projectId of knownProjectIds) {
|
|
176
|
-
if (projectHasTrackerRecords(root, company, projectId) && !mappings.
|
|
177
|
-
mappings.set(projectId
|
|
217
|
+
if (projectHasTrackerRecords(root, company, projectId) && ![...mappings.values()].some((mapping) => mapping.projectId === projectId)) {
|
|
218
|
+
mappings.set(`${projectId}:tracker`, {
|
|
178
219
|
projectId,
|
|
179
220
|
team: projectId,
|
|
180
221
|
statusPath: path.join(projectsRoot, projectId, "manager-status.json"),
|
|
@@ -183,10 +224,10 @@ export function loadProjectMappings(root, company, requestedProject) {
|
|
|
183
224
|
}
|
|
184
225
|
if (requestedProject !== undefined) {
|
|
185
226
|
const projectId = validSegment(requestedProject, "project");
|
|
186
|
-
if (!knownProjectIds.has(projectId) && !mappings.
|
|
227
|
+
if (!knownProjectIds.has(projectId) && ![...mappings.values()].some((mapping) => mapping.projectId === projectId)) {
|
|
187
228
|
return fail("tracker_project_not_found", "Project directory was not found for this company", { project_id: projectId, path: path.join(projectsRoot, projectId) });
|
|
188
229
|
}
|
|
189
|
-
return new Map([...mappings].filter(([
|
|
230
|
+
return new Map([...mappings].filter(([, mapping]) => mapping.projectId === projectId));
|
|
190
231
|
}
|
|
191
232
|
return mappings;
|
|
192
233
|
}
|
|
@@ -539,17 +580,18 @@ function readCompanyQuestions(root, company, projectId) {
|
|
|
539
580
|
surfaced_at: surfacedAt.get(question.question_id) ?? null,
|
|
540
581
|
}));
|
|
541
582
|
}
|
|
542
|
-
function readTeamInputs(root, company, projects, lanes) {
|
|
583
|
+
export function readTeamInputs(root, company, projects, lanes) {
|
|
543
584
|
const inputs = [];
|
|
544
585
|
for (const project of projects) {
|
|
545
586
|
const projectPath = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
546
587
|
const prdPath = path.join(projectPath, "prd.json");
|
|
547
588
|
const { stories, mtime } = readProject(root, company.slug, prdPath);
|
|
548
|
-
const teamLanes = lanes.filter((lane) => sameCompanyLane(lane, company) && laneMatchesProject(lane, project.projectId)
|
|
589
|
+
const teamLanes = lanes.filter((lane) => sameCompanyLane(lane, company) && laneMatchesProject(lane, project.projectId)
|
|
590
|
+
&& (lane.lane_id === project.leadLaneId || mappingIncludesStory(project, lane.story_id)));
|
|
549
591
|
const lead = project.leadLaneId === undefined
|
|
550
592
|
? undefined
|
|
551
593
|
: teamLanes.find((lane) => lane.lane_id === project.leadLaneId);
|
|
552
|
-
const openStories = stories.filter((story) => !storyIsDone(story));
|
|
594
|
+
const openStories = stories.filter((story) => mappingIncludesStory(project, String(story.id)) && !storyIsDone(story));
|
|
553
595
|
const openPrRefs = new Set();
|
|
554
596
|
for (const story of openStories) {
|
|
555
597
|
for (const ref of pullRequestReferences(story.prs ?? []))
|
|
@@ -658,7 +700,7 @@ async function readTrackerCheckRecords(root, company, projects) {
|
|
|
658
700
|
const blockers = [];
|
|
659
701
|
const kpis = [];
|
|
660
702
|
const directives = [];
|
|
661
|
-
for (const project of projects) {
|
|
703
|
+
for (const project of [...new Map(projects.map((project) => [project.projectId, project])).values()]) {
|
|
662
704
|
const absoluteDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
663
705
|
const store = createLocalTrackerStore(absoluteDirectory);
|
|
664
706
|
const records = {
|
|
@@ -715,7 +757,7 @@ export async function resolveTrackerCompany(deps, requested) {
|
|
|
715
757
|
fail("tracker_company_unresolved", "The active HQ company could not be resolved", {});
|
|
716
758
|
return company;
|
|
717
759
|
}
|
|
718
|
-
async function checkReport(root, company, projects, directiveProjects, projectFilter, minAgeMinutes, now, deps) {
|
|
760
|
+
async function checkReport(root, company, projects, directiveProjects, projectFilter, minAgeMinutes, now, deps, projectErrors) {
|
|
719
761
|
const selectedProjects = [...projects.values()];
|
|
720
762
|
const laneRecords = readScopedLanes(root, company, selectedProjects);
|
|
721
763
|
const warnings = laneRecords.flatMap((lane) => {
|
|
@@ -773,6 +815,7 @@ async function checkReport(root, company, projects, directiveProjects, projectFi
|
|
|
773
815
|
company: company.slug,
|
|
774
816
|
generated_at: now.toISOString(),
|
|
775
817
|
...(warnings.length === 0 ? {} : { warnings }),
|
|
818
|
+
...(projectErrors.length === 0 ? {} : { project_errors: projectErrors }),
|
|
776
819
|
checks,
|
|
777
820
|
};
|
|
778
821
|
}
|
|
@@ -855,13 +898,20 @@ export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
|
855
898
|
const root = deps.hqRoot();
|
|
856
899
|
const projectFilter = options.project === undefined ? undefined : validSegment(options.project, "project");
|
|
857
900
|
const minAgeMinutes = parseOrphanMinAge(options.minAge);
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
901
|
+
const allProjects = loadProjectMappings(root, company.slug);
|
|
902
|
+
const ambiguous = ambiguousProjectMappings([...allProjects.values()]);
|
|
903
|
+
const selectedAll = projectFilter === undefined ? [...allProjects.values()] : [...allProjects.values()].filter((mapping) => mapping.projectId === projectFilter);
|
|
904
|
+
const projectErrors = [...ambiguous].filter(([projectId]) => projectFilter === undefined || projectId === projectFilter).map(([project_id, teams]) => ({
|
|
905
|
+
project_id,
|
|
906
|
+
code: "tracker_checks_mapping_ambiguous",
|
|
907
|
+
teams,
|
|
908
|
+
message: "Team story prefixes overlap for this project",
|
|
909
|
+
}));
|
|
910
|
+
const projects = new Map(selectedAll.filter((mapping) => !ambiguous.has(mapping.projectId)).map((mapping, index) => [`${mapping.projectId}:${mapping.team}:${index}`, mapping]));
|
|
911
|
+
const directiveProjects = new Map([...allProjects.values()].filter((mapping) => !ambiguous.has(mapping.projectId)).map((mapping, index) => [`${mapping.projectId}:${mapping.team}:${index}`, mapping]));
|
|
912
|
+
const report = await checkReport(root, company, projects, directiveProjects, projectFilter, minAgeMinutes, deps.now(), deps, projectErrors);
|
|
863
913
|
console.log(JSON.stringify(report, null, 2));
|
|
864
|
-
if (report.checks.some((item) => item.result === "failing"))
|
|
914
|
+
if (report.checks.some((item) => item.result === "failing") || projectErrors.length > 0)
|
|
865
915
|
process.exitCode = 2;
|
|
866
916
|
}));
|
|
867
917
|
parent.command("orphans")
|
|
@@ -6,7 +6,7 @@ import { normalizeIsoTimestamp } from "../lib/lanes/timestamp.js";
|
|
|
6
6
|
import { mapSourceStoryStatus } from "../lib/mesh/story-status.js";
|
|
7
7
|
import { buildTrackerOwnerReport, buildTrackerTeamPage, renderTrackerOwnerReportText, renderTrackerTeamPageText, } from "../lib/lanes/tracker-report.js";
|
|
8
8
|
import { FIRST_TRACKER_REPORT_SINCE, readTrackerReportState, writeTrackerReportState, } from "../lib/lanes/tracker-report-state.js";
|
|
9
|
-
import { listTrackerRecordsTolerant, loadCompanyQuestionRecords, loadProjectMappings, pathForTrackerRecord, readProject, resolveTrackerCompany, } from "./lanes-checks.js";
|
|
9
|
+
import { listTrackerRecordsTolerant, loadCompanyQuestionRecords, loadProjectMappings, mappingIncludesStory, pathForTrackerRecord, readProject, resolveTrackerCompany, } from "./lanes-checks.js";
|
|
10
10
|
function fail(error, message, details = {}) {
|
|
11
11
|
throw new LanesError(error, message, details);
|
|
12
12
|
}
|
|
@@ -184,6 +184,8 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
184
184
|
errors.push({ path: prdPath, field: "id", message: "Story id must be a non-empty string" });
|
|
185
185
|
continue;
|
|
186
186
|
}
|
|
187
|
+
if (!mappingIncludesStory(project, id))
|
|
188
|
+
continue;
|
|
187
189
|
const title = typeof story.title === "string" ? story.title : "";
|
|
188
190
|
if (!title.trim()) {
|
|
189
191
|
errors.push({ path: prdPath, field: "title", story_id: id, message: "Story title must be a non-empty string" });
|
|
@@ -217,7 +219,8 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
217
219
|
stories.push({ id, title, status, references: refs.references, ...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}), source_path: prdPath });
|
|
218
220
|
}
|
|
219
221
|
const teamQuestions = [];
|
|
220
|
-
for (const record of questions.filter((question) => question.project_id === project.projectId
|
|
222
|
+
for (const record of questions.filter((question) => question.project_id === project.projectId
|
|
223
|
+
&& (typeof question.story_id !== "string" || mappingIncludesStory(project, question.story_id)))) {
|
|
221
224
|
const sourcePath = questionRecordPath(root, record.question_id);
|
|
222
225
|
try {
|
|
223
226
|
const body = deps.readQuestionBody(root, record);
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -144,6 +144,18 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
144
144
|
shellCwd: "caller",
|
|
145
145
|
shellHqRoot: name === "reconcile" ? "reconcile-default" : "none",
|
|
146
146
|
})),
|
|
147
|
+
{
|
|
148
|
+
name: "resolve-company",
|
|
149
|
+
kind: "native",
|
|
150
|
+
modulePath: "./commands/core-resolve-company-native.js",
|
|
151
|
+
exportName: "runResolveCompanyFast",
|
|
152
|
+
flagKey: "hq-core-native-fast-route",
|
|
153
|
+
familyFlagKey: "core-native-utilities",
|
|
154
|
+
asset: "core/scripts/resolve-company.sh",
|
|
155
|
+
root: "live",
|
|
156
|
+
shellCwd: "live",
|
|
157
|
+
shellHqRoot: "live",
|
|
158
|
+
},
|
|
147
159
|
...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
|
|
148
160
|
name: `delegate ${name}`,
|
|
149
161
|
kind: "native",
|