mandrel 2.9.0 → 2.11.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/.markdownlint.json +4 -0
- package/.agents/agents/acceptance-critic.md +30 -5
- package/.agents/agents/auditor.md +36 -19
- package/.agents/agents/plan-critic.md +31 -5
- package/.agents/agents/story-worker.md +91 -100
- package/.agents/docs/configuration.md +39 -25
- package/.agents/docs/execution-reference.md +13 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +131 -265
- package/.agents/rules/git-conventions.md +47 -83
- package/.agents/rules/orchestration-error-handling.md +28 -0
- package/.agents/schemas/agentrc.schema.json +36 -9
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/acceptance-eval.js +10 -6
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/check-test-temp-hygiene.js +438 -0
- package/.agents/scripts/deliver-recover.js +23 -6
- package/.agents/scripts/lib/audit-suite/index.js +5 -0
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +179 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/baselines/env-overrides.js +33 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/preview-gates.js +5 -0
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +10 -1
- package/.agents/scripts/lib/config/quality.js +13 -0
- package/.agents/scripts/lib/config/temp-paths.js +121 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +30 -0
- package/.agents/scripts/lib/config-settings-schema.js +12 -16
- package/.agents/scripts/lib/observability/metrics-ledger.js +217 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +7 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +45 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +97 -4
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +7 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +533 -93
- package/.agents/scripts/lib/orchestration/deliver-recover.js +137 -10
- package/.agents/scripts/lib/orchestration/merge-block-class.js +36 -15
- package/.agents/scripts/lib/orchestration/merge-poll.js +213 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -3
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +182 -9
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +29 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +31 -82
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +153 -2
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +221 -14
- package/.agents/scripts/lib/orchestration/resolve-stories.js +12 -1
- package/.agents/scripts/lib/orchestration/review-depth.js +9 -4
- package/.agents/scripts/lib/orchestration/review-providers/native.js +34 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +230 -79
- package/.agents/scripts/lib/orchestration/spec-budget.js +78 -0
- package/.agents/scripts/lib/orchestration/story-body-gate.js +72 -0
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +89 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +73 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +6 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +18 -62
- package/.agents/scripts/lib/templates/decomposer-prompts.js +13 -6
- package/.agents/scripts/lib/test-env.js +65 -0
- package/.agents/scripts/plan-context.js +84 -9
- package/.agents/scripts/plan-critics.js +115 -3
- package/.agents/scripts/plan-persist.js +11 -1
- package/.agents/scripts/plan-run-epilogue.js +1 -1
- package/.agents/scripts/resolve-stories.js +2 -0
- package/.agents/scripts/single-story-confirm-merge.js +65 -5
- package/.agents/scripts/stories-wave-tick.js +1 -1
- package/.agents/workflows/deliver.md +88 -230
- package/.agents/workflows/helpers/acceptance-self-eval.md +16 -5
- package/.agents/workflows/helpers/deliver-reference.md +171 -0
- package/.agents/workflows/helpers/deliver-story-reference.md +223 -0
- package/.agents/workflows/helpers/deliver-story.md +115 -432
- package/.agents/workflows/helpers/plan-reference.md +246 -0
- package/.agents/workflows/plan.md +108 -304
- package/docs/CHANGELOG.md +46 -0
- package/lib/cli/registry.js +31 -14
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +92 -0
- package/package.json +1 -1
|
@@ -30,11 +30,89 @@ import {
|
|
|
30
30
|
normalizeSupersedes,
|
|
31
31
|
} from './supersede-ops.js';
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Label prefix grouping the Stories one plan-persist run authored.
|
|
35
|
+
*
|
|
36
|
+
* Reintroduced (Story #4692) after Story #4540 retired it: #4540 was right
|
|
37
|
+
* that batch identity is the wrong axis for *ordering delivery across runs*
|
|
38
|
+
* (`/deliver` takes ids and resolves the graph from live state — that stays),
|
|
39
|
+
* but it is the correct axis for *grouping the Stories one plan run created*
|
|
40
|
+
* so a cohort is filterable and traceable in the GitHub UI. The label is
|
|
41
|
+
* metadata only; nothing in persist or delivery reads it as a
|
|
42
|
+
* delivery-resolution input.
|
|
43
|
+
*/
|
|
44
|
+
export const PLAN_RUN_LABEL_PREFIX = 'plan-run::';
|
|
45
|
+
|
|
46
|
+
/** Stable color for the cohort grouping label (`ensureLabels`). */
|
|
47
|
+
const PLAN_RUN_LABEL_COLOR = '#C5DEF5';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stable color for the `route::lite` ceremony-route hint (Story #4707;
|
|
51
|
+
* hint-only since Story #4722 — `/deliver` re-derives the route from the
|
|
52
|
+
* Story body's shape).
|
|
53
|
+
*/
|
|
54
|
+
const LITE_ROUTE_LABEL_COLOR = '#D4C5F9';
|
|
55
|
+
|
|
56
|
+
/** Length of the derived plan-run id (hex chars). */
|
|
57
|
+
const PLAN_RUN_ID_LENGTH = 8;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize a caller-supplied plan-run token. Kept shared so human-readable
|
|
61
|
+
* ids map to one canonical label shape.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} id
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function normalizePlanRunId(id) {
|
|
67
|
+
const token = String(id ?? '')
|
|
68
|
+
.trim()
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/^plan-run::/, '')
|
|
71
|
+
.replace(/[^a-z0-9._-]+/g, '-');
|
|
72
|
+
if (!token) {
|
|
73
|
+
throw new Error('plan-run id requires a non-empty planRunId');
|
|
74
|
+
}
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build a `plan-run::<id>` label from an explicit id.
|
|
80
|
+
*
|
|
81
|
+
* Unlike the pre-#4540 shape, this never mints a random token — a random id
|
|
82
|
+
* would split a resumed persist's cohort across two labels. Derive the id
|
|
83
|
+
* from the authored artifacts via {@link derivePlanRunId} instead.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} id
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function planRunLabel(id) {
|
|
89
|
+
return `${PLAN_RUN_LABEL_PREFIX}${normalizePlanRunId(id)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Derive the deterministic plan-run id for a cohort of assembled Stories.
|
|
94
|
+
*
|
|
95
|
+
* Hashes the **sorted** set of per-Story plan fingerprints (the same
|
|
96
|
+
* content identities the resumable-create contract adopts on), so the id is
|
|
97
|
+
* a pure function of the authored artifacts: the same `stories.json` yields
|
|
98
|
+
* the same `plan-run::<id>` on every run — a persist resumed after a
|
|
99
|
+
* mid-run failure applies the identical label to the newly-created
|
|
100
|
+
* remainder that the already-created (adopted) Stories carry — while a
|
|
101
|
+
* different plan derives a different label. Sorting makes the id
|
|
102
|
+
* independent of creation order.
|
|
103
|
+
*
|
|
104
|
+
* @param {string[]} fingerprints Per-Story plan fingerprints.
|
|
105
|
+
* @returns {string} Hex id, {@link PLAN_RUN_ID_LENGTH} chars.
|
|
106
|
+
*/
|
|
107
|
+
export function derivePlanRunId(fingerprints) {
|
|
108
|
+
const sorted = (Array.isArray(fingerprints) ? fingerprints : [])
|
|
109
|
+
.map(String)
|
|
110
|
+
.sort();
|
|
111
|
+
return createHash('sha256')
|
|
112
|
+
.update(sorted.join(' '))
|
|
113
|
+
.digest('hex')
|
|
114
|
+
.slice(0, PLAN_RUN_ID_LENGTH);
|
|
115
|
+
}
|
|
38
116
|
|
|
39
117
|
/**
|
|
40
118
|
* Marker prefix for the per-Story plan fingerprint appended to every
|
|
@@ -113,13 +191,19 @@ function planFingerprintMarker(fingerprint) {
|
|
|
113
191
|
/**
|
|
114
192
|
* Labels the authoring pass is never allowed to set. The `agent::*` axis is
|
|
115
193
|
* the runtime's lifecycle state (persist owns the terminal `agent::ready`
|
|
116
|
-
* flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy,
|
|
117
|
-
* `persona::*` is a retired axis
|
|
194
|
+
* flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy,
|
|
195
|
+
* `persona::*` is a retired axis, `plan-run::*` is the runtime-derived
|
|
196
|
+
* cohort grouping axis (Story #4692), and `route::*` is the runtime-derived
|
|
197
|
+
* ceremony-route axis (Story #4707) — a hand-authored entry on either
|
|
198
|
+
* derived axis would compete with the deterministic label persist applies
|
|
199
|
+
* itself.
|
|
118
200
|
*/
|
|
119
201
|
const FORBIDDEN_LABEL_PREFIXES = Object.freeze([
|
|
120
202
|
'agent::',
|
|
121
203
|
'type::',
|
|
122
204
|
'persona::',
|
|
205
|
+
PLAN_RUN_LABEL_PREFIX,
|
|
206
|
+
'route::',
|
|
123
207
|
]);
|
|
124
208
|
|
|
125
209
|
/** GitHub's own label-name ceiling. */
|
|
@@ -607,6 +691,64 @@ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
|
|
|
607
691
|
}
|
|
608
692
|
}
|
|
609
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Ensure a runtime-derived persist label (`plan-run::<id>` cohort grouping,
|
|
696
|
+
* `route::lite` route marker) exists before it is applied — GitHub's
|
|
697
|
+
* create-issue path does not auto-create unknown labels on every provider
|
|
698
|
+
* route, and an opaque derived label never exists yet.
|
|
699
|
+
*
|
|
700
|
+
* **Non-fatal by design**, matching the native-blocked_by mirroring posture:
|
|
701
|
+
* neither label is load-bearing for correctness (grouping is cosmetic, and
|
|
702
|
+
* the route label is a human-visible hint only — `/deliver` re-derives the
|
|
703
|
+
* route from the Story body's shape, Story #4722), so it is never a reason
|
|
704
|
+
* to fail persist. On an ensure
|
|
705
|
+
* failure (throw, or the label reported `missing` by the post-loop
|
|
706
|
+
* reconcile) the create loop proceeds **without** the label — applying an
|
|
707
|
+
* unensured label could fail the issue create itself, and the Stories matter
|
|
708
|
+
* more than their metadata. A provider that exposes no `ensureLabels` (test
|
|
709
|
+
* fakes, minimal providers) is assumed to accept arbitrary labels on create.
|
|
710
|
+
*
|
|
711
|
+
* @param {object} args
|
|
712
|
+
* @param {object} args.provider
|
|
713
|
+
* @param {string} args.label
|
|
714
|
+
* @param {string} args.color
|
|
715
|
+
* @param {string} args.description
|
|
716
|
+
* @param {string} args.role Human-readable role for the degrade warning.
|
|
717
|
+
* @returns {Promise<boolean>} Whether the create loop should apply the label.
|
|
718
|
+
*/
|
|
719
|
+
async function ensurePersistLabel({
|
|
720
|
+
provider,
|
|
721
|
+
label,
|
|
722
|
+
color,
|
|
723
|
+
description,
|
|
724
|
+
role,
|
|
725
|
+
}) {
|
|
726
|
+
if (typeof provider?.ensureLabels !== 'function') {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const result = await provider.ensureLabels([
|
|
731
|
+
{ name: label, color, description },
|
|
732
|
+
]);
|
|
733
|
+
if (Array.isArray(result?.missing) && result.missing.includes(label)) {
|
|
734
|
+
Logger.warn(
|
|
735
|
+
`[plan-persist] ${role} label "${label}" could not be verified ` +
|
|
736
|
+
'on the remote — creating the Stories without it. Add the label ' +
|
|
737
|
+
'by hand if you want it.',
|
|
738
|
+
);
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
return true;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
Logger.warn(
|
|
744
|
+
`[plan-persist] ${role} label ensure failed (${err.message}) — ` +
|
|
745
|
+
'creating the Stories without it. Add the label by hand if you ' +
|
|
746
|
+
'want it.',
|
|
747
|
+
);
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
610
752
|
/**
|
|
611
753
|
* Create Story issues via `provider.createIssue`, resumably.
|
|
612
754
|
*
|
|
@@ -631,24 +773,43 @@ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
|
|
|
631
773
|
* is named in a warning. Adoption never rewrites a body, so keying it on
|
|
632
774
|
* anything weaker than content would silently ship a stale one.
|
|
633
775
|
*
|
|
634
|
-
* Story
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
* the
|
|
638
|
-
*
|
|
776
|
+
* **Every created Story carries the cohort's `plan-run::<id>` grouping
|
|
777
|
+
* label** (Story #4692, metadata only). The id is deterministic over the
|
|
778
|
+
* authored artifacts ({@link derivePlanRunId}), so a resumed persist derives
|
|
779
|
+
* the identical label its adopted Stories already carry from their original
|
|
780
|
+
* create — no relabel call is needed on the resume path, and the cohort is
|
|
781
|
+
* never split across two labels. The label is ensured to exist before the
|
|
782
|
+
* first POST, **non-fatally**: grouping is cosmetic and never fails the run
|
|
783
|
+
* (see `ensureCohortLabel`). `/deliver` never reads it — delivery stays
|
|
784
|
+
* ids-only over live state (Story #4540's actual point).
|
|
639
785
|
*
|
|
640
786
|
* **Sibling order is mirrored into native GitHub `blocked_by` edges** once
|
|
641
787
|
* every id is known (Story #4544), so plan-created order stops depending on
|
|
642
788
|
* prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
|
|
643
789
|
*
|
|
790
|
+
* **A lite-routed cohort carries the `route::lite` hint** (Story #4707,
|
|
791
|
+
* hint-only since Story #4722). When the caller resolves the plan's
|
|
792
|
+
* effective complexity route to `lite` (the planner's recorded verdict,
|
|
793
|
+
* upheld by the shape backstop), it passes the label via `opts.routeLabel`
|
|
794
|
+
* and every created Story carries it — a **human-visible hint only**, never
|
|
795
|
+
* the control signal: `/deliver` re-derives the route from each Story body's
|
|
796
|
+
* own shape (`resolveStoryDispatchMode`), so a lost or failed label write
|
|
797
|
+
* cannot misroute delivery. A full-routed plan passes nothing and its
|
|
798
|
+
* Stories carry **no** route label. Like the cohort label, the ensure is
|
|
799
|
+
* non-fatal — the label is cosmetic either way.
|
|
800
|
+
*
|
|
644
801
|
* @param {object} args
|
|
645
802
|
* @param {object} args.provider
|
|
646
803
|
* @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
|
|
647
804
|
* @param {object} [args.opts]
|
|
648
805
|
* @param {boolean} [args.opts.dryRun=false]
|
|
806
|
+
* @param {string|null} [args.opts.routeLabel=null] Route marker label to
|
|
807
|
+
* apply to every created Story (`route::lite`), or null for none.
|
|
649
808
|
* @returns {Promise<{
|
|
650
809
|
* created: Array<{ slug: string, id: number, url?: string, title: string, adopted: boolean }>,
|
|
651
810
|
* dependencyEdges: { edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null,
|
|
811
|
+
* planRunLabel: string,
|
|
812
|
+
* routeLabel: string|null,
|
|
652
813
|
* }>}
|
|
653
814
|
*/
|
|
654
815
|
export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
@@ -659,6 +820,17 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
659
820
|
}
|
|
660
821
|
|
|
661
822
|
const list = Array.isArray(stories) ? stories : [];
|
|
823
|
+
const routeLabel =
|
|
824
|
+
typeof opts.routeLabel === 'string' && opts.routeLabel.trim() !== ''
|
|
825
|
+
? opts.routeLabel.trim()
|
|
826
|
+
: null;
|
|
827
|
+
|
|
828
|
+
// Derived once for the whole cohort, before any write — a pure function of
|
|
829
|
+
// the authored artifacts, so dry-run can report it write-free and a resume
|
|
830
|
+
// re-derives the identical label.
|
|
831
|
+
const cohortLabel = planRunLabel(
|
|
832
|
+
derivePlanRunId(list.map((story) => story.fingerprint)),
|
|
833
|
+
);
|
|
662
834
|
|
|
663
835
|
if (opts.dryRun) {
|
|
664
836
|
return {
|
|
@@ -670,9 +842,32 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
670
842
|
adopted: false,
|
|
671
843
|
})),
|
|
672
844
|
dependencyEdges: null,
|
|
845
|
+
planRunLabel: cohortLabel,
|
|
846
|
+
routeLabel,
|
|
673
847
|
};
|
|
674
848
|
}
|
|
675
849
|
|
|
850
|
+
const applyCohortLabel = await ensurePersistLabel({
|
|
851
|
+
provider,
|
|
852
|
+
label: cohortLabel,
|
|
853
|
+
color: PLAN_RUN_LABEL_COLOR,
|
|
854
|
+
description:
|
|
855
|
+
'Groups the Stories one /plan persist run authored (metadata ' +
|
|
856
|
+
'only — /deliver stays ids-only).',
|
|
857
|
+
role: 'cohort',
|
|
858
|
+
});
|
|
859
|
+
const applyRouteLabel =
|
|
860
|
+
routeLabel !== null &&
|
|
861
|
+
(await ensurePersistLabel({
|
|
862
|
+
provider,
|
|
863
|
+
label: routeLabel,
|
|
864
|
+
color: LITE_ROUTE_LABEL_COLOR,
|
|
865
|
+
description:
|
|
866
|
+
'Ceremony-lite hint (human-visible only): /deliver re-derives the ' +
|
|
867
|
+
'route from the Story body shape; every close gate runs unchanged.',
|
|
868
|
+
role: 'route-marker',
|
|
869
|
+
}));
|
|
870
|
+
|
|
676
871
|
const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
|
|
677
872
|
const created = [];
|
|
678
873
|
const idBySlug = new Map();
|
|
@@ -681,6 +876,9 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
681
876
|
const already = byFingerprint.get(story.fingerprint);
|
|
682
877
|
if (!already) warnOnDivergentSameTitleStory(story, idsByTitle);
|
|
683
878
|
if (already) {
|
|
879
|
+
// Adopted Stories already carry the cohort label from their original
|
|
880
|
+
// create — the deterministic derivation guarantees it is the same
|
|
881
|
+
// label this run derived, so no relabel call is needed here.
|
|
684
882
|
Logger.info(
|
|
685
883
|
`[plan-persist] resuming: Story "${story.slug}" already exists as ` +
|
|
686
884
|
`#${already.id} with byte-identical authored content ` +
|
|
@@ -700,7 +898,11 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
700
898
|
const result = await provider.createIssue({
|
|
701
899
|
title: story.title,
|
|
702
900
|
body: renderStoryBodyForCreate(story, idBySlug),
|
|
703
|
-
labels: [
|
|
901
|
+
labels: [
|
|
902
|
+
...story.labels,
|
|
903
|
+
...(applyCohortLabel ? [cohortLabel] : []),
|
|
904
|
+
...(applyRouteLabel ? [routeLabel] : []),
|
|
905
|
+
],
|
|
704
906
|
});
|
|
705
907
|
const id = result?.id ?? result?.number;
|
|
706
908
|
if (!Number.isInteger(id)) {
|
|
@@ -727,7 +929,12 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
727
929
|
idBySlug,
|
|
728
930
|
});
|
|
729
931
|
|
|
730
|
-
return {
|
|
932
|
+
return {
|
|
933
|
+
created,
|
|
934
|
+
dependencyEdges,
|
|
935
|
+
planRunLabel: cohortLabel,
|
|
936
|
+
routeLabel: applyRouteLabel ? routeLabel : null,
|
|
937
|
+
};
|
|
731
938
|
}
|
|
732
939
|
|
|
733
940
|
/**
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
extractChangePaths,
|
|
35
35
|
parse as parseStoryBody,
|
|
36
36
|
} from '../story-body/story-body.js';
|
|
37
|
+
import { resolveStoryDispatchMode } from './complexity-gate.js';
|
|
37
38
|
|
|
38
39
|
/** Labels/state that mean a blocker no longer gates its dependents. */
|
|
39
40
|
const DONE_LABEL = 'agent::done';
|
|
@@ -306,17 +307,27 @@ export function buildStoriesEnvelope({
|
|
|
306
307
|
nativeEdges = new Map(),
|
|
307
308
|
foreignDone = [],
|
|
308
309
|
warn,
|
|
310
|
+
config,
|
|
309
311
|
}) {
|
|
310
312
|
const sorted = [...stories].sort((a, b) => a.id - b.id);
|
|
311
313
|
const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
|
|
312
314
|
return {
|
|
313
315
|
kind: 'stories',
|
|
314
|
-
|
|
316
|
+
// `dispatchMode` (Story #4722): the resolver derives the per-Story
|
|
317
|
+
// execution mode from the fetched Story BODY's own shape (the shared
|
|
318
|
+
// shape function in `complexity-gate.js`) so `/deliver` reads one field —
|
|
319
|
+
// `inline` (lite-shaped: no story-worker / acceptance-critic sub-agent
|
|
320
|
+
// boots) or `subagent` (everything else, the conservative default). The
|
|
321
|
+
// `route::lite` label is a human-visible hint only, never the control
|
|
322
|
+
// signal: a lost label cannot misroute delivery. Model-side fan-out
|
|
323
|
+
// only; close gates are untouched.
|
|
324
|
+
stories: sorted.map(({ id, title, body, url, labels, state }) => ({
|
|
315
325
|
id,
|
|
316
326
|
title,
|
|
317
327
|
url,
|
|
318
328
|
labels,
|
|
319
329
|
state,
|
|
330
|
+
dispatchMode: resolveStoryDispatchMode({ body, labels, config }).mode,
|
|
320
331
|
})),
|
|
321
332
|
dag: storiesToDag(sorted, nativeEdges, warn),
|
|
322
333
|
done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
|
|
@@ -74,10 +74,15 @@ export const DEFAULT_DIFF_WIDTH = Object.freeze({
|
|
|
74
74
|
* changed-file set intersect any sensitive-path class registered in
|
|
75
75
|
* `audit-rules.json`?
|
|
76
76
|
*
|
|
77
|
-
* This is the **single source** of the derived level —
|
|
78
|
-
* ({@link resolveDepth})
|
|
79
|
-
* (`ceremony-routing.js#resolveCeremonyForRisk`)
|
|
80
|
-
*
|
|
77
|
+
* This is the **single source** of the derived level — the review depth
|
|
78
|
+
* ({@link resolveDepth}), the acceptance-critic fresh-vs-inline routing
|
|
79
|
+
* (`ceremony-routing.js#resolveCeremonyForRisk`), and the dispatch-side
|
|
80
|
+
* complexity routing (`complexity-gate.js#deriveStoryShape`, Story #4722)
|
|
81
|
+
* all consume what this returns, so no ceremony decision can disagree about
|
|
82
|
+
* how risky a change is. Dispatch reads the **predicted** shape (the Story's
|
|
83
|
+
* declared `changes[]` footprint) and close reads the **actual** diff — one
|
|
84
|
+
* taxonomy, two read points, which is what keeps a lite-shaped Story whose
|
|
85
|
+
* footprint touches a sensitive path on the full route with its fresh critic.
|
|
81
86
|
*
|
|
82
87
|
* Returns `null` — the fail-safe "no derivable signal" level — when the change
|
|
83
88
|
* set is empty/unknown or the manifest cannot be read. Both downstream
|
|
@@ -48,6 +48,10 @@ import {
|
|
|
48
48
|
calculateReport,
|
|
49
49
|
classifyReport,
|
|
50
50
|
} from '../../maintainability-engine.js';
|
|
51
|
+
import {
|
|
52
|
+
emitRuntimeFriction,
|
|
53
|
+
RUNTIME_FRICTION_CATEGORIES,
|
|
54
|
+
} from '../../observability/runtime-friction.js';
|
|
51
55
|
import { PROJECT_ROOT } from '../../project-root.js';
|
|
52
56
|
import { transpileIfNeeded } from '../../transpile.js';
|
|
53
57
|
|
|
@@ -456,8 +460,10 @@ export async function analyzeChangedFiles(
|
|
|
456
460
|
* Pure: turn a lint summary into Finding(s). Lint errors collapse into a
|
|
457
461
|
* single high-risk finding (the structured comment shows the count); lint
|
|
458
462
|
* warnings collapse into a single suggestion. An `executionFailed` summary
|
|
459
|
-
* produces
|
|
460
|
-
* a
|
|
463
|
+
* produces **zero** findings (Story #4699): a runner that could not execute
|
|
464
|
+
* is an operational degradation, not a code finding — the provider routes it
|
|
465
|
+
* to friction telemetry instead so severity counts reflect code findings
|
|
466
|
+
* only.
|
|
461
467
|
*
|
|
462
468
|
* @param {{ errors: number, warnings: number, parsed?: boolean, skipped?: boolean, mode?: string, executionFailed?: boolean, evidenceSkipped?: boolean }} lintSummary
|
|
463
469
|
* @returns {Finding[]}
|
|
@@ -466,20 +472,7 @@ export function buildLintFindings(lintSummary) {
|
|
|
466
472
|
if (lintSummary.mode === 'off') return [];
|
|
467
473
|
if (lintSummary.evidenceSkipped) return [];
|
|
468
474
|
if (lintSummary.skipped) return [];
|
|
469
|
-
if (lintSummary.executionFailed)
|
|
470
|
-
return [
|
|
471
|
-
{
|
|
472
|
-
severity: 'suggestion',
|
|
473
|
-
title: 'Lint runner could not execute',
|
|
474
|
-
body:
|
|
475
|
-
'The scoped lint runner produced no parseable output (binary missing, ' +
|
|
476
|
-
'parse failure, or environment issue). Verify with the canonical ' +
|
|
477
|
-
'`npm run lint` before merging — treating as a suggestion to avoid a ' +
|
|
478
|
-
'false high-risk signal.',
|
|
479
|
-
category: 'lint',
|
|
480
|
-
},
|
|
481
|
-
];
|
|
482
|
-
}
|
|
475
|
+
if (lintSummary.executionFailed) return [];
|
|
483
476
|
const findings = [];
|
|
484
477
|
if (lintSummary.errors > 0) {
|
|
485
478
|
findings.push({
|
|
@@ -550,6 +543,7 @@ async function runLintPhase({
|
|
|
550
543
|
* runScopedLintFn?: typeof runScopedLint,
|
|
551
544
|
* analyzeChangedFilesFn?: typeof analyzeChangedFiles,
|
|
552
545
|
* buildLintFindingsFn?: typeof buildLintFindings,
|
|
546
|
+
* emitToolDegradationFn?: typeof emitRuntimeFriction,
|
|
553
547
|
* logger?: { info?: Function, warn?: Function, error?: Function },
|
|
554
548
|
* scopeLint?: 'changed-only'|'off',
|
|
555
549
|
* }} [deps]
|
|
@@ -561,6 +555,7 @@ export function createNativeProvider(deps = {}) {
|
|
|
561
555
|
runScopedLintFn = runScopedLint,
|
|
562
556
|
analyzeChangedFilesFn = analyzeChangedFiles,
|
|
563
557
|
buildLintFindingsFn = buildLintFindings,
|
|
558
|
+
emitToolDegradationFn = emitRuntimeFriction,
|
|
564
559
|
logger,
|
|
565
560
|
scopeLint = 'changed-only',
|
|
566
561
|
} = deps;
|
|
@@ -624,6 +619,29 @@ export function createNativeProvider(deps = {}) {
|
|
|
624
619
|
logger,
|
|
625
620
|
});
|
|
626
621
|
|
|
622
|
+
if (lintSummary.executionFailed) {
|
|
623
|
+
// Story #4699 — a tool that could not execute is an operational
|
|
624
|
+
// degradation, not a code finding. Route it to friction telemetry
|
|
625
|
+
// (best-effort) so severity counts reflect code findings only.
|
|
626
|
+
logger?.warn?.(
|
|
627
|
+
'[native-review] Lint runner could not execute — recorded as friction telemetry, no finding emitted. Verify with the canonical `npm run lint` before merging.',
|
|
628
|
+
);
|
|
629
|
+
try {
|
|
630
|
+
await emitToolDegradationFn({
|
|
631
|
+
storyId: ticketId,
|
|
632
|
+
category: RUNTIME_FRICTION_CATEGORIES.TOOL_DEGRADED,
|
|
633
|
+
tool: 'native-review-lint',
|
|
634
|
+
details: {
|
|
635
|
+
surface: 'scoped-lint',
|
|
636
|
+
reason:
|
|
637
|
+
'lint runner produced no parseable output (binary missing, parse failure, or environment issue)',
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
} catch {
|
|
641
|
+
// Observability must never fail the review (best-effort contract).
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
627
645
|
const lintFindings = buildLintFindingsFn(lintSummary);
|
|
628
646
|
|
|
629
647
|
// Canonical ordering: critical (maintainability) first, then high
|
|
@@ -80,6 +80,7 @@ async function invokeStoryReviewCore({
|
|
|
80
80
|
provider,
|
|
81
81
|
runCodeReviewFn,
|
|
82
82
|
runLocalLensReviewFn,
|
|
83
|
+
appendFindingsYieldFn,
|
|
83
84
|
progress,
|
|
84
85
|
}) {
|
|
85
86
|
return runStoryReviewCore({
|
|
@@ -91,10 +92,11 @@ async function invokeStoryReviewCore({
|
|
|
91
92
|
progress,
|
|
92
93
|
progressTag: 'REVIEW',
|
|
93
94
|
runCodeReviewFn,
|
|
94
|
-
// Forward the
|
|
95
|
-
// `runStoryReviewCore` uses its
|
|
96
|
-
//
|
|
95
|
+
// Forward the seams only when the caller injects them; otherwise
|
|
96
|
+
// `runStoryReviewCore` uses its defaults. `undefined` deep-merges to
|
|
97
|
+
// the default via the destructuring default there.
|
|
97
98
|
...(runLocalLensReviewFn ? { runLocalLensReviewFn } : {}),
|
|
99
|
+
...(appendFindingsYieldFn ? { appendFindingsYieldFn } : {}),
|
|
98
100
|
});
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -163,6 +165,7 @@ async function postStoryReviewCrossRef({
|
|
|
163
165
|
* provider: object,
|
|
164
166
|
* runCodeReviewFn: Function,
|
|
165
167
|
* runLocalLensReviewFn?: Function,
|
|
168
|
+
* appendFindingsYieldFn?: Function,
|
|
166
169
|
* progress: (tag: string, msg: string) => void,
|
|
167
170
|
* }} args
|
|
168
171
|
* @returns {Promise<{
|
|
@@ -185,6 +188,7 @@ export async function runStoryScopeReview({
|
|
|
185
188
|
provider,
|
|
186
189
|
runCodeReviewFn,
|
|
187
190
|
runLocalLensReviewFn,
|
|
191
|
+
appendFindingsYieldFn,
|
|
188
192
|
progress,
|
|
189
193
|
}) {
|
|
190
194
|
if (prNumber == null) {
|
|
@@ -208,6 +212,7 @@ export async function runStoryScopeReview({
|
|
|
208
212
|
provider,
|
|
209
213
|
runCodeReviewFn,
|
|
210
214
|
runLocalLensReviewFn,
|
|
215
|
+
appendFindingsYieldFn,
|
|
211
216
|
progress,
|
|
212
217
|
});
|
|
213
218
|
|