@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
@@ -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,8 @@ import {
63
65
  synthesis_template,
64
66
  system_prompt_for,
65
67
  } from './prompts.js';
68
+ import { count_dissenters, dissent_quota_met, is_near_duplicate } from './debate_gates.js';
69
+ import { parse_stance_line, render_vote_tally, tally_stances } from './stance_tally.js';
66
70
 
67
71
  // ── Python-format / stdlib parity helpers ────────────────────────────────
68
72
  //
@@ -409,6 +413,21 @@ export interface ConsultOptions {
409
413
  | ((round_idx: number, responses: CouncilResponse[]) => void)
410
414
  | null;
411
415
  advisor_plans?: Map<string, AdvisorPlan> | null;
416
+ /**
417
+ * Phase 1 (stance tally): when true, the FINAL round's user prompt appends
418
+ * the mandatory `STANCE:` closing-line contract so `stance_tally.ts` can
419
+ * tally option-level verdicts deterministically. Default `false` → every
420
+ * round's prompt is byte-identical to today.
421
+ */
422
+ stance_tally?: boolean;
423
+ /**
424
+ * Phase 1 stance-repair transport (2026-07-12 council policy): confirm per
425
+ * member in interactive runs; `null`/absent → a missing/unparseable stance
426
+ * stays a repair-marker in the tally, never an unplanned billable call.
427
+ */
428
+ on_stance_repair?: ((member: string) => boolean) | null;
429
+ /** Receives each stance-repair response so the caller can account its cost. */
430
+ on_stance_repair_result?: ((r: CouncilResponse) => void) | null;
412
431
  }
413
432
 
