@event4u/agent-config 8.10.0 → 8.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +148 -0
  3. package/README.md +5 -5
  4. package/dist/agent-src/commands/challenge-me/vision.md +121 -32
  5. package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
  6. package/dist/agent-src/commands/commit/in-chunks.md +1 -1
  7. package/dist/agent-src/commands/commit.md +2 -1
  8. package/dist/agent-src/commands/cost/report.md +2 -1
  9. package/dist/agent-src/commands/feature/dev.md +4 -4
  10. package/dist/agent-src/commands/humanize.md +101 -0
  11. package/dist/agent-src/commands/jira-ticket.md +2 -2
  12. package/dist/agent-src/commands/memory/load.md +15 -0
  13. package/dist/agent-src/commands/optimize/project.md +12 -0
  14. package/dist/agent-src/commands/roadmap/create.md +12 -11
  15. package/dist/agent-src/commands/tests/create.md +1 -1
  16. package/dist/agent-src/commands/worktree/cleanup.md +22 -17
  17. package/dist/agent-src/commands/worktree/create.md +3 -1
  18. package/dist/agent-src/commands/worktree/status.md +12 -0
  19. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
  20. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
  21. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
  22. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +14 -0
  23. package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
  24. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
  25. package/dist/agent-src/rules/autonomous-execution.md +7 -37
  26. package/dist/agent-src/rules/broken-access-control.md +7 -45
  27. package/dist/agent-src/rules/code-comment-discipline.md +148 -0
  28. package/dist/agent-src/rules/context-hygiene.md +14 -0
  29. package/dist/agent-src/rules/delegation-policy.md +10 -8
  30. package/dist/agent-src/rules/design-fidelity.md +4 -0
  31. package/dist/agent-src/rules/git-history-discipline.md +4 -79
  32. package/dist/agent-src/rules/legal-safety-floor.md +11 -55
  33. package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
  34. package/dist/agent-src/rules/think-before-action.md +10 -0
  35. package/dist/agent-src/rules/verify-before-complete.md +12 -0
  36. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +13 -12
  37. package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
  38. package/dist/agent-src/skills/api-design/SKILL.md +7 -5
  39. package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
  40. package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
  41. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  42. package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
  43. package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
  44. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
  45. package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
  46. package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
  47. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
  48. package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
  49. package/dist/agent-src/skills/design-review/SKILL.md +23 -2
  50. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
  51. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
  52. package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
  53. package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
  54. package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
  55. package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
  56. package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
  57. package/dist/agent-src/skills/laravel/SKILL.md +14 -1
  58. package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
  59. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
  60. package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
  61. package/dist/agent-src/skills/mcp/SKILL.md +1 -1
  62. package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
  63. package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
  64. package/dist/agent-src/skills/performance/SKILL.md +1 -1
  65. package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
  66. package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
  67. package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
  68. package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
  69. package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
  70. package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
  71. package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
  72. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
  73. package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
  74. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
  75. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
  76. package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
  77. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
  78. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
  79. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
  80. package/dist/agent-src/templates/agent-settings.md +3 -2
  81. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
  82. package/dist/agent-src/templates/roadmaps.md +14 -0
  83. package/dist/cli/commands/recordTriggerEval.js +4 -14
  84. package/dist/cli/commands/recordTriggerEval.js.map +1 -1
  85. package/dist/discovery/deprecation-report.md +1 -1
  86. package/dist/discovery/discovery-manifest.json +147 -78
  87. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  88. package/dist/discovery/discovery-manifest.summary.md +6 -6
  89. package/dist/discovery/orphan-report.md +1 -1
  90. package/dist/discovery/packs.json +13 -10
  91. package/dist/discovery/trust-report.md +3 -3
  92. package/dist/discovery/workspaces.json +14 -9
  93. package/dist/install/install.mjs +12 -7
  94. package/dist/mcp/registry-manifest.json +4 -4
  95. package/dist/router.json +1 -1
  96. package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
  97. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  98. package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
  99. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
  100. package/dist/scripts/condense.js +12 -2
  101. package/dist/scripts/condense.js.map +1 -1
  102. package/dist/server/schemas/settings.js +5 -2
  103. package/dist/server/schemas/settings.js.map +1 -1
  104. package/docs/CLAIMS.md +24 -3
  105. package/docs/architecture.md +3 -3
  106. package/docs/benchmark.md +51 -0
  107. package/docs/catalog.md +8 -5
  108. package/docs/command-flows.md +2 -1
  109. package/docs/contracts/ai-council-config.md +92 -0
  110. package/docs/contracts/command-clusters.md +1 -0
  111. package/docs/contracts/evidence-based-pruning.md +1 -0
  112. package/docs/contracts/hook-architecture-v1.md +25 -0
  113. package/docs/contracts/kernel-membership.md +35 -0
  114. package/docs/contracts/measurement-baseline.md +1 -0
  115. package/docs/contracts/release-sizing.md +22 -0
  116. package/docs/contracts/write-engine.md +38 -1
  117. package/docs/customization.md +12 -0
  118. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
  119. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
  120. package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
  121. package/docs/decisions/ADR-028-root-layout.md +1 -1
  122. package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
  123. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
  124. package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
  125. package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
  126. package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
  127. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
  128. package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
  129. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
  130. package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
  131. package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
  132. package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
  133. package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
  134. package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
  135. package/docs/decisions/INDEX.md +11 -9
  136. package/docs/design/cross-model-parity-eval.md +82 -0
  137. package/docs/design/length-neutral-judge-rerun.md +92 -0
  138. package/docs/featured-skills.md +2 -2
  139. package/docs/getting-started-by-role.md +1 -1
  140. package/docs/getting-started.md +2 -2
  141. package/docs/governance-advantage.md +1 -1
  142. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
  143. package/docs/guidelines/code-clarity.md +106 -1
  144. package/docs/guidelines/design-antipatterns.md +20 -0
  145. package/docs/guidelines/php/php-coding-patterns.md +12 -2
  146. package/docs/proof.md +20 -14
  147. package/docs/wedge/production-validator/production-validator.md +27 -0
  148. package/package.json +1 -1
  149. package/src/config/agent-settings.template.yml +29 -9
  150. package/src/scripts/_cli/cmd_doctor.ts +34 -1
  151. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
  152. package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
  153. package/src/scripts/_lib/knowledge_global.ts +6 -2
  154. package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
  155. package/src/scripts/_lib/lexical_index.ts +15 -2
  156. package/src/scripts/_lib/orchestration_record.ts +14 -0
  157. package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
  158. package/src/scripts/ai_council/chairman.ts +90 -0
  159. package/src/scripts/ai_council/clients.ts +18 -3
  160. package/src/scripts/ai_council/config.ts +157 -0
  161. package/src/scripts/ai_council/debate_gates.ts +89 -0
  162. package/src/scripts/ai_council/orchestrator.ts +65 -2
  163. package/src/scripts/ai_council/prompts.ts +121 -11
  164. package/src/scripts/ai_council/replay.ts +17 -0
  165. package/src/scripts/ai_council/stance_tally.ts +184 -0
  166. package/src/scripts/ai_tells_rules.ts +433 -0
  167. package/src/scripts/bench_humanizer_eval.ts +240 -0
  168. package/src/scripts/build_discovery_manifest.ts +6 -0
  169. package/src/scripts/build_proof.ts +21 -9
  170. package/src/scripts/check_claims.ts +40 -0
  171. package/src/scripts/check_rule_invariants.ts +10 -3
  172. package/src/scripts/condense.ts +16 -2
  173. package/src/scripts/consumer_matrix.ts +41 -2
  174. package/src/scripts/council_cli.ts +147 -0
  175. package/src/scripts/detect_ai_tells.ts +312 -0
  176. package/src/scripts/install.ts +12 -0
  177. package/src/scripts/knowledge_global_cli.ts +1 -1
  178. package/src/scripts/lint_framework_leakage.ts +88 -7
  179. package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
  180. package/src/scripts/lint_knowledge_scale.ts +30 -0
  181. package/src/scripts/lint_originality_shingles.ts +210 -0
  182. package/src/scripts/orchestration_record.ts +1 -0
  183. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  184. package/src/scripts/self_review_gate.ts +41 -15
  185. package/src/scripts/skill_eval_coverage.ts +71 -3
  186. package/src/scripts/skill_linter.ts +1 -0
  187. package/src/scripts/trigger_eval_rotation.ts +344 -0
  188. package/src/scripts/update_counts.ts +5 -17
  189. package/src/scripts/worktree_cleanup_check.ts +263 -0
  190. package/src/server/schemas/settings.ts +8 -3
  191. package/src/scripts/analysis_freshness.ts +0 -362
  192. package/src/scripts/export_replay_corpus.ts +0 -208
