mandrel 1.61.0 → 1.63.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/docs/SDLC.md +10 -3
- package/.agents/docs/workflows.md +1 -1
- package/.agents/scripts/check-action-pinning.js +260 -0
- package/.agents/scripts/check-arch-cycles.js +38 -14
- package/.agents/scripts/epic-deliver-prepare.js +149 -104
- package/.agents/scripts/lib/baseline-snapshot.js +245 -141
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +171 -137
- package/.agents/scripts/lib/orchestration/code-review.js +206 -168
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +71 -5
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +16 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +101 -1
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +20 -42
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +12 -32
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +97 -60
- package/.agents/scripts/lib/orchestration/model-attribution.js +73 -45
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +97 -49
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +73 -69
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +109 -79
- package/.agents/scripts/lib/signals/detectors/common.js +107 -0
- package/.agents/scripts/lib/signals/detectors/hotspot.js +12 -18
- package/.agents/scripts/lib/signals/detectors/retry.js +3 -40
- package/.agents/scripts/lib/signals/detectors/rework.js +3 -40
- package/.agents/scripts/lib/story-body/story-body.js +102 -76
- package/.agents/scripts/providers/github/blocked-by-add.js +252 -0
- package/.agents/scripts/single-story-init.js +16 -3
- package/.agents/workflows/audit-architecture.md +9 -0
- package/.agents/workflows/deliver.md +87 -26
- package/.agents/workflows/helpers/deliver-epic.md +12 -5
- package/.agents/workflows/helpers/deliver-stories.md +13 -7
- package/.agents/workflows/plan.md +3 -1
- package/README.md +1 -1
- package/docs/CHANGELOG.md +40 -0
- package/lib/cli/registry.js +1 -1
- package/lib/cli/update.js +114 -8
- package/package.json +1 -1
|
@@ -238,183 +238,217 @@ export async function createFollowUpIssue({
|
|
|
238
238
|
* @param {(record: object, finding: object) => object} [opts.spec.decorateRecord]
|
|
239
239
|
* @returns {Promise<{ filed: object[], skipped: object[], errors: string[] }>}
|
|
240
240
|
*/
|
|
241
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Validate the `graduate` preconditions (toggle, epicId, provider shape,
|
|
243
|
+
* currentRepo shape). Returns `null` when all preconditions pass, or a
|
|
244
|
+
* `{ skipped?, errors? }` partial-envelope the caller short-circuits on.
|
|
245
|
+
* Story #4075 — extracted from `graduate` so the orchestrating body holds
|
|
246
|
+
* no guard-chain branching.
|
|
247
|
+
*/
|
|
248
|
+
function checkGraduatePreconditions({
|
|
242
249
|
epicId,
|
|
243
250
|
provider,
|
|
244
|
-
config,
|
|
245
251
|
currentRepo,
|
|
246
|
-
|
|
247
|
-
gitRef = 'HEAD',
|
|
248
|
-
classifier = defaultClassifier,
|
|
249
|
-
ghPath = 'gh',
|
|
250
|
-
spawnImpl,
|
|
251
|
-
cwd,
|
|
252
|
-
logger,
|
|
252
|
+
config,
|
|
253
253
|
spec,
|
|
254
254
|
}) {
|
|
255
|
-
const envelope = { filed: [], skipped: [], errors: [] };
|
|
256
|
-
const decorate =
|
|
257
|
-
typeof spec.decorateRecord === 'function'
|
|
258
|
-
? spec.decorateRecord
|
|
259
|
-
: (record) => record;
|
|
260
|
-
|
|
261
255
|
if (!spec.isAutoFileEnabled(config)) {
|
|
262
|
-
|
|
263
|
-
return envelope;
|
|
256
|
+
return { skipped: [{ reason: 'toggle-disabled' }] };
|
|
264
257
|
}
|
|
265
|
-
|
|
266
258
|
if (!Number.isInteger(epicId) || epicId < 1) {
|
|
267
|
-
|
|
268
|
-
return envelope;
|
|
259
|
+
return { errors: [`${spec.fnName}: missing or invalid epicId`] };
|
|
269
260
|
}
|
|
270
261
|
if (!provider || typeof provider.getTicketComments !== 'function') {
|
|
271
|
-
|
|
272
|
-
return envelope;
|
|
262
|
+
return { errors: [`${spec.fnName}: provider lacks getTicketComments`] };
|
|
273
263
|
}
|
|
274
264
|
if (
|
|
275
265
|
!currentRepo ||
|
|
276
266
|
typeof currentRepo.owner !== 'string' ||
|
|
277
267
|
typeof currentRepo.repo !== 'string'
|
|
278
268
|
) {
|
|
279
|
-
|
|
280
|
-
return envelope;
|
|
269
|
+
return { errors: [`${spec.fnName}: missing currentRepo {owner,repo}`] };
|
|
281
270
|
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
282
273
|
|
|
283
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Read the source structured comment off the Epic and parse its findings.
|
|
276
|
+
* Returns `{ findings }` on success, or `{ skipped?, errors? }` for the
|
|
277
|
+
* no-comment / parse-empty / fetch-error short-circuits. Story #4075 —
|
|
278
|
+
* extracted from `graduate`.
|
|
279
|
+
*/
|
|
280
|
+
async function loadGraduateFindings({ epicId, provider, spec }) {
|
|
284
281
|
let comments;
|
|
285
282
|
try {
|
|
286
283
|
comments = await provider.getTicketComments(epicId);
|
|
287
284
|
} catch (err) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (!Array.isArray(comments) || comments.length === 0) {
|
|
294
|
-
envelope.skipped.push({ reason: spec.noCommentReason });
|
|
295
|
-
return envelope;
|
|
285
|
+
return {
|
|
286
|
+
errors: [
|
|
287
|
+
`getTicketComments failed for epic #${epicId}: ${err?.message ?? err}`,
|
|
288
|
+
],
|
|
289
|
+
};
|
|
296
290
|
}
|
|
297
|
-
const matched = comments.filter(
|
|
291
|
+
const matched = (Array.isArray(comments) ? comments : []).filter(
|
|
298
292
|
(c) => typeof c?.body === 'string' && c.body.includes(spec.commentMarker),
|
|
299
293
|
);
|
|
300
294
|
if (matched.length === 0) {
|
|
301
|
-
|
|
302
|
-
return envelope;
|
|
295
|
+
return { skipped: [{ reason: spec.noCommentReason }] };
|
|
303
296
|
}
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
// 2. Parse findings.
|
|
307
|
-
const findings = spec.parseFindings(sourceComment.body);
|
|
297
|
+
const findings = spec.parseFindings(matched[matched.length - 1].body);
|
|
308
298
|
if (findings.length === 0) {
|
|
309
|
-
|
|
310
|
-
return envelope;
|
|
299
|
+
return { skipped: [{ reason: 'no-non-blocking-findings' }] };
|
|
311
300
|
}
|
|
301
|
+
return { findings };
|
|
302
|
+
}
|
|
312
303
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Route a single finding (path-exists probe → repo routing → idempotency
|
|
306
|
+
* probe → file) and fold the outcome into the running envelope. Story #4075
|
|
307
|
+
* — extracted from `graduate`'s per-finding loop body.
|
|
308
|
+
*/
|
|
309
|
+
async function processGraduateFinding({
|
|
310
|
+
finding,
|
|
311
|
+
envelope,
|
|
312
|
+
decorate,
|
|
313
|
+
epicId,
|
|
314
|
+
currentRepo,
|
|
315
|
+
frameworkRepo,
|
|
316
|
+
classifier,
|
|
317
|
+
gitRef,
|
|
318
|
+
ghPath,
|
|
319
|
+
spawnImpl,
|
|
320
|
+
cwd,
|
|
321
|
+
logger,
|
|
322
|
+
spec,
|
|
323
|
+
}) {
|
|
324
|
+
const skip = (reason) =>
|
|
325
|
+
envelope.skipped.push(
|
|
326
|
+
decorate(
|
|
327
|
+
{
|
|
328
|
+
index: finding.index,
|
|
329
|
+
reason,
|
|
330
|
+
path: finding.path,
|
|
331
|
+
severity: finding.severity,
|
|
332
|
+
},
|
|
333
|
+
finding,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
335
336
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
const exists = await probePathExists({
|
|
338
|
+
ref: gitRef,
|
|
339
|
+
path: finding.path,
|
|
340
|
+
spawnImpl,
|
|
341
|
+
cwd,
|
|
342
|
+
});
|
|
343
|
+
if (!exists) return skip('file-removed');
|
|
339
344
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
path: finding.path,
|
|
351
|
-
severity: finding.severity,
|
|
352
|
-
},
|
|
353
|
-
finding,
|
|
354
|
-
),
|
|
355
|
-
);
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
345
|
+
const source = classifier(finding.path, null);
|
|
346
|
+
const routedRepo =
|
|
347
|
+
source === 'framework' && frameworkRepo ? frameworkRepo : currentRepo;
|
|
348
|
+
const isCrossRepo =
|
|
349
|
+
routedRepo.owner !== currentRepo.owner ||
|
|
350
|
+
routedRepo.repo !== currentRepo.repo;
|
|
351
|
+
if (isCrossRepo) {
|
|
352
|
+
logger?.info?.(spec.buildCrossRepoLog({ finding, routedRepo, source }));
|
|
353
|
+
return skip('cross-repo-deferred');
|
|
354
|
+
}
|
|
358
355
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
356
|
+
const idMarker = spec.buildIdempotencyMarker(epicId, finding.index);
|
|
357
|
+
const alreadyFiled = await probeMarkerExists({
|
|
358
|
+
marker: idMarker,
|
|
359
|
+
owner: routedRepo.owner,
|
|
360
|
+
repo: routedRepo.repo,
|
|
361
|
+
ghPath,
|
|
362
|
+
spawnImpl,
|
|
363
|
+
cwd,
|
|
364
|
+
});
|
|
365
|
+
if (alreadyFiled) return skip('already-filed');
|
|
366
|
+
|
|
367
|
+
const { title, body, labels } = spec.buildFollowUp({
|
|
368
|
+
finding,
|
|
369
|
+
source,
|
|
370
|
+
epicId,
|
|
371
|
+
idMarker,
|
|
372
|
+
});
|
|
373
|
+
const created = await createFollowUpIssue({
|
|
374
|
+
owner: routedRepo.owner,
|
|
375
|
+
repo: routedRepo.repo,
|
|
376
|
+
title,
|
|
377
|
+
body,
|
|
378
|
+
labels,
|
|
379
|
+
ghPath,
|
|
380
|
+
spawnImpl,
|
|
381
|
+
cwd,
|
|
382
|
+
});
|
|
383
|
+
if (created.error) {
|
|
384
|
+
envelope.errors.push(
|
|
385
|
+
`finding ${finding.index} (${finding.path}): ${created.error}`,
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
envelope.filed.push(
|
|
390
|
+
decorate(
|
|
391
|
+
{
|
|
392
|
+
index: finding.index,
|
|
393
|
+
severity: finding.severity,
|
|
394
|
+
path: finding.path,
|
|
395
|
+
source,
|
|
396
|
+
repo: `${routedRepo.owner}/${routedRepo.repo}`,
|
|
397
|
+
url: created.url,
|
|
398
|
+
},
|
|
399
|
+
finding,
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function graduate({
|
|
405
|
+
epicId,
|
|
406
|
+
provider,
|
|
407
|
+
config,
|
|
408
|
+
currentRepo,
|
|
409
|
+
frameworkRepo,
|
|
410
|
+
gitRef = 'HEAD',
|
|
411
|
+
classifier = defaultClassifier,
|
|
412
|
+
ghPath = 'gh',
|
|
413
|
+
spawnImpl,
|
|
414
|
+
cwd,
|
|
415
|
+
logger,
|
|
416
|
+
spec,
|
|
417
|
+
}) {
|
|
418
|
+
const envelope = { filed: [], skipped: [], errors: [] };
|
|
419
|
+
const decorate =
|
|
420
|
+
typeof spec.decorateRecord === 'function'
|
|
421
|
+
? spec.decorateRecord
|
|
422
|
+
: (record) => record;
|
|
382
423
|
|
|
383
|
-
|
|
424
|
+
const precondition = checkGraduatePreconditions({
|
|
425
|
+
epicId,
|
|
426
|
+
provider,
|
|
427
|
+
currentRepo,
|
|
428
|
+
config,
|
|
429
|
+
spec,
|
|
430
|
+
});
|
|
431
|
+
if (precondition) return { ...envelope, ...precondition };
|
|
432
|
+
|
|
433
|
+
const loaded = await loadGraduateFindings({ epicId, provider, spec });
|
|
434
|
+
if (!loaded.findings) return { ...envelope, ...loaded };
|
|
435
|
+
|
|
436
|
+
for (const finding of loaded.findings) {
|
|
437
|
+
await processGraduateFinding({
|
|
384
438
|
finding,
|
|
385
|
-
|
|
439
|
+
envelope,
|
|
440
|
+
decorate,
|
|
386
441
|
epicId,
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
repo: routedRepo.repo,
|
|
392
|
-
title,
|
|
393
|
-
body,
|
|
394
|
-
labels,
|
|
442
|
+
currentRepo,
|
|
443
|
+
frameworkRepo,
|
|
444
|
+
classifier,
|
|
445
|
+
gitRef,
|
|
395
446
|
ghPath,
|
|
396
447
|
spawnImpl,
|
|
397
448
|
cwd,
|
|
449
|
+
logger,
|
|
450
|
+
spec,
|
|
398
451
|
});
|
|
399
|
-
if (created.error) {
|
|
400
|
-
envelope.errors.push(
|
|
401
|
-
`finding ${finding.index} (${finding.path}): ${created.error}`,
|
|
402
|
-
);
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
envelope.filed.push(
|
|
406
|
-
decorate(
|
|
407
|
-
{
|
|
408
|
-
index: finding.index,
|
|
409
|
-
severity: finding.severity,
|
|
410
|
-
path: finding.path,
|
|
411
|
-
source,
|
|
412
|
-
repo: `${routedRepo.owner}/${routedRepo.repo}`,
|
|
413
|
-
url: created.url,
|
|
414
|
-
},
|
|
415
|
-
finding,
|
|
416
|
-
),
|
|
417
|
-
);
|
|
418
452
|
}
|
|
419
453
|
|
|
420
454
|
return envelope;
|