mandrel 2.21.0 → 2.23.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/README.md +1 -1
- package/.agents/agents/story-worker.md +5 -0
- package/.agents/docs/configuration.md +1 -0
- package/.agents/instructions.md +14 -17
- package/.agents/rules/git-conventions.md +1 -1
- package/.agents/rules/known-tooling-behavior.md +114 -0
- package/.agents/schemas/agentrc.schema.json +6 -0
- package/.agents/scripts/check-context-budget.js +134 -2
- package/.agents/scripts/diagnose-friction.js +95 -4
- package/.agents/scripts/lib/audit-suite/selector.js +275 -162
- package/.agents/scripts/lib/config/temp-paths.js +51 -7
- package/.agents/scripts/lib/config-settings-schema-delivery.js +8 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +604 -57
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +72 -21
- package/.agents/scripts/lib/label-constants.js +12 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +41 -1
- package/.agents/scripts/lib/observability/signals-writer.js +133 -14
- package/.agents/scripts/lib/observability/source-classifier.js +131 -1
- package/.agents/scripts/lib/orchestration/code-review.js +12 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +51 -46
- package/.agents/scripts/lib/orchestration/resolve-stories.js +17 -14
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/review-providers/degraded-gates.js +222 -0
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +18 -3
- package/.agents/scripts/lib/orchestration/review-providers/native.js +82 -126
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +10 -0
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +300 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +69 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +18 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-outcome.js +66 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +5 -1
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +380 -13
- package/.agents/scripts/lib/story-body/story-body.js +248 -174
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/resolve-stories.js +52 -33
- package/.agents/scripts/single-story-confirm-merge.js +5 -7
- package/.agents/workflows/helpers/deliver-digest.md +8 -6
- package/.agents/workflows/helpers/deliver-reference.md +15 -12
- package/.agents/workflows/helpers/deliver-story-reference.md +23 -21
- package/.agents/workflows/helpers/deliver-story.md +2 -2
- package/.agents/workflows/helpers/plan-reference.md +45 -4
- package/.agents/workflows/plan.md +21 -16
- package/docs/CHANGELOG.md +34 -0
- package/package.json +1 -1
|
@@ -21,9 +21,11 @@ import { gitSpawn } from '../git-utils.js';
|
|
|
21
21
|
import { Logger } from '../Logger.js';
|
|
22
22
|
import { composeRoutedProposals } from './retro-proposals.js';
|
|
23
23
|
import {
|
|
24
|
+
assessRollupOutcome,
|
|
24
25
|
buildFollowUpsCommentBody,
|
|
25
26
|
gatherRunFrictionSignals,
|
|
26
27
|
resolveFollowUpRepos,
|
|
28
|
+
summarizeSignalCategories,
|
|
27
29
|
} from './story-follow-ups.js';
|
|
28
30
|
import { upsertStructuredComment } from './ticketing.js';
|
|
29
31
|
|
|
@@ -550,27 +552,35 @@ async function executeFollowUpRollup({
|
|
|
550
552
|
provider,
|
|
551
553
|
config,
|
|
552
554
|
cwd,
|
|
555
|
+
graduateFn = graduateRetroProposals,
|
|
553
556
|
}) {
|
|
554
557
|
// Shared with the story-scoped gather (Story #4649): `storyId` + `details`
|
|
555
558
|
// are what the composer's recovery-netting keys on, and two hand-rolled
|
|
556
559
|
// copies of this loop are how they got dropped in the first place.
|
|
557
|
-
const signals = await gatherRunFrictionSignals(
|
|
560
|
+
const { signals, window: frictionWindow } = await gatherRunFrictionSignals(
|
|
561
|
+
stories,
|
|
562
|
+
config,
|
|
563
|
+
);
|
|
558
564
|
const repos = resolveFollowUpRepos(config);
|
|
559
565
|
const primaryId = Number(stories[0]);
|
|
566
|
+
// Story #4850 — `runToken` and `anchorStoryIds` are INPUTS. This used to
|
|
567
|
+
// compose with the primary Story's numeric id standing in for the run and
|
|
568
|
+
// then rewrite the rendered title/body by regex over a `plan-run \d+`
|
|
569
|
+
// substring, which meant the composer's own wording could not be changed
|
|
570
|
+
// without silently breaking the patch. `anchorStoryIds` is what lets the
|
|
571
|
+
// composer tell a corpus confined to this run from one spanning the whole
|
|
572
|
+
// surviving window, so it never titles the latter as if it were the former.
|
|
560
573
|
const proposals = composeRoutedProposals({
|
|
561
574
|
anchorId: Number.isInteger(primaryId) ? primaryId : 1,
|
|
562
575
|
anchorKind: 'run',
|
|
576
|
+
runToken: String(planRunId ?? ''),
|
|
577
|
+
anchorStoryIds: stories,
|
|
563
578
|
frameworkRepo: repos.frameworkRepo,
|
|
564
579
|
consumerRepo: repos.consumerRepo,
|
|
565
580
|
signals,
|
|
566
581
|
unresolvedBlockedEvents: [],
|
|
567
582
|
});
|
|
568
|
-
|
|
569
|
-
for (const item of [...proposals.framework, ...proposals.consumer]) {
|
|
570
|
-
item.title = item.title.replace(/plan-run \d+/, `plan-run ${planRunId}`);
|
|
571
|
-
item.body = item.body.replace(/plan-run \d+/g, `plan-run ${planRunId}`);
|
|
572
|
-
}
|
|
573
|
-
const graduated = await graduateRetroProposals({
|
|
583
|
+
const graduated = await graduateFn({
|
|
574
584
|
epicId: primaryId,
|
|
575
585
|
provider,
|
|
576
586
|
config,
|
|
@@ -582,6 +592,16 @@ async function executeFollowUpRollup({
|
|
|
582
592
|
routedProposals: proposals,
|
|
583
593
|
cwd,
|
|
584
594
|
});
|
|
595
|
+
const categories = summarizeSignalCategories(signals);
|
|
596
|
+
const proposalCount = proposals.framework.length + proposals.consumer.length;
|
|
597
|
+
const outcome = assessRollupOutcome({
|
|
598
|
+
signalCount: signals.length,
|
|
599
|
+
proposalCount,
|
|
600
|
+
discardedCount: proposals.discarded.length,
|
|
601
|
+
filedCount: graduated.filed?.length ?? 0,
|
|
602
|
+
filingErrors: graduated.errors,
|
|
603
|
+
filingSkipped: graduated.skipped,
|
|
604
|
+
});
|
|
585
605
|
if (Number.isInteger(primaryId) && primaryId > 0) {
|
|
586
606
|
const body = buildFollowUpsCommentBody({
|
|
587
607
|
storyId: primaryId,
|
|
@@ -591,6 +611,10 @@ async function executeFollowUpRollup({
|
|
|
591
611
|
// render as a flagged claim ("0 signals across N Stories") rather than
|
|
592
612
|
// as "nothing to follow up".
|
|
593
613
|
storyCount: stories.length,
|
|
614
|
+
// Story #4828 — and the corpus is what lets a zero-proposal or
|
|
615
|
+
// zero-filed roll-up name what it saw instead of rendering as clean.
|
|
616
|
+
signalCount: signals.length,
|
|
617
|
+
categories,
|
|
594
618
|
}).replace(
|
|
595
619
|
`from Story #${primaryId}`,
|
|
596
620
|
`from plan-run \`${planRunId}\` (primary Story #${primaryId})`,
|
|
@@ -602,6 +626,39 @@ async function executeFollowUpRollup({
|
|
|
602
626
|
signalCount: signals.length,
|
|
603
627
|
storyCount: stories.length,
|
|
604
628
|
filed: graduated.filed?.length ?? 0,
|
|
629
|
+
// Story #4850 — the recurrence window the gather actually applied, and what
|
|
630
|
+
// it dropped. `signalCount` alone cannot distinguish "the window is bounded
|
|
631
|
+
// at 30 days and 40 rows aged out" from "nothing older exists", and an
|
|
632
|
+
// operator triaging a roll-up needs to know which corpus produced it.
|
|
633
|
+
frictionWindow,
|
|
634
|
+
// Story #4828 — everything below is what the roll-up saw and what became
|
|
635
|
+
// of it. The pre-#4828 result reported `signalCount` and `filed` and
|
|
636
|
+
// nothing in between, so nine signals routing into one proposal whose
|
|
637
|
+
// every filing attempt errored rendered as `{signalCount: 9, filed: 0,
|
|
638
|
+
// discarded: []}` — arithmetically consistent, and indistinguishable from
|
|
639
|
+
// a run with nothing to do.
|
|
640
|
+
proposalCount,
|
|
641
|
+
// The categories the corpus actually contained, so a zero-proposal
|
|
642
|
+
// roll-up names its own input rather than asserting emptiness.
|
|
643
|
+
categories,
|
|
644
|
+
filingErrors: Array.isArray(graduated.errors) ? graduated.errors : [],
|
|
645
|
+
filingSkipped: outcome.blockingSkipReasons,
|
|
646
|
+
// Signals in, nothing out — not even a below-threshold row.
|
|
647
|
+
zeroProposalSuspect: outcome.zeroProposals,
|
|
648
|
+
// Proposals cleared the threshold and the filer produced none of them.
|
|
649
|
+
unfiledProposalSuspect: outcome.unfiledProposals,
|
|
650
|
+
// Story #4824 — a roll-up that discards every candidate must still name
|
|
651
|
+
// what it discarded. Rendering that as "nothing to follow up" is how a
|
|
652
|
+
// defect recurring once per Story survived eighteen consecutive Stories.
|
|
653
|
+
// Surfaced on the step result so the CLI need not regex the comment body.
|
|
654
|
+
discarded: proposals.discarded.map((item) => ({
|
|
655
|
+
category: item.category,
|
|
656
|
+
occurrences: item.occurrences,
|
|
657
|
+
source: item.source,
|
|
658
|
+
storyCount: item.storyCount ?? null,
|
|
659
|
+
tools: item.tools ?? [],
|
|
660
|
+
fingerprint: item.fingerprint ?? null,
|
|
661
|
+
})),
|
|
605
662
|
// Story #4578 — zero signals across a multi-Story run is a claim, not a
|
|
606
663
|
// clean bill of health. Surfaced on the step result so the CLI can warn
|
|
607
664
|
// the operator without re-deriving it from the comment prose.
|
|
@@ -698,6 +755,9 @@ async function executeSiblingCoherence({ planRunId, stories, provider }) {
|
|
|
698
755
|
* @param {string} [args.cwd]
|
|
699
756
|
* @param {{ gitSpawn: Function }} [args.git] - Injection seam for tests.
|
|
700
757
|
* @param {typeof selectAudits} [args.selectAuditsFn] - Injection seam for tests.
|
|
758
|
+
* @param {typeof graduateRetroProposals} [args.graduateFn] - Injection seam so
|
|
759
|
+
* the roll-up's reporting layer can be asserted against a filer that fails
|
|
760
|
+
* (Story #4828) without spawning a real `gh`.
|
|
701
761
|
* @returns {Promise<object>}
|
|
702
762
|
*/
|
|
703
763
|
export async function runPlanRunEpilogue({
|
|
@@ -708,6 +768,7 @@ export async function runPlanRunEpilogue({
|
|
|
708
768
|
cwd = process.cwd(),
|
|
709
769
|
git = { gitSpawn },
|
|
710
770
|
selectAuditsFn = selectAudits,
|
|
771
|
+
graduateFn = graduateRetroProposals,
|
|
711
772
|
} = {}) {
|
|
712
773
|
const plan = planRunEpilogue({ planRunId, stories });
|
|
713
774
|
if (!plan.applicable) {
|
|
@@ -741,6 +802,7 @@ export async function runPlanRunEpilogue({
|
|
|
741
802
|
provider,
|
|
742
803
|
config,
|
|
743
804
|
cwd,
|
|
805
|
+
graduateFn,
|
|
744
806
|
}),
|
|
745
807
|
);
|
|
746
808
|
} else if (step.kind === 'sibling-coherence') {
|
|
@@ -28,8 +28,13 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { parsePrNumberFromUrl } from '../../../github-url.js';
|
|
31
|
+
import { degradationEnvelope } from '../../review-providers/degraded-gates.js';
|
|
31
32
|
import { runStoryReviewCore } from '../../story-close/phases/review-core.js';
|
|
32
33
|
import { postStructuredComment } from '../../ticketing/state.js';
|
|
34
|
+
import {
|
|
35
|
+
buildOutcomeTally,
|
|
36
|
+
formatReviewOutcomeLines,
|
|
37
|
+
} from './review-outcome.js';
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Extract the numeric PR ID from a `gh pr create` URL. The CLI returns a
|
|
@@ -62,13 +67,11 @@ export function buildStoryReviewCrossRefBody({
|
|
|
62
67
|
prNumber,
|
|
63
68
|
commentUrl,
|
|
64
69
|
severity,
|
|
70
|
+
degradations,
|
|
65
71
|
}) {
|
|
66
|
-
const tally =
|
|
67
|
-
`critical:${severity.critical} · high:${severity.high} · ` +
|
|
68
|
-
`medium:${severity.medium} · suggestion:${severity.suggestion}`;
|
|
69
72
|
return (
|
|
70
73
|
`🔬 Story-scope code review posted on PR [#${prNumber}](${prUrl}): ` +
|
|
71
|
-
`[view findings](${commentUrl}) — ${
|
|
74
|
+
`[view findings](${commentUrl}) — ${buildOutcomeTally({ severity, degradations })}.`
|
|
72
75
|
);
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -116,6 +119,7 @@ async function postStoryReviewCrossRef({
|
|
|
116
119
|
prNumber,
|
|
117
120
|
commentUrl,
|
|
118
121
|
severity,
|
|
122
|
+
degradations: result.degradations,
|
|
119
123
|
});
|
|
120
124
|
try {
|
|
121
125
|
await postStructuredComment(provider, storyId, 'notification', body);
|
|
@@ -174,6 +178,8 @@ async function postStoryReviewCrossRef({
|
|
|
174
178
|
* severity?: { critical: number, high: number, medium: number, suggestion: number },
|
|
175
179
|
* posted?: boolean,
|
|
176
180
|
* postedCommentId?: number|null,
|
|
181
|
+
* degraded?: boolean,
|
|
182
|
+
* degradations?: Array<object>,
|
|
177
183
|
* crossRefPosted?: boolean,
|
|
178
184
|
* localLensReview?: object,
|
|
179
185
|
* }>}
|
|
@@ -222,10 +228,13 @@ export async function runStoryScopeReview({
|
|
|
222
228
|
medium: 0,
|
|
223
229
|
suggestion: 0,
|
|
224
230
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
const outcome = formatReviewOutcomeLines({
|
|
232
|
+
severity: sev,
|
|
233
|
+
degradations: result.degradations,
|
|
234
|
+
prNumber,
|
|
235
|
+
posted: result.posted,
|
|
236
|
+
});
|
|
237
|
+
for (const line of outcome) progress('REVIEW', line);
|
|
229
238
|
|
|
230
239
|
const crossRefPosted = await postStoryReviewCrossRef({
|
|
231
240
|
provider,
|
|
@@ -242,6 +251,7 @@ export async function runStoryScopeReview({
|
|
|
242
251
|
severity: sev,
|
|
243
252
|
posted: result.posted,
|
|
244
253
|
postedCommentId: result.postedCommentId ?? null,
|
|
254
|
+
...degradationEnvelope(result.degradations),
|
|
245
255
|
crossRefPosted,
|
|
246
256
|
localLensReview: result.localLensReview,
|
|
247
257
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phases/review-outcome.js — operator-facing rendering of the Story-scope
|
|
3
|
+
* review outcome (Story #4839).
|
|
4
|
+
*
|
|
5
|
+
* The review phase used to report a severity tally and nothing else, which made
|
|
6
|
+
* "every gate ran and found nothing" and "a gate never ran" render identically.
|
|
7
|
+
* Both surfaces the operator actually reads — the close progress stream and the
|
|
8
|
+
* cross-reference comment on the Story — now state the degraded gates
|
|
9
|
+
* explicitly, and always state them (as `none` when healthy) so an absent line
|
|
10
|
+
* can never be mistaken for a clean gate.
|
|
11
|
+
*
|
|
12
|
+
* A degraded gate is **reported, not blocking**: the canonical `npm run lint`
|
|
13
|
+
* close-validation gate has already covered this diff before the review phase
|
|
14
|
+
* runs, so failing the merge on a secondary read of an already-gated surface
|
|
15
|
+
* would cost delivery without buying coverage. The rationale for that posture
|
|
16
|
+
* lives with the channel itself in
|
|
17
|
+
* [`review-providers/degraded-gates.js`](../../review-providers/degraded-gates.js).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { summarizeDegradations } from '../../review-providers/degraded-gates.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pure: the tally suffix shared by the progress line and the cross-reference
|
|
24
|
+
* comment — severity counts plus the degraded-gate state.
|
|
25
|
+
*
|
|
26
|
+
* @param {{ severity: { critical: number, high: number, medium: number, suggestion: number }, degradations?: unknown }} args
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
export function buildOutcomeTally({ severity, degradations }) {
|
|
30
|
+
return (
|
|
31
|
+
`critical:${severity.critical} · high:${severity.high} · ` +
|
|
32
|
+
`medium:${severity.medium} · suggestion:${severity.suggestion} · ` +
|
|
33
|
+
`degraded gates: ${summarizeDegradations(degradations)}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pure: the progress lines announcing a completed review. Always one line
|
|
39
|
+
* naming the tally; a second, explicitly-worded line when a gate did not run so
|
|
40
|
+
* the degradation cannot be skimmed past.
|
|
41
|
+
*
|
|
42
|
+
* @param {{
|
|
43
|
+
* severity: { critical: number, high: number, medium: number, suggestion: number },
|
|
44
|
+
* degradations?: unknown,
|
|
45
|
+
* prNumber: number,
|
|
46
|
+
* posted: boolean,
|
|
47
|
+
* }} args
|
|
48
|
+
* @returns {string[]}
|
|
49
|
+
*/
|
|
50
|
+
export function formatReviewOutcomeLines({
|
|
51
|
+
severity,
|
|
52
|
+
degradations,
|
|
53
|
+
prNumber,
|
|
54
|
+
posted,
|
|
55
|
+
}) {
|
|
56
|
+
const tally = buildOutcomeTally({ severity, degradations });
|
|
57
|
+
const lines = [`Findings — ${tally}. Posted to PR #${prNumber}: ${posted}.`];
|
|
58
|
+
if (summarizeDegradations(degradations) !== 'none') {
|
|
59
|
+
lines.push(
|
|
60
|
+
'⚠️ Review ran DEGRADED — the surface(s) above were not reviewed. The close ' +
|
|
61
|
+
'is not blocked (the canonical `npm run lint` close gate already covered ' +
|
|
62
|
+
'this diff), but this review does not vouch for them.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
import { Logger } from '../../../Logger.js';
|
|
40
40
|
import { runCodeReview } from '../../code-review.js';
|
|
41
|
+
import { summarizeDegradations } from '../../review-providers/degraded-gates.js';
|
|
41
42
|
import { emitBlockedCloseResult } from '../emit-blocked.js';
|
|
42
43
|
import { runLocalLensReview } from './local-lens-review.js';
|
|
43
44
|
import { runStoryReviewCore } from './review-core.js';
|
|
@@ -86,7 +87,10 @@ function buildCodeReviewBlockedExtra({ storyId, reviewResult }) {
|
|
|
86
87
|
function formatReviewSummary(reviewResult) {
|
|
87
88
|
const { high, medium, suggestion } = resolveSeverity(reviewResult);
|
|
88
89
|
const posted = reviewResult?.posted ?? false;
|
|
89
|
-
|
|
90
|
+
// Story #4839 — `degraded` is always stated: an absent degradation line must
|
|
91
|
+
// never be the way an operator concludes that every gate ran.
|
|
92
|
+
const degraded = summarizeDegradations(reviewResult?.degradations);
|
|
93
|
+
return `Review complete — high=${high} medium=${medium} suggestion=${suggestion} degraded=${degraded} (posted=${posted}).`;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/**
|