@@ -373,6 +373,13 @@ export interface MemberConfig {
373
373
  readonly binary: string | null;
374
374
  readonly model_ladder: readonly string[];
375
375
  readonly participate_low_impact: boolean;
376
+ /**
377
+ * Optional capability rank for chairman-`auto` tie-breaking (Phase 2;
378
+ * council 2026-07-12 — provider-family difference is primary, tier is the
379
+ * tie-break). Higher = stronger. `null` when unset; selection then falls
380
+ * back to deterministic config order.
381
+ */
382
+ readonly tier: number | null;
376
383
  }
377
384
 
378
385
  /**
@@ -451,6 +458,32 @@ export interface DecisionReplayConfig {
451
458
  readonly include_member_arguments: boolean;
452
459
  }
453
460
 
461
+ /** Option-level stance tally (road-to-opt-council-deliberation Phase 1). */
462
+ export interface StanceTallyConfig {
463
+ readonly enabled: boolean;
464
+ }
465
+
466
+ /** Chairman synthesis (road-to-opt-council-deliberation Phase 2). Default
467
+ * `host` = today's host-synthesis behaviour, byte-identical. */
468
+ export interface ChairmanConfig {
469
+ readonly mode: string; // one of _VALID_CHAIRMAN_MODES
470
+ readonly member: string | null; // required (and validated) when mode === 'member'
471
+ }
472
+
473
+ /** Debate enforcement gates (road-to-opt-council-deliberation Phase 3).
474
+ * Enables the anti-conformity directive + the deterministic post-round
475
+ * dissent-quota / novelty checks on the debate path. */
476
+ export interface DebateGatesConfig {
477
+ readonly enabled: boolean;
478
+ }
479
+
480
+ /** Pre-round-1 restatement pass (road-to-opt-council-deliberation Phase 3). */
481
+ export interface RestateConfig {
482
+ readonly enabled: boolean;
483
+ }
484
+
485
+ const _VALID_CHAIRMAN_MODES: ReadonlySet<string> = new Set(['host', 'member', 'auto']);
486
+
454
487
  /** Routing entry for one impact class (Phase 10). */
