mandrel 2.24.0 → 2.26.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 (189) hide show
  1. package/.agents/agents/acceptance-critic.md +19 -21
  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/baselines.md +21 -0
  6. package/.agents/audit-checklists/quality.md +3 -0
  7. package/.agents/docs/agentrc-reference.json +2 -1
  8. package/.agents/docs/configuration.md +2 -1
  9. package/.agents/docs/quality-gates.md +80 -18
  10. package/.agents/docs/workflows.md +4 -2
  11. package/.agents/instructions.md +1 -1
  12. package/.agents/rules/ci-remediation.md +68 -3
  13. package/.agents/schemas/agentrc.schema.json +6 -1
  14. package/.agents/schemas/audit-rules.json +15 -0
  15. package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
  16. package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
  17. package/.agents/schemas/baselines/crap.schema.json +13 -1
  18. package/.agents/schemas/crap-report.schema.json +37 -0
  19. package/.agents/schemas/model-attribution.schema.json +4 -0
  20. package/.agents/scripts/acceptance-eval.js +124 -15
  21. package/.agents/scripts/audit-baselines.js +136 -0
  22. package/.agents/scripts/audit-labels-bootstrap.js +16 -95
  23. package/.agents/scripts/audit-to-stories.js +33 -3
  24. package/.agents/scripts/check-arch-cycles.js +12 -93
  25. package/.agents/scripts/check-baseline-drift.js +16 -3
  26. package/.agents/scripts/check-baselines.js +19 -3
  27. package/.agents/scripts/check-context-budget.js +95 -14
  28. package/.agents/scripts/check-cyclomatic.js +214 -0
  29. package/.agents/scripts/check-schema-references.js +392 -0
  30. package/.agents/scripts/check-test-temp-hygiene.js +121 -1
  31. package/.agents/scripts/check-workflow-timeouts.js +291 -0
  32. package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
  33. package/.agents/scripts/deliver-light.js +3 -4
  34. package/.agents/scripts/deliver-recover.js +13 -0
  35. package/.agents/scripts/diagnose-friction.js +85 -19
  36. package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
  37. package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
  38. package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
  39. package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
  40. package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
  41. package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
  42. package/.agents/scripts/lib/audit-baselines/read.js +87 -0
  43. package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
  44. package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
  45. package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
  46. package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
  47. package/.agents/scripts/lib/audit-suite/index.js +0 -5
  48. package/.agents/scripts/lib/audit-suite/selector.js +9 -62
  49. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
  50. package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
  51. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
  52. package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
  53. package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
  54. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
  55. package/.agents/scripts/lib/baseline-loader.js +0 -0
  56. package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
  57. package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
  58. package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
  59. package/.agents/scripts/lib/baselines/git-base.js +0 -0
  60. package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
  61. package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
  62. package/.agents/scripts/lib/baselines/reader.js +10 -0
  63. package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
  64. package/.agents/scripts/lib/baselines/scope.js +39 -90
  65. package/.agents/scripts/lib/baselines/writer.js +16 -7
  66. package/.agents/scripts/lib/changed-files.js +8 -1
  67. package/.agents/scripts/lib/cli-args.js +115 -1
  68. package/.agents/scripts/lib/close-validation/runner.js +70 -25
  69. package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
  70. package/.agents/scripts/lib/crap-engine.js +124 -27
  71. package/.agents/scripts/lib/crap-method-identity.js +153 -0
  72. package/.agents/scripts/lib/crap-utils.js +86 -13
  73. package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
  74. package/.agents/scripts/lib/env-loader.js +46 -16
  75. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
  76. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
  77. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
  78. package/.agents/scripts/lib/findings/route-finding.js +123 -17
  79. package/.agents/scripts/lib/findings/severity.js +80 -2
  80. package/.agents/scripts/lib/git/cached-fetch.js +0 -0
  81. package/.agents/scripts/lib/git-utils.js +136 -80
  82. package/.agents/scripts/lib/import-graph.js +156 -0
  83. package/.agents/scripts/lib/label-constants.js +17 -0
  84. package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
  85. package/.agents/scripts/lib/observability/source-classifier.js +175 -2
  86. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
  87. package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
  88. package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
  89. package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
  90. package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
  91. package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
  92. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
  94. package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
  95. package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
  96. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
  97. package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
  98. package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
  99. package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
  100. package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
  101. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
  102. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
  103. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
  104. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
  105. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
  106. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
  107. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
  108. package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
  109. package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
  110. package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
  111. package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
  118. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
  119. package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
  120. package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
  121. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
  122. package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
  123. package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
  124. package/.agents/scripts/lib/reserved-test-ids.js +77 -0
  125. package/.agents/scripts/lib/story-adjacency.js +3 -3
  126. package/.agents/scripts/lib/story-plan.js +137 -42
  127. package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
  128. package/.agents/scripts/lib/test-runner-contract.js +134 -0
  129. package/.agents/scripts/lib/test-tiers.js +11 -2
  130. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  131. package/.agents/scripts/lib/util/concurrent-map.js +17 -0
  132. package/.agents/scripts/lib/util/parse-id-list.js +103 -0
  133. package/.agents/scripts/lib/util/poll-loop.js +18 -2
  134. package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
  135. package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
  136. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
  137. package/.agents/scripts/lib/workers/crap-worker.js +8 -13
  138. package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
  139. package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
  140. package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
  141. package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
  142. package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
  143. package/.agents/scripts/lib/worktree-manager.js +14 -0
  144. package/.agents/scripts/plan-run-epilogue.js +17 -5
  145. package/.agents/scripts/pr-watch-with-update.js +568 -213
  146. package/.agents/scripts/providers/github/tickets.js +33 -10
  147. package/.agents/scripts/provision-git-hooks.js +85 -0
  148. package/.agents/scripts/quality-preview.js +137 -28
  149. package/.agents/scripts/resolve-stories.js +4 -1
  150. package/.agents/scripts/run-coverage.js +86 -35
  151. package/.agents/scripts/run-lint.js +20 -0
  152. package/.agents/scripts/run-tests.js +36 -36
  153. package/.agents/scripts/single-story-close.js +34 -2
  154. package/.agents/scripts/single-story-confirm-merge.js +22 -6
  155. package/.agents/scripts/single-story-init.js +7 -0
  156. package/.agents/scripts/stories-wave-tick.js +308 -47
  157. package/.agents/scripts/story-plan.js +65 -9
  158. package/.agents/scripts/update-coverage-baseline.js +34 -4
  159. package/.agents/scripts/update-crap-baseline.js +42 -4
  160. package/.agents/scripts/update-duplication-baseline.js +209 -83
  161. package/.agents/scripts/update-maintainability-baseline.js +31 -4
  162. package/.agents/scripts/validate-docs-freshness.js +1 -0
  163. package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
  165. package/.agents/skills/skills.index.json +2 -2
  166. package/.agents/templates/single-story-body.md +16 -8
  167. package/.agents/workflows/audit-architecture.md +69 -13
  168. package/.agents/workflows/audit-baselines.md +289 -0
  169. package/.agents/workflows/audit-navigability.md +5 -4
  170. package/.agents/workflows/audit-quality.md +26 -0
  171. package/.agents/workflows/audit-to-stories.md +30 -4
  172. package/.agents/workflows/deliver.md +92 -97
  173. package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
  174. package/.agents/workflows/helpers/audit-lens-core.md +25 -4
  175. package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
  176. package/.agents/workflows/helpers/deliver-digest.md +41 -21
  177. package/.agents/workflows/helpers/deliver-reference.md +95 -4
  178. package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
  179. package/.agents/workflows/helpers/deliver-story.md +104 -119
  180. package/.agents/workflows/helpers/plan-reference.md +49 -5
  181. package/.agents/workflows/memory-consolidate.md +116 -0
  182. package/.agents/workflows/plan.md +62 -75
  183. package/README.md +13 -6
  184. package/docs/CHANGELOG.md +93 -0
  185. package/lib/cli/update.js +14 -11
  186. package/lib/cli/version-check.js +9 -1
  187. package/package.json +9 -4
  188. package/.agents/schemas/friction-event.schema.json +0 -56
  189. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
