@indigoai-us/hq-cli 5.345.42 → 5.345.44
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 +18 -0
- package/dist/command-catalog.generated.d.ts +2 -2
- package/dist/command-catalog.generated.js +2 -2
- package/dist/commands/db-remote-deps.js +4 -0
- package/dist/commands/install-global.d.ts +8 -1
- package/dist/commands/install-global.js +128 -25
- package/dist/commands/lanes-checks.d.ts +1 -0
- package/dist/commands/lanes-checks.js +6 -1
- package/dist/commands/lanes-message.js +1 -0
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +63 -6
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +218 -49
- package/dist/index.js +43 -7
- package/dist/lib/anywhere-person-setting.d.ts +2 -0
- package/dist/lib/anywhere-person-setting.js +2 -1
- package/dist/lib/anywhere-runtime-fast-flag.js +8 -2
- package/dist/lib/core-utils/sentry-report.js +8 -1
- package/dist/lib/daemon/anywhere-listener.d.ts +16 -0
- package/dist/lib/daemon/anywhere-listener.js +79 -0
- package/dist/lib/daemon/run.js +30 -23
- package/dist/lib/db/control-plane.d.ts +5 -0
- package/dist/lib/db/control-plane.js +7 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +10 -0
- package/dist/lib/doctor/checks/anywhere.js +45 -17
- package/dist/lib/flag-registry.d.ts +4 -0
- package/dist/lib/flag-registry.js +43 -2
- package/dist/lib/flag-snapshot-file.d.ts +17 -0
- package/dist/lib/flag-snapshot-file.js +153 -37
- package/dist/lib/lanes/flag-keys.d.ts +5 -0
- package/dist/lib/lanes/flag-keys.js +5 -0
- package/dist/lib/lanes/grants.js +1 -1
- package/dist/lib/lanes/reconcile.d.ts +2 -0
- package/dist/lib/lanes/reconcile.js +39 -1
- package/dist/lib/lanes/spawn.d.ts +4 -0
- package/dist/lib/lanes/spawn.js +132 -6
- package/dist/lib/lanes/store.js +35 -0
- package/dist/lib/lanes/tracker-report.d.ts +46 -0
- package/dist/lib/lanes/tracker-report.js +85 -1
- package/dist/lib/lanes/types.d.ts +3 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/main.js +5 -1
- package/dist/startup-registration.d.ts +3 -0
- package/dist/startup-registration.js +11 -4
- package/dist/utils/resident-package-preload.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.44] — 2026-10-07
|
|
6
|
+
- Quota-exhausted lanes can fail over within their provider, hold with per-account eligibility reasons when none is available, and retry when the earliest quota reset arrives. The default-off `lanes.quota-account-failover` gate controls the new recovery path.
|
|
7
|
+
|
|
8
|
+
- When a company's plan does not include remote databases, `hq db provision`, `usage`, `sql`, `dump` and `destroy` now show which plan includes them, the $10 per database app price and the upgrade link.
|
|
9
|
+
- `hq lanes report` can show a gated focus list, focus-guard override reasons, and per-team 24-hour focus share.
|
|
10
|
+
- Return a child's hire grant to its senior when the lane becomes terminal, let the senior adjust or clear the saved grant, report grant updates correctly, and reject over-cap increases while allowing reductions that repair oversubscription.
|
|
11
|
+
- Recover CLI startup when a package update temporarily removes a module from the installed command graph.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [5.345.43] — 2026-10-07
|
|
15
|
+
- The published core-command E2E workflow now retries the exact npm release install for up to 10 minutes while registry propagation completes.
|
|
16
|
+
|
|
17
|
+
- HQ Anywhere resolves person-level flags in unlinked folders. The daemon refreshes the flag and person setting every 60 seconds, then opens or closes its socket when either changes.
|
|
18
|
+
- Claude and Codex global installs use one skill prefix, keep company skills out of global scope, remove old company links during upgrades, and skip broken sources. Dry runs use `~/.hq/root` when run outside a checkout.
|
|
19
|
+
- Doctor checks for a live desktop-hosted daemon instead of treating the sync LaunchAgent as proof. Registry refreshes retain the daemon's machine scope and ID token.
|
|
20
|
+
- Let a failed lane's ancestors or same-company manager resume it, and backfill missing company attribution when reparenting it to a company senior.
|
|
21
|
+
- Let Claude lanes fall back to eligible accounts when every profile-pinned account is unavailable, behind the default-off `lanes.profile-pin-exhausted-fallback` flag.
|
|
22
|
+
|
|
5
23
|
## [5.345.42] — 2026-10-07
|
|
6
24
|
- `hq db usage --app <name>` shows one database app's included amounts, use, overage, read-only ceiling and estimated monthly charge.
|
|
7
25
|
|
|
@@ -12464,7 +12464,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12464
12464
|
readonly subcommands: readonly [];
|
|
12465
12465
|
}, {
|
|
12466
12466
|
readonly name: "resume";
|
|
12467
|
-
readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with
|
|
12467
|
+
readonly description: "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager";
|
|
12468
12468
|
readonly aliases: readonly [];
|
|
12469
12469
|
readonly hidden: false;
|
|
12470
12470
|
readonly helpOption: true;
|
|
@@ -12483,7 +12483,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
12483
12483
|
readonly hidden: false;
|
|
12484
12484
|
}, {
|
|
12485
12485
|
readonly flags: "--from-failed";
|
|
12486
|
-
readonly description: "Explicitly resume a failed lane as
|
|
12486
|
+
readonly description: "Explicitly resume a failed lane as an ancestor or same-company manager";
|
|
12487
12487
|
readonly hidden: false;
|
|
12488
12488
|
}, {
|
|
12489
12489
|
readonly flags: "--reason <text>";
|
|
@@ -15156,7 +15156,7 @@ export const COMMAND_CATALOG = [
|
|
|
15156
15156
|
},
|
|
15157
15157
|
{
|
|
15158
15158
|
"name": "resume",
|
|
15159
|
-
"description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with
|
|
15159
|
+
"description": "Resume an interrupted, done or blocked lane, or an awaiting_input lane with no answerable question; recover failed lanes with --from-failed as a senior ancestor or same-company manager",
|
|
15160
15160
|
"aliases": [],
|
|
15161
15161
|
"hidden": false,
|
|
15162
15162
|
"helpOption": true,
|
|
@@ -15179,7 +15179,7 @@ export const COMMAND_CATALOG = [
|
|
|
15179
15179
|
},
|
|
15180
15180
|
{
|
|
15181
15181
|
"flags": "--from-failed",
|
|
15182
|
-
"description": "Explicitly resume a failed lane as
|
|
15182
|
+
"description": "Explicitly resume a failed lane as an ancestor or same-company manager",
|
|
15183
15183
|
"hidden": false
|
|
15184
15184
|
},
|
|
15185
15185
|
{
|
|
@@ -42,6 +42,10 @@ export function describeRemoteError(error) {
|
|
|
42
42
|
const status = error.status;
|
|
43
43
|
const code = error.code;
|
|
44
44
|
if (code === "REMOTE_DB_NOT_AVAILABLE" || status === 501) {
|
|
45
|
+
const reason = error.reason;
|
|
46
|
+
const message = error.message;
|
|
47
|
+
if (reason === "plan" && typeof message === "string" && message)
|
|
48
|
+
return message;
|
|
45
49
|
return "Remote databases are not available yet.";
|
|
46
50
|
}
|
|
47
51
|
if (status === 402 || code === "PLAN_REQUIRED") {
|
|
@@ -75,8 +75,15 @@ export interface GlobalCommandOpts {
|
|
|
75
75
|
log?: (line: string) => void;
|
|
76
76
|
/** Test seam for the hq-pro person preference read. */
|
|
77
77
|
personSettingReader?: () => Promise<boolean | null>;
|
|
78
|
+
/** Root-discovery seams used by command tests and wrappers. */
|
|
79
|
+
home?: string;
|
|
80
|
+
cwd?: string;
|
|
78
81
|
}
|
|
79
|
-
export declare function resolveInstallRoot(explicit: string | undefined
|
|
82
|
+
export declare function resolveInstallRoot(explicit: string | undefined, options?: {
|
|
83
|
+
env?: NodeJS.ProcessEnv;
|
|
84
|
+
cwd?: string;
|
|
85
|
+
home?: string;
|
|
86
|
+
}): string;
|
|
80
87
|
/** Entry used by `hq install --global` (wired from pkg-install.ts). */
|
|
81
88
|
export declare function runInstallGlobal(opts: GlobalCommandOpts): Promise<void>;
|
|
82
89
|
export declare function registerUninstallCommand(program: Command): void;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* ~/.hq/anywhere/claude-install.json so uninstall can restore files byte-for-byte.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import { spawnSync } from 'node:child_process';
|
|
14
15
|
import { buildCodexServerDef, claudeConfigFormat, codexConfigFormat, codexRemovalFormat, isCodexInstalled, jsonFormat, mergeCodexServer, removeCodexServer, registerCodexServer, unregisterCodexServer, mergeClaudeServer, readConfigDoc, realpathOrSelf, registerClaudeServer, resolveEnv, unregisterClaudeServer, writeConfigAtomic, } from './mcp-registration.js';
|
|
@@ -20,6 +21,8 @@ import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js'
|
|
|
20
21
|
import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
|
|
21
22
|
import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
|
|
22
23
|
import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
|
|
24
|
+
import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
|
|
25
|
+
import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
|
|
23
26
|
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
24
27
|
import { readDaemonConfig } from '../lib/daemon/config.js';
|
|
25
28
|
import { daemonPaths } from '../lib/daemon/paths.js';
|
|
@@ -201,9 +204,13 @@ function mcpManifest(opts) {
|
|
|
201
204
|
}
|
|
202
205
|
return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
|
|
203
206
|
}
|
|
207
|
+
function skillLinkName(name) {
|
|
208
|
+
return name.startsWith(SKILL_LINK_PREFIX) ? name : `${SKILL_LINK_PREFIX}${name}`;
|
|
209
|
+
}
|
|
204
210
|
function planSkills(hqRoot, t) {
|
|
205
211
|
const source = hqSkillsSource(hqRoot);
|
|
206
|
-
const plan = { link: [], already: [], skipped: [] };
|
|
212
|
+
const plan = { link: [], already: [], skipped: [], unresolved: [], companyScoped: [] };
|
|
213
|
+
const companySlugs = new Set(Object.keys(readCompaniesManifestMap(hqRoot) ?? {}));
|
|
207
214
|
let names;
|
|
208
215
|
try {
|
|
209
216
|
names = fs
|
|
@@ -218,8 +225,23 @@ function planSkills(hqRoot, t) {
|
|
|
218
225
|
throw e;
|
|
219
226
|
}
|
|
220
227
|
for (const name of names) {
|
|
221
|
-
const
|
|
228
|
+
const namespace = name.includes(':') ? name.slice(0, name.indexOf(':')) : '';
|
|
229
|
+
if (namespace && companySlugs.has(namespace)) {
|
|
230
|
+
plan.companyScoped.push(name);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
222
233
|
const target = path.join(source, name);
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.statSync(target).isDirectory()) {
|
|
236
|
+
plan.unresolved.push(name);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
plan.unresolved.push(name);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const link = path.join(t.skillsDir, skillLinkName(name));
|
|
223
245
|
let st = null;
|
|
224
246
|
try {
|
|
225
247
|
st = fs.lstatSync(link);
|
|
@@ -237,6 +259,15 @@ function planSkills(hqRoot, t) {
|
|
|
237
259
|
}
|
|
238
260
|
return plan;
|
|
239
261
|
}
|
|
262
|
+
function reportSkippedSkills(plan, log, dryRun) {
|
|
263
|
+
const prefix = dryRun ? 'Would skip' : 'Skipped';
|
|
264
|
+
if (plan.unresolved.length > 0) {
|
|
265
|
+
log(`${prefix} skill source(s) that do not resolve to directories: ${plan.unresolved.join(', ')}`);
|
|
266
|
+
}
|
|
267
|
+
if (plan.companyScoped.length > 0) {
|
|
268
|
+
log(`${prefix} company-scoped skill(s) at global scope: ${plan.companyScoped.join(', ')}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
240
271
|
function hqSkillLinksForRoot(skillsDir, hqRoot) {
|
|
241
272
|
let names;
|
|
242
273
|
try {
|
|
@@ -261,6 +292,27 @@ function hqSkillLinksForRoot(skillsDir, hqRoot) {
|
|
|
261
292
|
}
|
|
262
293
|
}).map((name) => path.join(skillsDir, name));
|
|
263
294
|
}
|
|
295
|
+
function hqCompanySkillLinksForRoot(skillsDir, hqRoot) {
|
|
296
|
+
const companySlugs = new Set(Object.keys(readCompaniesManifestMap(hqRoot) ?? {}));
|
|
297
|
+
if (companySlugs.size === 0)
|
|
298
|
+
return [];
|
|
299
|
+
const source = hqSkillsSource(hqRoot);
|
|
300
|
+
return hqSkillLinksForRoot(skillsDir, hqRoot).filter((link) => {
|
|
301
|
+
try {
|
|
302
|
+
const target = path.resolve(skillsDir, fs.readlinkSync(link));
|
|
303
|
+
if (path.dirname(target) !== source)
|
|
304
|
+
return false;
|
|
305
|
+
const sourceName = path.basename(target);
|
|
306
|
+
const separator = sourceName.indexOf(':');
|
|
307
|
+
return separator > 0 && companySlugs.has(sourceName.slice(0, separator));
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error.code === 'ENOENT')
|
|
311
|
+
return false;
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
264
316
|
function removeSeeded(seeded, regFile) {
|
|
265
317
|
if (seeded.length === 0 || !fs.existsSync(regFile))
|
|
266
318
|
return;
|
|
@@ -320,7 +372,13 @@ export function installGlobalClaude(opts) {
|
|
|
320
372
|
: mdOld === null ? null : (removeClaudeMdBlock(mdOld) || null))
|
|
321
373
|
: prior ? prior.claudeMd.original : mdOld;
|
|
322
374
|
const mdNew = pluginSource ? mdOriginal : addClaudeMdBlock(mdOld, hqRoot);
|
|
323
|
-
const skills = pluginSource ? {
|
|
375
|
+
const skills = pluginSource ? {
|
|
376
|
+
link: [],
|
|
377
|
+
already: [],
|
|
378
|
+
skipped: [],
|
|
379
|
+
unresolved: [],
|
|
380
|
+
companyScoped: [],
|
|
381
|
+
} : planSkills(hqRoot, t);
|
|
324
382
|
const settingsOld = readOrNull(t.settingsJson);
|
|
325
383
|
const settingsDoc = readConfigDoc(t.settingsJson, jsonFormat).doc;
|
|
326
384
|
const directHooksRemoved = pluginSource
|
|
@@ -347,7 +405,9 @@ export function installGlobalClaude(opts) {
|
|
|
347
405
|
const settingsNext = pluginSource
|
|
348
406
|
? cleanupRoots.reduce((doc, root) => removeHqHooks(doc, root), settingsDoc)
|
|
349
407
|
: addHqHooks(settingsDoc, hqRoot, hookTarget);
|
|
350
|
-
const skillLinksToRemove =
|
|
408
|
+
const skillLinksToRemove = [...new Set(pluginSource
|
|
409
|
+
? cleanupRoots.flatMap((root) => hqSkillLinksForRoot(t.skillsDir, root))
|
|
410
|
+
: cleanupRoots.flatMap((root) => hqCompanySkillLinksForRoot(t.skillsDir, root)))];
|
|
351
411
|
const claudeDoc = readConfigDoc(t.claudeJson, claudeConfigFormat).doc;
|
|
352
412
|
// Collision check runs here too, so a dry run surfaces a conflicting "hq" server.
|
|
353
413
|
const claudeNext = pluginSource ? claudeDoc : mergeClaudeServer(HQ_MCP_SERVER_NAME, {
|
|
@@ -359,7 +419,7 @@ export function installGlobalClaude(opts) {
|
|
|
359
419
|
if (mdOld !== mdNew)
|
|
360
420
|
touched.push(t.claudeMd);
|
|
361
421
|
for (const s of skills.link)
|
|
362
|
-
touched.push(path.join(t.skillsDir,
|
|
422
|
+
touched.push(path.join(t.skillsDir, skillLinkName(s)));
|
|
363
423
|
touched.push(...skillLinksToRemove);
|
|
364
424
|
if (settingsNext !== settingsDoc)
|
|
365
425
|
touched.push(t.settingsJson);
|
|
@@ -374,8 +434,12 @@ export function installGlobalClaude(opts) {
|
|
|
374
434
|
for (const f of touched)
|
|
375
435
|
log(` ${f}`);
|
|
376
436
|
if (skills.skipped.length > 0) {
|
|
377
|
-
log(`Would skip ${skills.skipped.length} skill(s) whose
|
|
437
|
+
log(`Would skip ${skills.skipped.length} skill(s) whose target already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
|
|
378
438
|
}
|
|
439
|
+
if (!pluginSource && skillLinksToRemove.length > 0) {
|
|
440
|
+
log(`Would remove ${skillLinksToRemove.length} previous company-scoped skill link(s)`);
|
|
441
|
+
}
|
|
442
|
+
reportSkippedSkills(skills, log, true);
|
|
379
443
|
log(`\n--- ${t.settingsJson}`);
|
|
380
444
|
log(lineDiff(jsonFormat.serialize(settingsDoc), jsonFormat.serialize(settingsNext)));
|
|
381
445
|
log(`\n--- ${t.claudeJson}`);
|
|
@@ -429,11 +493,10 @@ export function installGlobalClaude(opts) {
|
|
|
429
493
|
if (!pluginSource)
|
|
430
494
|
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
431
495
|
for (const name of skills.link) {
|
|
432
|
-
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir,
|
|
496
|
+
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
|
|
433
497
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
fs.unlinkSync(link);
|
|
498
|
+
for (const link of skillLinksToRemove)
|
|
499
|
+
fs.unlinkSync(link);
|
|
437
500
|
if (pluginSource) {
|
|
438
501
|
// Migrate a prior direct install: remove only our old hook/MCP entries while
|
|
439
502
|
// retaining the original user settings for a later uninstall restore.
|
|
@@ -498,6 +561,7 @@ export function installGlobalClaude(opts) {
|
|
|
498
561
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
499
562
|
log(`HQ installed for Claude Code from ${hqRoot}.`);
|
|
500
563
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
564
|
+
reportSkippedSkills(skills, log, false);
|
|
501
565
|
if (pluginSource)
|
|
502
566
|
log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
|
|
503
567
|
else {
|
|
@@ -691,6 +755,7 @@ export function installGlobalCodex(opts) {
|
|
|
691
755
|
const charter = fs.readFileSync(codexCharterSource(hqRoot), 'utf-8');
|
|
692
756
|
const mdOld = readOrNull(t.agentsMd);
|
|
693
757
|
const mdNew = addAgentsMdBlock(mdOld, charter);
|
|
758
|
+
const cleanupRoots = [...new Set([hqRoot, ...(prior?.hqRoot ? [prior.hqRoot] : [])])];
|
|
694
759
|
const legacy = classifyLegacySkills(t.skillsDir, hqRoot);
|
|
695
760
|
const legacyPath = path.join(t.skillsDir, LEGACY_SKILLS_NAME);
|
|
696
761
|
const legacyBackupPath = path.join(t.skillsDir, LEGACY_BACKUP_NAME);
|
|
@@ -698,6 +763,7 @@ export function installGlobalCodex(opts) {
|
|
|
698
763
|
throw new Error(`${legacyBackupPath} already exists; move it away before installing.`);
|
|
699
764
|
}
|
|
700
765
|
const skills = planSkills(hqRoot, t);
|
|
766
|
+
const companySkillLinksToRemove = [...new Set(cleanupRoots.flatMap((root) => hqCompanySkillLinksForRoot(t.skillsDir, root)))];
|
|
701
767
|
const hooksOld = readOrNull(t.hooksJson);
|
|
702
768
|
const hooksDoc = readConfigDoc(t.hooksJson, jsonFormat).doc;
|
|
703
769
|
const hookTarget = hookTargetFor(hqRoot, home, opts.daemonInstalled, env);
|
|
@@ -719,7 +785,8 @@ export function installGlobalCodex(opts) {
|
|
|
719
785
|
if (legacy.kind === 'hq-symlink' || legacy.kind === 'hq-dir')
|
|
720
786
|
touched.push(path.join(t.skillsDir, LEGACY_SKILLS_NAME));
|
|
721
787
|
for (const s of skills.link)
|
|
722
|
-
touched.push(path.join(t.skillsDir,
|
|
788
|
+
touched.push(path.join(t.skillsDir, skillLinkName(s)));
|
|
789
|
+
touched.push(...companySkillLinksToRemove);
|
|
723
790
|
if (hooksNext !== hooksDoc)
|
|
724
791
|
touched.push(t.hooksJson);
|
|
725
792
|
if (tomlNext !== tomlDoc || legacyPackServerPresent)
|
|
@@ -736,8 +803,12 @@ export function installGlobalCodex(opts) {
|
|
|
736
803
|
else if (legacy.kind === 'foreign')
|
|
737
804
|
log(legacyWarning(t.skillsDir));
|
|
738
805
|
if (skills.skipped.length > 0) {
|
|
739
|
-
log(`Would skip ${skills.skipped.length} skill(s) whose
|
|
806
|
+
log(`Would skip ${skills.skipped.length} skill(s) whose target already exists and is not an HQ link: ${skills.skipped.join(', ')}`);
|
|
807
|
+
}
|
|
808
|
+
if (companySkillLinksToRemove.length > 0) {
|
|
809
|
+
log(`Would remove ${companySkillLinksToRemove.length} previous company-scoped skill link(s)`);
|
|
740
810
|
}
|
|
811
|
+
reportSkippedSkills(skills, log, true);
|
|
741
812
|
log(`\n--- ${t.hooksJson}`);
|
|
742
813
|
log(lineDiff(jsonFormat.serialize(hooksDoc), jsonFormat.serialize(hooksNext)));
|
|
743
814
|
log(`\n--- ${t.configToml}`);
|
|
@@ -761,8 +832,10 @@ export function installGlobalCodex(opts) {
|
|
|
761
832
|
log(legacyWarning(t.skillsDir));
|
|
762
833
|
fs.mkdirSync(t.skillsDir, { recursive: true });
|
|
763
834
|
for (const name of skills.link) {
|
|
764
|
-
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir,
|
|
835
|
+
fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
|
|
765
836
|
}
|
|
837
|
+
for (const link of companySkillLinksToRemove)
|
|
838
|
+
fs.unlinkSync(link);
|
|
766
839
|
writeConfigAtomic({
|
|
767
840
|
target: t.hooksJson,
|
|
768
841
|
pack: HQ_ANYWHERE_PACK,
|
|
@@ -803,6 +876,7 @@ export function installGlobalCodex(opts) {
|
|
|
803
876
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
804
877
|
log(`HQ installed for Codex from ${hqRoot}.`);
|
|
805
878
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
879
|
+
reportSkippedSkills(skills, log, false);
|
|
806
880
|
log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
|
|
807
881
|
log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
|
|
808
882
|
return touched;
|
|
@@ -946,34 +1020,63 @@ function assertRuntime(runtime) {
|
|
|
946
1020
|
}
|
|
947
1021
|
return r;
|
|
948
1022
|
}
|
|
949
|
-
export function resolveInstallRoot(explicit) {
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
if (
|
|
1023
|
+
export function resolveInstallRoot(explicit, options = {}) {
|
|
1024
|
+
const env = options.env ?? process.env;
|
|
1025
|
+
const fromFlag = explicit?.trim() || env.HQ_ROOT?.trim();
|
|
1026
|
+
if (fromFlag)
|
|
1027
|
+
return path.resolve(fromFlag);
|
|
1028
|
+
const cwdRoot = resolveHqRoot(options.cwd ?? process.cwd());
|
|
1029
|
+
if (cwdRoot)
|
|
1030
|
+
return cwdRoot;
|
|
1031
|
+
const home = options.home?.trim() || env.HOME?.trim() || os.homedir();
|
|
1032
|
+
try {
|
|
1033
|
+
const pointer = fs.readFileSync(path.join(home, '.hq', 'root'), 'utf8').trim();
|
|
1034
|
+
if (pointer) {
|
|
1035
|
+
const pointedRoot = resolveHqRoot(pointer);
|
|
1036
|
+
if (pointedRoot)
|
|
1037
|
+
return pointedRoot;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
// Fall through to the regular installer/config root discovery.
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const rootEnv = { ...env };
|
|
1045
|
+
delete rootEnv.HQ_ROOT;
|
|
1046
|
+
return resolveDefaultHqRoot({
|
|
1047
|
+
onMissing: 'throw',
|
|
1048
|
+
env: rootEnv,
|
|
1049
|
+
cwd: options.cwd,
|
|
1050
|
+
home,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
953
1054
|
throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
|
|
954
1055
|
}
|
|
955
|
-
return root;
|
|
956
1056
|
}
|
|
957
1057
|
/** Entry used by `hq install --global` (wired from pkg-install.ts). */
|
|
958
1058
|
export async function runInstallGlobal(opts) {
|
|
959
1059
|
const runtime = assertRuntime(opts.runtime);
|
|
960
1060
|
const env = opts.env ?? process.env;
|
|
961
|
-
if (runtime === 'chatgpt' || runtime === 'grok') {
|
|
1061
|
+
if (runtime === 'chatgpt' || runtime === 'grok' || opts.dryRun) {
|
|
962
1062
|
const enabled = await anywhereRuntimeAllowed(env, {
|
|
963
1063
|
...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
|
|
964
1064
|
onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
|
|
965
1065
|
});
|
|
966
1066
|
if (!enabled)
|
|
967
1067
|
throw new AnywhereRuntimeDisabledError();
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
(
|
|
972
|
-
|
|
1068
|
+
if (runtime === 'chatgpt' || runtime === 'grok') {
|
|
1069
|
+
const endpoint = resolveHqMcpEndpoint(env).toString();
|
|
1070
|
+
const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
|
|
1071
|
+
for (const line of lines)
|
|
1072
|
+
(opts.log ?? console.log)(line);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
973
1075
|
}
|
|
974
1076
|
const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
|
|
975
1077
|
install({
|
|
976
|
-
hqRoot: resolveInstallRoot(opts.hqRoot),
|
|
1078
|
+
hqRoot: resolveInstallRoot(opts.hqRoot, { env, cwd: opts.cwd, home: opts.home }),
|
|
1079
|
+
home: opts.home,
|
|
977
1080
|
dryRun: opts.dryRun,
|
|
978
1081
|
via: opts.via,
|
|
979
1082
|
marketplaceDir: opts.marketplaceDir,
|
|
@@ -22,6 +22,7 @@ export declare function mappingIncludesStory(mapping: ProjectMapping, storyId: s
|
|
|
22
22
|
export interface ProjectReadResult {
|
|
23
23
|
stories: Array<Record<string, unknown>>;
|
|
24
24
|
mtime: number;
|
|
25
|
+
focus_overrides?: unknown;
|
|
25
26
|
errors?: Array<{
|
|
26
27
|
path: string;
|
|
27
28
|
field: string;
|
|
@@ -272,7 +272,12 @@ export function readProject(root, company, prdPath, tolerantStories = false) {
|
|
|
272
272
|
return [story];
|
|
273
273
|
});
|
|
274
274
|
try {
|
|
275
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
stories,
|
|
277
|
+
mtime: fs.statSync(prdPath).mtimeMs,
|
|
278
|
+
...(parsed.focus_overrides === undefined ? {} : { focus_overrides: parsed.focus_overrides }),
|
|
279
|
+
...(tolerantStories ? { errors } : {}),
|
|
280
|
+
};
|
|
276
281
|
}
|
|
277
282
|
catch (err) {
|
|
278
283
|
return fail("tracker_checks_input_unreadable", `Project task list could not be stat'ed: ${err instanceof Error ? err.message : String(err)}`, { path: prdPath });
|
|
@@ -738,6 +738,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
738
738
|
...(automatic.preservedModel !== undefined ? { preservedModel: automatic.preservedModel } : {}),
|
|
739
739
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
740
740
|
...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
|
|
741
|
+
...(automatic.historyReason === "profile_pin_exhausted" ? { preserveAdmissionAccounts: true } : {}),
|
|
741
742
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
742
743
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|
|
743
744
|
pending: {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type QuestionBody } from "../lib/lanes/questions.js";
|
|
3
|
-
import type { QuestionRecord } from "../lib/lanes/types.js";
|
|
3
|
+
import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
4
4
|
import { type LaneChecksDeps } from "./lanes-checks.js";
|
|
5
5
|
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
6
6
|
export interface LaneReportsDeps extends LaneChecksDeps {
|
|
7
7
|
readQuestionBody: (root: string, question: QuestionRecord) => QuestionBody;
|
|
8
|
+
focusDashboardEnabled: (company: LaneCompanyRef) => Promise<boolean>;
|
|
8
9
|
}
|
|
9
10
|
export declare function registerLaneReportCommands(parent: Command, deps: LaneReportsDeps, wrap: WrapHandler): void;
|
|
10
11
|
export {};
|
|
@@ -163,7 +163,7 @@ function issueFromError(err, fallbackPath, fallbackField, ids = {}) {
|
|
|
163
163
|
...(typeof details.question_id === "string" ? { question_id: details.question_id } : ids.question_id ? { question_id: ids.question_id } : {}),
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
|
-
async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
166
|
+
async function teamInput(root, company, project, questions, deps, errors = [], includeFocusDashboard = false) {
|
|
167
167
|
const projectDirectory = path.join(root, "companies", company.slug, "projects", project.projectId);
|
|
168
168
|
const prdPath = path.join(projectDirectory, "prd.json");
|
|
169
169
|
let prd = { stories: [], mtime: 0 };
|
|
@@ -213,10 +213,64 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
213
213
|
}
|
|
214
214
|
normalizedUpdatedAt = normalized;
|
|
215
215
|
}
|
|
216
|
+
const ownerValue = includeFocusDashboard ? story.owner ?? story.assignee : undefined;
|
|
217
|
+
let owner = null;
|
|
218
|
+
if (ownerValue !== undefined && ownerValue !== null) {
|
|
219
|
+
if (typeof ownerValue === "string" && ownerValue.trim())
|
|
220
|
+
owner = ownerValue.trim();
|
|
221
|
+
else
|
|
222
|
+
errors.push({ path: prdPath, field: "owner", story_id: id, message: "Story owner must be a non-empty string when present" });
|
|
223
|
+
}
|
|
224
|
+
const deadlineValue = includeFocusDashboard ? story.deadline ?? story.due_at ?? story.due_date : undefined;
|
|
225
|
+
let deadline = null;
|
|
226
|
+
if (deadlineValue !== undefined && deadlineValue !== null) {
|
|
227
|
+
if (typeof deadlineValue === "string" && deadlineValue.trim())
|
|
228
|
+
deadline = deadlineValue.trim();
|
|
229
|
+
else
|
|
230
|
+
errors.push({ path: prdPath, field: "deadline", story_id: id, message: "Story deadline must be a non-empty string when present" });
|
|
231
|
+
}
|
|
216
232
|
const refs = storyReferences(story, prdPath, id, errors);
|
|
217
233
|
pullRequests.push(...refs.pullRequests);
|
|
218
234
|
releases.push(...refs.releases);
|
|
219
|
-
stories.push({
|
|
235
|
+
stories.push({
|
|
236
|
+
id,
|
|
237
|
+
title,
|
|
238
|
+
status,
|
|
239
|
+
focus_id: story.focus_id,
|
|
240
|
+
kpi: story.kpi,
|
|
241
|
+
owner,
|
|
242
|
+
deadline,
|
|
243
|
+
references: refs.references,
|
|
244
|
+
...(normalizedUpdatedAt ? { updated_at: normalizedUpdatedAt } : {}),
|
|
245
|
+
source_path: prdPath,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const focusOverrides = [];
|
|
249
|
+
if (includeFocusDashboard && prd.focus_overrides !== undefined) {
|
|
250
|
+
if (!Array.isArray(prd.focus_overrides)) {
|
|
251
|
+
errors.push({ path: prdPath, field: "focus_overrides", message: "Project focus_overrides must be an array" });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
for (const [index, raw] of prd.focus_overrides.entries()) {
|
|
255
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
256
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override must be an object" });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const record = raw;
|
|
260
|
+
if (["reason", "by_lane", "at", "story"].some((field) => typeof record[field] !== "string" || !record[field].trim())) {
|
|
261
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}]`, message: "Focus override needs non-empty reason, by_lane, at, and story fields" });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const at = normalizeIsoTimestamp(record.at);
|
|
265
|
+
if (at === null) {
|
|
266
|
+
errors.push({ path: prdPath, field: `focus_overrides[${index}].at`, message: "Focus override at must be a valid timestamp" });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const override = { reason: record.reason, by_lane: record.by_lane, at, story: record.story };
|
|
270
|
+
if (mappingIncludesStory(project, override.story))
|
|
271
|
+
focusOverrides.push(override);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
220
274
|
}
|
|
221
275
|
const teamQuestions = [];
|
|
222
276
|
for (const record of questions.filter((question) => question.project_id === project.projectId
|
|
@@ -236,6 +290,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
236
290
|
team: project.team,
|
|
237
291
|
project_id: project.projectId,
|
|
238
292
|
stories,
|
|
293
|
+
focus_overrides: focusOverrides,
|
|
239
294
|
pull_requests: pullRequests,
|
|
240
295
|
releases,
|
|
241
296
|
blockers: await trackerRecords(root, company, project, "blocker", errors),
|
|
@@ -246,7 +301,7 @@ async function teamInput(root, company, project, questions, deps, errors = []) {
|
|
|
246
301
|
errors,
|
|
247
302
|
};
|
|
248
303
|
}
|
|
249
|
-
async function allTeamInputs(root, company, deps) {
|
|
304
|
+
async function allTeamInputs(root, company, deps, includeFocusDashboard) {
|
|
250
305
|
const mappings = loadProjectMappings(root, company.slug);
|
|
251
306
|
const teams = [];
|
|
252
307
|
for (const project of [...mappings.values()].sort((a, b) => a.team.localeCompare(b.team) || a.projectId.localeCompare(b.projectId))) {
|
|
@@ -258,7 +313,7 @@ async function allTeamInputs(root, company, deps) {
|
|
|
258
313
|
catch (err) {
|
|
259
314
|
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", project.projectId), "questions"));
|
|
260
315
|
}
|
|
261
|
-
teams.push(await teamInput(root, company, project, questions, deps, errors));
|
|
316
|
+
teams.push(await teamInput(root, company, project, questions, deps, errors, includeFocusDashboard));
|
|
262
317
|
}
|
|
263
318
|
return teams;
|
|
264
319
|
}
|
|
@@ -282,6 +337,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
282
337
|
const company = await resolveTrackerCompany(deps, options.company);
|
|
283
338
|
const root = deps.hqRoot();
|
|
284
339
|
const generatedAt = deps.now().toISOString();
|
|
340
|
+
const focusDashboardEnabled = await deps.focusDashboardEnabled(company);
|
|
285
341
|
const previous = options.since === undefined ? readTrackerReportState(root, company.slug) : null;
|
|
286
342
|
const since = options.since ?? previous?.last_report_at ?? FIRST_TRACKER_REPORT_SINCE;
|
|
287
343
|
const input = {
|
|
@@ -289,7 +345,8 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
289
345
|
generated_at: generatedAt,
|
|
290
346
|
since,
|
|
291
347
|
since_source: options.since !== undefined ? "explicit" : previous ? "previous_report" : "first_run",
|
|
292
|
-
|
|
348
|
+
focus_dashboard_enabled: focusDashboardEnabled,
|
|
349
|
+
teams: await allTeamInputs(root, company, deps, focusDashboardEnabled),
|
|
293
350
|
};
|
|
294
351
|
const report = buildTrackerOwnerReport(input);
|
|
295
352
|
const hasErrors = report.teams.some((team) => team.errors.length > 0);
|
|
@@ -323,7 +380,7 @@ export function registerLaneReportCommands(parent, deps, wrap) {
|
|
|
323
380
|
catch (err) {
|
|
324
381
|
errors.push(issueFromError(err, path.join(root, "companies", company.slug, "projects", candidates[0].projectId), "questions"));
|
|
325
382
|
}
|
|
326
|
-
const input = await teamInput(root, company, candidates[0], questions, deps, errors);
|
|
383
|
+
const input = await teamInput(root, company, candidates[0], questions, deps, errors, false);
|
|
327
384
|
const page = buildTrackerTeamPage(input, company.slug, since, generatedAt);
|
|
328
385
|
if (page.errors.length > 0)
|
|
329
386
|
process.exitCode = 2;
|
|
@@ -409,6 +409,9 @@ export function createLaneResumeHandlers(deps) {
|
|
|
409
409
|
: {}),
|
|
410
410
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
411
411
|
...(refreshProfileSettings ? { refreshProfileSettings: true, accountSource: "balanced" } : {}),
|
|
412
|
+
...(automaticAccountSelection.historyReason === "profile_pin_exhausted"
|
|
413
|
+
? { preserveAdmissionAccounts: true }
|
|
414
|
+
: {}),
|
|
412
415
|
...(profileRepin !== undefined ? { profileRepin } : {}),
|
|
413
416
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
414
417
|
...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
|