414
433
  /**
@@ -467,13 +486,23 @@ export function consult(
467
486
  let last_results: CouncilResponse[] = [];
468
487
  let current_user_prompt = question.user_prompt;
469
488
 
489
+ const stance_tally = opts.stance_tally ?? false;
470
490
  for (let round_idx = 0; round_idx < rounds; round_idx++) {
491
+ // Phase 1: the FINAL round carries the mandatory STANCE closing-line
492
+ // contract when the tally is enabled. Off (default) → the original
493
+ // question object flows through untouched, byte-identical to today.
494
+ const is_final = round_idx === rounds - 1;
495
+ const base_prompt = round_idx === 0 ? question.user_prompt : current_user_prompt;
496
+ const prompt_for_round =
497
+ is_final && stance_tally
498
+ ? `${base_prompt}\n\n---\n\n${STANCE_LINE_CONTRACT}`
499
+ : base_prompt;
471
500
  const round_question =
472
- round_idx === 0
501
+ round_idx === 0 && prompt_for_round === question.user_prompt
473
502
  ? question
474
503
  : new CouncilQuestion({
475
504
  mode: question.mode,
476
- user_prompt: current_user_prompt,
505
+ user_prompt: prompt_for_round,
477
506
  max_tokens: question.max_tokens,
478
507
  });
479
508
  last_results = _run_round(members, round_question, resolvedBudget, spent, {
@@ -495,6 +524,46 @@ export function consult(
495
524
  }
496
525
  }
497
526
 
527
+ // Phase 1 stance repair: one bounded stance-line-only re-prompt per member
528
+ // whose final reply lacks a parseable STANCE line — dispatched only through
529
+ // the on_stance_repair transport, reusing _run_round (spend gate + ledger +
530
+ // stamping unchanged). The repaired stance line is APPENDED to the member's
531
+ // final text so the deterministic tally can read it.
532
+ const on_stance_repair = opts.on_stance_repair ?? null;
533
+ if (stance_tally && on_stance_repair !== null) {
534
+ for (let i = 0; i < last_results.length; i++) {
535
+ const r = last_results[i] as CouncilResponse;
536
+ if (r.error !== null || r.text.trim() === '' || parse_stance_line(r.text) !== null) {
537
+ continue;
538
+ }
539
+ const member = members[i];
540
+ if (member === undefined || !on_stance_repair(member.name)) {
541
+ continue;
542
+ }
543
+ const repairQ = new CouncilQuestion({
544
+ mode: question.mode,
545
+ user_prompt:
546
+ `Your reply is missing the mandatory closing stance line. ` +
547
+ `Reply with ONLY that single line for your final position.\n\n${STANCE_LINE_CONTRACT}`,
548
+ max_tokens: question.max_tokens,
549
+ });
550
+ const rr = _run_round([member], repairQ, resolvedBudget, spent, {
551
+ table,
552
+ on_overrun,
553
+ project,
554
+ original_ask,
555
+ advisor_plans,
556
+ });
557
+ const rep = rr[0];
558
+ if (rep !== undefined) {
559
+ opts.on_stance_repair_result?.(rep);
560
+ if (rep.error === null && parse_stance_line(rep.text) !== null) {
561
+ r.text = `${r.text.trim()}\n\n${rep.text.trim()}`;
562
+ }
563
+ }
564
+ }
565
+ }
566
+
498
567
  return last_results;
499
568
  }
500
569
 
@@ -838,6 +907,9 @@ function _augment_for_debate_round(
838
907
  original_prompt: string,
839
908
  prior_responses: CouncilResponse[],
840
909
  next_round_number: number,
910
+ // Phase 3: when the debate gates are enabled, prepend the anti-conformity
911
+ // directive. Default `false` keeps the prompt byte-identical to today.
912
+ anti_conformity = false,
841
913
  ): string {
842
914
  const blocks: string[] = [];
843
915
  let label_idx = 0;
@@ -853,6 +925,7 @@ function _augment_for_debate_round(
853
925
  return original_prompt;
854
926
  }
855
927
  const prior_block = blocks.join('\n\n');
928
+ const anti_conformity_block = anti_conformity ? `${ANTI_CONFORMITY_DIRECTIVE}\n\n` : '';
856
929
  return (
857
930
  `${original_prompt}\n\n` +
858
931
  `---\n\n` +
@@ -861,6 +934,7 @@ function _augment_for_debate_round(
861
934
  `debate. Below are anonymised positions from independent\n` +
862
935
  `reviewers in the previous round. You do NOT know which model\n` +
863
936
  `produced which position.\n\n` +
937
+ anti_conformity_block +
864
938
  `Identify the SINGLE strongest opposing position and write a\n` +
865
939
  `rebuttal addressed at its strongest steel-manned form. Do NOT\n` +
866
940
  `search for common ground — name the load-bearing flaw the\n` +
@@ -870,6 +944,90 @@ function _augment_for_debate_round(
870
944
  );
871
945
  }
872
946
 
947
+ /**
948
+ * Phase 3 post-round enforcement (2026-07-12 council policy): deterministic
949
+ * detectors flag members whose round added nothing (novelty near-duplicate of
950
+ * their own prior round) or when the round collapsed toward agreement (dissent
951
+ * quota unmet → the most-recently-converged member). At most ONE bounded repair
952
+ * re-prompt per member per round, dispatched only through the `on_repair`
953
+ * transport (confirm-interactive / auto-fire under --auto-continue); the repair
954
+ * call reuses `_run_round`, so the projected-spend gate, ledger, and metadata
955
+ * stamping apply unchanged. A successful repair REPLACES the member's entry so
956
+ * downstream rounds/persistence see the repaired reply.
957
+ */
958
+ function _run_debate_gate_repairs(
959
+ members: ExternalAIClient[],
960
+ results: CouncilResponse[],
961
+ prior: CouncilResponse[],
962
+ round_question: CouncilQuestion,
963
+ budget: CostBudget,
964
+ spent: Spent,
965
+ round_opts: {
966
+ table: PriceTable | null;
967
+ on_overrun: OnOverrunCallback | null;
968
+ project: ProjectContext | null;
969
+ original_ask: string;
970
+ advisor_plans: Map<string, AdvisorPlan> | null;
971
+ },
972
+ on_repair: ((member: string, reason: string) => boolean) | null,
973
+ ): CouncilResponse[] {
974
+ const flagged: Array<{ idx: number; reason: string }> = [];
975
+ const okText = (r: CouncilResponse | undefined): string =>
976
+ r !== undefined && r.error === null ? r.text : '';
977
+ for (let i = 0; i < results.length; i++) {
978
+ const curr = okText(results[i]);
979
+ const prev = okText(prior[i]);
980
+ if (curr !== '' && prev !== '' && is_near_duplicate(prev, curr)) {
981
+ flagged.push({ idx: i, reason: 'novelty: near-duplicate of your own prior round' });
982
+ }
983
+ }
984
+ const texts = results.map((r) => okText(r));
985
+ if (!dissent_quota_met(texts.filter((t) => t !== ''))) {
986
+ // Most-recently-converged heuristic: the LAST member with a real reply
987
+ // that carries no objection marker.
988
+ for (let i = results.length - 1; i >= 0; i--) {
989
+ const t = okText(results[i]);
990
+ if (t !== '' && count_dissenters([t]) === 0 && !flagged.some((f) => f.idx === i)) {
991
+ flagged.push({ idx: i, reason: 'dissent quota unmet: state a genuine objection or confirm with a named reason' });
992
+ break;
993
+ }
994
+ }
995
+ }
996
+ if (flagged.length === 0 || on_repair === null) {
997
+ return results;
998
+ }
999
+ const repaired = new Set<number>();
1000
+ const out = [...results];
1001
+ for (const f of flagged) {
1002
+ if (repaired.has(f.idx)) {
1003
+ continue; // hard cap: <= 1 repair per member per round
1004
+ }
1005
+ const member = members[f.idx];
1006
+ if (member === undefined) {
1007
+ continue;
1008
+ }
1009
+ if (!on_repair(member.name, f.reason)) {
1010
+ continue;
1011
+ }
1012
+ repaired.add(f.idx);
1013
+ const repairQ = new CouncilQuestion({
1014
+ mode: round_question.mode,
1015
+ user_prompt:
1016
+ `${round_question.user_prompt}\n\n---\n\n` +
1017
+ `REPAIR RE-PROMPT (${f.reason}). Your previous reply this round did not ` +
1018
+ `advance the debate. Respond again: either a genuinely updated position ` +
1019
+ `naming the specific flaw that moved you, or an explicit defense with NEW evidence.`,
1020
+ max_tokens: round_question.max_tokens,
1021
+ });
1022
+ const rr = _run_round([member], repairQ, budget, spent, round_opts);
1023
+ const r0 = rr[0];
1024
+ if (r0 !== undefined && r0.error === null && r0.text.trim() !== '') {
1025
+ out[f.idx] = r0;
1026
+ }
1027
+ }
1028
+ return out;
1029
+ }
1030
+
873
1031
  export interface RunDebateOptions {
874
1032
  budget?: CostBudget | null;
875
1033
  table?: PriceTable | null;
@@ -883,6 +1041,28 @@ export interface RunDebateOptions {
883
1041
  on_continue?: DebateContinuePrompt | null;
884
1042
  advisor_plans?: Map<string, AdvisorPlan> | null;
885
1043
  seed_round_1?: CouncilResponse[] | null;
1044
+ /**
1045
+ * Phase 3: when true (from `ai_council.debate_gates.enabled`), round-2+
1046
+ * prompts carry the anti-conformity directive. Default `false` → the debate
1047
+ * prompt is byte-identical to today.
1048
+ */
1049
+ debate_gates?: boolean;
1050
+ /**
1051
+ * Phase 3 repair transport, per the 2026-07-12 council policy
1052
+ * (`debate_gates.repair_action`): the CLI passes `() => true` under
1053
+ * `--auto-continue` (auto-fire under the cap) or an interactive one-line
1054
+ * confirm otherwise. `null`/absent → gates detect but never dispatch
1055
+ * (no unplanned spend without an explicit transport).
1056
+ */
1057
+ on_repair?: ((member: string, reason: string) => boolean) | null;
1058
+ /**
1059
+ * Phase 3 restate pass (`ai_council.restate.enabled` / `--restate`): a
1060
+ * pre-round-1 billable call per member collecting a <=50-word restatement
1061
+ * + one alternative framing. Default off -> no extra calls, byte-identical.
1062
+ */
1063
+ restate?: boolean;
1064
+ /** Receives the restate responses (rendering, divergence flags, cost). */
1065
+ on_restate?: ((responses: CouncilResponse[]) => void) | null;
886
1066
  }
887
1067
 
888
1068
  /**
@@ -944,6 +1124,27 @@ export function run_debate(
944
1124
  const all_rounds: CouncilResponse[][] = [];
945
1125
  let current_user_prompt = question.user_prompt;
946
1126
 
1127
+ // Phase 3 restate: one pre-round-1 call per member. Runs BEFORE any debate
1128
+ // spend so a diverging restatement can be flagged before round-2 cost.
1129
+ if (opts.restate === true) {
1130
+ const restateQ = new CouncilQuestion({
1131
+ mode: question.mode,
1132
+ user_prompt:
1133
+ `Before the debate begins: RESTATE the question below in at most 50 words, ` +
1134
+ `then offer ONE alternative framing of it. Do not argue a position yet.\n\n---\n\n` +
1135
+ `${question.user_prompt}`,
1136
+ max_tokens: question.max_tokens,
1137
+ });
1138
+ const restated = _run_round(members, restateQ, budget, spent, {
1139
+ table,
1140
+ on_overrun,
1141
+ project,
1142
+ original_ask,
1143
+ advisor_plans,
1144
+ });
1145
+ opts.on_restate?.(restated);
1146
+ }
1147
+
947
1148
  for (let round_idx = 0; round_idx < max_rounds; round_idx++) {
948
1149
  const round_number = round_idx + 1;
949
1150
  let results: CouncilResponse[];
@@ -969,6 +1170,23 @@ export function run_debate(
969
1170
  });
970
1171
  }
971
1172
 
1173
+ if (opts.debate_gates === true && round_idx > 0 && all_rounds.length > 0) {
1174
+ results = _run_debate_gate_repairs(
1175
+ members,
1176
+ results,
1177
+ all_rounds[all_rounds.length - 1] as CouncilResponse[],
1178
+ // round_idx > 0 here, so current_user_prompt IS this round's prompt.
1179
+ new CouncilQuestion({
1180
+ mode: question.mode,
1181
+ user_prompt: current_user_prompt,
1182
+ max_tokens: question.max_tokens,
1183
+ }),
1184
+ budget,
1185
+ spent,
1186
+ { table, on_overrun, project, original_ask, advisor_plans },
1187
+ opts.on_repair ?? null,
1188
+ );
1189
+ }
972
1190
  all_rounds.push(results);
973
1191
  if (on_round_complete !== null) {
974
1192
  on_round_complete(round_number, results);
@@ -981,6 +1199,7 @@ export function run_debate(
981
1199
  question.user_prompt,
982
1200
  results,
983
1201
  round_number + 1,
1202
+ opts.debate_gates ?? false,
984
1203
  );
985
1204
  // Hard-cap + continue-prompt gating before kicking off N+1.
986
1205
  let next_round_usd: number;
@@ -1433,6 +1652,18 @@ export interface RenderOptions {
1433
1652
  consensus?: ConsensusResult | null;
1434
1653
  peer_review?: PeerReviewResult | null;
1435
1654
  explain_confidence?: boolean | null;
1655
+ /**
1656
+ * Phase 1 (stance tally): when true, a `### Vote Tally` block — computed
1657
+ * deterministically from the final-round response texts — is rendered
1658
+ * before the Convergence / Divergence slot. Default `false` → byte-identical.
1659
+ */
1660
+ stance_tally?: boolean;
1661
+ /**
1662
+ * Phase 2 (chairman): when a chairman-authored synthesis exists (persisted
1663
+ * in the payload), it REPLACES the template in the Convergence/Divergence
1664
+ * slot, prefixed by the visible annotation. Absent/null → byte-identical.
1665
+ */
1666
+ chairman?: { member: string | null; annotation: string; text: string | null } | null;
1436
1667
  }
