@hone-ai/cli 1.37.0 → 1.38.1
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/hone-cli.js +259 -214
- package/lib/asset-fetch-list.js +7 -3
- package/lib/bundle-paths.js +118 -18
- package/lib/doctor-admin-merge.js +11 -3
- package/lib/git-remote.js +103 -0
- package/lib/judge-provider.js +39 -34
- package/lib/mcp-tools.js +42 -1
- package/lib/operator-question.js +94 -0
- package/lib/run-context.js +4 -3
- package/lib/run-report.js +29 -0
- package/lib/run-target.js +65 -12
- package/lib/story-provenance.js +19 -12
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* operator-question.js — HC-GF-234: render the operator question(s) a step asked and nobody answered.
|
|
3
|
+
*
|
|
4
|
+
* A step that ends by asking the operator something used to be recorded exactly like a step that
|
|
5
|
+
* did its work (`status: completed`, `gateResult: approved`). The server now stores
|
|
6
|
+
* `{ count, questions }` on the step's attempt row and carries it as `operatorQuestion` on
|
|
7
|
+
* `GET /orchestrate/:runId` and `.../step/:stepKey`. Four CLI surfaces print it — `--status`,
|
|
8
|
+
* `show step`, the human-gate prompt, and the NEEDS YOU list — and they share this module so the
|
|
9
|
+
* wording and the two honesty rules below cannot drift apart.
|
|
10
|
+
*
|
|
11
|
+
* PURE: data in, strings out. No fs, no console, no process — a leaf, so run-report.js can keep
|
|
12
|
+
* its "derived, never narrated" contract and require it.
|
|
13
|
+
*
|
|
14
|
+
* TWO HONESTY RULES.
|
|
15
|
+
* - `count` is the TRUE number of questions the step asked; `questions` holds at most the first
|
|
16
|
+
* five. When count exceeds what is stored, the surfaces say "+N more not stored" instead of
|
|
17
|
+
* letting a short list read as the whole of it.
|
|
18
|
+
* - The text is AGENT-WRITTEN and goes to a terminal. The server scrubs control characters
|
|
19
|
+
* before storing, but this CLI may talk to an older or different server, so it scrubs again
|
|
20
|
+
* at the point of printing: C0/C1 controls, DEL, and the bidirectional controls that reorder
|
|
21
|
+
* what a terminal draws.
|
|
22
|
+
*
|
|
23
|
+
* TOLERANT OF AN OLDER SERVER BY CONSTRUCTION: a missing key, null, or a payload of the wrong
|
|
24
|
+
* shape normalises to null, which is today's output (nothing printed).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// U+061C ARABIC LETTER MARK is the third bidi mark beside LRM/RLM (review round 3 F3). Written as
|
|
28
|
+
// \u escapes: a literal bidi character in this source would reorder how THIS file reads.
|
|
29
|
+
// eslint-disable-next-line no-control-regex -- the whole point is to match control chars.
|
|
30
|
+
const TERMINAL_UNSAFE_RE = /[\x00-\x1f\x7f-\x9f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
|
|
31
|
+
|
|
32
|
+
function scrub(text) {
|
|
33
|
+
return String(text == null ? '' : text).replace(TERMINAL_UNSAFE_RE, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {*} oq a step's `operatorQuestion`, straight off the network (not trusted to have a shape)
|
|
38
|
+
* @returns {{count:number, questions:string[], more:number}|null} null = the step did not ask
|
|
39
|
+
*/
|
|
40
|
+
function normalizeQuestion(oq) {
|
|
41
|
+
if (!oq || typeof oq !== 'object') return null;
|
|
42
|
+
const count = Math.floor(Number(oq.count));
|
|
43
|
+
if (!Number.isFinite(count) || count < 1) return null;
|
|
44
|
+
const questions = (Array.isArray(oq.questions) ? oq.questions : []).map(scrub);
|
|
45
|
+
return { count, questions, more: Math.max(0, count - questions.length) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Clips by CODE POINT, not UTF-16 unit (review round 3 F4): a `.slice(0, max)` that lands between
|
|
49
|
+
// the halves of an emoji prints a lone surrogate, which a terminal draws as U+FFFD.
|
|
50
|
+
function clip(text, max) {
|
|
51
|
+
if (!Number.isFinite(max)) return text;
|
|
52
|
+
const points = Array.from(text);
|
|
53
|
+
return points.length > max ? `${points.slice(0, max).join('')}...` : text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One entry per stored question (`- <question>`, clipped to `max` chars) and, when the step asked
|
|
58
|
+
* more than was stored, a closing `+ N more not stored`. Callers add their own indentation.
|
|
59
|
+
*/
|
|
60
|
+
function questionLines(asked, { max = Infinity } = {}) {
|
|
61
|
+
if (!asked) return [];
|
|
62
|
+
const lines = asked.questions.map((q) => `- ${clip(q, max)}`);
|
|
63
|
+
if (asked.more > 0) lines.push(`+ ${asked.more} more not stored`);
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countPhrase(count) {
|
|
68
|
+
return `${count} question${count === 1 ? '' : 's'}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The block printed at the human gate, right before "Approve?" — the one place the operator
|
|
73
|
+
* decides, and where approving silently consumes the question without answering it.
|
|
74
|
+
*
|
|
75
|
+
* SCOPED TO THE STEP THE RUN IS PAUSED AT (`stepKey`), the same discipline as
|
|
76
|
+
* hard-pause-remedy.js's remedyFor: the column is durable, so a question step_3b asked must not
|
|
77
|
+
* be re-printed as if it belonged to an unrelated step_4 gate.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} status a GET /orchestrate/:runId payload
|
|
80
|
+
* @param {string} stepKey the step the gate is for
|
|
81
|
+
* @returns {string[]} lines to print (empty when that step asked nothing)
|
|
82
|
+
*/
|
|
83
|
+
function gateQuestionLines(status, stepKey) {
|
|
84
|
+
const steps = status && Array.isArray(status.steps) ? status.steps : [];
|
|
85
|
+
const step = steps.find((x) => x && typeof x === 'object' && x.stepKey === stepKey);
|
|
86
|
+
const asked = step ? normalizeQuestion(step.operatorQuestion) : null;
|
|
87
|
+
if (!asked) return [];
|
|
88
|
+
return [
|
|
89
|
+
` ${stepKey} asked ${countPhrase(asked.count)} that nobody has answered — approving does not answer ${asked.count === 1 ? 'it' : 'them'}:`,
|
|
90
|
+
...questionLines(asked, { max: 240 }).map((l) => ` ${l}`),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { normalizeQuestion, questionLines, gateQuestionLines, countPhrase };
|
package/lib/run-context.js
CHANGED
|
@@ -38,7 +38,6 @@ const {
|
|
|
38
38
|
extractType, extractSecurityImplications, extractCrossRepoDependency,
|
|
39
39
|
} = require('./story-classifier-extract');
|
|
40
40
|
const { readArchitectConfigFromText } = require('./architect-config');
|
|
41
|
-
const { remoteHostFromUrl } = require('./story-provenance');
|
|
42
41
|
const path = require('path');
|
|
43
42
|
|
|
44
43
|
/**
|
|
@@ -121,7 +120,8 @@ async function issueStorySource(io, issueNumber, issue) {
|
|
|
121
120
|
author: (api.user && api.user.login) || null,
|
|
122
121
|
association: api.author_association,
|
|
123
122
|
html_url: api.html_url,
|
|
124
|
-
|
|
123
|
+
// HC-GF-283: the host BOTH gh calls were told about — an SSH alias already resolved to it.
|
|
124
|
+
remoteHost: (got && got.host) || null,
|
|
125
125
|
};
|
|
126
126
|
return { source, notes: [info(` → story provenance: ${source.author || 'unknown'} (author_association: ${source.association || 'unknown'})`)] };
|
|
127
127
|
} catch (e) {
|
|
@@ -311,7 +311,8 @@ function resolveRepoContext(io, opts = {}) {
|
|
|
311
311
|
* @param {object} io
|
|
312
312
|
* @param {(n: string) => Promise<{title:string, body?:string, url:string}>|{title:string, body?:string, url:string}} io.fetchIssue
|
|
313
313
|
* rejects or throws on failure
|
|
314
|
-
* @param {(n: string) => Promise<{owner:string, repo:string, remoteUrl:string, issue:object}>} io.fetchIssueProvenance
|
|
314
|
+
* @param {(n: string) => Promise<{owner:string, repo:string, remoteUrl:string, host:string, issue:object}>} io.fetchIssueProvenance
|
|
315
|
+
* `host` is the GitHub host the call was made against (HC-GF-283), which the provenance check compares with html_url's
|
|
315
316
|
* HC-GF-270: `gh api repos/<owner>/<repo>/issues/<n>`; rejects with gh's stderr on the error, and
|
|
316
317
|
* `where: {owner, repo}` once the remote was parsed
|
|
317
318
|
* @param {(id: string) => Promise<object>} io.fetchStoryContext GET /stories/:id/context; throws
|
package/lib/run-report.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* Rendering is separate so both are testable without a server.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// HC-GF-234: the one wording for a step's unanswered question, shared with `--status`, `show
|
|
20
|
+
// step` and the human-gate prompt. A pure leaf module, so this file stays pure.
|
|
21
|
+
const { normalizeQuestion, questionLines, countPhrase } = require('./operator-question');
|
|
22
|
+
|
|
19
23
|
/** Gate verdicts the pipeline already speaks. Do not invent synonyms. */
|
|
20
24
|
const ACTIONABLE_GATE_VERDICTS = ['fail', 'unavailable'];
|
|
21
25
|
|
|
@@ -324,6 +328,31 @@ function buildReport(run, extra = {}) {
|
|
|
324
328
|
}
|
|
325
329
|
}
|
|
326
330
|
|
|
331
|
+
// 1e. HC-GF-234: a step that ended by asking the operator something.
|
|
332
|
+
//
|
|
333
|
+
// `status: completed` / `gateResult: approved` cannot say this: batch mode has no human gate on
|
|
334
|
+
// steps 0-3b, and approving a gate records `approved` whether or not the question was read. The
|
|
335
|
+
// step row carries the question on its own column; this is the only place a TERMINAL run's
|
|
336
|
+
// report can tell "did its work" from "asked, and nobody here answered".
|
|
337
|
+
//
|
|
338
|
+
// Says only what is MEASURED — the step asked, and the run has no answer to it. It does not say
|
|
339
|
+
// the step produced no deliverable or that the run proceeded on a guess: nothing checks either.
|
|
340
|
+
//
|
|
341
|
+
// ADVISORY (HC-026c: a new signal REPORTS before it GATES): `hone run-report` exits 1 on any
|
|
342
|
+
// non-advisory item, and an agent that asks one benign question must not turn an adopter's
|
|
343
|
+
// passing script red overnight. Visible in the report, absent from the exit code.
|
|
344
|
+
for (const s of steps) {
|
|
345
|
+
const asked = normalizeQuestion(s.operatorQuestion);
|
|
346
|
+
if (!asked) continue;
|
|
347
|
+
needsYou.push({
|
|
348
|
+
what: `${s.stepKey} (${s.agent || 'agent'}) asked ${countPhrase(asked.count)} that nothing in this run answered`,
|
|
349
|
+
detail: questionLines(asked, { max: 200 }),
|
|
350
|
+
how: `hone show step ${run.runId} ${s.stepKey}`,
|
|
351
|
+
howLabel: 'inspect',
|
|
352
|
+
advisory: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
327
356
|
// 2. The last verification is ONE event, so it becomes ONE item with sub-lines.
|
|
328
357
|
// Listing "verification failed" and "lint failed" separately gave two entries
|
|
329
358
|
// for one problem resolved by one fix — and NEEDS YOU is a list of DISTINCT
|
package/lib/run-target.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* `error` and decides the exit code.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
21
23
|
const RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
24
|
|
|
23
25
|
// GET /orchestrate clamps `limit` to 1..100, lists newest first, and is scoped to the caller's org.
|
|
@@ -104,17 +106,55 @@ async function resolveRunTarget({ client, idOrStory, repoName, flag }) {
|
|
|
104
106
|
const isStepKey = (s) => typeof s === 'string' && /^step_[0-9a-z_]+$/i.test(s);
|
|
105
107
|
const isAttempt = (s) => (typeof s === 'string' || typeof s === 'number') && /^[1-9]\d{0,8}$/.test(String(s));
|
|
106
108
|
const isBatchId = isRunId;
|
|
109
|
+
// HC-GF-270-followup-2: POST /derive is the only producer of a derive job id (server/src/routes/derive.js),
|
|
110
|
+
// and it issues `drv_` + 12 lower-case hex characters into a TEXT key compared exactly, so no other shape
|
|
111
|
+
// can name a job. `hone approve --job` GETs and then POSTs under that id.
|
|
112
|
+
const isDeriveJobId = (s) => typeof s === 'string' && /^drv_[0-9a-f]{12}$/.test(s);
|
|
113
|
+
// A name that becomes ONE path segment, such as a repository name. Encoding makes any other character
|
|
114
|
+
// safe; these three it cannot, because `new URL()` resolves `.` and `..` as directory steps.
|
|
115
|
+
const isPathName = (s) => typeof s === 'string' && s !== '' && s !== '.' && s !== '..';
|
|
116
|
+
|
|
117
|
+
// HC-GF-270-followup-3: a story id names a directory (`.github/pipeline/<id>/`, `.github/metrics/<id>.yml`).
|
|
118
|
+
// The branch extractor (pipeline-status.js STORY_ID_PATTERN) emits only `[A-Za-z0-9_-]` but has no length cap,
|
|
119
|
+
// so an id read from a branch is checked too (step-5b, metrics collect, verify-pr); the fallbacks are
|
|
120
|
+
// `unknown-story` and `no-story`. No `.`, `/` or `\`, so `../` cannot leave the directory; 80 characters, the cap
|
|
121
|
+
// server/src/routes/stories.js applies (MAX_STORY_ID_LEN). Its STORY_ID_SHAPE is stricter (no `_`): it decides
|
|
122
|
+
// what the server stores, this decides what may name a local directory.
|
|
123
|
+
const isStoryId = (s) => typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/.test(s);
|
|
124
|
+
// A ref git or gh may receive as ONE argument. `;`, `$(` and quotes are harmless there (and legal in ref names);
|
|
125
|
+
// a leading `-` is not: `git diff --name-only '--output=x...HEAD'` writes a file. `--end-of-options` would
|
|
126
|
+
// also stop it, but only on git 2.24 and later — on an older git every diff would fail and an audit would pass
|
|
127
|
+
// on nothing, so the value is refused instead.
|
|
128
|
+
const isGitRev = (s) => typeof s === 'string' && s !== '' && !s.startsWith('-') && !s.includes('\0');
|
|
107
129
|
|
|
108
130
|
const KINDS = {
|
|
109
131
|
runId: { ok: isRunId, why: 'must be a run ID (a UUID)', hint: ' — run IDs: hone workflows' },
|
|
110
132
|
batchId: { ok: isBatchId, why: 'must be a batch ID (a UUID)', hint: ' — batch IDs: hone queue-stories --list' },
|
|
111
133
|
stepKey: { ok: isStepKey, why: 'must name a step, like step_4', hint: '' },
|
|
112
134
|
attempt: { ok: isAttempt, why: 'must be a whole number from 1, like 2', hint: '' },
|
|
135
|
+
deriveJobId: { ok: isDeriveJobId, why: 'must be a derive job ID, like drv_0123456789ab', hint: ' — the ID "hone derive --refresh" printed' },
|
|
136
|
+
pathName: { ok: isPathName, why: 'must be a name, not empty, "." or ".."', hint: '' },
|
|
137
|
+
storyId: { ok: isStoryId, why: 'must be a story ID, like HC-GF-270 (letters, digits, "-" and "_")', hint: '' },
|
|
138
|
+
gitRev: { ok: isGitRev, why: 'must be a git branch or revision, not empty and not starting with "-"', hint: '' },
|
|
139
|
+
count: { ok: isAttempt, why: 'must be a whole number from 1, like 15', hint: '' },
|
|
113
140
|
};
|
|
114
141
|
|
|
142
|
+
/**
|
|
143
|
+
* HC-GF-270-followup-3: the directory a story id names under `baseDir`, refusing any id that is not one — or that
|
|
144
|
+
* resolves anywhere but one level under `baseDir`, checked independently of the pattern. Every verb that writes
|
|
145
|
+
* here refused a bad id first (`runArgsError`, kind `storyId`), so a throw here is a bug, not an input error.
|
|
146
|
+
*/
|
|
147
|
+
function storyDir(baseDir, storyId) {
|
|
148
|
+
const base = path.resolve(String(baseDir));
|
|
149
|
+
if (!isStoryId(storyId)) throw new TypeError(`storyDir: ${JSON.stringify(storyId)} is not a story ID`);
|
|
150
|
+
const dir = path.resolve(base, storyId);
|
|
151
|
+
if (path.dirname(dir) !== base) throw new TypeError(`storyDir: ${JSON.stringify(storyId)} leaves ${base}`);
|
|
152
|
+
return dir;
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
/**
|
|
116
156
|
* The first failed check's message, or null when every check passes.
|
|
117
|
-
* @param {Array<{ label: string, kind: 'runId'|'batchId'|'stepKey'|'attempt', value: any, optional?: boolean }>} checks
|
|
157
|
+
* @param {Array<{ label: string, kind: 'runId'|'batchId'|'stepKey'|'attempt'|'deriveJobId'|'pathName'|'storyId'|'gitRev'|'count', value: any, optional?: boolean }>} checks
|
|
118
158
|
* `label` is the flag or positional as the operator typed it. An `optional` check passes when the value
|
|
119
159
|
* is undefined (a flag that was not given); an empty string is always checked.
|
|
120
160
|
*/
|
|
@@ -130,23 +170,29 @@ function runArgsError(checks) {
|
|
|
130
170
|
}
|
|
131
171
|
|
|
132
172
|
/**
|
|
133
|
-
* The one way
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
173
|
+
* The one way the CLI builds a path on the Hone API: `/<prefix>/<segment>/…?<query>`. `prefix` is a
|
|
174
|
+
* literal resource name (`derive`, `skills`, …), never a value. Each segment is encoded exactly once, and
|
|
175
|
+
* the segments encoding cannot make safe (empty, `.`, `..`, null, undefined) throw: every caller-supplied
|
|
176
|
+
* value has passed `runArgsError` first, so a throw here is a bug, not an input error. Query values that
|
|
177
|
+
* are null, undefined, false or '' are dropped; the rest go through URLSearchParams.
|
|
137
178
|
*
|
|
138
179
|
* Callers pass raw values. A pre-encoded segment would be encoded twice (`%` becomes `%25`), and the
|
|
139
|
-
* HC-GF-270-followup-
|
|
180
|
+
* HC-GF-270-followup-2 scan fails on one.
|
|
181
|
+
*
|
|
182
|
+
* HC-GF-270-followup-2: generalized from orchestratePath. HC-GF-270-followup-1 closed the defect under
|
|
183
|
+
* `/orchestrate/`; `/derive/`, `/skills/`, `/prompts/`, `/scripts/` and `/stories/` were still built by
|
|
184
|
+
* hand, and `hone approve --job` POSTed under an id nothing checked.
|
|
140
185
|
*/
|
|
141
|
-
function
|
|
142
|
-
if (
|
|
186
|
+
function apiPath(prefix, segments, query) {
|
|
187
|
+
if (typeof prefix !== 'string' || !/^[a-z][a-z0-9-]*$/.test(prefix)) throw new TypeError(`apiPath: prefix must be a resource name, got ${JSON.stringify(prefix)}`);
|
|
188
|
+
if (!Array.isArray(segments)) throw new TypeError('apiPath: segments must be an array');
|
|
143
189
|
const parts = segments.map((v, i) => {
|
|
144
|
-
if (v === null || v === undefined) throw new TypeError(`
|
|
190
|
+
if (v === null || v === undefined) throw new TypeError(`apiPath: segment ${i} is ${v}`);
|
|
145
191
|
const s = String(v);
|
|
146
|
-
if (s === '' || s === '.' || s === '..') throw new TypeError(`
|
|
192
|
+
if (s === '' || s === '.' || s === '..') throw new TypeError(`apiPath: segment ${i} is ${JSON.stringify(s)}`);
|
|
147
193
|
return encodeURIComponent(s);
|
|
148
194
|
});
|
|
149
|
-
const base = ['',
|
|
195
|
+
const base = ['', prefix, ...parts].join('/');
|
|
150
196
|
const qs = new URLSearchParams();
|
|
151
197
|
for (const [k, v] of Object.entries(query || {})) {
|
|
152
198
|
if (v === null || v === undefined || v === false || v === '') continue;
|
|
@@ -156,7 +202,14 @@ function orchestratePath(segments, query) {
|
|
|
156
202
|
return q ? `${base}?${q}` : base;
|
|
157
203
|
}
|
|
158
204
|
|
|
205
|
+
/** An `/orchestrate/…` path: `apiPath('orchestrate', …)` with at least one segment (HC-GF-270-followup-1). */
|
|
206
|
+
function orchestratePath(segments, query) {
|
|
207
|
+
if (!Array.isArray(segments) || segments.length === 0) throw new TypeError('orchestratePath: segments must be a non-empty array');
|
|
208
|
+
return apiPath('orchestrate', segments, query);
|
|
209
|
+
}
|
|
210
|
+
|
|
159
211
|
module.exports = {
|
|
160
212
|
resolveRunTarget, isRunId, LOOKUP_LIMIT,
|
|
161
|
-
isStepKey, isAttempt, isBatchId,
|
|
213
|
+
isStepKey, isAttempt, isBatchId, isDeriveJobId, isPathName, isStoryId, isGitRev, storyDir,
|
|
214
|
+
runArgsError, apiPath, orchestratePath,
|
|
162
215
|
};
|
package/lib/story-provenance.js
CHANGED
|
@@ -53,11 +53,12 @@ function isIssueShapedId(storyId) {
|
|
|
53
53
|
/**
|
|
54
54
|
* Is this a HOSTNAME we may compare against `html_url`'s host?
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
* remote's host
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
56
|
+
* HC-GF-270's binding decision 2 (operator, step_1 gate of run fa6e00c4) compared hosts ONLY when
|
|
57
|
+
* the remote's host was a dotted domain, because an SSH alias such as `github-work` is not a
|
|
58
|
+
* hostname and comparing it would refuse every collaborator's issue for every operator who uses
|
|
59
|
+
* one. HC-GF-283 removes the reason for the exception: run-story now resolves an alias with
|
|
60
|
+
* `ssh -G` (cli/lib/git-remote.js) and records the REAL host, so the comparison runs always, and a
|
|
61
|
+
* recorded host that is still not a hostname is a host nobody established — untrusted.
|
|
61
62
|
*/
|
|
62
63
|
function isDottedDomain(host) {
|
|
63
64
|
const h = String(host == null ? '' : host).trim();
|
|
@@ -237,13 +238,18 @@ function assessStorySource(source, storyId) {
|
|
|
237
238
|
+ 'or the request redirected to a different repository');
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
// HC-GF-283: ALWAYS. An unresolved alias left gh on its default host, where the same owner/repo
|
|
242
|
+
// name can belong to someone else — the case binding decision 2's exception let through.
|
|
243
|
+
const rh = String(source.remoteHost == null ? '' : source.remoteHost).trim().toLowerCase();
|
|
244
|
+
if (!isDottedDomain(rh)) {
|
|
245
|
+
return no('issue', rh
|
|
246
|
+
? `the git remote's host was recorded as '${rh}', which is not a hostname — an SSH alias that was never `
|
|
247
|
+
+ `resolved (a run started before HC-GF-283), so which GitHub host issue ${refLabel(source)} was read from cannot be checked`
|
|
248
|
+
: `no git remote host was recorded for issue ${refLabel(source)}, so which GitHub host it was read from cannot be checked`);
|
|
249
|
+
}
|
|
250
|
+
if (rh !== url.hostname.toLowerCase()) {
|
|
251
|
+
return no('issue', `the git remote's host ${rh} is not ${url.hostname.toLowerCase()} — the issue was read `
|
|
252
|
+
+ 'from a different GitHub host than this repository lives on, where someone else can own the same name');
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
return yes('issue', `issue ${refLabel(source)} was authored by ${source.author || 'an unknown user'} `
|
|
@@ -258,6 +264,7 @@ function assessStorySource(source, storyId) {
|
|
|
258
264
|
module.exports = {
|
|
259
265
|
assessStorySource,
|
|
260
266
|
remoteHostFromUrl,
|
|
267
|
+
rawRemoteHost,
|
|
261
268
|
isIssueShapedId,
|
|
262
269
|
isDottedDomain,
|
|
263
270
|
refLabel,
|