@@ -31,15 +31,13 @@ import {
31
31
  renderAcceptanceSpecSystemPrompt,
32
32
  renderTechSpecSystemPrompt,
33
33
  } from '../templates/spec-author-prompts.js';
34
- import { concurrentMap } from '../util/concurrent-map.js';
34
+ import { concurrentMap, FANOUT_CONCURRENCY } from '../util/concurrent-map.js';
35
35
  import { buildComplexitySignals } from './complexity-gate.js';
36
36
  import { parseDeliverySlicingTable } from './consolidation-precondition.js';
37
37
  import { buildDocsDigest } from './docs-digest.js';
38
38
  import { buildAuthoringContext } from './planning/authoring-context.js';
39
39
  import { buildDecomposerSystemPrompt } from './planning/decomposer-context.js';
40
40
 
41
- /** Bounded concurrency for `--tickets` source-ticket hydration. */
42
- const SOURCE_TICKET_FETCH_CONCURRENCY = 4;
43
41
  /**
44
42
  * Envelope byte ceiling (regression guard for the design's named PR2 risk:
45
43
  * two envelopes → one bigger one). This is the **only** live bound on
@@ -229,7 +227,8 @@ export function renderStoriesTemplate({ complexitySignals = null } = {}) {
229
227
  'codes, security invariants, and load-bearing constraints with ' +
230
228
  'their why. Implementation choices belong to the deliverer unless ' +
231
229
  'load-bearing. No per-file behavior paragraphs, no current-state ' +
232
- 'narration. Keep it under ~250 words (soft advisory budget). ' +
230
+ 'narration. Aim for ~250 words; an advisory warning fires past 350, ' +
231
+ 'and it never fails the persist. ' +
233
232
  'Delete this field when acceptance[] carries the whole contract.',
234
233
  changes: buildTemplateChanges(complexitySignals),
235
234
  non_goals: [],
@@ -813,6 +812,84 @@ async function searchStoryDuplicates({
813
812
  }
814
813
  }
815
814
 
815
+ /**
816
+ * Gather the three independent envelope inputs — the open-Story duplicate
817
+ * search, the folded authoring context, and the inline docs digest — under
818
+ * bounded concurrency (Story #4952).
819
+ *
820
+ * None of the three reads a value the others produce, so the result is a pure
821
+ * function of `seed` and the injected config: the assembled envelope is
822
+ * **byte-identical** to the serial build for the same inputs, whichever order
823
+ * the three happen to settle in. `concurrentMap` preserves input order, so the
824
+ * destructuring below is positional and stable.
825
+ *
826
+ * `docsContextFiles` is emptied for the `buildAuthoringContext` call: the
827
+ * per-plan digest-file path needs a plan id that does not exist yet — the
828
+ * inline digest gathered alongside it replaces that pointer.
829
+ *
830
+ * @param {{
831
+ * seed: string,
832
+ * epicTitle: string,
833
+ * excludeIds?: Iterable<number|string>,
834
+ * provider: object,
835
+ * config: object,
836
+ * settings: object,
837
+ * cwd?: string,
838
+ * }} args
839
+ * @returns {Promise<{
840
+ * duplicates: Array<object>,
841
+ * authoring: object,
842
+ * docsContext: { mode: 'digest-inline', digest: string }|null,
843
+ * }>}
844
+ */
845
+ async function gatherEnvelopeInputs({
846
+ seed,
847
+ epicTitle,
848
+ excludeIds = [],
849
+ provider,
850
+ config,
851
+ settings,
852
+ cwd,
853
+ }) {
854
+ const paths = settings?.paths ?? {};
855
+ const [duplicates, authoring, inlineDigest] = await concurrentMap(
856
+ [
857
+ () => searchStoryDuplicates({ seed, provider, config, excludeIds }),
858
+ () =>
859
+ buildAuthoringContext(
860
+ 0,
861
+ /* provider (unused behind the prefetch seam) */ {},
862
+ { ...settings, docsContextFiles: [] },
863
+ {
864
+ epic: { id: 0, title: epicTitle, body: seed },
865
+ github: config.github ?? null,
866
+ cwd,
867
+ },
868
+ ),
869
+ () =>
870
+ buildDocsDigest({
871
+ docsContextFiles: settings?.docsContextFiles,
872
+ docsRoot: paths.docsRoot,
873
+ }),
874
+ ],
875
+ (gather) => gather(),
876
+ // The per-mode envelope gathers (Story #4952): the duplicate search, the
877
+ // authoring-context fold and the docs digest have no data dependency on
878
+ // one another, so their serialization was incidental and `/plan` paid it
879
+ // with the operator waiting at Gate #1.
880
+ { concurrency: FANOUT_CONCURRENCY },
881
+ );
882
+
883
+ return {
884
+ duplicates,
885
+ authoring,
886
+ docsContext:
887
+ inlineDigest == null
888
+ ? null
889
+ : { mode: 'digest-inline', digest: inlineDigest },
890
+ };
891
+ }
892
+
816
893
  /**
817
894
  * Build the seed-file (ideation) envelope. No parent ticket
818
895
  * exists yet — creation moves to the persist half — so the open-Story
@@ -836,37 +913,17 @@ async function buildSeedFileModeEnvelope({
836
913
  );
837
914
  }
838
915
 
839
- const duplicates = await searchStoryDuplicates({
916
+ // Dup search, the authoring-context fold grounded in the seed prose, and the
917
+ // inline docs digest are independent — gathered concurrently (Story #4952).
918
+ const { duplicates, authoring, docsContext } = await gatherEnvelopeInputs({
840
919
  seed: content,
920
+ epicTitle: seedFilePath ?? 'seed',
841
921
  provider,
842
922
  config,
923
+ settings,
924
+ cwd,
843
925
  });
844
926
 
845
- // Fold the authoring-context builders grounded in the seed prose.
846
- // `docsContextFiles` is emptied for this call: the per-plan digest-file
847
- // path needs a plan id that does not exist yet — the inline digest
848
- // below replaces it.
849
- const authoring = await buildAuthoringContext(
850
- 0,
851
- /* provider (unused behind the prefetch seam) */ {},
852
- { ...settings, docsContextFiles: [] },
853
- {
854
- epic: { id: 0, title: seedFilePath ?? 'seed', body: content },
855
- github: config.github ?? null,
856
- cwd,
857
- },
858
- );
859
-
860
- const paths = settings?.paths ?? {};
861
- const inlineDigest = await buildDocsDigest({
862
- docsContextFiles: settings?.docsContextFiles,
863
- docsRoot: paths.docsRoot,
864
- });
865
- const docsContext =
866
- inlineDigest == null
867
- ? null
868
- : { mode: 'digest-inline', digest: inlineDigest };
869
-
870
927
  const limits = getLimits(config);