455
488
  export interface DecisionResolutionEntry {
456
489
  readonly mode: string;
@@ -519,6 +552,10 @@ export interface CouncilConfig {
519
552
  readonly model_downgrade: ModelDowngradeConfig;
520
553
  readonly debate: DebateConfig;
521
554
  readonly decision_replay: DecisionReplayConfig;
555
+ readonly stance_tally: StanceTallyConfig;
556
+ readonly chairman: ChairmanConfig;
557
+ readonly debate_gates: DebateGatesConfig;
558
+ readonly restate: RestateConfig;
522
559
  readonly decision_resolution: DecisionResolutionConfig;
523
560
  readonly routing: RoutingConfig;
524
561
  readonly low_impact: LowImpactConfig;
@@ -748,6 +785,20 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
748
785
  _asDict(_getOr(raw, 'decision_replay', {})),
749
786
  'decision_replay',
750
787
  );
788
+ const stance_tally = _build_stance_tally(
789
+ _asDict(_getOr(raw, 'stance_tally', {})),
790
+ 'stance_tally',
791
+ );
792
+ const chairman = _build_chairman(
793
+ _asDict(_getOr(raw, 'chairman', {})),
794
+ 'chairman',
795
+ members,
796
+ );
797
+ const debate_gates = _build_debate_gates(
798
+ _asDict(_getOr(raw, 'debate_gates', {})),
799
+ 'debate_gates',
800
+ );
801
+ const restate = _build_restate(_asDict(_getOr(raw, 'restate', {})), 'restate');
751
802
  const decision_resolution = _build_decision_resolution(
752
803
  _asDict(_getOr(raw, 'decision_resolution', {})),
753
804
  );
@@ -774,6 +825,10 @@ export function _build_config(raw: Dict, source_path: PathLike): CouncilConfig {
774
825
  model_downgrade,
775
826
  debate,
776
827
  decision_replay,
828
+ stance_tally,
829
+ chairman,
830
+ debate_gates,
831
+ restate,
777
832
  decision_resolution,
778
833
  routing,
779
834
  low_impact,
@@ -927,6 +982,96 @@ function _build_decision_replay(d: Dict, scope: string): DecisionReplayConfig {
927
982
  };
928
983
  }
929
984
 
985
+ /**
986
+ * `ai_council.stance_tally` — option-level stance tally (Phase 1). Default-off;
987
+ * an absent block yields `{ enabled: false }`, keeping the council path
988
+ * byte-identical. Per-field type validation (mirrors `_build_decision_replay`);
989
+ * a non-bool `enabled` is rejected rather than silently coerced.
990
+ */
991
+ function _build_stance_tally(d: Dict, scope: string): StanceTallyConfig {
992
+ if (!_isDict(d)) {
993
+ throw new CouncilConfigError(`\`${scope}\` must be a mapping.`);
994
+ }
995
+ const enabled = _get(d, 'enabled', false);
996
+ if (!_isBool(enabled)) {
997
+ throw new CouncilConfigError(`\`${scope}.enabled\` must be a bool.`);
998
+ }
999
+ return { enabled: Boolean(enabled) };
1000
+ }
1001
+
1002
+ /**
1003
+ * `ai_council.chairman` (Phase 2). Default `{ mode: 'host', member: null }` =
1004
+ * today's host synthesis, byte-identical. Enum-validates `mode`; when
1005
+ * `mode: 'member'` the named member must exist AND be enabled (fail-closed at
1006
+ * load, mirroring the advisor cross-validation). `mode: 'auto'` needs no member.
1007
+ */
1008
+ function _build_chairman(
1009
+ d: Dict,
1010
+ scope: string,
1011
+ members: ReadonlyMap<string, MemberConfig>,
1012
+ ): ChairmanConfig {
1013
+ if (!_isDict(d)) {
1014
+ throw new CouncilConfigError(`\`${scope}\` must be a mapping.`);
1015
+ }
1016
+ const mode = _get(d, 'mode', 'host');
1017
+ if (!(_isStr(mode) && _VALID_CHAIRMAN_MODES.has(mode))) {
1018
+ throw new CouncilConfigError(
1019
+ `\`${scope}.mode\`=${_pyRepr(mode)} not in ${_sortedListRepr(_VALID_CHAIRMAN_MODES)}.`,
1020
+ );
1021
+ }
1022
+ let member: string | null = null;
1023
+ const rawMember = _get(d, 'member', null);
1024
+ if (rawMember !== null && rawMember !== undefined) {
1025
+ if (!_isStr(rawMember)) {
1026
+ throw new CouncilConfigError(`\`${scope}.member\` must be a string.`);
1027
+ }
1028
+ member = rawMember;
1029
+ }
1030
+ if (mode === 'member') {
1031
+ if (member === null) {
1032
+ throw new CouncilConfigError(
1033
+ `\`${scope}.mode\` is 'member' but \`${scope}.member\` is unset.`,
1034
+ );
1035
+ }
1036
+ const bound = members.get(member);
1037
+ if (bound === undefined) {
1038
+ throw new CouncilConfigError(
1039
+ `\`${scope}.member\`=${_pyReprStr(member)}: no such member in the \`members\` block.`,
1040
+ );
1041
+ }
1042
+ if (!bound.enabled) {
1043
+ throw new CouncilConfigError(
1044
+ `\`${scope}.member\`=${_pyReprStr(member)}: member exists but is disabled.`,
1045
+ );
1046
+ }
1047
+ }
1048
+ return { mode, member };
1049
+ }
1050
+
1051
+ /** `ai_council.debate_gates` (Phase 3). `{enabled}`, default false. */
1052
+ function _build_debate_gates(d: Dict, scope: string): DebateGatesConfig {
1053
+ if (!_isDict(d)) {
1054
+ throw new CouncilConfigError(`\`${scope}\` must be a mapping.`);
1055
+ }
1056
+ const enabled = _get(d, 'enabled', false);
1057
+ if (!_isBool(enabled)) {
1058
+ throw new CouncilConfigError(`\`${scope}.enabled\` must be a bool.`);
1059
+ }
1060
+ return { enabled: Boolean(enabled) };
1061
+ }
1062
+
1063
+ /** `ai_council.restate` (Phase 3). `{enabled}`, default false. */
1064
+ function _build_restate(d: Dict, scope: string): RestateConfig {
1065
+ if (!_isDict(d)) {
1066
+ throw new CouncilConfigError(`\`${scope}\` must be a mapping.`);
1067
+ }
1068
+ const enabled = _get(d, 'enabled', false);
1069
+ if (!_isBool(enabled)) {
1070
+ throw new CouncilConfigError(`\`${scope}.enabled\` must be a bool.`);
1071
+ }
1072
+ return { enabled: Boolean(enabled) };
1073
+ }
1074
+
930
1075
  const _VALID_RESOLUTION_MODES: ReadonlySet<string> = new Set([
931
1076
  'agent',
932
1077
  'council',
@@ -1529,6 +1674,17 @@ function _build_member(
1529
1674
  `(got ${_pyTypeName(participate_raw)}).`,
1530
1675
  );
1531
1676
  }
1677
+ const tier_raw = _get(cfg, 'tier', null);
1678
+ let tier: number | null = null;
1679
+ if (tier_raw !== null && tier_raw !== undefined) {
1680
+ if (typeof tier_raw !== 'number' || !Number.isInteger(tier_raw) || tier_raw < 1) {
1681
+ throw new CouncilConfigError(
1682
+ `members.${name}.tier must be an integer >= 1 when set ` +
1683
+ `(got ${_pyRepr(tier_raw)}).`,
1684
+ );
1685
+ }
1686
+ tier = tier_raw;
1687
+ }
1532
1688
  return {
1533
1689
  name,
1534
1690
  enabled: member_enabled,
@@ -1538,6 +1694,7 @@ function _build_member(
1538
1694
  binary: (binary as string | null) ?? null,
1539
1695
  model_ladder: ladder,
1540
1696
  participate_low_impact: participate_raw,
1697
+ tier,
1541
1698
  };
1542
1699
  }
1543
1700
 
@@ -0,0 +1,89 @@
1
+ // Debate enforcement-gate detectors (road-to-opt-council-deliberation Phase 3).
2
+ //
3
+ // Pure, deterministic checks on the debate path — no LLM call. They decide
4
+ // WHETHER a bounded repair re-prompt is warranted; the re-prompt DISPATCH (one
5
+ // billable call per member per round, under a hard cap) and its auto-fire-vs-
6
+ // confirm POLICY live in the run path and are gated behind
7
+ // `ai_council.debate_gates.enabled` + the /council:design policy decision.
8
+ //
9
+ // The anti-conformity directive (the prompt-level half of the gates) is in
10
+ // `prompts.ts` (`ANTI_CONFORMITY_DIRECTIVE`) and wired into the debate augmenter.
11
+
12
+ import { jaccardSimilarity } from '../_lib/text_similarity.js';
13
+
14
+ /** Near-duplicate bar: reuse the existing shared MERGE-level Jaccard threshold. */
15
+ export const NOVELTY_DUP_THRESHOLD = 0.8;
16
+
17
+ /** Minimum distinct dissenting members for the dissent quota to be satisfied. */
18
+ export const DISSENT_QUOTA = 2;
19
+
20
+ // Objection markers — the engine has no dissent parser (existing "dissent" is
21
+ // LLM-score-derived, not text-marker-derived), so the marker is defined here.
22
+ const _OBJECTION_RE =
23
+ /\b(disagree|disagrees|object|objection|reject|however|but\b|flaw|flawed|wrong|counter|counter-position|dissent|contradict|refute)\b/i;
24
+
25
+ /**
26
+ * Novelty gate: a member's round-N reply is a **near-duplicate** of its own
27
+ * round-(N-1) reply when their normalised token similarity meets the threshold.
28
+ * Token-set Jaccard is order-independent and lowercased, so this is the
29
+ * "normalised near-duplicate" the gate targets. A duplicate → the member added
30
+ * nothing this round and warrants one targeted re-prompt.
31
+ */
32
+ export function is_near_duplicate(
33
+ prevText: string,
34
+ currText: string,
35
+ threshold = NOVELTY_DUP_THRESHOLD,
36
+ ): boolean {
37
+ if (prevText.trim().length === 0 || currText.trim().length === 0) {
38
+ return false;
39
+ }
40
+ return jaccardSimilarity(prevText, currText) >= threshold;
41
+ }
42
+
43
+ /**
44
+ * Dissent quota: at least `quota` members voice an explicit objection (carry an
45
+ * objection marker). Below quota, the round has collapsed toward agreement and
46
+ * warrants one targeted dissent re-prompt to the most-recently-converged member.
47
+ * Empty / error replies do not count.
48
+ */
49
+ export function count_dissenters(texts: readonly string[]): number {
50
+ let n = 0;
51
+ for (const t of texts) {
52
+ if (t.trim().length > 0 && _OBJECTION_RE.test(t)) {
53
+ n += 1;
54
+ }
55
+ }
56
+ return n;
57
+ }
58
+
59
+ /** True when the dissent quota is satisfied (≥ `quota` members objected). */
60
+ export function dissent_quota_met(texts: readonly string[], quota = DISSENT_QUOTA): boolean {
61
+ return count_dissenters(texts) >= quota;
62
+ }
63
+
64
+ /** What the run path does when a gate warrants a repair re-prompt. */
65
+ export type RepairAction = 'fire' | 'confirm' | 'skip';
66
+
67
+ /**
68
+ * Repair-call policy — decided by the contested-design council pass
69
+ * (anthropic/claude-sonnet-4-5 + openai/gpt-4o, 2-round debate, 2026-07-12):
70
+ * a cost estimate is an **upper bound, not a spend commitment**, and repairs
71
+ * are failure-mode responses rather than the mainline path. So:
72
+ *
73
+ * - unattended runs (`--auto-continue`) **auto-fire** under the hard cap
74
+ * (≤ 1 repair per member per round);
75
+ * - interactive runs get a **one-line confirm** before each repair call;
76
+ * - the cap is absolute: a member that already received its repair this round
77
+ * is never re-prompted again (`skip`), regardless of mode;
78
+ * - manual-transport members follow the same policy (deliberation-flow impact,
79
+ * not cost, governs — the repair "call" is one more paste block).
80
+ */
81
+ export function repair_action(opts: {
82
+ auto_continue: boolean;
83
+ already_repaired: boolean;
84
+ }): RepairAction {
85
+ if (opts.already_repaired) {
86
+ return 'skip';
87
+ }
88
+ return opts.auto_continue ? 'fire' : 'confirm';
89
+ }
@@ -56,6 +56,8 @@ import type { ProjectContext } from './project_context.js';
56
56
  import type { AdvisorPlan } from './advisors.js';
57
57
  import {
58
58
  advisor_system_prompt,
59
+ ANTI_CONFORMITY_DIRECTIVE,
60
+ STANCE_LINE_CONTRACT,
59
61
  build_extraction_user_prompt,
60
62
  build_peer_review_user_prompt,
61
63
  build_scoring_user_prompt,
@@ -63,6 +65,7 @@ import {
63
65
  synthesis_template,
64
66
  system_prompt_for,
65
67
  } from './prompts.js';
68
+ import { render_vote_tally, tally_stances } from './stance_tally.js';
66
69
 
67
70
  // ── Python-format / stdlib parity helpers ────────────────────────────────
68
71
  //
@@ -409,6 +412,13 @@ export interface ConsultOptions {
409
412
  | ((round_idx: number, responses: CouncilResponse[]) => void)
410
413
  | null;
411
414
  advisor_plans?: Map<string, AdvisorPlan> | null;
415
+ /**
416
+ * Phase 1 (stance tally): when true, the FINAL round's user prompt appends
417
+ * the mandatory `STANCE:` closing-line contract so `stance_tally.ts` can
418
+ * tally option-level verdicts deterministically. Default `false` → every
419
+ * round's prompt is byte-identical to today.
420
+ */
421
+ stance_tally?: boolean;
412
422
  }
413
423
 
414
424
  /**
@@ -467,13 +477,23 @@ export function consult(
467
477
  let last_results: CouncilResponse[] = [];
468
478
  let current_user_prompt = question.user_prompt;
469
479
 
480
+ const stance_tally = opts.stance_tally ?? false;
470
481
  for (let round_idx = 0; round_idx < rounds; round_idx++) {
482
+ // Phase 1: the FINAL round carries the mandatory STANCE closing-line
483
+ // contract when the tally is enabled. Off (default) → the original
484
+ // question object flows through untouched, byte-identical to today.
485
+ const is_final = round_idx === rounds - 1;
486
+ const base_prompt = round_idx === 0 ? question.user_prompt : current_user_prompt;
487
+ const prompt_for_round =
488
+ is_final && stance_tally
489
+ ? `${base_prompt}\n\n---\n\n${STANCE_LINE_CONTRACT}`
490
+ : base_prompt;
471
491
  const round_question =
472
- round_idx === 0
492
+ round_idx === 0 && prompt_for_round === question.user_prompt
473
493
  ? question
474
494
  : new CouncilQuestion({
475
495
  mode: question.mode,
476
- user_prompt: current_user_prompt,
496
+ user_prompt: prompt_for_round,
477
497
  max_tokens: question.max_tokens,
478
498
  });
479
499
  last_results = _run_round(members, round_question, resolvedBudget, spent, {
@@ -838,6 +858,9 @@ function _augment_for_debate_round(
838
858
  original_prompt: string,
839
859
  prior_responses: CouncilResponse[],
840
860
  next_round_number: number,
861
+ // Phase 3: when the debate gates are enabled, prepend the anti-conformity
862
+ // directive. Default `false` keeps the prompt byte-identical to today.
863
+ anti_conformity = false,
841
864
  ): string {
842
865
  const blocks: string[] = [];
843
866
  let label_idx = 0;
@@ -853,6 +876,7 @@ function _augment_for_debate_round(
853
876
  return original_prompt;
854
877
  }
855
878
  const prior_block = blocks.join('\n\n');
879
+ const anti_conformity_block = anti_conformity ? `${ANTI_CONFORMITY_DIRECTIVE}\n\n` : '';
856
880
  return (
857
881
  `${original_prompt}\n\n` +
858
882
  `---\n\n` +
@@ -861,6 +885,7 @@ function _augment_for_debate_round(
861
885
  `debate. Below are anonymised positions from independent\n` +
862
886
  `reviewers in the previous round. You do NOT know which model\n` +
863
887
  `produced which position.\n\n` +
888
+ anti_conformity_block +
864
889
  `Identify the SINGLE strongest opposing position and write a\n` +
865
890
  `rebuttal addressed at its strongest steel-manned form. Do NOT\n` +
866
891
  `search for common ground — name the load-bearing flaw the\n` +
@@ -883,6 +908,12 @@ export interface RunDebateOptions {
883
908
  on_continue?: DebateContinuePrompt | null;
884
909
  advisor_plans?: Map<string, AdvisorPlan> | null;
885
910
  seed_round_1?: CouncilResponse[] | null;
911
+ /**
912
+ * Phase 3: when true (from `ai_council.debate_gates.enabled`), round-2+
913
+ * prompts carry the anti-conformity directive. Default `false` → the debate
914
+ * prompt is byte-identical to today.
915
+ */
916
+ debate_gates?: boolean;
886
917
  }
887
918
 
888
919
  /**
@@ -981,6 +1012,7 @@ export function run_debate(
981
1012
  question.user_prompt,
982
1013
  results,
983
1014
  round_number + 1,
1015
+ opts.debate_gates ?? false,
984
1016
  );
985
1017
  // Hard-cap + continue-prompt gating before kicking off N+1.
986
1018
  let next_round_usd: number;
@@ -1433,6 +1465,18 @@ export interface RenderOptions {
1433
1465
  consensus?: ConsensusResult | null;
1434
1466
  peer_review?: PeerReviewResult | null;
1435
1467
  explain_confidence?: boolean | null;
1468
+ /**
1469
+ * Phase 1 (stance tally): when true, a `### Vote Tally` block — computed
1470
+ * deterministically from the final-round response texts — is rendered
1471
+ * before the Convergence / Divergence slot. Default `false` → byte-identical.
1472
+ */
1473
+ stance_tally?: boolean;
1474
+ /**
1475
+ * Phase 2 (chairman): when a chairman-authored synthesis exists (persisted
1476
+ * in the payload), it REPLACES the template in the Convergence/Divergence
1477
+ * slot, prefixed by the visible annotation. Absent/null → byte-identical.
1478
+ */
1479
+ chairman?: { member: string | null; annotation: string; text: string | null } | null;
1436
1480
  }
1437
1481
 
1438
1482
  /**
@@ -1505,6 +1549,25 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
1505
1549
  } else {
1506
1550
  body = '*to be summarised by the host agent*';
1507
1551
  }
1552
+ const chairman = opts.chairman ?? null;
1553
+ if (chairman !== null && chairman.text !== null && chairman.text.trim() !== '') {
1554
+ // Chairman-authored synthesis replaces the template; the annotation is
1555
+ // always visible (never a silent substitution).
1556
+ body = `_${chairman.annotation}_\n\n${chairman.text.trim()}`;
1557
+ } else if (chairman !== null) {
1558
+ body = `_${chairman.annotation}_\n\n${body}`;
1559
+ }
1560
+ if (opts.stance_tally === true) {
1561
+ // Phase 1: deterministic option-level tally from the final-round
1562
+ // response texts (the STANCE closing lines live IN the texts, so the
1563
+ // render is a pure projection — no config or transport needed here).
1564
+ const tally = tally_stances(
1565
+ responses
1566
+ .filter((r) => !r.error)
1567
+ .map((r) => ({ member: `${r.provider}:${r.model}`, text: r.text })),
1568
+ );
1569
+ blocks.push(render_vote_tally(tally));
1570
+ }
1508
1571
  blocks.push(`## Convergence / Divergence\n\n${body}`);
1509
1572
  return blocks.join('\n\n---\n\n');
1510
1573
  }
@@ -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';