mandrel 2.59.0 → 2.60.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 +11 -9
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +6 -6
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +8 -4
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +4 -5
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -25
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +76 -95
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +35 -18
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -15
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -28
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +65 -54
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +22 -21
- package/.agents/workflows/mandrel-update.md +36 -21
- package/docs/CHANGELOG.md +35 -0
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +2 -1
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* tip vs base (deterministic `selectAudits` — host walks lenses).
|
|
7
7
|
* 2. Rolls up friction follow-ups across every Story in the run and
|
|
8
8
|
* files/posts them on the primary Story.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4. Reports what the per-Story land tails left the run's container Epics
|
|
9
|
+
* 3. Reports what the per-Story land tails left the run's container Epics
|
|
11
10
|
* in — closed, or still open (Story #5139; read-only since #5280).
|
|
12
11
|
*
|
|
13
12
|
* There is no inert planner-only path: `planRunEpilogue` enumerates steps
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
assessRollupOutcome,
|
|
28
27
|
buildFollowUpsCommentBody,
|
|
29
28
|
gatherRunFrictionSignals,
|
|
29
|
+
publishFollowUpsRollup,
|
|
30
30
|
resolveFollowUpRepos,
|
|
31
31
|
summarizeSignalCategories,
|
|
32
32
|
} from './story-follow-ups.js';
|
|
@@ -34,12 +34,23 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Canonical epilogue step kinds, in execution order.
|
|
37
|
-
*
|
|
37
|
+
*
|
|
38
|
+
* A fourth step — a cross-Story spec/acceptance coherence check — was removed
|
|
39
|
+
* in Story #5341: it re-read every Story body to post a comment nothing read
|
|
40
|
+
* back, on the one run shape (N>1) where that re-read costs the most. ADR
|
|
41
|
+
* `20260917-5341` carries the reasoning.
|
|
42
|
+
*
|
|
43
|
+
* `audit-roster` is **opt-in** since Story #5343 (`--audit-roster`): it is
|
|
44
|
+
* the one step that asks the host to spend a sub-agent per selected lens,
|
|
45
|
+
* and a run whose operator did not ask for an audit sweep paid that cost —
|
|
46
|
+
* plus a `plan-run-audit-roster` comment nobody acted on — on every N>1 run.
|
|
47
|
+
* The other two steps are reporting and stay on by default.
|
|
48
|
+
*
|
|
49
|
+
* @type {readonly ['audit-roster', 'follow-up-rollup', 'epic-close']}
|
|
38
50
|
*/
|
|
39
51
|
export const RUN_EPILOGUE_STEP_KINDS = Object.freeze([
|
|
40
52
|
'audit-roster',
|
|
41
53
|
'follow-up-rollup',
|
|
42
|
-
'sibling-coherence',
|
|
43
54
|
'epic-close',
|
|
44
55
|
]);
|
|
45
56
|
|
|
@@ -159,9 +170,16 @@ function normalizeStoryIds(stories) {
|
|
|
159
170
|
* @param {object} args
|
|
160
171
|
* @param {string} args.planRunId
|
|
161
172
|
* @param {Array<string|number|{ id?: string|number, slug?: string }>} args.stories
|
|
173
|
+
* @param {boolean} [args.auditRoster] Opt into the cross-Story audit roster
|
|
174
|
+
* (Story #5343). Default `false`: the roster spawns auditors and posts a
|
|
175
|
+
* comment the operator has to act on, so it runs only when asked for.
|
|
162
176
|
* @returns {object}
|
|
163
177
|
*/
|
|
164
|
-
export function planRunEpilogue({
|
|
178
|
+
export function planRunEpilogue({
|
|
179
|
+
planRunId,
|
|
180
|
+
stories,
|
|
181
|
+
auditRoster = false,
|
|
182
|
+
} = {}) {
|
|
165
183
|
const ids = normalizeStoryIds(stories);
|
|
166
184
|
const runId =
|
|
167
185
|
typeof planRunId === 'string' && planRunId.trim() !== ''
|
|
@@ -189,21 +207,20 @@ export function planRunEpilogue({ planRunId, stories } = {}) {
|
|
|
189
207
|
`adhoc-${[...ids].sort((a, b) => Number(a) - Number(b)).join('-')}`;
|
|
190
208
|
|
|
191
209
|
const steps = [
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
...(auditRoster
|
|
211
|
+
? [
|
|
212
|
+
{
|
|
213
|
+
kind: 'audit-roster',
|
|
214
|
+
description: `Select cross-Story audit lenses for run ${effectiveRunId}`,
|
|
215
|
+
stories: ids,
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
: []),
|
|
197
219
|
{
|
|
198
220
|
kind: 'follow-up-rollup',
|
|
199
221
|
description: `Friction follow-up roll-up for run ${effectiveRunId}`,
|
|
200
222
|
stories: ids,
|
|
201
223
|
},
|
|
202
|
-
{
|
|
203
|
-
kind: 'sibling-coherence',
|
|
204
|
-
description: `Sibling-coherence check across the ${ids.length} Story specs of run ${effectiveRunId}`,
|
|
205
|
-
stories: ids,
|
|
206
|
-
},
|
|
207
224
|
{
|
|
208
225
|
kind: 'epic-close',
|
|
209
226
|
description: `Report the container Epic state the land tails of run ${effectiveRunId} left behind`,
|
|
@@ -526,48 +543,24 @@ function resolveBaseRef(config) {
|
|
|
526
543
|
return `origin/${branch}`;
|
|
527
544
|
}
|
|
528
545
|
|
|
529
|
-
|
|
546
|
+
/**
|
|
547
|
+
* Render the `plan-run-audit-roster` comment body.
|
|
548
|
+
*
|
|
549
|
+
* Split out of `executeAuditRoster` in Story #5341: the executor's job is to
|
|
550
|
+
* resolve the diff and select the lenses, and a hundred lines of rendering
|
|
551
|
+
* between those two facts and the result is what hid them.
|
|
552
|
+
*
|
|
553
|
+
* @param {object} args
|
|
554
|
+
* @returns {string}
|
|
555
|
+
*/
|
|
556
|
+
function renderAuditRosterBody({
|
|
530
557
|
planRunId,
|
|
531
558
|
stories,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
git,
|
|
536
|
-
selectAuditsFn,
|
|
559
|
+
diff,
|
|
560
|
+
lensGrounding,
|
|
561
|
+
selectedAudits,
|
|
537
562
|
}) {
|
|
538
|
-
|
|
539
|
-
const diff = resolveCombinedDiff({
|
|
540
|
-
stories,
|
|
541
|
-
cwd,
|
|
542
|
-
baseRef: resolveBaseRef(config),
|
|
543
|
-
git,
|
|
544
|
-
});
|
|
545
|
-
// Hand `selectAudits` the change set we just resolved — never a git range for
|
|
546
|
-
// it to re-derive. This function runs in the main checkout *after* the run's
|
|
547
|
-
// Stories merged, so every range it could name (`main...HEAD`) is empty by
|
|
548
|
-
// construction; asking for one is how the roster came to select lenses from
|
|
549
|
-
// zero files while printing the correct file list beside them (Story #4571).
|
|
550
|
-
const lensGrounding = diff.resolved ? 'diff' : 'keyword-only';
|
|
551
|
-
let selectedAudits = [];
|
|
552
|
-
if (Number.isInteger(primaryId) && primaryId > 0) {
|
|
553
|
-
const selected = await selectAuditsFn({
|
|
554
|
-
ticketId: primaryId,
|
|
555
|
-
gate: 'gate3',
|
|
556
|
-
provider,
|
|
557
|
-
changedFiles: diff.resolved ? diff.changedFiles : [],
|
|
558
|
-
});
|
|
559
|
-
selectedAudits = Array.isArray(selected?.selectedAudits)
|
|
560
|
-
? selected.selectedAudits
|
|
561
|
-
: Array.isArray(selected)
|
|
562
|
-
? selected
|
|
563
|
-
: [];
|
|
564
|
-
}
|
|
565
|
-
if (!diff.resolved) {
|
|
566
|
-
Logger.warn(
|
|
567
|
-
`[run-epilogue] plan-run ${planRunId}: combined landed diff unavailable — ${diff.reason}`,
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
const body = [
|
|
563
|
+
return [
|
|
571
564
|
'### plan-run-audit-roster',
|
|
572
565
|
'',
|
|
573
566
|
`Cross-Story audit roster for plan-run \`${planRunId}\`.`,
|
|
@@ -613,6 +606,56 @@ async function executeAuditRoster({
|
|
|
613
606
|
),
|
|
614
607
|
'```',
|
|
615
608
|
].join('\n');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function executeAuditRoster({
|
|
612
|
+
planRunId,
|
|
613
|
+
stories,
|
|
614
|
+
cwd,
|
|
615
|
+
provider,
|
|
616
|
+
config,
|
|
617
|
+
git,
|
|
618
|
+
selectAuditsFn,
|
|
619
|
+
}) {
|
|
620
|
+
const primaryId = Number(stories[0]);
|
|
621
|
+
const diff = resolveCombinedDiff({
|
|
622
|
+
stories,
|
|
623
|
+
cwd,
|
|
624
|
+
baseRef: resolveBaseRef(config),
|
|
625
|
+
git,
|
|
626
|
+
});
|
|
627
|
+
// Hand `selectAudits` the change set we just resolved — never a git range for
|
|
628
|
+
// it to re-derive. This function runs in the main checkout *after* the run's
|
|
629
|
+
// Stories merged, so every range it could name (`main...HEAD`) is empty by
|
|
630
|
+
// construction; asking for one is how the roster came to select lenses from
|
|
631
|
+
// zero files while printing the correct file list beside them (Story #4571).
|
|
632
|
+
const lensGrounding = diff.resolved ? 'diff' : 'keyword-only';
|
|
633
|
+
let selectedAudits = [];
|
|
634
|
+
if (Number.isInteger(primaryId) && primaryId > 0) {
|
|
635
|
+
const selected = await selectAuditsFn({
|
|
636
|
+
ticketId: primaryId,
|
|
637
|
+
gate: 'gate3',
|
|
638
|
+
provider,
|
|
639
|
+
changedFiles: diff.resolved ? diff.changedFiles : [],
|
|
640
|
+
});
|
|
641
|
+
selectedAudits = Array.isArray(selected?.selectedAudits)
|
|
642
|
+
? selected.selectedAudits
|
|
643
|
+
: Array.isArray(selected)
|
|
644
|
+
? selected
|
|
645
|
+
: [];
|
|
646
|
+
}
|
|
647
|
+
if (!diff.resolved) {
|
|
648
|
+
Logger.warn(
|
|
649
|
+
`[run-epilogue] plan-run ${planRunId}: combined landed diff unavailable — ${diff.reason}`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const body = renderAuditRosterBody({
|
|
653
|
+
planRunId,
|
|
654
|
+
stories,
|
|
655
|
+
diff,
|
|
656
|
+
lensGrounding,
|
|
657
|
+
selectedAudits,
|
|
658
|
+
});
|
|
616
659
|
if (Number.isInteger(primaryId) && primaryId > 0) {
|
|
617
660
|
await upsertStructuredComment(
|
|
618
661
|
provider,
|
|
@@ -644,31 +687,29 @@ async function executeAuditRoster({
|
|
|
644
687
|
};
|
|
645
688
|
}
|
|
646
689
|
|
|
647
|
-
|
|
690
|
+
/**
|
|
691
|
+
* Compose the run-scope routed proposals.
|
|
692
|
+
*
|
|
693
|
+
* Story #4850 — `runToken` and `anchorStoryIds` are INPUTS. This used to
|
|
694
|
+
* compose with the primary Story's numeric id standing in for the run and then
|
|
695
|
+
* rewrite the rendered title/body by regex over a `plan-run \d+` substring,
|
|
696
|
+
* which meant the composer's own wording could not be changed without silently
|
|
697
|
+
* breaking the patch. `anchorStoryIds` is what lets the composer tell a corpus
|
|
698
|
+
* confined to this run from one spanning the whole surviving window, so it
|
|
699
|
+
* never titles the latter as if it were the former.
|
|
700
|
+
*
|
|
701
|
+
* @param {object} args
|
|
702
|
+
* @returns {object}
|
|
703
|
+
*/
|
|
704
|
+
function composeRunProposals({
|
|
705
|
+
primaryId,
|
|
648
706
|
planRunId,
|
|
649
707
|
stories,
|
|
650
|
-
|
|
708
|
+
signals,
|
|
651
709
|
config,
|
|
652
|
-
cwd,
|
|
653
|
-
graduateFn = graduateRetroProposals,
|
|
654
710
|
}) {
|
|
655
|
-
// Shared with the story-scoped gather (Story #4649): `storyId` + `details`
|
|
656
|
-
// are what the composer's recovery-netting keys on, and two hand-rolled
|
|
657
|
-
// copies of this loop are how they got dropped in the first place.
|
|
658
|
-
const { signals, window: frictionWindow } = await gatherRunFrictionSignals(
|
|
659
|
-
stories,
|
|
660
|
-
config,
|
|
661
|
-
);
|
|
662
711
|
const repos = resolveFollowUpRepos(config);
|
|
663
|
-
|
|
664
|
-
// Story #4850 — `runToken` and `anchorStoryIds` are INPUTS. This used to
|
|
665
|
-
// compose with the primary Story's numeric id standing in for the run and
|
|
666
|
-
// then rewrite the rendered title/body by regex over a `plan-run \d+`
|
|
667
|
-
// substring, which meant the composer's own wording could not be changed
|
|
668
|
-
// without silently breaking the patch. `anchorStoryIds` is what lets the
|
|
669
|
-
// composer tell a corpus confined to this run from one spanning the whole
|
|
670
|
-
// surviving window, so it never titles the latter as if it were the former.
|
|
671
|
-
const proposals = composeRoutedProposals({
|
|
712
|
+
return composeRoutedProposals({
|
|
672
713
|
anchorId: Number.isInteger(primaryId) ? primaryId : 1,
|
|
673
714
|
anchorKind: 'run',
|
|
674
715
|
runToken: String(planRunId ?? ''),
|
|
@@ -678,7 +719,24 @@ async function executeFollowUpRollup({
|
|
|
678
719
|
signals,
|
|
679
720
|
unresolvedBlockedEvents: [],
|
|
680
721
|
});
|
|
681
|
-
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Hand the run-scope proposals to the graduator (injectable for tests).
|
|
726
|
+
*
|
|
727
|
+
* @param {object} args
|
|
728
|
+
* @returns {Promise<object>}
|
|
729
|
+
*/
|
|
730
|
+
function fileRunProposals({
|
|
731
|
+
primaryId,
|
|
732
|
+
proposals,
|
|
733
|
+
provider,
|
|
734
|
+
config,
|
|
735
|
+
cwd,
|
|
736
|
+
graduateFn,
|
|
737
|
+
}) {
|
|
738
|
+
const repos = resolveFollowUpRepos(config);
|
|
739
|
+
return graduateFn({
|
|
682
740
|
epicId: primaryId,
|
|
683
741
|
provider,
|
|
684
742
|
config,
|
|
@@ -690,6 +748,39 @@ async function executeFollowUpRollup({
|
|
|
690
748
|
routedProposals: proposals,
|
|
691
749
|
cwd,
|
|
692
750
|
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function executeFollowUpRollup({
|
|
754
|
+
planRunId,
|
|
755
|
+
stories,
|
|
756
|
+
provider,
|
|
757
|
+
config,
|
|
758
|
+
cwd,
|
|
759
|
+
graduateFn = graduateRetroProposals,
|
|
760
|
+
}) {
|
|
761
|
+
// Shared with the story-scoped gather (Story #4649): `storyId` + `details`
|
|
762
|
+
// are what the composer's recovery-netting keys on, and two hand-rolled
|
|
763
|
+
// copies of this loop are how they got dropped in the first place.
|
|
764
|
+
const { signals, window: frictionWindow } = await gatherRunFrictionSignals(
|
|
765
|
+
stories,
|
|
766
|
+
config,
|
|
767
|
+
);
|
|
768
|
+
const primaryId = Number(stories[0]);
|
|
769
|
+
const proposals = composeRunProposals({
|
|
770
|
+
primaryId,
|
|
771
|
+
planRunId,
|
|
772
|
+
stories,
|
|
773
|
+
signals,
|
|
774
|
+
config,
|
|
775
|
+
});
|
|
776
|
+
const graduated = await fileRunProposals({
|
|
777
|
+
primaryId,
|
|
778
|
+
proposals,
|
|
779
|
+
provider,
|
|
780
|
+
config,
|
|
781
|
+
cwd,
|
|
782
|
+
graduateFn,
|
|
783
|
+
});
|
|
693
784
|
const categories = summarizeSignalCategories(signals);
|
|
694
785
|
const proposalCount = proposals.framework.length + proposals.consumer.length;
|
|
695
786
|
const outcome = assessRollupOutcome({
|
|
@@ -700,29 +791,98 @@ async function executeFollowUpRollup({
|
|
|
700
791
|
filingErrors: graduated.errors,
|
|
701
792
|
filingSkipped: graduated.skipped,
|
|
702
793
|
});
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
794
|
+
await publishRunRollup({
|
|
795
|
+
primaryId,
|
|
796
|
+
planRunId,
|
|
797
|
+
provider,
|
|
798
|
+
config,
|
|
799
|
+
proposals,
|
|
800
|
+
graduated,
|
|
801
|
+
storyCount: stories.length,
|
|
802
|
+
signalCount: signals.length,
|
|
803
|
+
categories,
|
|
804
|
+
filedCount: graduated.filed?.length ?? 0,
|
|
805
|
+
});
|
|
806
|
+
return buildRollupStepResult({
|
|
807
|
+
signals,
|
|
808
|
+
storyCount: stories.length,
|
|
809
|
+
graduated,
|
|
810
|
+
proposals,
|
|
811
|
+
proposalCount,
|
|
812
|
+
categories,
|
|
813
|
+
outcome,
|
|
814
|
+
frictionWindow,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Render the run-scope roll-up and publish it — the one place that decides
|
|
820
|
+
* where it goes.
|
|
821
|
+
*
|
|
822
|
+
* @param {object} args
|
|
823
|
+
* @returns {Promise<void>}
|
|
824
|
+
*/
|
|
825
|
+
async function publishRunRollup({
|
|
826
|
+
primaryId,
|
|
827
|
+
planRunId,
|
|
828
|
+
provider,
|
|
829
|
+
config,
|
|
830
|
+
proposals,
|
|
831
|
+
graduated,
|
|
832
|
+
storyCount,
|
|
833
|
+
signalCount,
|
|
834
|
+
categories,
|
|
835
|
+
filedCount,
|
|
836
|
+
}) {
|
|
837
|
+
if (!Number.isInteger(primaryId) || primaryId <= 0) return;
|
|
838
|
+
const body = buildFollowUpsCommentBody({
|
|
839
|
+
storyId: primaryId,
|
|
840
|
+
proposals,
|
|
841
|
+
graduated,
|
|
842
|
+
// Story #4578 — the run's Story count is what lets an empty roll-up
|
|
843
|
+
// render as a flagged claim ("0 signals across N Stories") rather than
|
|
844
|
+
// as "nothing to follow up".
|
|
845
|
+
storyCount,
|
|
846
|
+
// Story #4828 — and the corpus is what lets a zero-proposal or
|
|
847
|
+
// zero-filed roll-up name what it saw instead of rendering as clean.
|
|
848
|
+
signalCount,
|
|
849
|
+
categories,
|
|
850
|
+
}).replace(
|
|
851
|
+
`from Story #${primaryId}`,
|
|
852
|
+
`from plan-run \`${planRunId}\` (primary Story #${primaryId})`,
|
|
853
|
+
);
|
|
854
|
+
await publishFollowUpsRollup({
|
|
855
|
+
anchorId: primaryId,
|
|
856
|
+
body,
|
|
857
|
+
filedCount,
|
|
858
|
+
provider,
|
|
859
|
+
config,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Assemble the `follow-up-rollup` step result. Split out of
|
|
865
|
+
* `executeFollowUpRollup` in Story #5341: the literal had grown to carry
|
|
866
|
+
* every suspicion the roll-up can raise, and the reporting shape is what
|
|
867
|
+
* callers read, not how it was gathered.
|
|
868
|
+
*
|
|
869
|
+
* @param {object} args
|
|
870
|
+
* @returns {object}
|
|
871
|
+
*/
|
|
872
|
+
function buildRollupStepResult({
|
|
873
|
+
signals,
|
|
874
|
+
storyCount,
|
|
875
|
+
graduated,
|
|
876
|
+
proposals,
|
|
877
|
+
proposalCount,
|
|
878
|
+
categories,
|
|
879
|
+
outcome,
|
|
880
|
+
frictionWindow,
|
|
881
|
+
}) {
|
|
722
882
|
return {
|
|
723
883
|
kind: 'follow-up-rollup',
|
|
724
884
|
signalCount: signals.length,
|
|
725
|
-
storyCount
|
|
885
|
+
storyCount,
|
|
726
886
|
filed: graduated.filed?.length ?? 0,
|
|
727
887
|
// Story #4850 — the recurrence window the gather actually applied, and what
|
|
728
888
|
// it dropped. `signalCount` alone cannot distinguish "the window is bounded
|
|
@@ -760,87 +920,10 @@ async function executeFollowUpRollup({
|
|
|
760
920
|
// Story #4578 — zero signals across a multi-Story run is a claim, not a
|
|
761
921
|
// clean bill of health. Surfaced on the step result so the CLI can warn
|
|
762
922
|
// the operator without re-deriving it from the comment prose.
|
|
763
|
-
emptyRollupSuspect: signals.length === 0 &&
|
|
923
|
+
emptyRollupSuspect: signals.length === 0 && storyCount > 1,
|
|
764
924
|
};
|
|
765
925
|
}
|
|
766
926
|
|
|
767
|
-
function extractSection(body, heading) {
|
|
768
|
-
if (typeof body !== 'string') return '';
|
|
769
|
-
const re = new RegExp(
|
|
770
|
-
`(?:^|\\n)## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`,
|
|
771
|
-
'i',
|
|
772
|
-
);
|
|
773
|
-
const match = body.match(re);
|
|
774
|
-
return match ? match[1].trim() : '';
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
async function executeSiblingCoherence({ planRunId, stories, provider }) {
|
|
778
|
-
const findings = [];
|
|
779
|
-
const bodies = [];
|
|
780
|
-
for (const raw of stories) {
|
|
781
|
-
const sid = Number(raw);
|
|
782
|
-
if (!Number.isInteger(sid) || sid <= 0) continue;
|
|
783
|
-
const ticket = await provider.getTicket(sid);
|
|
784
|
-
bodies.push({
|
|
785
|
-
id: sid,
|
|
786
|
-
title: ticket?.title ?? '',
|
|
787
|
-
acceptance: extractSection(ticket?.body ?? '', 'Acceptance'),
|
|
788
|
-
spec: extractSection(ticket?.body ?? '', 'Spec'),
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
const withAcceptance = bodies.filter((b) => b.acceptance.length > 0);
|
|
792
|
-
if (withAcceptance.length > 0 && withAcceptance.length < bodies.length) {
|
|
793
|
-
const missing = bodies
|
|
794
|
-
.filter((b) => b.acceptance.length === 0)
|
|
795
|
-
.map((b) => `#${b.id}`);
|
|
796
|
-
findings.push(
|
|
797
|
-
`Stories missing ## Acceptance while siblings declare ACs: ${missing.join(', ')}`,
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
// Detect identical non-empty Spec blobs (likely copy-paste drift).
|
|
801
|
-
const specMap = new Map();
|
|
802
|
-
for (const b of bodies) {
|
|
803
|
-
if (!b.spec) continue;
|
|
804
|
-
const key = b.spec.replace(/\s+/g, ' ').slice(0, 400);
|
|
805
|
-
if (!specMap.has(key)) specMap.set(key, []);
|
|
806
|
-
specMap.get(key).push(b.id);
|
|
807
|
-
}
|
|
808
|
-
for (const ids of specMap.values()) {
|
|
809
|
-
if (ids.length > 1) {
|
|
810
|
-
findings.push(
|
|
811
|
-
`Duplicate ## Spec prose across Stories ${ids.map((id) => `#${id}`).join(', ')} — split or dedupe.`,
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const primaryId = Number(stories[0]);
|
|
816
|
-
const body = [
|
|
817
|
-
'### plan-run-sibling-coherence',
|
|
818
|
-
'',
|
|
819
|
-
`Sibling-coherence check for plan-run \`${planRunId}\`.`,
|
|
820
|
-
'',
|
|
821
|
-
findings.length === 0
|
|
822
|
-
? '_No coherence findings._'
|
|
823
|
-
: findings.map((f) => `- ${f}`).join('\n'),
|
|
824
|
-
'',
|
|
825
|
-
'```json',
|
|
826
|
-
JSON.stringify(
|
|
827
|
-
{ planRunId, stories: stories.map(Number), findings },
|
|
828
|
-
null,
|
|
829
|
-
2,
|
|
830
|
-
),
|
|
831
|
-
'```',
|
|
832
|
-
].join('\n');
|
|
833
|
-
if (Number.isInteger(primaryId) && primaryId > 0) {
|
|
834
|
-
await upsertStructuredComment(
|
|
835
|
-
provider,
|
|
836
|
-
primaryId,
|
|
837
|
-
'plan-run-sibling-coherence',
|
|
838
|
-
body,
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
return { kind: 'sibling-coherence', findings };
|
|
842
|
-
}
|
|
843
|
-
|
|
844
927
|
/**
|
|
845
928
|
* Execute the per-run epilogue. Throws only on programmer misuse; step
|
|
846
929
|
* failures are collected into `errors[]`.
|
|
@@ -856,6 +939,8 @@ async function executeSiblingCoherence({ planRunId, stories, provider }) {
|
|
|
856
939
|
* @param {typeof graduateRetroProposals} [args.graduateFn] - Injection seam so
|
|
857
940
|
* the roll-up's reporting layer can be asserted against a filer that fails
|
|
858
941
|
* (Story #4828) without spawning a real `gh`.
|
|
942
|
+
* @param {boolean} [args.auditRoster] Opt into the audit-roster step
|
|
943
|
+
* (Story #5343); default `false`.
|
|
859
944
|
* @returns {Promise<object>}
|
|
860
945
|
*/
|
|
861
946
|
export async function runPlanRunEpilogue({
|
|
@@ -867,8 +952,9 @@ export async function runPlanRunEpilogue({
|
|
|
867
952
|
git = { gitSpawn },
|
|
868
953
|
selectAuditsFn = selectAudits,
|
|
869
954
|
graduateFn = graduateRetroProposals,
|
|
955
|
+
auditRoster = false,
|
|
870
956
|
} = {}) {
|
|
871
|
-
const plan = planRunEpilogue({ planRunId, stories });
|
|
957
|
+
const plan = planRunEpilogue({ planRunId, stories, auditRoster });
|
|
872
958
|
if (!plan.applicable) {
|
|
873
959
|
return { ...plan, results: [], errors: [] };
|
|
874
960
|
}
|
|
@@ -903,14 +989,6 @@ export async function runPlanRunEpilogue({
|
|
|
903
989
|
graduateFn,
|
|
904
990
|
}),
|
|
905
991
|
);
|
|
906
|
-
} else if (step.kind === 'sibling-coherence') {
|
|
907
|
-
results.push(
|
|
908
|
-
await executeSiblingCoherence({
|
|
909
|
-
planRunId: plan.planRunId,
|
|
910
|
-
stories: plan.stories,
|
|
911
|
-
provider,
|
|
912
|
-
}),
|
|
913
|
-
);
|
|
914
992
|
} else if (step.kind === 'epic-close') {
|
|
915
993
|
results.push(
|
|
916
994
|
await executeEpicClose({ stories: plan.stories, provider }),
|