mandrel 2.55.0 → 2.57.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 (131) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -34
  3. package/.agents/docs/agentrc-reference.json +4 -30
  4. package/.agents/docs/configuration.md +11 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/rules/ci-remediation.md +39 -21
  9. package/.agents/schemas/agentrc.schema.json +28 -185
  10. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  11. package/.agents/scripts/acceptance-eval.js +107 -17
  12. package/.agents/scripts/audit-to-stories.js +222 -75
  13. package/.agents/scripts/ceremony-derive.js +191 -0
  14. package/.agents/scripts/check-context-budget.js +28 -33
  15. package/.agents/scripts/check-cyclomatic.js +4 -3
  16. package/.agents/scripts/deliver-light.js +31 -94
  17. package/.agents/scripts/file-ci-gap.js +306 -0
  18. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  19. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
  20. package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
  21. package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
  22. package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
  23. package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
  24. package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
  25. package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
  26. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
  27. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  28. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  29. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  30. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  31. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  32. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  33. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  34. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  35. package/.agents/scripts/lib/config/explain.js +0 -19
  36. package/.agents/scripts/lib/config/limits.js +18 -78
  37. package/.agents/scripts/lib/config/quality.js +6 -3
  38. package/.agents/scripts/lib/config/runners.js +3 -2
  39. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  40. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  41. package/.agents/scripts/lib/config-settings-schema.js +49 -143
  42. package/.agents/scripts/lib/crap-engine.js +35 -4
  43. package/.agents/scripts/lib/crap-utils.js +17 -1
  44. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  45. package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
  46. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
  47. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
  48. package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
  49. package/.agents/scripts/lib/findings/route-finding.js +38 -0
  50. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  51. package/.agents/scripts/lib/github/framework-repo.js +148 -2
  52. package/.agents/scripts/lib/label-constants.js +6 -1
  53. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  54. package/.agents/scripts/lib/observability/source-classifier.js +2 -0
  55. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  56. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  57. package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
  58. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
  59. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  60. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  61. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  62. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  63. package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
  64. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  65. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  66. package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
  67. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
  68. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
  69. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
  70. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  71. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  72. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  73. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
  74. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  75. package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
  76. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  77. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  78. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  79. package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
  80. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  81. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  82. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  83. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  84. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  85. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  86. package/.agents/scripts/lib/story-body/story-body.js +17 -237
  87. package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
  88. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  89. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  90. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  91. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  92. package/.agents/scripts/lib/test-run-credit.js +266 -0
  93. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  94. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  95. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  96. package/.agents/scripts/plan-context.js +7 -9
  97. package/.agents/scripts/plan-critics.js +28 -54
  98. package/.agents/scripts/plan-persist.js +25 -68
  99. package/.agents/scripts/pr-watch-with-update.js +3 -2
  100. package/.agents/scripts/quality-preview.js +51 -0
  101. package/.agents/scripts/run-tests.js +12 -0
  102. package/.agents/scripts/stories-wave-tick.js +23 -45
  103. package/.agents/scripts/test-isolate.js +13 -180
  104. package/.agents/scripts/update-coverage-baseline.js +25 -70
  105. package/.agents/scripts/update-crap-baseline.js +19 -123
  106. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  107. package/.agents/workflows/audit-clean-code.md +4 -3
  108. package/.agents/workflows/audit-to-stories.md +63 -27
  109. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  110. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  111. package/.agents/workflows/helpers/code-review.md +2 -3
  112. package/.agents/workflows/helpers/deliver-digest.md +41 -57
  113. package/.agents/workflows/helpers/deliver-light.md +40 -105
  114. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  115. package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
  116. package/.agents/workflows/helpers/deliver-story.md +9 -13
  117. package/.agents/workflows/helpers/plan-reference.md +132 -196
  118. package/.agents/workflows/mandrel-plan.md +28 -41
  119. package/.agents/workflows/memory-consolidate.md +9 -13
  120. package/docs/CHANGELOG.md +33 -0
  121. package/lib/migrations/index.js +4 -0
  122. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  123. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  124. package/package.json +1 -1
  125. package/.agents/scripts/lib/framework-version.js +0 -39
  126. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  127. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  128. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  129. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  130. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  131. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lib/audit-to-stories/ledger.js — Cross-run audit findings ledger.
2
+ * lib/findings/audit-ledger.js — Cross-run audit findings ledger.
3
3
  *
4
4
  * Without a committed memory of what a prior sweep already saw, every
5
5
  * `/audit-to-stories` run re-litigates the whole backlog from zero: it cannot
