@event4u/agent-config 8.10.0 → 8.12.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 (226) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +67 -149
  4. package/README.md +6 -6
  5. package/dist/agent-src/commands/challenge-me/vision.md +121 -32
  6. package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
  7. package/dist/agent-src/commands/commit/in-chunks.md +1 -1
  8. package/dist/agent-src/commands/commit.md +2 -1
  9. package/dist/agent-src/commands/cost/report.md +2 -1
  10. package/dist/agent-src/commands/feature/dev.md +4 -4
  11. package/dist/agent-src/commands/humanize.md +101 -0
  12. package/dist/agent-src/commands/jira-ticket.md +2 -2
  13. package/dist/agent-src/commands/memory/load.md +15 -0
  14. package/dist/agent-src/commands/optimize/project.md +12 -0
  15. package/dist/agent-src/commands/roadmap/create.md +12 -11
  16. package/dist/agent-src/commands/tests/create.md +1 -1
  17. package/dist/agent-src/commands/worktree/cleanup.md +22 -17
  18. package/dist/agent-src/commands/worktree/create.md +3 -1
  19. package/dist/agent-src/commands/worktree/status.md +12 -0
  20. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
  21. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
  22. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
  23. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  24. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
  25. package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
  26. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
  27. package/dist/agent-src/rules/architecture.md +6 -51
  28. package/dist/agent-src/rules/autonomous-execution.md +7 -37
  29. package/dist/agent-src/rules/broken-access-control.md +7 -45
  30. package/dist/agent-src/rules/code-comment-discipline.md +64 -0
  31. package/dist/agent-src/rules/context-hygiene.md +14 -0
  32. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  33. package/dist/agent-src/rules/delegation-policy.md +10 -8
  34. package/dist/agent-src/rules/design-fidelity.md +4 -0
  35. package/dist/agent-src/rules/git-history-discipline.md +4 -79
  36. package/dist/agent-src/rules/improve-before-implement.md +7 -66
  37. package/dist/agent-src/rules/legal-safety-floor.md +11 -55
  38. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  39. package/dist/agent-src/rules/persona-governance.md +5 -47
  40. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  41. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  42. package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
  43. package/dist/agent-src/rules/think-before-action.md +10 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  45. package/dist/agent-src/rules/verify-before-complete.md +12 -0
  46. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
  47. package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
  48. package/dist/agent-src/skills/api-design/SKILL.md +7 -5
  49. package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
  51. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  52. package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
  53. package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
  54. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
  55. package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
  56. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  57. package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
  58. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
  59. package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
  60. package/dist/agent-src/skills/design-review/SKILL.md +23 -2
  61. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
  62. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
  63. package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
  64. package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
  65. package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
  66. package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
  67. package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
  68. package/dist/agent-src/skills/laravel/SKILL.md +14 -1
  69. package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
  70. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
  71. package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
  72. package/dist/agent-src/skills/mcp/SKILL.md +1 -1
  73. package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
  74. package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
  75. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  76. package/dist/agent-src/skills/performance/SKILL.md +1 -1
  77. package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
  78. package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
  79. package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
  80. package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
  81. package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
  82. package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
  83. package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
  84. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
  85. package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
  86. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
  87. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
  88. package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
  89. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
  90. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
  91. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
  92. package/dist/agent-src/templates/agent-settings.md +3 -2
  93. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
  94. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  95. package/dist/agent-src/templates/roadmaps.md +14 -0
  96. package/dist/cli/commands/recordTriggerEval.js +4 -14
  97. package/dist/cli/commands/recordTriggerEval.js.map +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +157 -88
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +6 -6
  102. package/dist/discovery/orphan-report.md +1 -1
  103. package/dist/discovery/packs.json +13 -10
  104. package/dist/discovery/trust-report.md +3 -3
  105. package/dist/discovery/workspaces.json +14 -9
  106. package/dist/install/install.mjs +12 -7
  107. package/dist/mcp/registry-manifest.json +4 -4
  108. package/dist/router.json +1 -1
  109. package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
  110. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  111. package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
  112. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
  113. package/dist/scripts/condense.js +12 -2
  114. package/dist/scripts/condense.js.map +1 -1
  115. package/dist/server/schemas/settings.js +5 -2
  116. package/dist/server/schemas/settings.js.map +1 -1
  117. package/docs/CLAIMS.md +54 -5
  118. package/docs/architecture.md +4 -4
  119. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  120. package/docs/benchmark.md +90 -0
  121. package/docs/catalog.md +8 -5
  122. package/docs/command-flows.md +2 -1
  123. package/docs/contracts/ai-council-config.md +97 -0
  124. package/docs/contracts/command-clusters.md +1 -0
  125. package/docs/contracts/evidence-based-pruning.md +1 -0
  126. package/docs/contracts/hook-architecture-v1.md +25 -0
  127. package/docs/contracts/kernel-membership.md +35 -0
  128. package/docs/contracts/measurement-baseline.md +1 -0
  129. package/docs/contracts/persona-schema.md +49 -0
  130. package/docs/contracts/provider-lifecycle.md +29 -4
  131. package/docs/contracts/release-sizing.md +22 -0
  132. package/docs/contracts/write-engine.md +38 -1
  133. package/docs/customization.md +12 -0
  134. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
  135. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
  136. package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
  137. package/docs/decisions/ADR-028-root-layout.md +1 -1
  138. package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
  139. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
  140. package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
  141. package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
  142. package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
  143. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
  144. package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
  145. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
  146. package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
  147. package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
  148. package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
  149. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  150. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  151. package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
  152. package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
  153. package/docs/decisions/INDEX.md +13 -9
  154. package/docs/design/council-vs-solo-baseline.md +92 -0
  155. package/docs/design/cross-model-parity-eval.md +82 -0
  156. package/docs/design/length-neutral-judge-rerun.md +92 -0
  157. package/docs/design/utilization-window-criteria.md +51 -0
  158. package/docs/featured-skills.md +2 -2
  159. package/docs/getting-started-by-role.md +1 -1
  160. package/docs/getting-started.md +2 -2
  161. package/docs/governance-advantage.md +1 -1
  162. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  163. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
  164. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  165. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  166. package/docs/guidelines/code-clarity.md +191 -1
  167. package/docs/guidelines/design-antipatterns.md +20 -0
  168. package/docs/guidelines/php/php-coding-patterns.md +12 -2
  169. package/docs/maintainers/branch-protection.md +87 -0
  170. package/docs/maintainers/system-map.md +85 -0
  171. package/docs/proof.md +32 -15
  172. package/docs/wedge/production-validator/production-validator.md +27 -0
  173. package/package.json +1 -1
  174. package/src/config/agent-settings.template.yml +38 -9
  175. package/src/scripts/_cli/cmd_doctor.ts +129 -1
  176. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
  177. package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
  178. package/src/scripts/_lib/knowledge_global.ts +50 -2
  179. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  180. package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
  181. package/src/scripts/_lib/lexical_index.ts +15 -2
  182. package/src/scripts/_lib/orchestration_record.ts +14 -0
  183. package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
  184. package/src/scripts/ai_council/chairman.ts +90 -0
  185. package/src/scripts/ai_council/clients.ts +18 -3
  186. package/src/scripts/ai_council/config.ts +157 -0
  187. package/src/scripts/ai_council/debate_gates.ts +89 -0
  188. package/src/scripts/ai_council/orchestrator.ts +252 -2
  189. package/src/scripts/ai_council/prompts.ts +121 -11
  190. package/src/scripts/ai_council/replay.ts +17 -0
  191. package/src/scripts/ai_council/stance_tally.ts +184 -0
  192. package/src/scripts/ai_tells_rules.ts +433 -0
  193. package/src/scripts/bench_humanizer_eval.ts +240 -0
  194. package/src/scripts/bench_persona_placebo.ts +308 -0
  195. package/src/scripts/bench_quality_rerun.ts +357 -0
  196. package/src/scripts/build_discovery_manifest.ts +6 -0
  197. package/src/scripts/build_proof.ts +30 -9
  198. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  199. package/src/scripts/check_claims.ts +40 -0
  200. package/src/scripts/check_knowledge_cards.ts +33 -4
  201. package/src/scripts/check_portability.ts +1 -0
  202. package/src/scripts/check_rule_invariants.ts +10 -3
  203. package/src/scripts/complexity_report.ts +776 -0
  204. package/src/scripts/condense.ts +16 -2
  205. package/src/scripts/consumer_matrix.ts +292 -14
  206. package/src/scripts/council_cli.ts +203 -1
  207. package/src/scripts/detect_ai_tells.ts +312 -0
  208. package/src/scripts/explain_run.ts +593 -0
  209. package/src/scripts/install.ts +12 -0
  210. package/src/scripts/knowledge_global_cli.ts +235 -46
  211. package/src/scripts/lint_framework_leakage.ts +88 -7
  212. package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
  213. package/src/scripts/lint_knowledge_scale.ts +30 -0
  214. package/src/scripts/lint_originality_shingles.ts +210 -0
  215. package/src/scripts/orchestration_record.ts +1 -0
  216. package/src/scripts/rule_backlinks.ts +0 -0
  217. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  218. package/src/scripts/self_review_gate.ts +41 -15
  219. package/src/scripts/skill_eval_coverage.ts +71 -3
  220. package/src/scripts/skill_linter.ts +1 -0
  221. package/src/scripts/trigger_eval_rotation.ts +344 -0
  222. package/src/scripts/update_counts.ts +5 -17
  223. package/src/scripts/worktree_cleanup_check.ts +263 -0
  224. package/src/server/schemas/settings.ts +8 -3
  225. package/src/scripts/analysis_freshness.ts +0 -362
  226. package/src/scripts/export_replay_corpus.ts +0 -208
