mandrel 2.24.0 → 2.25.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.
Files changed (88) hide show
  1. package/.agents/agents/acceptance-critic.md +13 -19
  2. package/.agents/agents/auditor.md +16 -19
  3. package/.agents/agents/story-worker.md +41 -54
  4. package/.agents/audit-checklists/architecture.md +5 -1
  5. package/.agents/audit-checklists/quality.md +3 -0
  6. package/.agents/docs/agentrc-reference.json +2 -1
  7. package/.agents/docs/configuration.md +2 -1
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/rules/ci-remediation.md +68 -3
  10. package/.agents/schemas/agentrc.schema.json +6 -1
  11. package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
  12. package/.agents/schemas/baselines/crap.schema.json +5 -1
  13. package/.agents/schemas/crap-report.schema.json +37 -0
  14. package/.agents/scripts/acceptance-eval.js +35 -9
  15. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  16. package/.agents/scripts/audit-to-stories.js +33 -3
  17. package/.agents/scripts/check-context-budget.js +95 -14
  18. package/.agents/scripts/check-test-temp-hygiene.js +83 -0
  19. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  20. package/.agents/scripts/deliver-light.js +3 -4
  21. package/.agents/scripts/deliver-recover.js +13 -0
  22. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  23. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  24. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  25. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  26. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  27. package/.agents/scripts/lib/baseline-loader.js +0 -0
  28. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  29. package/.agents/scripts/lib/baselines/kinds/crap.js +396 -51
  30. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  31. package/.agents/scripts/lib/baselines/reader.js +10 -0
  32. package/.agents/scripts/lib/baselines/writer.js +7 -3
  33. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  34. package/.agents/scripts/lib/crap-engine.js +97 -19
  35. package/.agents/scripts/lib/crap-utils.js +73 -13
  36. package/.agents/scripts/lib/env-loader.js +46 -16
  37. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  38. package/.agents/scripts/lib/findings/severity.js +80 -2
  39. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  40. package/.agents/scripts/lib/label-constants.js +17 -0
  41. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  42. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  43. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  44. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  45. package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
  46. package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
  47. package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
  48. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +7 -0
  49. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +13 -1
  50. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  51. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  52. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  53. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -34
  54. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  55. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +50 -13
  56. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  57. package/.agents/scripts/lib/story-plan.js +137 -42
  58. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  59. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  60. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  61. package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
  62. package/.agents/scripts/lib/wave-runner/ready-set.js +73 -9
  63. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +6 -3
  64. package/.agents/scripts/lib/workers/crap-worker.js +6 -3
  65. package/.agents/scripts/pr-watch-with-update.js +568 -213
  66. package/.agents/scripts/quality-preview.js +25 -0
  67. package/.agents/scripts/run-tests.js +10 -0
  68. package/.agents/scripts/single-story-close.js +6 -0
  69. package/.agents/scripts/single-story-init.js +7 -0
  70. package/.agents/scripts/stories-wave-tick.js +95 -10
  71. package/.agents/scripts/story-plan.js +65 -9
  72. package/.agents/scripts/update-crap-baseline.js +42 -4
  73. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  74. package/.agents/templates/single-story-body.md +16 -8
  75. package/.agents/workflows/audit-architecture.md +69 -13
  76. package/.agents/workflows/audit-quality.md +26 -0
  77. package/.agents/workflows/audit-to-stories.md +30 -4
  78. package/.agents/workflows/deliver.md +81 -95
  79. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  80. package/.agents/workflows/helpers/deliver-reference.md +18 -3
  81. package/.agents/workflows/helpers/deliver-story-reference.md +127 -46
  82. package/.agents/workflows/helpers/deliver-story.md +104 -119
  83. package/.agents/workflows/helpers/plan-reference.md +34 -0
  84. package/.agents/workflows/plan.md +60 -76
  85. package/docs/CHANGELOG.md +29 -0
  86. package/lib/cli/update.js +14 -11
  87. package/lib/cli/version-check.js +9 -1
  88. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ /**
2
+ * lib/audit-to-stories/audit-label-taxonomy.js — the closed set of GitHub
3
+ * labels an audit sweep may create or generate (Story #4877).
4
+ *
5
+ * There are two sides to a label axis and they used to be written down in
6
+ * different places, which is how they drifted:
7
+ *
8
+ * - the **creator** — `audit-labels-bootstrap.js`, which ran `gh label create`
9
+ * over the `audit::<lens>` list and nothing else;
10
+ * - the **generator** — `audit-to-stories/build-story-body.js`, which emitted
11
+ * `type::story`, `agent::ready`, `audit::<lens>` and — as a bare string
12
+ * literal — `risk::high`.
13
+ *
14
+ * `risk::high` was therefore generated by the filer and defined by **no**
15
+ * taxonomy: not `LABEL_TAXONOMY` (the repo-wide bootstrap), not the audit
16
+ * bootstrap. A generated label the repository has never created is the exact
17
+ * failure shape that made every `gh issue create` in the feedback loop fail in
18
+ * Story #4828. This module is the one list both sides read, so the creator
19
+ * cannot fall behind the generator again.
20
+ *
21
+ * It lives under `lib/` rather than inside the bootstrap CLI so the generator
22
+ * can import it without a lib → top-level-script edge (and without dragging the
23
+ * CLI's config/`gh` dependencies into a pure body builder).
24
+ *
25
+ * Label *names* come from `lib/label-constants.js` — the repo-wide SSOT for
26
+ * label spelling — so a rename still lands in one place.
27
+ */
28
+
29
+ import {
30
+ AGENT_LABELS,
31
+ LABEL_COLORS,
32
+ RISK_LABELS,
33
+ TYPE_LABELS,
34
+ } from '../label-constants.js';
35
+ import { AUDIT_LENSES } from './audit-lenses.js';
36
+
37
+ /**
38
+ * Per-lens label presentation, keyed by canonical lens name. A lens absent from
39
+ * this map falls back to {@link DEFAULT_LENS_META}, so adding an `audit-*`
40
+ * workflow to `AUDIT_LENSES` still yields a label without registering
41
+ * colour/description here first.
42
+ */
43
+ const LENS_META = Object.freeze({
44
+ accessibility: {
45
+ color: 'c5def5',
46
+ description: 'Audit-sourced finding: WCAG accessibility conformance',
47
+ },
48
+ architecture: {
49
+ color: '6f42c1',
50
+ description: 'Audit-sourced finding: architectural concerns',
51
+ },
52
+ 'clean-code': {
53
+ color: '0e8a16',
54
+ description: 'Audit-sourced finding: clean-code / maintainability',
55
+ },
56
+ dependencies: {
57
+ color: 'd4c5f9',
58
+ description: 'Audit-sourced finding: dependencies / supply chain',
59
+ },
60
+ devops: {
61
+ color: 'fbca04',
62
+ description: 'Audit-sourced finding: DevOps / CI / CD',
63
+ },
64
+ documentation: {
65
+ color: '1d76db',
66
+ description: 'Audit-sourced finding: documentation staleness / gaps',
67
+ },
68
+ navigability: {
69
+ color: 'bfdadc',
70
+ description: 'Audit-sourced finding: route / nav reachability',
71
+ },
72
+ performance: {
73
+ color: 'b60205',
74
+ description: 'Audit-sourced finding: performance / latency',
75
+ },
76
+ privacy: {
77
+ color: 'fef2c0',
78
+ description: 'Audit-sourced finding: privacy / data handling',
79
+ },
80
+ quality: {
81
+ color: '0052cc',
82
+ description: 'Audit-sourced finding: test quality / coverage gaps',
83
+ },
84
+ security: {
85
+ color: 'b60205',
86
+ description: 'Audit-sourced finding: security / OWASP',
87
+ },
88
+ seo: {
89
+ color: 'fbca04',
90
+ description: 'Audit-sourced finding: SEO / discoverability',
91
+ },
92
+ sre: {
93
+ color: '0052cc',
94
+ description: 'Audit-sourced finding: SRE / observability / reliability',
95
+ },
96
+ 'ux-ui': {
97
+ color: 'd4c5f9',
98
+ description: 'Audit-sourced finding: UX / UI concerns',
99
+ },
100
+ });
101
+
102
+ const DEFAULT_LENS_META = Object.freeze({
103
+ color: 'ededed',
104
+ description: 'Audit-sourced finding',
105
+ });
106
+
107
+ /** `gh label create --color` wants a bare hex triplet, not a CSS `#rrggbb`. */
108
+ function hex(color) {
109
+ return String(color).replace('#', '');
110
+ }
111
+
112
+ /**
113
+ * The `audit::<lens>` half of the taxonomy, one per `/audit-<lens>` workflow.
114
+ * Derived from the same `AUDIT_LENSES` SSOT that `build-story-body.js` derives
115
+ * its labels from, so the creator and the deriver cannot drift (Story #4195).
116
+ */
117
+ const AUDIT_LENS_LABELS = Object.freeze(
118
+ AUDIT_LENSES.map((name) => ({
119
+ name: `audit::${name}`,
120
+ ...(LENS_META[name] ?? DEFAULT_LENS_META),
121
+ })),
122
+ );
123
+
124
+ /**
125
+ * The story-axis labels an audit-generated Story carries alongside its
126
+ * `audit::<lens>` labels.
127
+ *
128
+ * The `type::` / `agent::` entries are also in `LABEL_TAXONOMY` (the repo-wide
129
+ * bootstrap). Creating a label twice is a no-op, so the overlap costs nothing
130
+ * and means an audit sweep does not silently depend on the repo-wide bootstrap
131
+ * having run first.
132
+ */
133
+ const AUDIT_STORY_AXIS_LABELS = Object.freeze([
134
+ {
135
+ name: TYPE_LABELS.STORY,
136
+ color: hex(LABEL_COLORS.TYPE),
137
+ description: 'Story work item',
138
+ },
139
+ {
140
+ name: AGENT_LABELS.READY,
141
+ color: hex(LABEL_COLORS.AGENT),
142
+ description:
143
+ 'Parking state — frozen dispatch manifest exists; awaiting local /deliver',
144
+ },
145
+ {
146
+ name: RISK_LABELS.HIGH,
147
+ color: hex(LABEL_COLORS.RISK_HIGH),
148
+ description:
149
+ 'Planning/audit metadata: review this first (Critical finding present)',
150
+ },
151
+ ]);
152
+
153
+ /**
154
+ * Every label the audit sweep defines — the closed set the bootstrap creates
155
+ * and {@link definesAuditLabel} answers against.
156
+ */
157
+ export const AUDIT_LABEL_TAXONOMY = Object.freeze([
158
+ ...AUDIT_LENS_LABELS,
159
+ ...AUDIT_STORY_AXIS_LABELS,
160
+ ]);
161
+
162
+ const DEFINED_NAMES = new Set(AUDIT_LABEL_TAXONOMY.map((l) => l.name));
163
+
164
+ /**
165
+ * True when `name` is a label this taxonomy defines.
166
+ *
167
+ * The enforcement point is the label **generator** (`build-story-body.js`),
168
+ * which routes every label it derives through this predicate and refuses to
169
+ * emit one the taxonomy does not define — closing the drift in the direction it
170
+ * actually happened: a generator inventing names the bootstrap never created.
171
+ *
172
+ * @param {unknown} name
173
+ * @returns {boolean}
174
+ */
175
+ export function definesAuditLabel(name) {
176
+ return typeof name === 'string' && DEFINED_NAMES.has(name);
177
+ }
@@ -22,14 +22,16 @@
22
22
  * structured contract.