871
928
  const heuristics = resolveRiskHeuristics(config);
872
929
 
@@ -892,7 +949,7 @@ async function buildSeedFileModeEnvelope({
892
949
  docsContext,
893
950
  bddRunner: authoring.bddRunner,
894
951
  bddScenarios: authoring.bddScenarios,
895
- memoryFreshness: authoring.memoryFreshness,
952
+ memoryPoolAdvisory: authoring.memoryPoolAdvisory,
896
953
  priorFeedback: authoring.priorFeedback,
897
954
  ticketSchema: TICKET_SCHEMA_DESCRIPTOR,
898
955
  maxTickets: limits.maxTickets,
@@ -975,7 +1032,8 @@ async function fetchSourceTickets(ticketIds, provider) {
975
1032
  state: ticket.state ?? undefined,
976
1033
  };
977
1034
  },
978
- { concurrency: SOURCE_TICKET_FETCH_CONCURRENCY },
1035
+ // `--tickets` source-ticket hydration: one independent read per id.
1036
+ { concurrency: FANOUT_CONCURRENCY },
979
1037
  );
980
1038
  }
981
1039
 
@@ -1001,37 +1059,18 @@ async function buildTicketsModeEnvelope({
1001
1059
  .map((t) => `# ${t.title}\n\n${t.body}`)
1002
1060
  .join('\n\n---\n\n');
1003
1061
 
1004
- const duplicates = await searchStoryDuplicates({
1062
+ // Same three independent gathers as seed-file mode, concurrent under the
1063
+ // same bound (Story #4952); only the source-ticket hydration above is a
1064
+ // genuine data dependency, because `seed` is derived from it.
1065
+ const { duplicates, authoring, docsContext } = await gatherEnvelopeInputs({
1005
1066
  seed,
1067
+ epicTitle: sourceTickets[0]?.title ?? 'tickets',
1068
+ excludeIds: ticketIds,
1006
1069
  provider,
1007
1070
  config,
1008
- excludeIds: ticketIds,
1009
- });
1010
-
1011
- const authoring = await buildAuthoringContext(
1012
- 0,
1013
- {},
1014
- { ...settings, docsContextFiles: [] },
1015
- {
1016
- epic: {
1017
- id: 0,
1018
- title: sourceTickets[0]?.title ?? 'tickets',
1019
- body: seed,
1020
- },
1021
- github: config.github ?? null,
1022
- cwd,
1023
- },
1024
- );
1025
-
1026
- const paths = settings?.paths ?? {};
1027
- const inlineDigest = await buildDocsDigest({
1028
- docsContextFiles: settings?.docsContextFiles,
1029
- docsRoot: paths.docsRoot,
1071
+ settings,
1072
+ cwd,
1030
1073
  });
1031
- const docsContext =
1032
- inlineDigest == null
1033
- ? null
1034
- : { mode: 'digest-inline', digest: inlineDigest };
1035
1074
 
1036
1075
  const limits = getLimits(config);
1037
1076
  const heuristics = resolveRiskHeuristics(config);
@@ -1053,7 +1092,7 @@ async function buildTicketsModeEnvelope({
1053
1092
  docsContext,
1054
1093
  bddRunner: authoring.bddRunner,
1055
1094
  bddScenarios: authoring.bddScenarios,
1056
- memoryFreshness: authoring.memoryFreshness,
1095
+ memoryPoolAdvisory: authoring.memoryPoolAdvisory,
1057
1096
  priorFeedback: authoring.priorFeedback,
1058
1097
  ticketSchema: TICKET_SCHEMA_DESCRIPTOR,
1059
1098
  maxTickets: limits.maxTickets,
@@ -1147,12 +1186,26 @@ async function buildAmendmentModeEnvelope({
1147
1186
 
1148
1187
  const heuristics = resolveRiskHeuristics(config);
1149
1188
  const limits = getLimits(config);
1150
- const duplicates = await searchStoryDuplicates({
1151
- seed: priorBody,
1152
- provider,
1153
- config,
1154
- excludeIds: [amendsId],
1155
- });
1189
+ // Story #4952 this builder's independent-gather set has exactly one
1190
+ // member. `provider.getTicket` above is a hard data dependency (the prior
1191
+ // body IS the seed), and the mode deliberately carries no authoring-context
1192
+ // fold and no docs digest — the prior artifacts are the grounding. It still
1193
+ // goes through the same bounded gather as the other two builders so one file
1194
+ // does not carry two ways of gathering independent envelope inputs.
1195
+ const [duplicates] = await concurrentMap(
1196
+ [
1197
+ () =>
1198
+ searchStoryDuplicates({
1199
+ seed: priorBody,
1200
+ provider,
1201
+ config,
1202
+ excludeIds: [amendsId],
1203
+ }),
1204
+ ],
1205
+ (gather) => gather(),
1206
+ // Same independent-gather fan-out as the seed-mode envelope above.
1207
+ { concurrency: FANOUT_CONCURRENCY },
1208
+ );
1156
1209
 
1157
1210
  return {
1158
1211
  mode: 'amends',
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { Logger } from '../../Logger.js';
9
9
  import {
10
+ CONFLICT_KINDS,
10
11
  renderFanOutEvidence,
11
12
  renderFanOutRemedy,
12
13
  renderHardConflictError,
@@ -54,6 +55,15 @@ export function enforceFanOutGate(
54
55
  }
55
56
 
56
57
  /**
58
+ * Report every soft finding the validator produced, each under its own kind.
59
+ *
60
+ * Only the {@link CONFLICT_KINDS} are cross-Story conflicts. The advisory
61
+ * kinds — `spec-word-budget` and the sizing findings — are single-Story
62
+ * nudges, and announcing them as conflicts overstated them and taught readers
63
+ * to discount the whole channel (Story #4907). `fan-out-warning` is excluded
64
+ * throughout: {@link enforceFanOutGate} owns it and has already either thrown
65
+ * or logged the override.
66
+ *
57
67
  * @param {object[]} findings
58
68
  * @param {string} [tag]
59
69
  */
@@ -62,10 +72,26 @@ export function surfaceSoftConflictFindings(findings, tag = 'plan-persist') {
62
72
  (f) => f?.severity === 'soft' && f?.kind !== 'fan-out-warning',
63
73
  );
64
74
  if (soft.length === 0) return;
65
- Logger.warn(
66
- `[${tag}] ${soft.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
67
- );
68
- for (const finding of soft) {
69
- Logger.warn(`[${tag}] soft conflict: ${renderHardConflictError(finding)}`);
75
+ const conflicts = soft.filter((f) => CONFLICT_KINDS.has(f?.kind));
76
+ const advisories = soft.filter((f) => !CONFLICT_KINDS.has(f?.kind));
77
+ if (conflicts.length > 0) {
78
+ Logger.warn(
79
+ `[${tag}] ${conflicts.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
80
+ );
81
+ for (const finding of conflicts) {
82
+ Logger.warn(
83
+ `[${tag}] soft conflict: ${renderHardConflictError(finding)}`,
84
+ );
85
+ }
86
+ }
87
+ if (advisories.length > 0) {
88
+ Logger.warn(
89
+ `[${tag}] ${advisories.length} advisory finding(s) — the persist proceeds:`,
90
+ );
91
+ for (const finding of advisories) {
92
+ Logger.warn(
93
+ `[${tag}] advisory (${finding.kind}): ${renderHardConflictError(finding)}`,
94
+ );
95
+ }
70
96
  }
71
97
  }