1437
1668
 
1438
1669
  /**
@@ -1505,6 +1736,25 @@ export function render(responses: CouncilResponse[], opts: RenderOptions = {}):
1505
1736
  } else {
1506
1737
  body = '*to be summarised by the host agent*';
1507
1738
  }
1739
+ const chairman = opts.chairman ?? null;
1740
+ if (chairman !== null && chairman.text !== null && chairman.text.trim() !== '') {
1741
+ // Chairman-authored synthesis replaces the template; the annotation is
1742
+ // always visible (never a silent substitution).
1743
+ body = `_${chairman.annotation}_\n\n${chairman.text.trim()}`;
1744
+ } else if (chairman !== null) {
1745
+ body = `_${chairman.annotation}_\n\n${body}`;
1746
+ }
1747
+ if (opts.stance_tally === true) {
1748
+ // Phase 1: deterministic option-level tally from the final-round
1749
+ // response texts (the STANCE closing lines live IN the texts, so the
1750
+ // render is a pure projection — no config or transport needed here).
1751
+ const tally = tally_stances(
1752
+ responses
1753
+ .filter((r) => !r.error)
1754
+ .map((r) => ({ member: `${r.provider}:${r.model}`, text: r.text })),
1755
+ );
1756
+ blocks.push(render_vote_tally(tally));
1757
+ }
1508
1758
  blocks.push(`## Convergence / Divergence\n\n${body}`);
1509
1759
  return blocks.join('\n\n---\n\n');
1510
1760
  }