23
23
  */
24
24
 
25
+ import { AGENT_LABELS, RISK_LABELS, TYPE_LABELS } from '../label-constants.js';
25
26
  import { serialize } from '../story-body/story-body.js';
27
+ import { definesAuditLabel } from './audit-label-taxonomy.js';
26
28
  import { auditLabelsForFindings } from './audit-lenses.js';
27
29
  import {
28
30
  renderFingerprintFooter,
29
31
  renderSemanticKeyFooter,
30
32
  } from './finding-adapter.js';
31
33
 
32
- const STATIC_LABELS = Object.freeze(['type::story', 'agent::ready']);
34
+ const STATIC_LABELS = Object.freeze([TYPE_LABELS.STORY, AGENT_LABELS.READY]);
33
35
 
34
36
  // The verify[] contract every generated audit Story carries. These commands
35
37
  // exist in this repo's harness (package.json scripts) so the Story satisfies
@@ -183,8 +185,38 @@ function labelsForGroup(group) {
183
185
  const hasCritical = (group.findings ?? []).some(
184
186
  (f) => f.severity === 'critical',
185
187
  );
186
- if (hasCritical) labels.push('risk::high');
187
- return uniq(labels);
188
+ if (hasCritical) labels.push(RISK_LABELS.HIGH);
189
+ return assertLabelsInTaxonomy(uniq(labels));
190
+ }
191
+
192
+ /**
193
+ * Refuse to generate a label the audit bootstrap taxonomy does not define
194
+ * (Story #4877).
195
+ *
196
+ * Story #4195 fixed half of this: `audit::<dimension>` labels minted from
197
+ * free-form dimension prose ("stale-description", "dry") named labels that did
198
+ * not exist, so derivation moved to the closed lens list. The other half stayed
199
+ * open — `risk::high` was a bare string literal here, defined by no taxonomy —
200
+ * and nothing checked the generated set against anything at all. Throwing is
201
+ * deliberate: a label the repo has never created is dropped or fails the create
202
+ * outright, and a filer that silently loses `risk::high` on a Critical merge is
203
+ * worse than a loud failure at the point of generation.
204
+ *
205
+ * @param {string[]} labels
206
+ * @returns {string[]} the same labels, when every one is defined.
207
+ * @throws {Error} naming the offending labels.
208
+ */
209
+ function assertLabelsInTaxonomy(labels) {
210
+ const undefinedLabels = labels.filter((l) => !definesAuditLabel(l));
211
+ if (undefinedLabels.length > 0) {
212
+ throw new Error(
213
+ `buildStoryBody: generated label(s) ${undefinedLabels.join(', ')} are not ` +
214
+ 'defined by the audit label taxonomy (audit-label-taxonomy.js). Add ' +
215
+ 'them there — or stop generating them — rather than emitting a label ' +
216
+ 'the repository does not have.',
217
+ );
218
+ }
219
+ return labels;
188
220
  }
