@indigoai-us/hq-cli 5.345.53 → 5.345.55
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 +24 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +1 -1
- package/assets/bot-workers/setup/skills/agents-and-team.md +1 -1
- package/assets/bot-workers/setup/skills/pricing.md +1 -1
- package/dist/command-catalog.generated.d.ts +103 -2
- package/dist/command-catalog.generated.js +124 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-session-start.d.ts +22 -0
- package/dist/commands/lanes-session-start.js +215 -0
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.js +4 -2
- package/dist/lib/billing/plan-lock.d.ts +2 -2
- package/dist/lib/billing/plan-lock.js +2 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/dist/utils/manifest.d.ts +7 -1
- package/dist/utils/manifest.js +14 -6
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hq.computer/schemas/import-scan/scan-event.v1.schema.json",
|
|
4
|
+
"title": "HQ import scan event (v1)",
|
|
5
|
+
"description": "One line of `hq import scan --json --stream` / `scan.sh --progress-json`. Each type is also addressable as #/$defs/<type>. Unknown extra fields are allowed within v1; hq-cli forwards only the fields listed here.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["v", "type"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"v": { "const": 1 },
|
|
10
|
+
"type": { "enum": ["start", "source", "count", "company", "project", "error", "done"] }
|
|
11
|
+
},
|
|
12
|
+
"oneOf": [
|
|
13
|
+
{ "$ref": "#/$defs/start" },
|
|
14
|
+
{ "$ref": "#/$defs/source" },
|
|
15
|
+
{ "$ref": "#/$defs/nonNegative" },
|
|
16
|
+
{ "$ref": "#/$defs/company" },
|
|
17
|
+
{ "$ref": "#/$defs/project" },
|
|
18
|
+
{ "$ref": "#/$defs/error" },
|
|
19
|
+
{ "$ref": "#/$defs/done" }
|
|
20
|
+
],
|
|
21
|
+
"$defs": {
|
|
22
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
23
|
+
"nonNegative": { "type": "integer", "minimum": 0 },
|
|
24
|
+
"baseName": {
|
|
25
|
+
"description": "A folder or repository basename: a name without \"/\".",
|
|
26
|
+
"allOf": [{ "$ref": "#/$defs/name" }, { "pattern": "^[^/]+$" }]
|
|
27
|
+
},
|
|
28
|
+
"start": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["v", "type", "sources"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"v": { "const": 1 },
|
|
33
|
+
"type": { "const": "start" },
|
|
34
|
+
"sources": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["id", "label"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"id": { "$ref": "#/$defs/name" },
|
|
41
|
+
"label": { "$ref": "#/$defs/name" }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"source": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["v", "type", "id", "status"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"v": { "const": 1 },
|
|
52
|
+
"type": { "const": "source" },
|
|
53
|
+
"id": { "$ref": "#/$defs/name" },
|
|
54
|
+
"status": { "enum": ["scanning", "done", "skipped", "error"] },
|
|
55
|
+
"counts": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"propertyNames": { "pattern": "^[a-z0-9_-]{1,64}$" },
|
|
58
|
+
"additionalProperties": { "$ref": "#/$defs/nonNegative" }
|
|
59
|
+
},
|
|
60
|
+
"message": { "type": "string", "maxLength": 2000 }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"count": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"required": ["v", "type", "source", "key", "value"],
|
|
66
|
+
"properties": {
|
|
67
|
+
"v": { "const": 1 },
|
|
68
|
+
"type": { "const": "count" },
|
|
69
|
+
"source": { "$ref": "#/$defs/name" },
|
|
70
|
+
"key": { "type": "string", "pattern": "^[a-z0-9_-]{1,64}$" },
|
|
71
|
+
"value": { "$ref": "#/$defs/nonNegative" }
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"company": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": ["v", "type", "id", "name", "basis"],
|
|
77
|
+
"properties": {
|
|
78
|
+
"v": { "const": 1 },
|
|
79
|
+
"type": { "const": "company" },
|
|
80
|
+
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "maxLength": 128 },
|
|
81
|
+
"name": { "$ref": "#/$defs/name" },
|
|
82
|
+
"basis": { "enum": ["hq-company", "repo-org", "folder"] }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"project": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"required": ["v", "type", "id", "name", "company", "basis"],
|
|
88
|
+
"properties": {
|
|
89
|
+
"v": { "const": 1 },
|
|
90
|
+
"type": { "const": "project" },
|
|
91
|
+
"id": { "type": "string", "pattern": "^p_[0-9a-f]{12}$" },
|
|
92
|
+
"name": { "$ref": "#/$defs/baseName" },
|
|
93
|
+
"company": { "type": ["string", "null"] },
|
|
94
|
+
"basis": { "enum": ["repo", "claude-code-cwd", "codex-cwd"] }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"error": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"required": ["v", "type", "source", "message"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"v": { "const": 1 },
|
|
102
|
+
"type": { "const": "error" },
|
|
103
|
+
"source": { "$ref": "#/$defs/name" },
|
|
104
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 2000 },
|
|
105
|
+
"code": { "enum": ["scanner_outdated", "scan_failed"] }
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"done": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": ["v", "type", "report", "summary"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"v": { "const": 1 },
|
|
113
|
+
"type": { "const": "done" },
|
|
114
|
+
"report": { "type": ["string", "null"] },
|
|
115
|
+
"summary": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"required": ["companies", "projects", "sessions"],
|
|
118
|
+
"properties": {
|
|
119
|
+
"companies": { "$ref": "#/$defs/nonNegative" },
|
|
120
|
+
"projects": { "$ref": "#/$defs/nonNegative" },
|
|
121
|
+
"sessions": { "$ref": "#/$defs/nonNegative" }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates one line of the import-scan event stream against
|
|
3
|
+
* `scan-event.schema.json` (Ajv, draft 2020-12) and rebuilds the event from
|
|
4
|
+
* the v1 fields only.
|
|
5
|
+
*
|
|
6
|
+
* Validation dispatches on `type`: the common envelope is checked first, then
|
|
7
|
+
* the line is validated against `#/$defs/<type>` alone, so a drop reason names
|
|
8
|
+
* the real problem instead of the first failing `oneOf` branch.
|
|
9
|
+
*
|
|
10
|
+
* Failure reasons name the type, the schema path and Ajv's generic message
|
|
11
|
+
* only. They never echo values from the line, so a malformed event that
|
|
12
|
+
* happens to carry content cannot leak through the warning hq-cli prints.
|
|
13
|
+
*/
|
|
14
|
+
import { type ScanEvent } from "./events.js";
|
|
15
|
+
export declare const SCAN_EVENT_SCHEMA_PATH: string;
|
|
16
|
+
export type ParsedScanLine = {
|
|
17
|
+
ok: true;
|
|
18
|
+
event: ScanEvent;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
/** Parse and validate one JSON Lines record. */
|
|
24
|
+
export declare function parseScanEventLine(line: string): ParsedScanLine;
|
|
25
|
+
export declare function validateScanEvent(value: unknown): ParsedScanLine;
|
|
26
|
+
/**
|
|
27
|
+
* Rebuild an already-validated event from the v1 fields only, so fields a
|
|
28
|
+
* scanner adds (or smuggles) never reach stdout.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pickScanEvent(event: ScanEvent): ScanEvent;
|
|
31
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates one line of the import-scan event stream against
|
|
3
|
+
* `scan-event.schema.json` (Ajv, draft 2020-12) and rebuilds the event from
|
|
4
|
+
* the v1 fields only.
|
|
5
|
+
*
|
|
6
|
+
* Validation dispatches on `type`: the common envelope is checked first, then
|
|
7
|
+
* the line is validated against `#/$defs/<type>` alone, so a drop reason names
|
|
8
|
+
* the real problem instead of the first failing `oneOf` branch.
|
|
9
|
+
*
|
|
10
|
+
* Failure reasons name the type, the schema path and Ajv's generic message
|
|
11
|
+
* only. They never echo values from the line, so a malformed event that
|
|
12
|
+
* happens to carry content cannot leak through the warning hq-cli prints.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
18
|
+
import { SCAN_EVENT_TYPES, SCAN_EVENT_VERSION } from "./events.js";
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
export const SCAN_EVENT_SCHEMA_PATH = join(HERE, "scan-event.schema.json");
|
|
21
|
+
let validators;
|
|
22
|
+
function getValidators() {
|
|
23
|
+
if (validators)
|
|
24
|
+
return validators;
|
|
25
|
+
const schema = JSON.parse(readFileSync(SCAN_EVENT_SCHEMA_PATH, "utf8"));
|
|
26
|
+
const ajv = new Ajv2020({ allErrors: false, strict: false, validateFormats: false });
|
|
27
|
+
ajv.addSchema(schema);
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const type of SCAN_EVENT_TYPES) {
|
|
30
|
+
const fn = ajv.getSchema(`${schema.$id}#/$defs/${type}`);
|
|
31
|
+
if (!fn)
|
|
32
|
+
throw new Error(`scan-event schema has no $defs/${type}`);
|
|
33
|
+
map.set(type, fn);
|
|
34
|
+
}
|
|
35
|
+
validators = map;
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
/** Parse and validate one JSON Lines record. */
|
|
39
|
+
export function parseScanEventLine(line) {
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = JSON.parse(line);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { ok: false, reason: "not valid JSON" };
|
|
46
|
+
}
|
|
47
|
+
return validateScanEvent(value);
|
|
48
|
+
}
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
export function validateScanEvent(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return { ok: false, reason: "not a JSON object" };
|
|
55
|
+
if (value.v !== SCAN_EVENT_VERSION)
|
|
56
|
+
return { ok: false, reason: "missing or unsupported version (v)" };
|
|
57
|
+
const type = value.type;
|
|
58
|
+
if (typeof type !== "string" || !SCAN_EVENT_TYPES.includes(type)) {
|
|
59
|
+
return { ok: false, reason: "missing or unknown event type" };
|
|
60
|
+
}
|
|
61
|
+
const validate = getValidators().get(type);
|
|
62
|
+
if (!validate(value)) {
|
|
63
|
+
const first = validate.errors?.[0];
|
|
64
|
+
const where = first ? `${first.instancePath || "/"} ${first.message ?? first.keyword}` : "schema";
|
|
65
|
+
return { ok: false, reason: `event "${type}" does not match the v1 schema (${where})` };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, event: pickScanEvent(value) };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Rebuild an already-validated event from the v1 fields only, so fields a
|
|
71
|
+
* scanner adds (or smuggles) never reach stdout.
|
|
72
|
+
*/
|
|
73
|
+
export function pickScanEvent(event) {
|
|
74
|
+
const v = SCAN_EVENT_VERSION;
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case "start":
|
|
77
|
+
return { v, type: "start", sources: event.sources.map((s) => ({ id: s.id, label: s.label })) };
|
|
78
|
+
case "source": {
|
|
79
|
+
const out = { v, type: "source", id: event.id, status: event.status };
|
|
80
|
+
if (event.counts)
|
|
81
|
+
out.counts = Object.fromEntries(Object.entries(event.counts));
|
|
82
|
+
if (event.message !== undefined)
|
|
83
|
+
out.message = event.message;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
case "count":
|
|
87
|
+
return { v, type: "count", source: event.source, key: event.key, value: event.value };
|
|
88
|
+
case "company":
|
|
89
|
+
return { v, type: "company", id: event.id, name: event.name, basis: event.basis };
|
|
90
|
+
case "project":
|
|
91
|
+
return { v, type: "project", id: event.id, name: event.name, company: event.company, basis: event.basis };
|
|
92
|
+
case "error": {
|
|
93
|
+
const out = { v, type: "error", source: event.source, message: event.message };
|
|
94
|
+
if (event.code !== undefined)
|
|
95
|
+
out.code = event.code;
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
case "done":
|
|
99
|
+
return {
|
|
100
|
+
v,
|
|
101
|
+
type: "done",
|
|
102
|
+
report: event.report,
|
|
103
|
+
summary: {
|
|
104
|
+
companies: event.summary.companies,
|
|
105
|
+
projects: event.summary.projects,
|
|
106
|
+
sessions: event.summary.sessions,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -30,4 +30,6 @@ export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot
|
|
|
30
30
|
export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
|
|
31
31
|
/** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
|
|
32
32
|
export declare const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
|
|
33
|
+
/** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
|
|
34
|
+
export declare const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
|
|
33
35
|
//# sourceMappingURL=flag-keys.d.ts.map
|
|
@@ -30,4 +30,6 @@ export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost
|
|
|
30
30
|
export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
|
|
31
31
|
/** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
|
|
32
32
|
export const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
|
|
33
|
+
/** Enables conduct-mode SessionStart bootstrapping (defaults on; operator kill switch). */
|
|
34
|
+
export const LANE_CONDUCT_SESSION_START_FLAG = "lanes.conduct-session-start";
|
|
33
35
|
//# sourceMappingURL=flag-keys.js.map
|
|
@@ -422,7 +422,7 @@ export function monitorReminderText() {
|
|
|
422
422
|
"For active Claude senior lanes, arm one live Claude Code Monitor task per uncovered lane:",
|
|
423
423
|
'Monitor(command="hq lanes watch <lane-id>...", timeout_ms=1800000, persistent=true)',
|
|
424
424
|
"Use `hq lanes monitor-check --session <id>` to verify coverage. `persistent=true` is requested where the installed Monitor tool honors it; Claude Code caps Monitor tasks at 30 minutes, so re-arm after expiry.",
|
|
425
|
-
"The caller must pass `--engine codex` or `--engine grok` (or set HQ_CONDUCT_ENGINE) for sessions whose engine has no Claude Monitor tool; those engines are never required to provide one.",
|
|
425
|
+
"The caller must pass `--engine codex` or `--engine grok` (or set HQ_LANE_ENGINE; deprecated alias HQ_CONDUCT_ENGINE) for sessions whose engine has no Claude Monitor tool; those engines are never required to provide one.",
|
|
426
426
|
].join("\n");
|
|
427
427
|
}
|
|
428
428
|
//# sourceMappingURL=monitor.js.map
|
|
@@ -11,6 +11,8 @@ export interface LanesFlagClient {
|
|
|
11
11
|
}
|
|
12
12
|
export type LanesFlagClientFactory = (options: FlagClientOptions) => LanesFlagClient;
|
|
13
13
|
export interface ReadLanesFlagOptions {
|
|
14
|
+
/** Fallback for a missing registry row; kill switches should normally use true. */
|
|
15
|
+
defaultValue?: boolean;
|
|
14
16
|
company?: LaneCompanyRef | null;
|
|
15
17
|
companyUidSource?: string;
|
|
16
18
|
env?: NodeJS.ProcessEnv;
|
|
@@ -155,7 +155,7 @@ async function readRegistryValue(flagKey, endpoint, options) {
|
|
|
155
155
|
rememberFailure(error);
|
|
156
156
|
}
|
|
157
157
|
try {
|
|
158
|
-
resolution = client.explain(flagKey, { fallback: false });
|
|
158
|
+
resolution = client.explain(flagKey, { fallback: options.defaultValue ?? false });
|
|
159
159
|
}
|
|
160
160
|
catch (error) {
|
|
161
161
|
rememberFailure(error);
|
package/dist/utils/manifest.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import type { ModulesManifest, ModuleDefinition, ModuleLock, SyncState } from '../types.js';
|
|
2
|
-
export declare function findHqRoot(
|
|
4
|
+
export declare function findHqRoot(options?: {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
pathApi?: Pick<typeof path, 'dirname' | 'join'>;
|
|
7
|
+
existsSync?: typeof fs.existsSync;
|
|
8
|
+
}): string;
|
|
3
9
|
/**
|
|
4
10
|
* Resolve the manifest path for a given HQ root.
|
|
5
11
|
*
|
package/dist/utils/manifest.js
CHANGED
|
@@ -5,15 +5,23 @@ import { parseUserYaml } from './user-yaml-error.js';
|
|
|
5
5
|
const MANIFEST_FILE = 'modules.yaml';
|
|
6
6
|
const LOCK_FILE = 'modules.lock';
|
|
7
7
|
const STATE_FILE = '.hq-sync-state.json';
|
|
8
|
-
export function findHqRoot() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
export function findHqRoot(options = {}) {
|
|
9
|
+
const cwd = options.cwd ?? process.cwd();
|
|
10
|
+
const pathApi = options.pathApi ?? path;
|
|
11
|
+
const existsSync = options.existsSync ?? fs.existsSync;
|
|
12
|
+
let dir = cwd;
|
|
13
|
+
while (true) {
|
|
14
|
+
const parent = pathApi.dirname(dir);
|
|
15
|
+
// Check the fixed point before looking for markers so POSIX keeps its
|
|
16
|
+
// existing behavior of never checking `/` itself.
|
|
17
|
+
if (parent === dir)
|
|
18
|
+
break;
|
|
19
|
+
if (existsSync(pathApi.join(dir, '.claude')) || existsSync(pathApi.join(dir, 'workers'))) {
|
|
12
20
|
return dir;
|
|
13
21
|
}
|
|
14
|
-
dir =
|
|
22
|
+
dir = parent;
|
|
15
23
|
}
|
|
16
|
-
return
|
|
24
|
+
return cwd;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Resolve the manifest path for a given HQ root.
|