@@ -148,6 +148,28 @@ one of several independent reviewers. Round-specific instructions:
148
148
  End each round with: a one-line position summary and the single
149
149
  piece of evidence that would change your mind.`;
150
150
 
151
+ /**
152
+ * Mandatory closing-line contract appended to the FINAL round when
153
+ * `ai_council.stance_tally` is enabled (Phase 1). Peers backing the same option
154
+ * MUST reuse the same label so the deterministic tally can canonicalise it;
155
+ * `abstain` is allowed. Parsed by `stance_tally.parse_stance_line` — the tally
156
+ * never infers a stance from the surrounding prose, so an omitted or malformed
157
+ * line becomes a repair-marker, not a guess.
158
+ */
159
+ export const STANCE_LINE_CONTRACT = `Close your reply with EXACTLY this line, and nothing after it:
160
+ STANCE: <option-label> | CONFIDENCE: high|med|low | DEALBREAKER: yes|no
161
+ Reuse the SAME <option-label> as any peer backing the same option (match their wording); use \`abstain\` only if you genuinely cannot choose. CONFIDENCE is your certainty in the pick; DEALBREAKER is \`yes\` only if you would block on the alternative.`;
162
+
163
+ /**
164
+ * Anti-conformity directive appended to round-2+ debate prompts when
165
+ * `ai_council.debate_gates` is enabled (road-to-opt-council-deliberation
166
+ * Phase 3). Counters the round-over-round convergence-to-consensus drift the
167
+ * `rounds:N` path otherwise invites. Byte-identical across the api / cli /
168
+ * manual transports (it becomes part of the shared `user_prompt`, which every
169
+ * client's `ask()` receives verbatim), so no per-transport special-casing.
170
+ */
171
+ export const ANTI_CONFORMITY_DIRECTIVE = `Anti-conformity rule for this round: defend a position you still believe is correct. Change your position ONLY when a specific, named flaw in it has been identified — and you must name that flaw explicitly to justify the change. Do not soften or converge merely because other reviewers disagree; agreement without a named reason is conformity, not reasoning.`;
172
+
151
173
  const _MODE_TABLE: Record<string, string> = {
152
174
  prompt: PROMPT_MODE,
153
175
  roadmap: ROADMAP_MODE,
@@ -233,8 +255,15 @@ read it directly from a response.
233
255
  A single sentence: which course the host agent should advise the
234
256
  user to take, grounded in the strongest converged point.
235
257
 
236
- ### Next step
237
- One concrete next action the user can take in their current turn.`;
258
+ ### Kill criteria
259
+ Observable conditions that would falsify this recommendation without
260
+ re-convening the council. Each entry names a threshold or a specific
261
+ event — never a vague "if it goes wrong". Omit only if genuinely none
262
+ exist, and say so explicitly.
263
+
264
+ ### Concrete next step
265
+ Exactly one artefact-producing action the user can take in their
266
+ current turn.`;
238
267
 
239
268
  export const PR_SYNTHESIS = `Summarise the council with the PR-review shape below.
240
269
 
@@ -251,7 +280,17 @@ and the host agent confirms are load-bearing. Maximum five.
251
280
 
252
281
  ### Recommendation
253
282
  APPROVE / REQUEST_CHANGES / REJECT and a single sentence justifying
254
- the verdict, anchored on the strongest consensus or must-fix line.`;
283
+ the verdict, anchored on the strongest consensus or must-fix line.
284
+
285
+ ### Kill criteria
286
+ Observable conditions that would falsify this verdict without re-convening
287
+ the council (a failing check, a missing test, a regression signal). Each
288
+ entry names a threshold or a specific event. Omit only if genuinely none
289
+ exist, and say so.
290
+
291
+ ### Concrete next step
292
+ Exactly one artefact-producing action the author can take in their current
293
+ turn.`;
255
294
 
256
295
  export const ANALYSIS_SYNTHESIS = `Summarise the council with the analysis-lens shape below.
257
296
 
@@ -270,19 +309,41 @@ metadata shape as Top-10.
270
309
  ### Outliers
271
310
  Single-reviewer findings the others did not engage with. Keep them
272
311
  — they are signal for a future deeper analysis pass — but mark each
273
- as \`unverified-by-council\`.`;
274
-
275
- // Creative lenses — open-ended prose, no template. The renderer keeps
276
- // the bare "Convergence / Divergence" slot so the host agent can write
277
- // free-form synthesis.
278
- const _CREATIVE_PASSTHROUGH = '';
312
+ as \`unverified-by-council\`.
313
+
314
+ ### Kill criteria
315
+ Observable conditions that would falsify the analysis's leading finding
316
+ without re-convening the council. Each entry names a threshold or event.
317
+ Omit only if genuinely none exist, and say so.
318
+
319
+ ### Concrete next step
320
+ Exactly one artefact-producing action the user can take in their current
321
+ turn.`;
322
+
323
+ // Creative lenses — free-form prose synthesis, then the two required
324
+ // verdict-discipline sections. The body stays open-ended (the host agent
325
+ // writes the Convergence / Divergence prose on its own merits); only the
326
+ // falsifiable close is mandated, so every lens carries Kill criteria +
327
+ // Concrete next step (road-to-opt-council-deliberation Phase 0).
328
+ export const CREATIVE_SYNTHESIS = `Write a free-form convergence / divergence
329
+ synthesis of the council in prose — no fixed section shape for the body.
330
+ Then close with exactly these two sections:
331
+
332
+ ### Kill criteria
333
+ Observable conditions that would falsify the direction you converged on,
334
+ without re-convening the council. Each entry names a threshold or event.
335
+ Omit only if genuinely none exist, and say so.
336
+
337
+ ### Concrete next step
338
+ Exactly one artefact-producing action the user can take in their current
339
+ turn.`;
279
340
 
280
341
  const _SYNTHESIS_TABLE: Record<string, string> = {
281
342
  default: DEFAULT_SYNTHESIS,
282
343
  pr: PR_SYNTHESIS,
283
344
  analysis: ANALYSIS_SYNTHESIS,
284
- design: _CREATIVE_PASSTHROUGH,
285
- optimize: _CREATIVE_PASSTHROUGH,
345
+ design: CREATIVE_SYNTHESIS,
346
+ optimize: CREATIVE_SYNTHESIS,
286
347
  };
287
348
 
288
349
  // Input modes inherit the `default` decision template. Lens overrides
@@ -371,6 +432,55 @@ export function all_synthesis_modes(): string[] {
371
432
  return Object.keys(_SYNTHESIS_TABLE).sort();
372
433
  }
373
434
 
435
+ /**
436
+ * Raised when a completed synthesis is missing a required verdict-discipline
437
+ * section (Kill criteria / Concrete next step) or carries it empty. Named so
438
+ * the render step fails loudly rather than emitting an unfalsifiable verdict.
439
+ * Mirrors the `CouncilConfigError` named-error precedent in `config.ts`.
440
+ */
441
+ export class SynthesisRenderError extends Error {
442
+ constructor(message: string) {
443
+ super(message);
444
+ this.name = 'SynthesisRenderError';
445
+ }
446
+ }
447
+
448
+ /** The two sections every lens synthesis must close with (Phase 0). */
449
+ export const REQUIRED_SYNTHESIS_SECTIONS = ['Kill criteria', 'Concrete next step'] as const;
450
+
451
+ /**
452
+ * Validate a **completed** synthesis (the host- or chairman-authored text that
453
+ * fills the Convergence / Divergence slot) — not the template prompt. Every
454
+ * lens must close with a non-empty `### Kill criteria` and `### Concrete next
455
+ * step`; a missing or placeholder-empty section throws `SynthesisRenderError`
456
+ * naming the first offender. Called on the synthesis-emit path (the chairman
457
+ * synthesis in Phase 2, and any future synthesis-record step); safe to call on
458
+ * decision and creative lenses alike, since both now require the two sections.
459
+ */
460
+ export function assert_synthesis_sections(text: string): void {
461
+ const lines = text.split('\n');
462
+ const isHeading = (l: string): boolean => /^#{2,4}\s/.test(l);
463
+ for (const section of REQUIRED_SYNTHESIS_SECTIONS) {
464
+ const headingRe = new RegExp(`^#{2,4}\\s+${section}\\s*$`, 'i');
465
+ const idx = lines.findIndex((l) => headingRe.test(l));
466
+ if (idx === -1) {
467
+ throw new SynthesisRenderError(
468
+ `synthesis is missing the required "### ${section}" section`,
469
+ );
470
+ }
471
+ let body = '';
472
+ for (let i = idx + 1; i < lines.length; i++) {
473
+ if (isHeading(lines[i] as string)) break;
474
+ body += lines[i];
475
+ }
476
+ if (body.trim().length === 0) {
477
+ throw new SynthesisRenderError(
478
+ `synthesis "### ${section}" section is empty (placeholder)`,
479
+ );
480
+ }
481
+ }
482
+ }
483
+
374
484
  /**
375
485
  * Drop any *whole line* containing a host-agent identity substring.
376
486
  *
@@ -308,6 +308,23 @@ export function render_decision_replay(inputs: DecisionReplayInputs): string {
308
308
  }
309
309
  lines.push(`**Synthesis verdict**: ${verdict} consensus — ${f.source} sourced.\n`);
310
310
  }
311
+ // Aggregate render lines (Phase 0 fold): one evidence-label line + one
312
+ // provider-spread line — folded into the trailer, not a new template
313
+ // section (gap-table FOLD row: evidence labels + provider spread → replay).
314
+ const evTally: Record<string, number> = { H: 0, M: 0, L: 0 };
315
+ for (const f of ranked) {
316
+ const em = _metaGet(inputs.metadata, f.id);
317
+ const q = (em?.evidence_quality ?? '').toUpperCase().charAt(0);
318
+ if (q === 'H' || q === 'M' || q === 'L') {
319
+ evTally[q] = (evTally[q] as number) + 1;
320
+ }
321
+ }
322
+ const providers = new Set(inputs.deliberation.map((r) => r.provider));
323
+ lines.push(
324
+ `- **Evidence spread**: ${evTally.H} high · ${evTally.M} medium · ` +
325
+ `${evTally.L} low (across ${ranked.length} finding(s))\n` +
326
+ `- **Provider spread**: ${providers.size} distinct provider(s) deliberated\n`,
327
+ );
311
328
  const mode_label = inputs.include_member_arguments ? 'full' : 'redacted (counts only)';
312
329
  lines.push(`---\n\n_artefact mode: ${mode_label}_\n`);
313
330
  return _pyRStrip(lines.join('\n')) + '\n';
@@ -0,0 +1,184 @@
1
+ // Option-level stance tally (road-to-opt-council-deliberation Phase 1).
2
+ //
3
+ // `consensus.ts` scores *findings*; nothing here produces an *option-level*
4
+ // verdict for "A or B?" questions. This module engineers Source G's prose
5
+ // tally deterministically: parse each member's mandatory final-round stance
6
+ // line, weight by confidence, and decide consensus vs. an honest split against
7
+ // a ⅔ threshold. Everything countable lands in TS with tests — never inferred
8
+ // from prose (an unparseable line is a repair-marker, not a guess).
9
+ //
10
+ // Default-off behind `ai_council.stance_tally.enabled`; additive — with the key
11
+ // false the council path is byte-identical to today.
12
+
13
+ /** Confidence tier from a stance line. */
14
+ export type Confidence = 'high' | 'med' | 'low';
15
+
16
+ /** Contribution weight per confidence tier (Source G's factors). */
17
+ export const CONFIDENCE_FACTOR: Record<Confidence, number> = {
18
+ high: 1.0,
19
+ med: 0.75,
20
+ low: 0.5,
21
+ };
22
+
23
+ /** Consensus requires an option to clear two-thirds of the base-weight total. */
24
+ export const CONSENSUS_FRACTION = 2 / 3;
25
+
26
+ /** The canonical label for an explicit abstention. */
27
+ export const ABSTAIN_LABEL = 'abstain';
28
+
29
+ /** A parsed stance line bound to its member. */
30
+ export interface StanceLine {
31
+ member: string; // e.g. "openai:gpt-4o"
32
+ label: string; // canonical (casefolded) key; `abstain` is special
33
+ display: string; // first-seen original label text (rendering)
34
+ confidence: Confidence;
35
+ dealbreaker: boolean;
36
+ }
37
+
38
+ /** Per-option accumulation across members. */
39
+ export interface OptionTally {
40
+ label: string; // display label (first-seen casing)
41
+ weight: number; // sum of confidence factors of backers
42
+ backers: Array<{ member: string; confidence: Confidence }>;
43
+ dealbreakers: number;
44
+ }
45
+
46
+ /** The full tally verdict. `consensus` is null on a split — never a forced winner. */
47
+ export interface StanceTallyResult {
48
+ options: OptionTally[]; // non-abstain options, weight desc
49
+ w_total: number; // base-weight denominator (every voting member, abstain included)
50
+ threshold: number; // CONSENSUS_FRACTION * w_total
51
+ consensus: OptionTally | null; // the option clearing the threshold, or null
52
+ split: boolean; // true when no option clears the threshold
53
+ abstain_count: number;
54
+ needs_repair: string[]; // members whose stance line was missing/unparseable
55
+ }
56
+
57
+ // Tolerant of whitespace and case; requires the three explicit fields in order.
58
+ // `medium` is accepted as an alias for `med`.
59
+ const _STANCE_RE =
60
+ /STANCE:\s*(.+?)\s*\|\s*CONFIDENCE:\s*(high|med(?:ium)?|low)\s*\|\s*DEALBREAKER:\s*(yes|no)/gi;
61
+
62
+ /**
63
+ * Parse the member's stance line from their reply text. Returns the LAST stance
64
+ * line if several appear (the final, most-authoritative one), or `null` when no
65
+ * well-formed line exists — the caller treats null as a repair-marker and never
66
+ * infers a stance from the surrounding prose.
67
+ */
68
+ export function parse_stance_line(text: string): Omit<StanceLine, 'member'> | null {
69
+ _STANCE_RE.lastIndex = 0;
70
+ let last: RegExpExecArray | null = null;
71
+ let m: RegExpExecArray | null;
72
+ while ((m = _STANCE_RE.exec(text)) !== null) {
73
+ last = m;
74
+ }
75
+ if (last === null) {
76
+ return null;
77
+ }
78
+ const rawLabel = (last[1] ?? '').trim();
79
+ if (rawLabel.length === 0) {
80
+ return null;
81
+ }
82
+ let conf = (last[2] ?? '').toLowerCase();
83
+ if (conf === 'medium') {
84
+ conf = 'med';
85
+ }
86
+ return {
87
+ label: rawLabel.toLowerCase(),
88
+ display: rawLabel,
89
+ confidence: conf as Confidence,
90
+ dealbreaker: (last[3] ?? '').toLowerCase() === 'yes',
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Tally option-level stances across members. Base weight is 1.0 per member;
96
+ * `W_total` sums those base weights over every member with a parseable stance
97
+ * (abstentions included — they raise the bar without backing any option). An
98
+ * option's weight is the sum of its backers' confidence factors. Consensus iff
99
+ * the leading option's weight ≥ ⅔ × W_total; otherwise a structured split is
100
+ * returned — never a forced winner, never an auto-added round.
101
+ */
102
+ export function tally_stances(
103
+ members: ReadonlyArray<{ member: string; text: string }>,
104
+ ): StanceTallyResult {
105
+ const parsed: StanceLine[] = [];
106
+ const needs_repair: string[] = [];
107
+ for (const { member, text } of members) {
108
+ const p = parse_stance_line(text);
109
+ if (p === null) {
110
+ needs_repair.push(member);
111
+ continue;
112
+ }
113
+ parsed.push({ member, ...p });
114
+ }
115
+
116
+ const w_total = parsed.length; // base weight 1.0 each, abstain included
117
+ const threshold = CONSENSUS_FRACTION * w_total;
118
+ let abstain_count = 0;
119
+
120
+ const byLabel = new Map<string, OptionTally>();
121
+ for (const p of parsed) {
122
+ if (p.label === ABSTAIN_LABEL) {
123
+ abstain_count += 1;
124
+ continue;
125
+ }
126
+ let o = byLabel.get(p.label);
127
+ if (o === undefined) {
128
+ o = { label: p.display, weight: 0, backers: [], dealbreakers: 0 };
129
+ byLabel.set(p.label, o);
130
+ }
131
+ o.weight += CONFIDENCE_FACTOR[p.confidence];
132
+ o.backers.push({ member: p.member, confidence: p.confidence });
133
+ if (p.dealbreaker) {
134
+ o.dealbreakers += 1;
135
+ }
136
+ }
137
+
138
+ const options = [...byLabel.values()].sort(
139
+ (a, b) => b.weight - a.weight || a.label.localeCompare(b.label),
140
+ );
141
+ const top = options[0] ?? null;
142
+ // Strict-≥ against the threshold; a floating-point epsilon guards the
143
+ // exact-boundary case (e.g. 2.0 ≥ ⅔×3) from representation drift.
144
+ const consensus = top !== null && top.weight + 1e-9 >= threshold ? top : null;
145
+
146
+ return {
147
+ options,
148
+ w_total,
149
+ threshold,
150
+ consensus,
151
+ split: consensus === null,
152
+ abstain_count,
153
+ needs_repair,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Render the **Vote Tally** verdict section: one line per option, the threshold,
159
+ * and a cleared-or-escalated line. Deterministic — pure projection of the tally.
160
+ */
161
+ export function render_vote_tally(result: StanceTallyResult): string {
162
+ const lines: string[] = ['### Vote Tally'];
163
+ if (result.options.length === 0) {
164
+ lines.push('- (no option-level stances parsed)');
165
+ }
166
+ for (const o of result.options) {
167
+ const backers = o.backers.map((b) => `${b.member} (${b.confidence})`).join(', ');
168
+ const db = o.dealbreakers > 0 ? ` · ${o.dealbreakers} dealbreaker(s)` : '';
169
+ lines.push(`- ${o.label} — ${o.weight.toFixed(2)} (${backers})${db}`);
170
+ }
171
+ if (result.abstain_count > 0) {
172
+ lines.push(`- abstain — ${result.abstain_count} member(s) (raises the bar)`);
173
+ }
174
+ lines.push(`Threshold: ⅔ × ${result.w_total} = ${result.threshold.toFixed(2)}`);
175
+ lines.push(
176
+ result.consensus !== null
177
+ ? `Cleared: ${result.consensus.label} (${result.consensus.weight.toFixed(2)})`
178
+ : 'Escalated: no option cleared the threshold — the split is returned to the user, not forced.',
179
+ );
180
+ if (result.needs_repair.length > 0) {
181
+ lines.push(`Unparsed stance (repair needed): ${result.needs_repair.join(', ')}`);
182
+ }
183
+ return lines.join('\n');
184
+ }