@@ -30,10 +30,7 @@
30
30
 
31
31
  import nodeFs from 'node:fs';
32
32
  import nodePath from 'node:path';
33
- import {
34
- fingerprintAuditFinding,
35
- semanticKeyForAuditFinding,
36
- } from './finding-adapter.js';
33
+ import { fingerprintFinding, semanticKeyFor } from './route-finding.js';
37
34
 
38
35
  export const DEFAULT_LEDGER_PATH = 'baselines/audit-ledger.json';
39
36
  const LEDGER_SCHEMA_URL =
@@ -54,13 +51,24 @@ function createEmptyLedger(now = new Date().toISOString()) {
54
51
  /**
55
52
  * Compute a finding's stable identity: its fingerprint (title-sensitive) and
56
53
  * its location-based semantic key (title-insensitive).
54
+ *
55
+ * The projection onto the canonical identity is the **caller's**, injected as
56
+ * `toCanonical`. That is what lets this module live beside `route-finding.js`
57
+ * in the shared findings layer: the audit pipeline's own adapter
58
+ * (`lib/audit-to-stories/finding-adapter.js`) imports *from* here, so importing
59
+ * it back would close a `findings → audit-to-stories → findings` cycle that
60
+ * `check-arch-cycles` rightly refuses. A caller that already holds canonical
61
+ * findings passes nothing.
62
+ *
57
63
  * @param {object} finding — a parsed/stamped audit finding.
64
+ * @param {(finding: object) => object} [toCanonical]
58
65
  * @returns {{ fingerprint: string, semanticKey: string }}
59
66
  */
60
- function findingIdentity(finding) {
67
+ function findingIdentity(finding, toCanonical) {
68
+ const canonical = toCanonical ? toCanonical(finding) : finding;
61
69
  return {
62
- fingerprint: fingerprintAuditFinding(finding).full,
63
- semanticKey: semanticKeyForAuditFinding(finding),
70
+ fingerprint: fingerprintFinding(canonical).full,
71
+ semanticKey: semanticKeyFor(canonical),
64
72
  };
65
73
  }
66
74
 
@@ -132,6 +140,19 @@ function resolveIssueState(id, existing, issueStates) {
132
140
  };
133
141
  }
134
142
 
143
+ /**
144
+ * The four verdicts the policy can reach, spelled once. Deduping them keeps
145
+ * `decideStatus` readable as the decision table it is, rather than eight
146
+ * near-identical object literals.
147
+ * @type {Record<string, { status: string, action: string }>}
148
+ */
149
+ const VERDICT = Object.freeze({
150
+ filed: Object.freeze({ status: 'filed', action: 'known' }),
151
+ propose: Object.freeze({ status: 'new', action: 'propose' }),
152
+ suppress: Object.freeze({ status: 'accepted-risk', action: 'suppress' }),
153
+ regressed: Object.freeze({ status: 'regressed', action: 'regressed' }),
154
+ });
155
+
135
156
  /**
136
157
  * Decide the finding's next status + action from its prior ledger state and
137
158
  * the live Issue state. This is the whole reconciliation policy in one place.
@@ -140,31 +161,41 @@ function resolveIssueState(id, existing, issueStates) {
140
161
  * @returns {{ status: string, action: 'propose'|'known'|'suppress'|'regressed' }}
141
162
  */
142
163
  function decideStatus(existing, issue) {
143
- // A closed Issue is the strongest signal — its close reason drives the verdict.
164
+ // A closed Issue is the strongest signal — its close reason drives the
165
+ // verdict, and it is read FIRST so a recorded `filed` can never outrank it.
144
166
  if (issue && issue.state === 'closed') {
145
- if (issue.stateReason === 'not_planned') {
146
- return { status: 'accepted-risk', action: 'suppress' };
147
- }
148
- // Closed as completed (or unspecified) but the finding is in this scan →
149
- // it came back. That is a regression, not a fresh proposal.
150
- return { status: 'regressed', action: 'regressed' };
167
+ return issue.stateReason === 'not_planned'
168
+ ? VERDICT.suppress
169
+ : // Closed as completed (or unspecified) but the finding is in this scan
170
+ // → it came back. That is a regression, not a fresh proposal.
171
+ VERDICT.regressed;
151
172
  }
152
173
 
153
- if (!existing) return { status: 'new', action: 'propose' };
174
+ // An OPEN tracking Issue means the finding has been filed, whatever the prior
175
+ // entry said — including when there is no prior entry at all. Until Story
176
+ // #5305 nothing in the package ever assigned `filed`, so this fell through to
177
+ // `new`/`propose` on every run and the `filed` arm below was unreachable in
178
+ // production: the ledger suppressed nothing, and only the GitHub-search dedup
179
+ // stopped a sweep re-filing what it had already filed. Reading it before the
180
+ // `!existing` guard is what makes a record pass correct on its FIRST run
181
+ // rather than its second.
182
+ const unseen = issue?.state === 'open' ? VERDICT.filed : VERDICT.propose;
183
+ if (!existing) return unseen;
154
184
 
155
185
  switch (existing.status) {
156
186
  case 'accepted-risk':
157
- return { status: 'accepted-risk', action: 'suppress' };
187
+ return VERDICT.suppress;
158
188
  case 'filed':
159
- return { status: 'filed', action: 'known' };
189
+ return VERDICT.filed;
190
+ // Recorded fixed, yet detected again with no closed-Issue evidence → treat
191
+ // as a regression the operator should look at. An open Issue does not
192
+ // soften that: the finding came back either way.
160
193
  case 'fixed':
161
- // Recorded fixed, yet detected again with no closed-Issue evidence →
162
- // treat as a regression the operator should look at.
163
- return { status: 'regressed', action: 'regressed' };
194
+ return VERDICT.regressed;
164
195
  case 'regressed':
165
- return { status: 'regressed', action: 'regressed' };
196
+ return VERDICT.regressed;
166
197
  default:
167
- return { status: 'new', action: 'propose' };
198
+ return unseen;
168
199
  }
169
200
  }
170
201
 
@@ -178,6 +209,8 @@ function decideStatus(existing, issue) {
178
209
  * Live Issue state keyed by fingerprint (or semanticKey). Optional — when a
179
210
  * prior entry already records the Issue, that is used.
180
211
  * @param {string} [params.now] — ISO timestamp for firstSeen/lastSeen stamping.
212
+ * @param {(finding: object) => object} [params.toCanonical] — projection onto
213
+ * the canonical identity; omit when `findings` are already canonical.
181
214
  * @returns {{
182
215
  * ledger: { $schema: string, generatedAt: string, entries: object[] },
183
216
  * classifications: Array<{ fingerprint: string, semanticKey: string, status: string, action: string, issue: object|null }>,
@@ -188,6 +221,7 @@ export function reconcileLedger({
188
221
  findings,
189
222
  issueStates = {},
190
223
  now = new Date().toISOString(),
224
+ toCanonical,
191
225
  } = {}) {
192
226
  if (!Array.isArray(findings)) {
193
227
  throw new Error('reconcileLedger: findings must be an array');
@@ -199,7 +233,7 @@ export function reconcileLedger({
199
233
  const classifications = [];
200
234
 
201
235
  for (const finding of findings) {
202
- const id = findingIdentity(finding);
236
+ const id = findingIdentity(finding, toCanonical);
203
237
  const existing =
204
238
  byFingerprint.get(id.fingerprint) ??
205
239
  (id.semanticKey ? bySemanticKey.get(id.semanticKey) : undefined) ??
@@ -254,3 +288,76 @@ export function reconcileLedger({
254
288
  classifications,
255
289
  };
256
290
  }
291
+
292
+ /**
293
+ * Record a set of already-known identities as filed against one Issue.
294
+ *
295
+ * The finding-shaped {@link reconcileLedger} cannot serve this caller:
296
+ * `plan-persist` never sees findings. It holds the provenance identities it
297
+ * stamped on a Story body — fingerprints and semantic keys, as strings — plus
298
+ * the issue number it just created. That is enough to record the filing, and
299
+ * demanding a finding it does not have would be the reason the recommended
300
+ * planning path never reached this ledger at all.
301
+ *
302
+ * An identity already carrying a **closed** Issue is left exactly as it is: a
303
+ * finding whose tracking Issue was closed `not_planned` is `accepted-risk` and
304
+ * must stay suppressed, and one closed as completed is a `regressed` the
305
+ * operator still needs to see. A fresh filing never overwrites either verdict.
306
+ *
307
+ * @param {object} params
308
+ * @param {{ entries?: object[] }} [params.ledger] — prior ledger (default empty).
309
+ * @param {Array<{ fingerprint: string, semanticKey?: string, title?: string, dimension?: string, primaryFile?: string }>} params.identities
310
+ * @param {{ number: number }} params.issue — the Issue these identities were filed as.
311
+ * @param {string} [params.now]
312
+ * @returns {{ ledger: object, recorded: number, skipped: number }}
313
+ */
314
+ export function recordFiledIdentities({
315
+ ledger = createEmptyLedger(),
316
+ identities,
317
+ issue,
318
+ now = new Date().toISOString(),
319
+ } = {}) {
320
+ if (!Array.isArray(identities)) {
321
+ throw new Error('recordFiledIdentities: identities must be an array');
322
+ }
323
+ if (!issue || typeof issue.number !== 'number') {
324
+ throw new Error('recordFiledIdentities: issue.number must be a number');
325
+ }
326
+
327
+ const { byFingerprint } = indexLedger(ledger);
328
+ const next = new Map(byFingerprint);
329
+ let recorded = 0;
330
+ let skipped = 0;
331
+
332
+ for (const identity of identities) {
333
+ const fingerprint = identity?.fingerprint;
334
+ if (typeof fingerprint !== 'string' || fingerprint.length === 0) continue;
335
+ const existing = byFingerprint.get(fingerprint) ?? null;
336
+ if (existing?.issue && existing.issue.state === 'closed') {
337
+ skipped += 1;
338
+ continue;
339
+ }
340
+ next.set(fingerprint, {
341
+ fingerprint,
342
+ semanticKey: identity.semanticKey ?? existing?.semanticKey ?? '',
343
+ title: identity.title ?? existing?.title ?? '',
344
+ dimension: identity.dimension ?? existing?.dimension ?? '',
345
+ primaryFile: identity.primaryFile ?? existing?.primaryFile ?? '',
346
+ status: 'filed',
347
+ issue: { number: issue.number, state: 'open', stateReason: null },
348
+ firstSeen: existing?.firstSeen ?? now,
349
+ lastSeen: now,
350
+ });
351
+ recorded += 1;
352
+ }
353
+
354
+ return {
355
+ ledger: {
356
+ $schema: ledger?.$schema ?? LEDGER_SCHEMA_URL,
357
+ generatedAt: now,
358
+ entries: [...next.values()],
359
+ },
360
+ recorded,
361
+ skipped,
362
+ };
363
+ }
@@ -36,6 +36,7 @@ import { fingerprintSeverity } from './severity.js';
36
36
  const SEP = '␟'; // unit separator — keeps fingerprint fields unambiguous
37
37
  const MARKER = 'audit-fingerprints:';
38
38
  const SEMANTIC_MARKER = 'audit-semantic-keys:';
39
+ const LABEL_MARKER = 'audit-labels:';
39
40
  export const SHA1_RE = /^[0-9a-f]{40}$/;
40
41
  // A semantic key round-trips through a comma-joined footer, so it must not
41
42
  // carry a comma or a `>` (which would truncate the HTML comment). Both are
@@ -175,6 +176,43 @@ export function parseSemanticKeyFooter(body) {
175
176
  );
176
177
  }
177
178
 
179
+ /**
180
+ * Render the machine-readable audit-label footer
181
+ * (`<!-- audit-labels: audit::x,audit::y -->`).
182
+ *
183
+ * The dedup corpus is listed by `audit::*` label, so a Story carrying none is
184
+ * absent from the pool an indexed run matches against — and with an index in
185
+ * play the exact lookup is answered locally and never reaches the provider, so
186
+ * a fingerprint footer alone cannot rescue it. Carrying the labels through the
187
+ * seed is what lets the planning path stamp them without the authoring agent
188
+ * being asked to notice them (Story #5307).
189
+ *
190
+ * @param {string | string[]} labels
191
+ * @returns {string}
192
+ */
193
+ export function auditLabelFooter(labels) {
194
+ const list = (Array.isArray(labels) ? labels : [labels])
195
+ .filter((l) => typeof l === 'string' && l.startsWith('audit::'))
196
+ .map((l) => l.replace(/[,>]/g, ' ').trim())
197
+ .filter((l) => l.length > 0);
198
+ if (list.length === 0) return '';
199
+ return `<!-- ${LABEL_MARKER} ${[...new Set(list)].sort().join(',')} -->`;
200
+ }
201
+
202
+ /**
203
+ * Read every `audit-labels` footer out of a body, de-duplicated.
204
+ *
205
+ * @param {string} body
206
+ * @returns {string[]}
207
+ */
208
+ export function parseAuditLabelFooter(body) {
209
+ return parseAllFooterValues(
210
+ body,
211
+ /<!--\s*audit-labels:\s*([^>]*?)\s*-->/g,
212
+ (s) => s.startsWith('audit::'),
213
+ );
214
+ }
215
+
178
216
  /**
179
217
  * Render the machine-readable fingerprint footer for one or more shas.
180
218
  *