mandrel 2.25.0 → 2.26.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 +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -464,6 +464,156 @@ function renderDiscardedItem(item) {
|
|
|
464
464
|
return parts.join(' — ');
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Normalize the two loosely-typed inputs into the four buckets every section
|
|
469
|
+
* renderer reads. Absorbs the optional-chain / nullish-coalesce cluster that
|
|
470
|
+
* otherwise all lands on `buildFollowUpsCommentBody` (Story #4926).
|
|
471
|
+
*
|
|
472
|
+
* @param {object|null|undefined} proposals
|
|
473
|
+
* @param {object|null|undefined} graduated
|
|
474
|
+
* @returns {{ filed: object[], framework: object[], consumer: object[],
|
|
475
|
+
* discarded: object[], filingErrors: string[] }}
|
|
476
|
+
*/
|
|
477
|
+
function normalizeRollupBuckets(proposals, graduated) {
|
|
478
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
479
|
+
return {
|
|
480
|
+
filed: asArray(graduated?.filed),
|
|
481
|
+
framework: asArray(proposals?.framework),
|
|
482
|
+
consumer: asArray(proposals?.consumer),
|
|
483
|
+
discarded: asArray(proposals?.discarded),
|
|
484
|
+
filingErrors: asArray(graduated?.errors),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Render the "**Filed**" section, or nothing when no proposal was filed.
|
|
490
|
+
*
|
|
491
|
+
* @param {object[]} filed
|
|
492
|
+
* @returns {string[]}
|
|
493
|
+
*/
|
|
494
|
+
function renderFiledSection(filed) {
|
|
495
|
+
if (filed.length === 0) return [];
|
|
496
|
+
return [
|
|
497
|
+
'**Filed**',
|
|
498
|
+
...filed.map(
|
|
499
|
+
(item) =>
|
|
500
|
+
`- ${item.source}: ${item.title}${item.url ? ` — ${item.url}` : ''}`,
|
|
501
|
+
),
|
|
502
|
+
'',
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Render the "**Actionable (not auto-filed)**" fallback — the command stanzas
|
|
508
|
+
* an operator runs by hand when auto-filing produced nothing.
|
|
509
|
+
*
|
|
510
|
+
* @param {object[]} actionable framework + consumer, in that order
|
|
511
|
+
* @param {object[]} filed
|
|
512
|
+
* @returns {string[]}
|
|
513
|
+
*/
|
|
514
|
+
function renderActionableSection(actionable, filed) {
|
|
515
|
+
if (actionable.length === 0 || filed.length > 0) return [];
|
|
516
|
+
const lines = ['**Actionable (not auto-filed)**'];
|
|
517
|
+
for (const item of actionable) {
|
|
518
|
+
lines.push(`- ${item.source}: ${item.title}`, '', '```bash', item.command);
|
|
519
|
+
lines.push('```');
|
|
520
|
+
}
|
|
521
|
+
lines.push('');
|
|
522
|
+
return lines;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Render the "**Below threshold (not filed)**" section.
|
|
527
|
+
*
|
|
528
|
+
* @param {object[]} discarded
|
|
529
|
+
* @returns {string[]}
|
|
530
|
+
*/
|
|
531
|
+
function renderDiscardedSection(discarded) {
|
|
532
|
+
if (discarded.length === 0) return [];
|
|
533
|
+
return [
|
|
534
|
+
'**Below threshold (not filed)**',
|
|
535
|
+
...discarded.map(
|
|
536
|
+
(item) => `- ${item.source}: ${renderDiscardedItem(item)}`,
|
|
537
|
+
),
|
|
538
|
+
'',
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Render the all-empty branch. Story #4828 — "no proposals" has two readings,
|
|
544
|
+
* and only one of them is a quiet run. Signals gathered but nothing routed is
|
|
545
|
+
* the third instance of the silence Stories #4578 and #4824 each fixed once.
|
|
546
|
+
*
|
|
547
|
+
* @param {{ empty: boolean, outcome: {zeroProposals: boolean},
|
|
548
|
+
* signalCount: number, categories: object[], storyCount: number }} args
|
|
549
|
+
* @returns {string[]}
|
|
550
|
+
*/
|
|
551
|
+
function renderEmptySection({
|
|
552
|
+
empty,
|
|
553
|
+
outcome,
|
|
554
|
+
signalCount,
|
|
555
|
+
categories,
|
|
556
|
+
storyCount,
|
|
557
|
+
}) {
|
|
558
|
+
if (!empty) return [];
|
|
559
|
+
return [
|
|
560
|
+
...(outcome.zeroProposals
|
|
561
|
+
? renderZeroProposalLines(signalCount, categories)
|
|
562
|
+
: renderEmptyRollupLines(storyCount)),
|
|
563
|
+
'',
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Build the machine-readable twin of the rendered prose. Every suspect flag
|
|
569
|
+
* above has a field here so a caller never has to regex the body.
|
|
570
|
+
*
|
|
571
|
+
* @param {object} args
|
|
572
|
+
* @returns {object}
|
|
573
|
+
*/
|
|
574
|
+
function buildRollupPayload({
|
|
575
|
+
storyId,
|
|
576
|
+
storyCount,
|
|
577
|
+
signalCount,
|
|
578
|
+
categories,
|
|
579
|
+
buckets,
|
|
580
|
+
empty,
|
|
581
|
+
outcome,
|
|
582
|
+
}) {
|
|
583
|
+
const { filed, framework, consumer, discarded, filingErrors } = buckets;
|
|
584
|
+
return {
|
|
585
|
+
storyId,
|
|
586
|
+
storyCount,
|
|
587
|
+
// Story #4828 — the corpus the roll-up actually read. Without it a
|
|
588
|
+
// reader cannot tell "0 proposals because nothing recurred" from
|
|
589
|
+
// "0 proposals because routing broke".
|
|
590
|
+
signalCount,
|
|
591
|
+
categories,
|
|
592
|
+
framework: framework.map((i) => i.category),
|
|
593
|
+
consumer: consumer.map((i) => i.category),
|
|
594
|
+
// Story #4824 — the machine-readable twin of the row above. A bare
|
|
595
|
+
// category list could not distinguish a genuine one-off from a
|
|
596
|
+
// recurrence the window was too narrow to see, so the count, the
|
|
597
|
+
// cross-Story span, and the shape fingerprint ride along.
|
|
598
|
+
discarded: discarded.map((i) => ({
|
|
599
|
+
category: i.category,
|
|
600
|
+
occurrences: i.occurrences,
|
|
601
|
+
storyCount: i.storyCount ?? null,
|
|
602
|
+
fingerprint: i.fingerprint ?? null,
|
|
603
|
+
})),
|
|
604
|
+
filed: filed.map((i) => ({ category: i.category, url: i.url ?? null })),
|
|
605
|
+
// Story #4578 — an empty roll-up over N>1 Stories is a claim worth
|
|
606
|
+
// flagging, not a success. Machine-readable twin of the warning
|
|
607
|
+
// prose so a caller need not regex the body.
|
|
608
|
+
emptyRollupSuspect: storyCount > 1 && empty && signalCount === 0,
|
|
609
|
+
// Story #4828 — the two remaining shapes that used to render as
|
|
610
|
+
// success. Machine-readable twins of the warning prose above.
|
|
611
|
+
zeroProposalSuspect: outcome.zeroProposals,
|
|
612
|
+
unfiledProposalSuspect: outcome.unfiledProposals,
|
|
613
|
+
filingErrors,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
467
617
|
/**
|
|
468
618
|
* @param {{
|
|
469
619
|
* storyId: number,
|
|
@@ -487,13 +637,14 @@ export function buildFollowUpsCommentBody({
|
|
|
487
637
|
signalCount = 0,
|
|
488
638
|
categories = [],
|
|
489
639
|
}) {
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const
|
|
640
|
+
const buckets = normalizeRollupBuckets(proposals, graduated);
|
|
641
|
+
const { filed, framework, consumer, discarded } = buckets;
|
|
642
|
+
const actionable = [...framework, ...consumer];
|
|
643
|
+
const empty =
|
|
644
|
+
filed.length === 0 && actionable.length === 0 && discarded.length === 0;
|
|
494
645
|
const outcome = assessRollupOutcome({
|
|
495
646
|
signalCount,
|
|
496
|
-
proposalCount:
|
|
647
|
+
proposalCount: actionable.length,
|
|
497
648
|
discardedCount: discarded.length,
|
|
498
649
|
filedCount: filed.length,
|
|
499
650
|
filingErrors: graduated?.errors,
|
|
@@ -504,103 +655,39 @@ export function buildFollowUpsCommentBody({
|
|
|
504
655
|
'',
|
|
505
656
|
`Actionable follow-ups captured from Story #${storyId} after merge.`,
|
|
506
657
|
'',
|
|
658
|
+
...(outcome.unfiledProposals
|
|
659
|
+
? [
|
|
660
|
+
...renderUnfiledProposalLines(
|
|
661
|
+
actionable.length,
|
|
662
|
+
buckets.filingErrors,
|
|
663
|
+
outcome.blockingSkipReasons,
|
|
664
|
+
),
|
|
665
|
+
'',
|
|
666
|
+
]
|
|
667
|
+
: []),
|
|
668
|
+
...renderFiledSection(filed),
|
|
669
|
+
...renderActionableSection(actionable, filed),
|
|
670
|
+
...renderDiscardedSection(discarded),
|
|
671
|
+
...renderEmptySection({
|
|
672
|
+
empty,
|
|
673
|
+
outcome,
|
|
674
|
+
signalCount,
|
|
675
|
+
categories,
|
|
676
|
+
storyCount,
|
|
677
|
+
}),
|
|
678
|
+
'```json',
|
|
507
679
|
];
|
|
508
|
-
if (outcome.unfiledProposals) {
|
|
509
|
-
lines.push(
|
|
510
|
-
...renderUnfiledProposalLines(
|
|
511
|
-
framework.length + consumer.length,
|
|
512
|
-
Array.isArray(graduated?.errors) ? graduated.errors : [],
|
|
513
|
-
outcome.blockingSkipReasons,
|
|
514
|
-
),
|
|
515
|
-
'',
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
if (filed.length > 0) {
|
|
519
|
-
lines.push('**Filed**');
|
|
520
|
-
for (const item of filed) {
|
|
521
|
-
lines.push(
|
|
522
|
-
`- ${item.source}: ${item.title}${item.url ? ` — ${item.url}` : ''}`,
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
lines.push('');
|
|
526
|
-
}
|
|
527
|
-
if (framework.length + consumer.length > 0 && filed.length === 0) {
|
|
528
|
-
lines.push('**Actionable (not auto-filed)**');
|
|
529
|
-
for (const item of [...framework, ...consumer]) {
|
|
530
|
-
lines.push(`- ${item.source}: ${item.title}`);
|
|
531
|
-
lines.push('');
|
|
532
|
-
lines.push('```bash');
|
|
533
|
-
lines.push(item.command);
|
|
534
|
-
lines.push('```');
|
|
535
|
-
}
|
|
536
|
-
lines.push('');
|
|
537
|
-
}
|
|
538
|
-
if (discarded.length > 0) {
|
|
539
|
-
lines.push('**Below threshold (not filed)**');
|
|
540
|
-
for (const item of discarded) {
|
|
541
|
-
lines.push(`- ${item.source}: ${renderDiscardedItem(item)}`);
|
|
542
|
-
}
|
|
543
|
-
lines.push('');
|
|
544
|
-
}
|
|
545
|
-
if (
|
|
546
|
-
filed.length === 0 &&
|
|
547
|
-
framework.length === 0 &&
|
|
548
|
-
consumer.length === 0 &&
|
|
549
|
-
discarded.length === 0
|
|
550
|
-
) {
|
|
551
|
-
// Story #4828 — "no proposals" has two readings, and only one of them is
|
|
552
|
-
// a quiet run. Signals gathered but nothing routed is the third instance
|
|
553
|
-
// of the silence Stories #4578 and #4824 each fixed once.
|
|
554
|
-
lines.push(
|
|
555
|
-
...(outcome.zeroProposals
|
|
556
|
-
? renderZeroProposalLines(signalCount, categories)
|
|
557
|
-
: renderEmptyRollupLines(storyCount)),
|
|
558
|
-
);
|
|
559
|
-
lines.push('');
|
|
560
|
-
}
|
|
561
|
-
lines.push('```json');
|
|
562
680
|
lines.push(
|
|
563
681
|
JSON.stringify(
|
|
564
|
-
{
|
|
682
|
+
buildRollupPayload({
|
|
565
683
|
storyId,
|
|
566
684
|
storyCount,
|
|
567
|
-
// Story #4828 — the corpus the roll-up actually read. Without it a
|
|
568
|
-
// reader cannot tell "0 proposals because nothing recurred" from
|
|
569
|
-
// "0 proposals because routing broke".
|
|
570
685
|
signalCount,
|
|
571
686
|
categories,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// recurrence the window was too narrow to see, so the count, the
|
|
577
|
-
// cross-Story span, and the shape fingerprint ride along.
|
|
578
|
-
discarded: discarded.map((i) => ({
|
|
579
|
-
category: i.category,
|
|
580
|
-
occurrences: i.occurrences,
|
|
581
|
-
storyCount: i.storyCount ?? null,
|
|
582
|
-
fingerprint: i.fingerprint ?? null,
|
|
583
|
-
})),
|
|
584
|
-
filed: filed.map((i) => ({
|
|
585
|
-
category: i.category,
|
|
586
|
-
url: i.url ?? null,
|
|
587
|
-
})),
|
|
588
|
-
// Story #4578 — an empty roll-up over N>1 Stories is a claim worth
|
|
589
|
-
// flagging, not a success. Machine-readable twin of the warning
|
|
590
|
-
// prose so a caller need not regex the body.
|
|
591
|
-
emptyRollupSuspect:
|
|
592
|
-
storyCount > 1 &&
|
|
593
|
-
filed.length === 0 &&
|
|
594
|
-
framework.length === 0 &&
|
|
595
|
-
consumer.length === 0 &&
|
|
596
|
-
discarded.length === 0 &&
|
|
597
|
-
signalCount === 0,
|
|
598
|
-
// Story #4828 — the two remaining shapes that used to render as
|
|
599
|
-
// success. Machine-readable twins of the warning prose above.
|
|
600
|
-
zeroProposalSuspect: outcome.zeroProposals,
|
|
601
|
-
unfiledProposalSuspect: outcome.unfiledProposals,
|
|
602
|
-
filingErrors: Array.isArray(graduated?.errors) ? graduated.errors : [],
|
|
603
|
-
},
|
|
687
|
+
buckets,
|
|
688
|
+
empty,
|
|
689
|
+
outcome,
|
|
690
|
+
}),
|
|
604
691
|
null,
|
|
605
692
|
2,
|
|
606
693
|
),
|
|
@@ -816,6 +816,28 @@ export function renderFanOutRemedy(finding) {
|
|
|
816
816
|
);
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
/**
|
|
820
|
+
* The finding kinds that are genuinely **cross-Story conflicts** — the SSOT
|
|
821
|
+
* for that question (Story #4907).
|
|
822
|
+
*
|
|
823
|
+
* Two readers need it and must not disagree: the validator, which renders
|
|
824
|
+
* these through {@link renderHardConflictError} when policy upgrades them to
|
|
825
|
+
* `errors[]`, and the persist soft-finding surface, which announces a
|
|
826
|
+
* conflict as a conflict and every other soft kind (`spec-word-budget`,
|
|
827
|
+
* `merge-candidate`, `unanchored-constant`, `missing-reason-to-exist`) as the
|
|
828
|
+
* advisory it is. A second copy of this list is how the two drift back apart,
|
|
829
|
+
* so it is defined exactly once and imported.
|
|
830
|
+
*/
|
|
831
|
+
export const CONFLICT_KINDS = Object.freeze(
|
|
832
|
+
new Set([
|
|
833
|
+
'shared-editor',
|
|
834
|
+
'implicit-cross-story-dep',
|
|
835
|
+
'cross-cutting-registries',
|
|
836
|
+
'fan-out-warning',
|
|
837
|
+
'missing-bdd-scaffold',
|
|
838
|
+
]),
|
|
839
|
+
);
|
|
840
|
+
|
|
819
841
|
/**
|
|
820
842
|
* Render a `'hard'`-severity conflict finding as a human-readable error
|
|
821
843
|
* message. Used by the validator when policy flags upgrade a finding to
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseStoryBodyOrThrow,
|
|
11
11
|
} from './story-body-gate.js';
|
|
12
12
|
import {
|
|
13
|
+
CONFLICT_KINDS,
|
|
13
14
|
computeConflictFindings,
|
|
14
15
|
renderHardConflictError,
|
|
15
16
|
} from './ticket-validator-conflicts.js';
|
|
@@ -666,25 +667,18 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
|
|
|
666
667
|
policy: opts.conflictPolicy,
|
|
667
668
|
});
|
|
668
669
|
// Advisory `## Spec` word-budget pass (Story #4723) — soft findings only,
|
|
669
|
-
// surfaced as warnings here and via the persist soft-finding channel;
|
|
670
670
|
// never promoted to `errors[]`, so an over-budget Spec cannot fail the
|
|
671
671
|
// persist. Runs after `assertStoryBodiesParse`, so string bodies parse.
|
|
672
|
+
// This pass computes but does not report: the sole production caller always
|
|
673
|
+
// runs the persist soft-finding surface, which reports every soft kind
|
|
674
|
+
// uniformly. Warning here too made `spec-word-budget` the only kind logged
|
|
675
|
+
// twice per run (Story #4907).
|
|
672
676
|
const specBudgetFindings = computeSpecBudgetFindings({ stories });
|
|
673
|
-
for (const finding of specBudgetFindings) {
|
|
674
|
-
Logger.warn(`[ticket-validator] spec-word-budget: ${finding.message}`);
|
|
675
|
-
}
|
|
676
677
|
const findings = [
|
|
677
678
|
...sizingFindings,
|
|
678
679
|
...conflictFindings,
|
|
679
680
|
...specBudgetFindings,
|
|
680
681
|
];
|
|
681
|
-
const CONFLICT_KINDS = new Set([
|
|
682
|
-
'shared-editor',
|
|
683
|
-
'implicit-cross-story-dep',
|
|
684
|
-
'cross-cutting-registries',
|
|
685
|
-
'fan-out-warning',
|
|
686
|
-
'missing-bdd-scaffold',
|
|
687
|
-
]);
|
|
688
682
|
const errors = findings
|
|
689
683
|
.filter((f) => f.severity === 'hard')
|
|
690
684
|
.map((f) =>
|
|
@@ -180,7 +180,7 @@ export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
|
|
|
180
180
|
* authoritative entry — the label set is the actual race-detection signal).
|
|
181
181
|
* Bounded to 1-9 digits to mirror the wave-marker safety margin.
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
const CLAIM_TYPE_PATTERN = /^claim-([0-9]{1,9})$/;
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
186
|
* Lifecycle-listener marker pattern (Story #2239 / #2241 / #2242). The
|
|
@@ -189,7 +189,7 @@ export const CLAIM_TYPE_PATTERN = /^claim-([0-9]{1,9})$/;
|
|
|
189
189
|
* `lifecycle-epic-unblocked`). Treated as a generic prefix so future
|
|
190
190
|
* listener-owned events can mint new markers without touching this enum.
|
|
191
191
|
*/
|
|
192
|
-
|
|
192
|
+
const LIFECYCLE_TYPE_PATTERN = /^lifecycle-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
195
|
* @param {string} type
|
|
@@ -273,7 +273,7 @@ export function structuredCommentMarker(type, attrs = null) {
|
|
|
273
273
|
* cannot leak across boundaries. Tests reset via the exported
|
|
274
274
|
* `_resetStructuredCommentCache()` seam.
|
|
275
275
|
*/
|
|
276
|
-
|
|
276
|
+
const _structuredCommentCache = new WeakMap();
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* Build a well-formed ticket snapshot for a Story that has zero child
|
|
@@ -359,7 +359,7 @@ export function structuredCommentCacheKey(ticketId, type, attrs) {
|
|
|
359
359
|
* - Scoped per-provider via WeakMap so test fakes never share state
|
|
360
360
|
* with the real GitHubProvider.
|
|
361
361
|
*/
|
|
362
|
-
|
|
362
|
+
const _rawCommentsCache = new WeakMap();
|
|
363
363
|
|
|
364
364
|
/**
|
|
365
365
|
* Lookup (or lazily create) the per-provider raw-comments cache.
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* records into the `Map<storyId, number[]>` adjacency the kernel consumes.
|
|
8
8
|
* This module is now the one home for that step; the live consumer is:
|
|
9
9
|
*
|
|
10
|
-
* - `lib/wave-runner/ready-set.js` (`
|
|
10
|
+
* - `lib/wave-runner/ready-set.js` (`planReadySet`, the path-agnostic
|
|
11
11
|
* continuous scheduler the `stories-wave-tick.js` adapter dispatches through)
|
|
12
12
|
* - `stories-wave-tick.js` (for cycle detection, before delegating
|
|
13
|
-
* selection to `
|
|
13
|
+
* selection to `planReadySet`)
|
|
14
14
|
*
|
|
15
15
|
* (Pre-v2: `epic-runner/phases/build-wave-dag.js` and `dispatch-pipeline.js`
|
|
16
16
|
* also called here; both entry seams were deleted.)
|
|
@@ -44,7 +44,7 @@ import { parseBlockedBy } from './dependency-parser.js';
|
|
|
44
44
|
* @param {object} [opts]
|
|
45
45
|
* @param {boolean} [opts.dropForeign=false] When `false` (the v2 default,
|
|
46
46
|
* matching the `/deliver` path — `stories-wave-tick.js` and the
|
|
47
|
-
* `
|
|
47
|
+
* `planReadySet` core), the operator-DAG contract is preserved: a
|
|
48
48
|
* dependency on an id absent from the input is treated as not-yet-done
|
|
49
49
|
* and withholds the dependent until it completes. When `true` (the
|
|
50
50
|
* pre-v2 Epic-scoped-wrapper semantics), edges pointing at ids outside
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract every test-running entrypoint shares: the `node --test` flag
|
|
3
|
+
* set, and the per-tier preflight.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a module and not two literals (Story #4936)
|
|
6
|
+
*
|
|
7
|
+
* The repository has two full-tier runners — `run-tests.js` (`npm test`) and
|
|
8
|
+
* `run-coverage.js` (`npm run test:coverage`, the *required* CI job). When
|
|
9
|
+
* they disagree about a `node --test` flag, they disagree about whether a
|
|
10
|
+
* test can execute at all: `--experimental-test-module-mocks` decides whether
|
|
11
|
+
* `t.mock.module` works, so a divergence makes a suite pass under one runner
|
|
12
|
+
* and fail under the other with no source defect. `FULL_TIER_GLOBS` in
|
|
13
|
+
* [`test-tiers.js`](test-tiers.js) already solved the sibling problem for
|
|
14
|
+
* *which files* run; this module is the same fix for *how they are run*.
|
|
15
|
+
* Both runners import from here; neither restates a literal.
|
|
16
|
+
*
|
|
17
|
+
* ## The preflight is invoked, never hooked
|
|
18
|
+
*
|
|
19
|
+
* `.npmrc` sets `ignore-scripts=true` as deliberate defence against malicious
|
|
20
|
+
* postinstall hooks (CWE-1357). That setting is correct and stays — but it
|
|
21
|
+
* suppresses every `pre*` / `post*` lifecycle script for `npm run` as well as
|
|
22
|
+
* for installs, so a `pretest` entry in `package.json` never fires. The
|
|
23
|
+
* repository carried `pretest`, `pretest:quick` and `pretest:integration`
|
|
24
|
+
* entries that therefore ran for no tier at all, and Story #4922 worked
|
|
25
|
+
* around it for the coverage tier alone by having CI name `pretest:coverage`
|
|
26
|
+
* explicitly. `runTierPreflight` closes the gap for every tier by moving the
|
|
27
|
+
* invocation into the runners, where it executes under `npm test`,
|
|
28
|
+
* `npm run test:coverage`, and a bare `node .agents/scripts/run-tests.js`
|
|
29
|
+
* alike — no lifecycle hook involved.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import process from 'node:process';
|
|
36
|
+
|
|
37
|
+
/** Minimum and maximum bounds for `--test-concurrency`. */
|
|
38
|
+
export const TEST_CONCURRENCY_MIN = 1;
|
|
39
|
+
export const TEST_CONCURRENCY_MAX = 16;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the `--test-concurrency` value for the current host.
|
|
43
|
+
*
|
|
44
|
+
* Uses `os.availableParallelism()` (Node ≥18.14 / ≥20.0) clamped to the
|
|
45
|
+
* range `[TEST_CONCURRENCY_MIN, TEST_CONCURRENCY_MAX]`. The `parallelism`
|
|
46
|
+
* parameter is injected in tests so the clamping logic is verifiable
|
|
47
|
+
* without touching the OS.
|
|
48
|
+
*
|
|
49
|
+
* @param {number} [parallelism] - defaults to `os.availableParallelism()`
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function resolveTestConcurrency(
|
|
53
|
+
parallelism = os.availableParallelism(),
|
|
54
|
+
) {
|
|
55
|
+
return Math.min(
|
|
56
|
+
TEST_CONCURRENCY_MAX,
|
|
57
|
+
Math.max(TEST_CONCURRENCY_MIN, parallelism),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fixed `node --test` flags applied to every spawn of every runner.
|
|
63
|
+
*
|
|
64
|
+
* `--test-concurrency` is derived at startup from the host's available
|
|
65
|
+
* parallelism so the value suits the machine running the suite rather than
|
|
66
|
+
* being pinned to the historical constant of 8.
|
|
67
|
+
*
|
|
68
|
+
* This is the **single** declaration. A flag added here reaches both runners;
|
|
69
|
+
* a flag added to one runner's argv builder instead is caught by the
|
|
70
|
+
* flag-set equality assertion in `tests/scripts/run-coverage.test.js`.
|
|
71
|
+
*/
|
|
72
|
+
export const TEST_RUNNER_FLAGS = Object.freeze([
|
|
73
|
+
'--experimental-test-module-mocks',
|
|
74
|
+
'--test',
|
|
75
|
+
`--test-concurrency=${resolveTestConcurrency()}`,
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Preflight scripts per tier, repo-relative. These are the definitions that
|
|
80
|
+
* used to live in `package.json` as inert `pretest*` entries.
|
|
81
|
+
*
|
|
82
|
+
* `full` carries the skills validator on top of the state-probe wrapper
|
|
83
|
+
* because the full tier is the release-shaped run (and the surface the
|
|
84
|
+
* coverage tier measures); `quick` and `integration` run the state probe
|
|
85
|
+
* only, matching the tiers' historical `pretest:quick` / `pretest:integration`
|
|
86
|
+
* definitions. The coverage runner runs the full tier, so it shares `full`.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately not exported: a second importer would be a second place to
|
|
89
|
+
* read the tier→preflight mapping from, and tests assert the mapping through
|
|
90
|
+
* the spawns `runTierPreflight` actually issues — which is the thing that
|
|
91
|
+
* has to be true.
|
|
92
|
+
*/
|
|
93
|
+
const TIER_PREFLIGHT_SCRIPTS = Object.freeze({
|
|
94
|
+
full: Object.freeze([
|
|
95
|
+
'.agents/scripts/test-wrapper.js',
|
|
96
|
+
'.agents/scripts/validate-skills.js',
|
|
97
|
+
]),
|
|
98
|
+
quick: Object.freeze(['.agents/scripts/test-wrapper.js']),
|
|
99
|
+
integration: Object.freeze(['.agents/scripts/test-wrapper.js']),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run the preflight for a tier, in order, stopping at the first failure.
|
|
104
|
+
*
|
|
105
|
+
* Mirrors npm's own `pre<script>` semantics — a failed preflight aborts
|
|
106
|
+
* before the test runner is spawned and propagates its exit code (the
|
|
107
|
+
* wrapper reserves 2 for "preflight refused") — except that it actually
|
|
108
|
+
* executes, which the npm hook does not under `ignore-scripts=true`.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} [opts]
|
|
111
|
+
* @param {'full' | 'quick' | 'integration'} [opts.tier]
|
|
112
|
+
* @param {string} [opts.repoRoot] Absolute repository root.
|
|
113
|
+
* @param {typeof spawnSync} [opts.spawn] Injected in tests.
|
|
114
|
+
* @param {string} [opts.execPath] Node binary to spawn; injected in tests.
|
|
115
|
+
* @returns {number} 0 when every preflight script passed, else the first
|
|
116
|
+
* non-zero exit code.
|
|
117
|
+
*/
|
|
118
|
+
export function runTierPreflight({
|
|
119
|
+
tier = 'full',
|
|
120
|
+
repoRoot = process.cwd(),
|
|
121
|
+
spawn = spawnSync,
|
|
122
|
+
execPath = process.execPath,
|
|
123
|
+
} = {}) {
|
|
124
|
+
for (const script of TIER_PREFLIGHT_SCRIPTS[tier] ?? []) {
|
|
125
|
+
const run = spawn(execPath, [path.join(repoRoot, script)], {
|
|
126
|
+
cwd: repoRoot,
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
});
|
|
129
|
+
if (run.error) throw run.error;
|
|
130
|
+
const status = run.status ?? 1;
|
|
131
|
+
if (status !== 0) return status;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
@@ -35,7 +35,9 @@ const matchesIntegration = picomatch(INTEGRATION_INCLUDE, { dot: true });
|
|
|
35
35
|
* same way (Story #4195). Without each root here, both the quick /
|
|
36
36
|
* integration walk and the full-tier glob set miss the colocated tests,
|
|
37
37
|
* leaving that coverage dark in `npm test`. The matching full-tier globs
|
|
38
|
-
* live in `FULL_TIER_GLOBS
|
|
38
|
+
* live in the exported `FULL_TIER_GLOBS` — every full-tier runner
|
|
39
|
+
* (`run-tests.js`, `run-coverage.js`) MUST consume that constant rather than
|
|
40
|
+
* restate a glob literal, or a runner silently walks a narrower surface.
|
|
39
41
|
*/
|
|
40
42
|
const TEST_WALK_ROOTS = ['tests', 'lib', '.agents/scripts'];
|
|
41
43
|
|
|
@@ -44,8 +46,15 @@ const TEST_WALK_ROOTS = ['tests', 'lib', '.agents/scripts'];
|
|
|
44
46
|
* The `tests` glob is a flat recursive sweep; the `lib` and `.agents/scripts`
|
|
45
47
|
* globs are scoped to `__tests__` subtrees so they only match colocated
|
|
46
48
|
* tests, never the shipped source modules themselves.
|
|
49
|
+
*
|
|
50
|
+
* Exported because the full tier has two runners, not one: `run-tests.js`
|
|
51
|
+
* (via `listTestFilesForTier`) and `run-coverage.js`. Story #4922 — the
|
|
52
|
+
* coverage runner used to restate `tests/**` on its own, so the 47 colocated
|
|
53
|
+
* `__tests__` files ran under `npm test` but were absent from the measured
|
|
54
|
+
* surface, leaving the coverage and CRAP numbers computed over code the
|
|
55
|
+
* measuring run never executed. Consume this constant; never restate a glob.
|
|
47
56
|
*/
|
|
48
|
-
const FULL_TIER_GLOBS = [
|
|
57
|
+
export const FULL_TIER_GLOBS = [
|
|
49
58
|
'tests/**/*.test.js',
|
|
50
59
|
'lib/**/__tests__/**/*.test.js',
|
|
51
60
|
'.agents/scripts/**/__tests__/**/*.test.js',
|
|
@@ -13,6 +13,23 @@
|
|
|
13
13
|
* and would leak otherwise.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The one bound every independent-write fan-out over the GitHub API uses
|
|
18
|
+
* (Story #4952 raised those loops off serial; Story #4961 made this the single
|
|
19
|
+
* owner of the number they share). Imported by the `/plan` context gathers,
|
|
20
|
+
* the persist checkpoint fan-out, the `agent::ready` flips and the supersede
|
|
21
|
+
* close loop, so re-tuning the policy is one edit rather than six.
|
|
22
|
+
*
|
|
23
|
+
* **Why bounded and not unbounded.** Every unit in those fan-outs is its own
|
|
24
|
+
* API round-trip, so an unbounded map over an N-Story plan dispatches N writes
|
|
25
|
+
* at once — and GitHub answers a burst with a secondary rate limit rather than
|
|
26
|
+
* with throughput. The goal is overlapping unrelated waits, not saturating the
|
|
27
|
+
* API, and four is enough to collapse the latency the serial loops paid while
|
|
28
|
+
* staying well under the burst threshold. Callers whose ordering is
|
|
29
|
+
* load-bearing (`createStoryIssues`) stay serial instead of importing this.
|
|
30
|
+
*/
|
|
31
|
+
export const FANOUT_CONCURRENCY = 4;
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* @template T, R
|
|
18
35
|
* @param {ReadonlyArray<T>} items
|