mandrel 2.64.0 → 2.66.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/.agents/agents/acceptance-critic.md +8 -7
- package/.agents/agents/auditor.md +20 -20
- package/.agents/agents/plan-critic.md +8 -7
- package/.agents/agents/story-worker.md +7 -7
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +1 -9
- package/.agents/docs/configuration.md +8 -7
- package/.agents/docs/execution-reference.md +27 -5
- package/.agents/instructions.md +10 -12
- package/.agents/rules/ci-remediation.md +3 -3
- package/.agents/rules/gherkin-standards.md +3 -2
- package/.agents/rules/git-conventions-reference.md +12 -3
- package/.agents/rules/git-conventions.md +9 -7
- package/.agents/rules/testing-standards.md +8 -7
- package/.agents/runtime-deps.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -13
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
- package/.agents/scripts/bootstrap.js +102 -91
- package/.agents/scripts/check-context-budget.js +1 -1
- package/.agents/scripts/lib/ITicketingProvider.js +1 -3
- package/.agents/scripts/lib/audit-suite/findings.js +1 -17
- package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
- package/.agents/scripts/lib/audit-suite/index.js +0 -6
- package/.agents/scripts/lib/audit-suite/selector.js +0 -31
- package/.agents/scripts/lib/baselines/duplication-scanner.js +17 -7
- package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
- package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
- package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +91 -107
- package/.agents/scripts/lib/cli/standard-args.js +60 -76
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/config/gates/shared.js +3 -3
- package/.agents/scripts/lib/config/review-chain-default.js +13 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
- package/.agents/scripts/lib/doc-tiers.js +25 -6
- package/.agents/scripts/lib/feedback-loop/graduate-steps.js +205 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +47 -782
- package/.agents/scripts/lib/feedback-loop/graduator-gh.js +449 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/close-telemetry.js +330 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
- package/.agents/scripts/lib/observability/runtime-friction.js +2 -0
- package/.agents/scripts/lib/observability/signal-validator.js +17 -5
- package/.agents/scripts/lib/orchestration/code-review.js +33 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
- package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
- package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
- package/.agents/scripts/lib/orchestration/plan-metrics.js +76 -63
- package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +29 -4
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
- package/.agents/scripts/lib/orchestration/run-epilogue.js +6 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +2 -12
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +370 -268
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +21 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +4 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +393 -313
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +19 -36
- package/.agents/scripts/lib/signals/detectors/common.js +63 -51
- package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
- package/.agents/scripts/lib/transpile.js +28 -3
- package/.agents/scripts/providers/github/issues.js +14 -23
- package/.agents/scripts/single-story-close.js +10 -2
- package/.agents/scripts/single-story-confirm-merge.js +267 -238
- package/.agents/scripts/sync-claude-agents.js +1 -1
- package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -2
- package/.agents/workflows/audit-architecture.md +5 -4
- package/.agents/workflows/audit-documentation.md +5 -5
- package/.agents/workflows/audit-performance.md +10 -10
- package/.agents/workflows/audit-quality.md +42 -7
- package/.agents/workflows/helpers/acceptance-self-eval.md +9 -9
- package/.agents/workflows/helpers/audit-lens-core.md +30 -57
- package/.agents/workflows/helpers/code-review.md +15 -38
- package/.agents/workflows/helpers/deliver-digest.md +2 -2
- package/.agents/workflows/helpers/deliver-reference.md +7 -3
- package/.agents/workflows/helpers/deliver-story.md +9 -1
- package/.agents/workflows/helpers/parallel-tooling.md +16 -18
- package/.agents/workflows/helpers/plan-reference.md +9 -8
- package/.agents/workflows/mandrel-deliver.md +3 -2
- package/.agents/workflows/mandrel-plan.md +11 -7
- package/.agents/workflows/mandrel-update.md +5 -3
- package/docs/CHANGELOG.md +57 -0
- package/lib/cli/claude-code-version.js +73 -0
- package/lib/cli/doctor.js +2 -2
- package/lib/cli/guarded-sync.js +87 -0
- package/lib/cli/registry.js +9 -0
- package/lib/cli/sync-agents.js +9 -92
- package/lib/cli/sync-commands.js +9 -101
- package/lib/cli/uninstall.js +37 -9
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
- package/package.json +3 -2
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
- package/.agents/scripts/lib/audit-suite/runner.js +0 -205
- package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
- package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
|
@@ -5,19 +5,38 @@
|
|
|
5
5
|
* (content-hash markers survive sibling reordering).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { spawn as defaultSpawn } from 'node:child_process';
|
|
9
8
|
import { createHash } from 'node:crypto';
|
|
10
9
|
|
|
11
10
|
import { inNodeTestContext } from '../config/temp-paths.js';
|
|
12
|
-
import { routeOwnership } from '../github/framework-repo.js';
|
|
13
|
-
import { LABEL_COLORS } from '../label-constants.js';
|
|
14
11
|
import { classifyPathSource as defaultClassifier } from '../observability/source-classifier.js';
|
|
15
12
|
import { upsertStructuredComment } from '../orchestration/ticketing.js';
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
import {
|
|
14
|
+
dedupFinding,
|
|
15
|
+
fileFinding,
|
|
16
|
+
probeFindingPath,
|
|
17
|
+
recordRecurrence,
|
|
18
|
+
routeFinding,
|
|
19
|
+
} from './graduate-steps.js';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_RUN_CHILD_TIMEOUT_MS,
|
|
22
|
+
searchFollowUpByMarker,
|
|
23
|
+
} from './graduator-gh.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
createFollowUpIssue,
|
|
27
|
+
ensureIssueLabels,
|
|
28
|
+
probePathStatus,
|
|
29
|
+
runChild,
|
|
30
|
+
updateFollowUpIssue,
|
|
31
|
+
} from './graduator-gh.js';
|
|
18
32
|
|
|
19
33
|
export const DEFAULT_MAX_FILINGS_PER_RUN = 20;
|
|
20
34
|
|
|
35
|
+
/** @returns {Promise<boolean>} */
|
|
36
|
+
export async function probeMarkerExists(opts) {
|
|
37
|
+
return (await searchFollowUpByMarker(opts)) !== null;
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
/** Registered in `STRUCTURED_COMMENT_TYPES`. */
|
|
22
41
|
const CROSS_REPO_DEFERRED_COMMENT_TYPE = 'cross-repo-deferred';
|
|
23
42
|
|
|
@@ -78,93 +97,6 @@ export function contentFingerprint({ category, path, title } = {}) {
|
|
|
78
97
|
return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
/**
|
|
82
|
-
* The feedback loop's single spawn helper. Never throws: spawn errors land in
|
|
83
|
-
* `spawnError`; an overrun is SIGKILL'd and resolves `timedOut: true`.
|
|
84
|
-
* `timeoutMs` of `0`/`Infinity` disables the watchdog.
|
|
85
|
-
*
|
|
86
|
-
* @param {object} opts
|
|
87
|
-
* @param {string} opts.cmd
|
|
88
|
-
* @param {string[]} opts.args
|
|
89
|
-
* @param {Function} [opts.spawnImpl]
|
|
90
|
-
* @param {string} [opts.cwd]
|
|
91
|
-
* @param {number} [opts.timeoutMs]
|
|
92
|
-
* @returns {Promise<{ code: number|null, stdout: string, stderr: string, spawnError: Error|null, timedOut: boolean }>}
|
|
93
|
-
*/
|
|
94
|
-
export function runChild({
|
|
95
|
-
cmd,
|
|
96
|
-
args,
|
|
97
|
-
spawnImpl = defaultSpawn,
|
|
98
|
-
cwd,
|
|
99
|
-
timeoutMs = DEFAULT_RUN_CHILD_TIMEOUT_MS,
|
|
100
|
-
}) {
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
let child;
|
|
103
|
-
try {
|
|
104
|
-
child = spawnImpl(cmd, args, {
|
|
105
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
-
cwd,
|
|
107
|
-
});
|
|
108
|
-
} catch (err) {
|
|
109
|
-
resolve({
|
|
110
|
-
code: null,
|
|
111
|
-
stdout: '',
|
|
112
|
-
stderr: '',
|
|
113
|
-
spawnError: err,
|
|
114
|
-
timedOut: false,
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
let stdout = '';
|
|
119
|
-
let stderr = '';
|
|
120
|
-
let spawnError = null;
|
|
121
|
-
let settled = false;
|
|
122
|
-
let timer = null;
|
|
123
|
-
const finish = (result) => {
|
|
124
|
-
if (settled) return;
|
|
125
|
-
settled = true;
|
|
126
|
-
if (timer) clearTimeout(timer);
|
|
127
|
-
resolve(result);
|
|
128
|
-
};
|
|
129
|
-
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
130
|
-
timer = setTimeout(() => {
|
|
131
|
-
try {
|
|
132
|
-
child.kill?.('SIGKILL');
|
|
133
|
-
} catch {
|
|
134
|
-
// already dead / stub child
|
|
135
|
-
}
|
|
136
|
-
finish({
|
|
137
|
-
code: null,
|
|
138
|
-
stdout,
|
|
139
|
-
stderr,
|
|
140
|
-
spawnError: Object.assign(
|
|
141
|
-
new Error(
|
|
142
|
-
`child process '${cmd}' exceeded ${timeoutMs}ms and was killed`,
|
|
143
|
-
),
|
|
144
|
-
{ code: 'ETIMEDOUT' },
|
|
145
|
-
),
|
|
146
|
-
timedOut: true,
|
|
147
|
-
});
|
|
148
|
-
}, timeoutMs);
|
|
149
|
-
// Deliberately NOT unref'd: a child whose handles close early (or a
|
|
150
|
-
// stub) leaves the loop idle, and an unref'd watchdog would never fire.
|
|
151
|
-
// `finish()` always clears it.
|
|
152
|
-
}
|
|
153
|
-
child.stdout?.on('data', (chunk) => {
|
|
154
|
-
stdout += chunk.toString();
|
|
155
|
-
});
|
|
156
|
-
child.stderr?.on('data', (chunk) => {
|
|
157
|
-
stderr += chunk.toString();
|
|
158
|
-
});
|
|
159
|
-
child.on('error', (err) => {
|
|
160
|
-
spawnError = err;
|
|
161
|
-
});
|
|
162
|
-
child.on('close', (code) => {
|
|
163
|
-
finish({ code, stdout, stderr, spawnError, timedOut: false });
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
100
|
/**
|
|
169
101
|
* Opt-in `delivery.feedbackLoop.<key>` reader: only an explicit `true`
|
|
170
102
|
* enables filing, because unattended filings were dominated by noise.
|
|
@@ -178,416 +110,6 @@ export function makeIsAutoFileEnabled(toggleKey) {
|
|
|
178
110
|
};
|
|
179
111
|
}
|
|
180
112
|
|
|
181
|
-
/**
|
|
182
|
-
* `git cat-file -e <ref>:<path>`; a spawn failure/timeout is `probeError`,
|
|
183
|
-
* distinct from a confirmed-missing file.
|
|
184
|
-
*
|
|
185
|
-
* @param {object} opts
|
|
186
|
-
* @param {string} opts.ref
|
|
187
|
-
* @param {string} opts.path
|
|
188
|
-
* @param {Function} [opts.spawnImpl]
|
|
189
|
-
* @param {string} [opts.cwd]
|
|
190
|
-
* @param {number} [opts.timeoutMs]
|
|
191
|
-
* @returns {Promise<{ exists: boolean, probeError: boolean }>}
|
|
192
|
-
*/
|
|
193
|
-
export async function probePathStatus({
|
|
194
|
-
ref,
|
|
195
|
-
path,
|
|
196
|
-
spawnImpl,
|
|
197
|
-
cwd,
|
|
198
|
-
timeoutMs,
|
|
199
|
-
}) {
|
|
200
|
-
const res = await runChild({
|
|
201
|
-
cmd: 'git',
|
|
202
|
-
args: ['cat-file', '-e', `${ref}:${path}`],
|
|
203
|
-
spawnImpl,
|
|
204
|
-
cwd,
|
|
205
|
-
timeoutMs,
|
|
206
|
-
});
|
|
207
|
-
if (res.spawnError || res.timedOut) {
|
|
208
|
-
return { exists: false, probeError: true };
|
|
209
|
-
}
|
|
210
|
-
return { exists: res.code === 0, probeError: false };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* GitHub search indexes the text inside an HTML-comment marker, but a query
|
|
215
|
-
* carrying the `<!--`/`-->` delimiters never matches it; strip them.
|
|
216
|
-
*
|
|
217
|
-
* @param {string} marker
|
|
218
|
-
* @returns {string}
|
|
219
|
-
*/
|
|
220
|
-
function normalizeMarkerQuery(marker) {
|
|
221
|
-
if (typeof marker !== 'string') return '';
|
|
222
|
-
return marker.replaceAll('<!--', '').replaceAll('-->', '').trim();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* `state` defaults to `''`, never `'open'`: an unknown state must not
|
|
227
|
-
* authorize editing an issue.
|
|
228
|
-
*
|
|
229
|
-
* @param {object} row
|
|
230
|
-
* @returns {{ number: number|null, state: string, url: string }}
|
|
231
|
-
*/
|
|
232
|
-
function toFollowUpRef(row) {
|
|
233
|
-
const number = Number(row?.number);
|
|
234
|
-
return {
|
|
235
|
-
number: Number.isInteger(number) && number > 0 ? number : null,
|
|
236
|
-
state: String(row?.state ?? '').toLowerCase(),
|
|
237
|
-
url: typeof row?.url === 'string' ? row.url : '',
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* `null` on no match OR an undecidable probe — degrade toward filing: a
|
|
243
|
-
* duplicate beats a swallowed finding.
|
|
244
|
-
*
|
|
245
|
-
* @returns {Promise<{ number: number|null, state: string, url: string }|null>}
|
|
246
|
-
*/
|
|
247
|
-
async function searchFollowUpByMarker({
|
|
248
|
-
marker,
|
|
249
|
-
owner,
|
|
250
|
-
repo,
|
|
251
|
-
ghPath,
|
|
252
|
-
spawnImpl,
|
|
253
|
-
cwd,
|
|
254
|
-
timeoutMs,
|
|
255
|
-
}) {
|
|
256
|
-
const args = [
|
|
257
|
-
'search',
|
|
258
|
-
'issues',
|
|
259
|
-
normalizeMarkerQuery(marker),
|
|
260
|
-
'--repo',
|
|
261
|
-
`${owner}/${repo}`,
|
|
262
|
-
'--json',
|
|
263
|
-
'number,state,url',
|
|
264
|
-
'--limit',
|
|
265
|
-
'1',
|
|
266
|
-
];
|
|
267
|
-
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd, timeoutMs });
|
|
268
|
-
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const parsed = JSON.parse(res.stdout || '[]');
|
|
273
|
-
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
274
|
-
return toFollowUpRef(parsed[0]);
|
|
275
|
-
} catch {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** @returns {Promise<boolean>} */
|
|
281
|
-
export async function probeMarkerExists(opts) {
|
|
282
|
-
return (await searchFollowUpByMarker(opts)) !== null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Strongly-consistent last gate before creating: the search index can lag
|
|
287
|
-
* long enough to miss a duplicate filed seconds earlier, while a
|
|
288
|
-
* label-scoped `gh issue list --state all` cannot. Any of `markers` matching
|
|
289
|
-
* counts, so a marker-format change does not re-file the backlog. `null` on
|
|
290
|
-
* no match or error (degrade toward filing).
|
|
291
|
-
*
|
|
292
|
-
* @param {object} opts
|
|
293
|
-
* @param {string[]} opts.markers
|
|
294
|
-
* @param {string} opts.owner
|
|
295
|
-
* @param {string} opts.repo
|
|
296
|
-
* @param {string[]} [opts.labels]
|
|
297
|
-
* @param {string} [opts.ghPath]
|
|
298
|
-
* @param {Function} [opts.spawnImpl]
|
|
299
|
-
* @param {string} [opts.cwd]
|
|
300
|
-
* @param {number} [opts.timeoutMs]
|
|
301
|
-
* @returns {Promise<{ number: number|null, state: string, url: string }|null>}
|
|
302
|
-
*/
|
|
303
|
-
async function findExistingFollowUp({
|
|
304
|
-
markers,
|
|
305
|
-
owner,
|
|
306
|
-
repo,
|
|
307
|
-
labels,
|
|
308
|
-
ghPath,
|
|
309
|
-
spawnImpl,
|
|
310
|
-
cwd,
|
|
311
|
-
timeoutMs,
|
|
312
|
-
}) {
|
|
313
|
-
const tokens = (Array.isArray(markers) ? markers : []).filter(
|
|
314
|
-
(m) => typeof m === 'string' && m.length > 0,
|
|
315
|
-
);
|
|
316
|
-
if (tokens.length === 0) return null;
|
|
317
|
-
const args = [
|
|
318
|
-
'issue',
|
|
319
|
-
'list',
|
|
320
|
-
'--repo',
|
|
321
|
-
`${owner}/${repo}`,
|
|
322
|
-
'--state',
|
|
323
|
-
'all',
|
|
324
|
-
'--json',
|
|
325
|
-
'number,body,state,url',
|
|
326
|
-
];
|
|
327
|
-
for (const label of Array.isArray(labels) ? labels : []) {
|
|
328
|
-
args.push('--label', label);
|
|
329
|
-
}
|
|
330
|
-
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd, timeoutMs });
|
|
331
|
-
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
let parsed;
|
|
335
|
-
try {
|
|
336
|
-
parsed = JSON.parse(res.stdout || '[]');
|
|
337
|
-
} catch {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
if (!Array.isArray(parsed)) return null;
|
|
341
|
-
const matches = parsed.filter(
|
|
342
|
-
(issue) =>
|
|
343
|
-
typeof issue?.body === 'string' &&
|
|
344
|
-
tokens.some((token) => issue.body.includes(token)),
|
|
345
|
-
);
|
|
346
|
-
if (matches.length === 0) return null;
|
|
347
|
-
// Prefer an open match: a closed one is a decided follow-up.
|
|
348
|
-
const open = matches.find(
|
|
349
|
-
(issue) => String(issue?.state ?? '').toLowerCase() === 'open',
|
|
350
|
-
);
|
|
351
|
-
return toFollowUpRef(open ?? matches[0]);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Recurrence path: refresh the body only, leaving human-curated labels,
|
|
356
|
-
* title, assignees and state untouched.
|
|
357
|
-
*
|
|
358
|
-
* @returns {Promise<{ url: string|null, error: string|null }>}
|
|
359
|
-
*/
|
|
360
|
-
export async function updateFollowUpIssue({
|
|
361
|
-
owner,
|
|
362
|
-
repo,
|
|
363
|
-
number,
|
|
364
|
-
body,
|
|
365
|
-
ghPath,
|
|
366
|
-
spawnImpl,
|
|
367
|
-
cwd,
|
|
368
|
-
timeoutMs,
|
|
369
|
-
}) {
|
|
370
|
-
const res = await runChild({
|
|
371
|
-
cmd: ghPath,
|
|
372
|
-
args: [
|
|
373
|
-
'issue',
|
|
374
|
-
'edit',
|
|
375
|
-
String(number),
|
|
376
|
-
'--repo',
|
|
377
|
-
`${owner}/${repo}`,
|
|
378
|
-
'--body',
|
|
379
|
-
body,
|
|
380
|
-
],
|
|
381
|
-
spawnImpl,
|
|
382
|
-
cwd,
|
|
383
|
-
timeoutMs,
|
|
384
|
-
});
|
|
385
|
-
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
386
|
-
return {
|
|
387
|
-
url: null,
|
|
388
|
-
error: res.spawnError
|
|
389
|
-
? `gh issue edit spawn failed: ${res.spawnError.message}`
|
|
390
|
-
: `gh issue edit exited ${res.code}: ${(res.stderr || '').trim()}`,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
return { url: (res.stdout || '').trim(), error: null };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const FRICTION_LABEL_PREFIX = 'friction::';
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Only `meta::*` and `friction::<category>` labels reach this path.
|
|
400
|
-
*
|
|
401
|
-
* @param {string} name
|
|
402
|
-
* @returns {{ color: string, description: string }}
|
|
403
|
-
*/
|
|
404
|
-
function describeMintedLabel(name) {
|
|
405
|
-
if (name.startsWith(FRICTION_LABEL_PREFIX)) {
|
|
406
|
-
return {
|
|
407
|
-
color: LABEL_COLORS.FRICTION,
|
|
408
|
-
description: `Recurring friction category "${name.slice(FRICTION_LABEL_PREFIX.length)}" (minted by the feedback loop)`,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return {
|
|
412
|
-
color: LABEL_COLORS.META,
|
|
413
|
-
description: 'Feedback-loop routing axis (minted by the feedback loop)',
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Memoized per repo. `known: null` means unverifiable, not "no labels".
|
|
419
|
-
*
|
|
420
|
-
* @returns {Promise<{ known: Set<string>|null, error: string|null }>}
|
|
421
|
-
*/
|
|
422
|
-
async function readLiveLabelNames({
|
|
423
|
-
owner,
|
|
424
|
-
repo,
|
|
425
|
-
labelCache,
|
|
426
|
-
ghPath,
|
|
427
|
-
spawnImpl,
|
|
428
|
-
cwd,
|
|
429
|
-
timeoutMs,
|
|
430
|
-
}) {
|
|
431
|
-
const key = `${owner}/${repo}`;
|
|
432
|
-
const cached = labelCache?.get(key);
|
|
433
|
-
if (cached) return { known: cached, error: null };
|
|
434
|
-
const res = await runChild({
|
|
435
|
-
cmd: ghPath,
|
|
436
|
-
args: ['label', 'list', '--repo', key, '--limit', '500', '--json', 'name'],
|
|
437
|
-
spawnImpl,
|
|
438
|
-
cwd,
|
|
439
|
-
timeoutMs,
|
|
440
|
-
});
|
|
441
|
-
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
442
|
-
return {
|
|
443
|
-
known: null,
|
|
444
|
-
error: `gh label list ${key} failed: ${res.spawnError?.message ?? (res.stderr || '').trim()}`,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
let parsed;
|
|
448
|
-
try {
|
|
449
|
-
parsed = JSON.parse(res.stdout || '[]');
|
|
450
|
-
} catch {
|
|
451
|
-
return {
|
|
452
|
-
known: null,
|
|
453
|
-
error: `gh label list ${key} returned unparseable JSON`,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
if (!Array.isArray(parsed)) {
|
|
457
|
-
return { known: null, error: `gh label list ${key} returned a non-array` };
|
|
458
|
-
}
|
|
459
|
-
const known = new Set();
|
|
460
|
-
for (const row of parsed) {
|
|
461
|
-
if (row && typeof row.name === 'string') known.add(row.name);
|
|
462
|
-
}
|
|
463
|
-
labelCache?.set(key, known);
|
|
464
|
-
return { known, error: null };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Mint absent labels: `gh issue create` fails the whole call on any unknown
|
|
469
|
-
* `--label`, and `friction::<category>` names come from live telemetry so no
|
|
470
|
-
* bootstrap can pre-create them. An unreadable live set reports nothing
|
|
471
|
-
* `missing`, so the caller still attempts the filing.
|
|
472
|
-
*
|
|
473
|
-
* @param {object} opts
|
|
474
|
-
* @param {string} opts.owner
|
|
475
|
-
* @param {string} opts.repo
|
|
476
|
-
* @param {string[]} opts.labels
|
|
477
|
-
* @param {Map<string, Set<string>>} [opts.labelCache]
|
|
478
|
-
* @param {string} [opts.ghPath]
|
|
479
|
-
* @param {Function} [opts.spawnImpl]
|
|
480
|
-
* @param {string} [opts.cwd]
|
|
481
|
-
* @param {number} [opts.timeoutMs]
|
|
482
|
-
* @returns {Promise<{ created: string[], missing: string[], errors: string[] }>}
|
|
483
|
-
*/
|
|
484
|
-
export async function ensureIssueLabels({
|
|
485
|
-
owner,
|
|
486
|
-
repo,
|
|
487
|
-
labels,
|
|
488
|
-
labelCache,
|
|
489
|
-
ghPath = 'gh',
|
|
490
|
-
spawnImpl,
|
|
491
|
-
cwd,
|
|
492
|
-
timeoutMs,
|
|
493
|
-
}) {
|
|
494
|
-
const wanted = (Array.isArray(labels) ? labels : []).filter(
|
|
495
|
-
(name) => typeof name === 'string' && name.trim().length > 0,
|
|
496
|
-
);
|
|
497
|
-
if (wanted.length === 0) return { created: [], missing: [], errors: [] };
|
|
498
|
-
|
|
499
|
-
const { known, error } = await readLiveLabelNames({
|
|
500
|
-
owner,
|
|
501
|
-
repo,
|
|
502
|
-
labelCache,
|
|
503
|
-
ghPath,
|
|
504
|
-
spawnImpl,
|
|
505
|
-
cwd,
|
|
506
|
-
timeoutMs,
|
|
507
|
-
});
|
|
508
|
-
if (!known) return { created: [], missing: [], errors: error ? [error] : [] };
|
|
509
|
-
|
|
510
|
-
const created = [];
|
|
511
|
-
const missing = [];
|
|
512
|
-
const errors = [];
|
|
513
|
-
for (const name of wanted) {
|
|
514
|
-
if (known.has(name)) continue;
|
|
515
|
-
const { color, description } = describeMintedLabel(name);
|
|
516
|
-
const res = await runChild({
|
|
517
|
-
cmd: ghPath,
|
|
518
|
-
args: [
|
|
519
|
-
'label',
|
|
520
|
-
'create',
|
|
521
|
-
name,
|
|
522
|
-
'--repo',
|
|
523
|
-
`${owner}/${repo}`,
|
|
524
|
-
'--color',
|
|
525
|
-
color.replace(/^#/, ''),
|
|
526
|
-
'--description',
|
|
527
|
-
description,
|
|
528
|
-
],
|
|
529
|
-
spawnImpl,
|
|
530
|
-
cwd,
|
|
531
|
-
timeoutMs,
|
|
532
|
-
});
|
|
533
|
-
const failed =
|
|
534
|
-
Boolean(res.spawnError) ||
|
|
535
|
-
(typeof res.code === 'number' && res.code !== 0);
|
|
536
|
-
// A concurrent mint between list and create is success.
|
|
537
|
-
const raced = /label\b[\s\S]*?already exists/i.test(
|
|
538
|
-
`${res.stderr ?? ''}${res.spawnError?.message ?? ''}`,
|
|
539
|
-
);
|
|
540
|
-
if (!failed || raced) {
|
|
541
|
-
known.add(name);
|
|
542
|
-
if (!raced) created.push(name);
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
missing.push(name);
|
|
546
|
-
errors.push(
|
|
547
|
-
`gh label create "${name}" in ${owner}/${repo} failed: ${res.spawnError?.message ?? (res.stderr || '').trim()}`,
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
return { created, missing, errors };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** Resolves `{ url, error }`; exactly one is non-null. */
|
|
554
|
-
export async function createFollowUpIssue({
|
|
555
|
-
owner,
|
|
556
|
-
repo,
|
|
557
|
-
title,
|
|
558
|
-
body,
|
|
559
|
-
labels,
|
|
560
|
-
ghPath,
|
|
561
|
-
spawnImpl,
|
|
562
|
-
cwd,
|
|
563
|
-
timeoutMs,
|
|
564
|
-
}) {
|
|
565
|
-
const args = [
|
|
566
|
-
'issue',
|
|
567
|
-
'create',
|
|
568
|
-
'--repo',
|
|
569
|
-
`${owner}/${repo}`,
|
|
570
|
-
'--title',
|
|
571
|
-
title,
|
|
572
|
-
'--body',
|
|
573
|
-
body,
|
|
574
|
-
];
|
|
575
|
-
for (const label of labels) {
|
|
576
|
-
args.push('--label', label);
|
|
577
|
-
}
|
|
578
|
-
const res = await runChild({ cmd: ghPath, args, spawnImpl, cwd, timeoutMs });
|
|
579
|
-
if (res.spawnError || (typeof res.code === 'number' && res.code !== 0)) {
|
|
580
|
-
return {
|
|
581
|
-
url: null,
|
|
582
|
-
error: res.spawnError
|
|
583
|
-
? `gh issue create spawn failed: ${res.spawnError.message}`
|
|
584
|
-
: `gh issue create exited ${res.code}: ${(res.stderr || '').trim()}`,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
const url = (res.stdout || '').trim();
|
|
588
|
-
return { url, error: null };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
113
|
/**
|
|
592
114
|
* `null` when all pass, else a partial envelope to short-circuit on. The
|
|
593
115
|
* provider is deliberately not gated: only the best-effort cross-repo upsert
|
|
@@ -610,125 +132,8 @@ function checkGraduatePreconditions({ epicId, currentRepo, config, spec }) {
|
|
|
610
132
|
return null;
|
|
611
133
|
}
|
|
612
134
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
* pre-fingerprint filings are not re-filed. `existing` identifies the match
|
|
616
|
-
* so a recurrence can update it.
|
|
617
|
-
*/
|
|
618
|
-
async function resolveAlreadyFiled({
|
|
619
|
-
finding,
|
|
620
|
-
epicId,
|
|
621
|
-
routedRepo,
|
|
622
|
-
contentMarker,
|
|
623
|
-
ghPath,
|
|
624
|
-
spawnImpl,
|
|
625
|
-
cwd,
|
|
626
|
-
timeoutMs,
|
|
627
|
-
spec,
|
|
628
|
-
}) {
|
|
629
|
-
const probe = (marker) =>
|
|
630
|
-
searchFollowUpByMarker({
|
|
631
|
-
marker,
|
|
632
|
-
owner: routedRepo.owner,
|
|
633
|
-
repo: routedRepo.repo,
|
|
634
|
-
ghPath,
|
|
635
|
-
spawnImpl,
|
|
636
|
-
cwd,
|
|
637
|
-
timeoutMs,
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
const hit = await probe(contentMarker);
|
|
641
|
-
if (hit) return { alreadyFiled: true, existing: hit };
|
|
642
|
-
if (typeof spec.buildLegacyMarker === 'function') {
|
|
643
|
-
const legacyMarker = spec.buildLegacyMarker(epicId, finding.index);
|
|
644
|
-
if (legacyMarker) {
|
|
645
|
-
const legacyHit = await probe(legacyMarker);
|
|
646
|
-
if (legacyHit) return { alreadyFiled: true, existing: legacyHit };
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return { alreadyFiled: false, existing: null };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* An open, numbered match is refreshed and recorded on `filed` as
|
|
654
|
-
* `action: 'updated'` (a live write, counted like a creation). Anything else
|
|
655
|
-
* is `already-filed` and untouched: a closed follow-up is a human's decision.
|
|
656
|
-
*
|
|
657
|
-
* @returns {Promise<boolean>} `true` when the recurrence was handled here.
|
|
658
|
-
*/
|
|
659
|
-
async function resolveFollowUpRecurrence({
|
|
660
|
-
existing,
|
|
661
|
-
body,
|
|
662
|
-
finding,
|
|
663
|
-
source,
|
|
664
|
-
routedRepo,
|
|
665
|
-
envelope,
|
|
666
|
-
decorate,
|
|
667
|
-
skip,
|
|
668
|
-
ghPath,
|
|
669
|
-
spawnImpl,
|
|
670
|
-
cwd,
|
|
671
|
-
timeoutMs,
|
|
672
|
-
}) {
|
|
673
|
-
if (existing.state !== 'open' || existing.number === null) {
|
|
674
|
-
skip('already-filed');
|
|
675
|
-
return true;
|
|
676
|
-
}
|
|
677
|
-
const updated = await updateFollowUpIssue({
|
|
678
|
-
owner: routedRepo.owner,
|
|
679
|
-
repo: routedRepo.repo,
|
|
680
|
-
number: existing.number,
|
|
681
|
-
body,
|
|
682
|
-
ghPath,
|
|
683
|
-
spawnImpl,
|
|
684
|
-
cwd,
|
|
685
|
-
timeoutMs,
|
|
686
|
-
});
|
|
687
|
-
if (updated.error) {
|
|
688
|
-
envelope.errors.push(
|
|
689
|
-
`finding ${finding.index} (${finding.path}): ${updated.error}`,
|
|
690
|
-
);
|
|
691
|
-
return true;
|
|
692
|
-
}
|
|
693
|
-
envelope.filed.push(
|
|
694
|
-
decorate(
|
|
695
|
-
{
|
|
696
|
-
index: finding.index,
|
|
697
|
-
action: 'updated',
|
|
698
|
-
issueNumber: existing.number,
|
|
699
|
-
severity: finding.severity,
|
|
700
|
-
path: finding.path,
|
|
701
|
-
source,
|
|
702
|
-
repo: `${routedRepo.owner}/${routedRepo.repo}`,
|
|
703
|
-
url: updated.url || existing.url || null,
|
|
704
|
-
},
|
|
705
|
-
finding,
|
|
706
|
-
),
|
|
707
|
-
);
|
|
708
|
-
return true;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
async function processGraduateFinding({
|
|
712
|
-
finding,
|
|
713
|
-
envelope,
|
|
714
|
-
decorate,
|
|
715
|
-
epicId,
|
|
716
|
-
currentRepo,
|
|
717
|
-
repos,
|
|
718
|
-
classifier,
|
|
719
|
-
gitRef,
|
|
720
|
-
ghPath,
|
|
721
|
-
spawnImpl,
|
|
722
|
-
cwd,
|
|
723
|
-
timeoutMs,
|
|
724
|
-
maxFilingsPerRun,
|
|
725
|
-
crossRepoDeferred,
|
|
726
|
-
filedMarkers,
|
|
727
|
-
labelCache,
|
|
728
|
-
logger,
|
|
729
|
-
spec,
|
|
730
|
-
}) {
|
|
731
|
-
const skip = (reason) =>
|
|
135
|
+
function makeSkip({ envelope, decorate, finding }) {
|
|
136
|
+
return (reason) =>
|
|
732
137
|
envelope.skipped.push(
|
|
733
138
|
decorate(
|
|
734
139
|
{
|
|
@@ -740,174 +145,36 @@ async function processGraduateFinding({
|
|
|
740
145
|
finding,
|
|
741
146
|
),
|
|
742
147
|
);
|
|
148
|
+
}
|
|
743
149
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
path: finding.path,
|
|
751
|
-
spawnImpl,
|
|
752
|
-
cwd,
|
|
753
|
-
timeoutMs,
|
|
754
|
-
});
|
|
755
|
-
if (probeError) return skip('probe-error');
|
|
756
|
-
if (!exists) return skip('file-removed');
|
|
757
|
-
}
|
|
150
|
+
/** Walk one finding; every outcome lands on `ctx.envelope`. */
|
|
151
|
+
async function processGraduateFinding(ctx) {
|
|
152
|
+
const { finding, epicId, spec } = ctx;
|
|
153
|
+
const skip = makeSkip(ctx);
|
|
154
|
+
const pathSkip = await probeFindingPath(ctx);
|
|
155
|
+
if (pathSkip) return skip(pathSkip);
|
|
758
156
|
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (!routing.routable) {
|
|
764
|
-
const logLine = `[${spec.fnName}] unroutable ${source} finding (${routing.missingKey} is unset) — not filed: ${finding.title ?? finding.path ?? `finding ${finding.index}`}`;
|
|
765
|
-
logger?.warn?.(logLine);
|
|
766
|
-
crossRepoDeferred.push({
|
|
767
|
-
finding,
|
|
768
|
-
routedRepo: null,
|
|
769
|
-
source,
|
|
770
|
-
logLine,
|
|
771
|
-
missingKey: routing.missingKey,
|
|
772
|
-
});
|
|
773
|
-
return skip('unroutable');
|
|
774
|
-
}
|
|
775
|
-
const routedRepo = routing.routedRepo;
|
|
776
|
-
if (routing.crossRepo) {
|
|
777
|
-
const logLine = spec.buildCrossRepoLog({ finding, routedRepo, source });
|
|
778
|
-
logger?.info?.(logLine);
|
|
779
|
-
crossRepoDeferred.push({ finding, routedRepo, source, logLine });
|
|
780
|
-
return skip('cross-repo-deferred');
|
|
157
|
+
const route = routeFinding(ctx);
|
|
158
|
+
if (route.deferred) {
|
|
159
|
+
ctx.crossRepoDeferred.push(route.deferred);
|
|
160
|
+
return skip(route.skipReason);
|
|
781
161
|
}
|
|
782
162
|
|
|
783
163
|
const contentMarker = spec.buildContentMarker(epicId, finding);
|
|
784
|
-
|
|
785
164
|
// In-process memo closes the same-invocation race the search index cannot.
|
|
786
|
-
if (filedMarkers?.has(contentMarker)) return skip('already-filed');
|
|
787
|
-
|
|
788
|
-
const { alreadyFiled, existing } = await resolveAlreadyFiled({
|
|
789
|
-
finding,
|
|
790
|
-
epicId,
|
|
791
|
-
routedRepo,
|
|
792
|
-
contentMarker,
|
|
793
|
-
ghPath,
|
|
794
|
-
spawnImpl,
|
|
795
|
-
cwd,
|
|
796
|
-
timeoutMs,
|
|
797
|
-
spec,
|
|
798
|
-
});
|
|
165
|
+
if (ctx.filedMarkers?.has(contentMarker)) return skip('already-filed');
|
|
799
166
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
167
|
+
const walk = { ...ctx, ...route, contentMarker, skip };
|
|
168
|
+
const existing = await dedupFinding(walk);
|
|
169
|
+
// The strong read scopes by these labels and a recurrence writes this body.
|
|
170
|
+
const followUp = spec.buildFollowUp({
|
|
803
171
|
finding,
|
|
804
|
-
source,
|
|
172
|
+
source: route.source,
|
|
805
173
|
epicId,
|
|
806
174
|
idMarker: contentMarker,
|
|
807
175
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
filedMarkers?.add(contentMarker);
|
|
811
|
-
await resolveFollowUpRecurrence({
|
|
812
|
-
existing,
|
|
813
|
-
body,
|
|
814
|
-
finding,
|
|
815
|
-
source,
|
|
816
|
-
routedRepo,
|
|
817
|
-
envelope,
|
|
818
|
-
decorate,
|
|
819
|
-
skip,
|
|
820
|
-
ghPath,
|
|
821
|
-
spawnImpl,
|
|
822
|
-
cwd,
|
|
823
|
-
timeoutMs,
|
|
824
|
-
});
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
if (envelope.filed.length >= maxFilingsPerRun) return skip('cap-reached');
|
|
829
|
-
|
|
830
|
-
// Before the strong read too: `gh issue list --label <absent>` exits 0
|
|
831
|
-
// with `[]`, silently defeating the dedup confirm.
|
|
832
|
-
const ensured = await ensureIssueLabels({
|
|
833
|
-
owner: routedRepo.owner,
|
|
834
|
-
repo: routedRepo.repo,
|
|
835
|
-
labels,
|
|
836
|
-
labelCache,
|
|
837
|
-
ghPath,
|
|
838
|
-
spawnImpl,
|
|
839
|
-
cwd,
|
|
840
|
-
timeoutMs,
|
|
841
|
-
});
|
|
842
|
-
envelope.errors.push(...ensured.errors);
|
|
843
|
-
if (ensured.missing.length > 0) {
|
|
844
|
-
return skip('label-ensure-failed');
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
const confirmed = await findExistingFollowUp({
|
|
848
|
-
markers:
|
|
849
|
-
typeof spec.buildMatchTokens === 'function'
|
|
850
|
-
? spec.buildMatchTokens({ epicId, finding, contentMarker })
|
|
851
|
-
: [contentMarker],
|
|
852
|
-
owner: routedRepo.owner,
|
|
853
|
-
repo: routedRepo.repo,
|
|
854
|
-
labels,
|
|
855
|
-
ghPath,
|
|
856
|
-
spawnImpl,
|
|
857
|
-
cwd,
|
|
858
|
-
timeoutMs,
|
|
859
|
-
});
|
|
860
|
-
if (confirmed) {
|
|
861
|
-
filedMarkers?.add(contentMarker);
|
|
862
|
-
await resolveFollowUpRecurrence({
|
|
863
|
-
existing: confirmed,
|
|
864
|
-
body,
|
|
865
|
-
finding,
|
|
866
|
-
source,
|
|
867
|
-
routedRepo,
|
|
868
|
-
envelope,
|
|
869
|
-
decorate,
|
|
870
|
-
skip,
|
|
871
|
-
ghPath,
|
|
872
|
-
spawnImpl,
|
|
873
|
-
cwd,
|
|
874
|
-
timeoutMs,
|
|
875
|
-
});
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const created = await createFollowUpIssue({
|
|
880
|
-
owner: routedRepo.owner,
|
|
881
|
-
repo: routedRepo.repo,
|
|
882
|
-
title,
|
|
883
|
-
body,
|
|
884
|
-
labels,
|
|
885
|
-
ghPath,
|
|
886
|
-
spawnImpl,
|
|
887
|
-
cwd,
|
|
888
|
-
timeoutMs,
|
|
889
|
-
});
|
|
890
|
-
if (created.error) {
|
|
891
|
-
envelope.errors.push(
|
|
892
|
-
`finding ${finding.index} (${finding.path}): ${created.error}`,
|
|
893
|
-
);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
filedMarkers?.add(contentMarker);
|
|
897
|
-
envelope.filed.push(
|
|
898
|
-
decorate(
|
|
899
|
-
{
|
|
900
|
-
index: finding.index,
|
|
901
|
-
action: 'created',
|
|
902
|
-
severity: finding.severity,
|
|
903
|
-
path: finding.path,
|
|
904
|
-
source,
|
|
905
|
-
repo: `${routedRepo.owner}/${routedRepo.repo}`,
|
|
906
|
-
url: created.url,
|
|
907
|
-
},
|
|
908
|
-
finding,
|
|
909
|
-
),
|
|
910
|
-
);
|
|
176
|
+
if (existing) return recordRecurrence(walk, existing, followUp.body);
|
|
177
|
+
return fileFinding(walk, followUp);
|
|
911
178
|
}
|
|
912
179
|
|
|
913
180
|
function renderCrossRepoDeferredBody(deferred, spec) {
|
|
@@ -1062,6 +329,7 @@ export async function graduate({
|
|
|
1062
329
|
framework: frameworkRepo ?? null,
|
|
1063
330
|
platform: platformRepo ?? null,
|
|
1064
331
|
};
|
|
332
|
+
const gh = { ghPath, spawnImpl, cwd, timeoutMs };
|
|
1065
333
|
const crossRepoDeferred = [];
|
|
1066
334
|
for (const finding of findings) {
|
|
1067
335
|
await processGraduateFinding({
|
|
@@ -1073,10 +341,7 @@ export async function graduate({
|
|
|
1073
341
|
repos,
|
|
1074
342
|
classifier,
|
|
1075
343
|
gitRef,
|
|
1076
|
-
|
|
1077
|
-
spawnImpl,
|
|
1078
|
-
cwd,
|
|
1079
|
-
timeoutMs,
|
|
344
|
+
gh,
|
|
1080
345
|
maxFilingsPerRun,
|
|
1081
346
|
crossRepoDeferred,
|
|
1082
347
|
filedMarkers,
|