189
221
 
190
222
  /**
@@ -21,7 +21,7 @@
21
21
  * Pure: no I/O.
22
22
  */
23
23
 
24
- const SEVERITY_RANK = { critical: 3, high: 2, medium: 1, low: 0, null: -1 };
24
+ import { highestSeverity as highestSeverityOf } from '../findings/severity.js';
25
25
 
26
26
  function dirOf(filePath) {
27
27
  if (typeof filePath !== 'string' || filePath.length === 0) return '';
@@ -37,17 +37,26 @@ function pickPrimaryFile(finding) {
37
37
  return null;
38
38
  }
39
39
 
40
+ /**
41
+ * The highest severity across a group's findings, ranked by the severity SSOT
42
+ * ({@link highestSeverityOf}) rather than a local copy of the scale.
43
+ *
44
+ * The rank map this replaces knew four levels and not `info`, so an Info
45
+ * finding tied with a finding carrying no severity at all — the same partial
46
+ * vocabulary this Story removes everywhere else.
47
+ *
48
+ * A group whose findings all lack a usable severity still reports `null`, not
49
+ * the SSOT's `info` floor: absent is not the same claim as "graded lowest",
50
+ * and the callers that tally and threshold on this value distinguish them.
51
+ *
52
+ * @param {Array<{ severity?: string }>} findings
53
+ * @returns {string|null} a canonical severity, or null when none is stated.
54
+ */
40
55
  function highestSeverity(findings) {
41
- let best = null;
42
- let bestRank = -2;
43
- for (const f of findings) {
44
- const r = SEVERITY_RANK[f.severity ?? 'null'] ?? -1;
45
- if (r > bestRank) {
46
- bestRank = r;
47
- best = f.severity ?? null;
48
- }
49
- }
50
- return best;
56
+ const stated = findings
57
+ .map((f) => f?.severity)
58
+ .filter((value) => typeof value === 'string' && value.length > 0);
59
+ return stated.length === 0 ? null : highestSeverityOf(stated);
51
60
  }
52
61
 
53
62
  /**
@@ -17,14 +17,7 @@
17
17
 
18
18
  import path from 'node:path';
19
19
 
20
- const SEVERITY_ALIASES = Object.freeze({
21
- critical: 'critical',
22
- high: 'high',
23
- medium: 'medium',
24
- mod: 'medium',
25
- moderate: 'medium',
26
- low: 'low',
27
- });
20
+ import { normalizeSeverity } from '../findings/severity.js';
28
21
 
29
22
  const KEY_LINE = /^\s*-\s*\*\*([^:*]+):\*\*\s*(.*)$/;
30
23
  const HEADING_FINDING = /^###\s+(.+?)\s*$/;
@@ -44,6 +37,27 @@ function unwrapInlineCode(value) {
44
37
  return trimmed;
45
38
  }
46
39
 
40
+ /**
41
+ * Resolve a raw severity/impact token to a canonical level, or `null` when the
42
+ * token carries no recognisable severity at all.
43
+ *
44
+ * The vocabulary itself is NOT written down here (Story #4877). This module used
45
+ * to carry its own alias table covering `critical|high|medium|mod|moderate|low`
46
+ * — four of the canonical five levels, missing `info`. A lens that graded a
47
+ * finding `Info` or `Informational` (which the shared severity scale now
48
+ * sanctions) therefore parsed to `null`, tallied as `unknown`, and was dropped
49
+ * by every severity-filtered run, `--severity low` included. Delegating to the
50
+ * canonical normaliser in `lib/findings/severity.js` means this parser cannot
51
+ * know a narrower vocabulary than the rest of the pipeline.
52
+ *
53
+ * `null` — rather than the normaliser's `info` fallback — remains the
54
+ * no-severity answer, because {@link deriveSeverity} walks several candidate
55
+ * keys and needs to distinguish "this key had no severity" from "this key said
56
+ * `info`".
57
+ *
58
+ * @param {unknown} token
59
+ * @returns {string|null}
60
+ */
47
61
  function normaliseSeverity(token) {
48
62
  if (typeof token !== 'string') return null;
49
63
  const cleaned = token
@@ -53,7 +67,7 @@ function normaliseSeverity(token) {
53
67
  .trim();
54
68
  if (!cleaned) return null;
55
69
  for (const word of cleaned.split(/[\s|/,]+/)) {
56
- const hit = SEVERITY_ALIASES[word];
70
+ const hit = normalizeSeverity(word, null);
57
71
  if (hit) return hit;
58
72
  }
59
73
  return null;
@@ -17,6 +17,7 @@
17
17
  * Pure: returns a string. The caller decides where to persist it.
18
18
  */
19
19
 
20
+ import { SEVERITIES } from '../findings/severity.js';
20
21
  import {
21
22
  renderFingerprintFooter,
22
23
  renderSemanticKeyFooter,
@@ -37,10 +38,16 @@ const DIMENSION_LABEL = {
37
38
  architecture: 'Architecture',
38
39
  };
39
40
 
40
- const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'];
41
+ /**
42
+ * The severity profile in the seed's Problem Statement is ordered and bucketed
43
+ * by the canonical scale (Story #4877) rather than by a fourth local copy of
44
+ * it. The list this replaces omitted `info`, so an informational finding was
45
+ * absent from the profile the planner reads even when it survived the filter.
46
+ */
47
+ const SEVERITY_ORDER = SEVERITIES;
41
48
 
42
49
  function tallySeverities(findings) {
43
- const tally = { critical: 0, high: 0, medium: 0, low: 0 };
50
+ const tally = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
44
51
  for (const f of findings) {
45
52
  if (Object.hasOwn(tally, f.severity)) tally[f.severity] += 1;
46
53
  }