@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +67 -149
- package/README.md +6 -6
- package/dist/agent-src/commands/challenge-me/vision.md +121 -32
- package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
- package/dist/agent-src/commands/commit/in-chunks.md +1 -1
- package/dist/agent-src/commands/commit.md +2 -1
- package/dist/agent-src/commands/cost/report.md +2 -1
- package/dist/agent-src/commands/feature/dev.md +4 -4
- package/dist/agent-src/commands/humanize.md +101 -0
- package/dist/agent-src/commands/jira-ticket.md +2 -2
- package/dist/agent-src/commands/memory/load.md +15 -0
- package/dist/agent-src/commands/optimize/project.md +12 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -11
- package/dist/agent-src/commands/tests/create.md +1 -1
- package/dist/agent-src/commands/worktree/cleanup.md +22 -17
- package/dist/agent-src/commands/worktree/create.md +3 -1
- package/dist/agent-src/commands/worktree/status.md +12 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
- package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
- package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/autonomous-execution.md +7 -37
- package/dist/agent-src/rules/broken-access-control.md +7 -45
- package/dist/agent-src/rules/code-comment-discipline.md +64 -0
- package/dist/agent-src/rules/context-hygiene.md +14 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/delegation-policy.md +10 -8
- package/dist/agent-src/rules/design-fidelity.md +4 -0
- package/dist/agent-src/rules/git-history-discipline.md +4 -79
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/legal-safety-floor.md +11 -55
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
- package/dist/agent-src/rules/think-before-action.md +10 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/rules/verify-before-complete.md +12 -0
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
- package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
- package/dist/agent-src/skills/api-design/SKILL.md +7 -5
- package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
- package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
- package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
- package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
- package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
- package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
- package/dist/agent-src/skills/design-review/SKILL.md +23 -2
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
- package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
- package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
- package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
- package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
- package/dist/agent-src/skills/laravel/SKILL.md +14 -1
- package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
- package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
- package/dist/agent-src/skills/mcp/SKILL.md +1 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
- package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/skills/performance/SKILL.md +1 -1
- package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
- package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
- package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
- package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
- package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
- package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
- package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
- package/dist/agent-src/templates/agent-settings.md +3 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/agent-src/templates/roadmaps.md +14 -0
- package/dist/cli/commands/recordTriggerEval.js +4 -14
- package/dist/cli/commands/recordTriggerEval.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +157 -88
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +14 -9
- package/dist/install/install.mjs +12 -7
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
- package/dist/scripts/condense.js +12 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +5 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +54 -5
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +90 -0
- package/docs/catalog.md +8 -5
- package/docs/command-flows.md +2 -1
- package/docs/contracts/ai-council-config.md +97 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/evidence-based-pruning.md +1 -0
- package/docs/contracts/hook-architecture-v1.md +25 -0
- package/docs/contracts/kernel-membership.md +35 -0
- package/docs/contracts/measurement-baseline.md +1 -0
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/contracts/release-sizing.md +22 -0
- package/docs/contracts/write-engine.md +38 -1
- package/docs/customization.md +12 -0
- package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
- package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
- package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
- package/docs/decisions/ADR-028-root-layout.md +1 -1
- package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
- package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
- package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
- package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
- package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
- package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
- package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
- package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
- package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
- package/docs/decisions/INDEX.md +13 -9
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/cross-model-parity-eval.md +82 -0
- package/docs/design/length-neutral-judge-rerun.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +191 -1
- package/docs/guidelines/design-antipatterns.md +20 -0
- package/docs/guidelines/php/php-coding-patterns.md +12 -2
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +32 -15
- package/docs/wedge/production-validator/production-validator.md +27 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +38 -9
- package/src/scripts/_cli/cmd_doctor.ts +129 -1
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
- package/src/scripts/_lib/knowledge_global.ts +50 -2
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
- package/src/scripts/_lib/lexical_index.ts +15 -2
- package/src/scripts/_lib/orchestration_record.ts +14 -0
- package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
- package/src/scripts/ai_council/chairman.ts +90 -0
- package/src/scripts/ai_council/clients.ts +18 -3
- package/src/scripts/ai_council/config.ts +157 -0
- package/src/scripts/ai_council/debate_gates.ts +89 -0
- package/src/scripts/ai_council/orchestrator.ts +252 -2
- package/src/scripts/ai_council/prompts.ts +121 -11
- package/src/scripts/ai_council/replay.ts +17 -0
- package/src/scripts/ai_council/stance_tally.ts +184 -0
- package/src/scripts/ai_tells_rules.ts +433 -0
- package/src/scripts/bench_humanizer_eval.ts +240 -0
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_discovery_manifest.ts +6 -0
- package/src/scripts/build_proof.ts +30 -9
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_claims.ts +40 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/check_rule_invariants.ts +10 -3
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/consumer_matrix.ts +292 -14
- package/src/scripts/council_cli.ts +203 -1
- package/src/scripts/detect_ai_tells.ts +312 -0
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/install.ts +12 -0
- package/src/scripts/knowledge_global_cli.ts +235 -46
- package/src/scripts/lint_framework_leakage.ts +88 -7
- package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
- package/src/scripts/lint_knowledge_scale.ts +30 -0
- package/src/scripts/lint_originality_shingles.ts +210 -0
- package/src/scripts/orchestration_record.ts +1 -0
- package/src/scripts/rule_backlinks.ts +0 -0
- package/src/scripts/schemas/agent-settings.schema.json +11 -0
- package/src/scripts/self_review_gate.ts +41 -15
- package/src/scripts/skill_eval_coverage.ts +71 -3
- package/src/scripts/skill_linter.ts +1 -0
- package/src/scripts/trigger_eval_rotation.ts +344 -0
- package/src/scripts/update_counts.ts +5 -17
- package/src/scripts/worktree_cleanup_check.ts +263 -0
- package/src/server/schemas/settings.ts +8 -3
- package/src/scripts/analysis_freshness.ts +0 -362
- 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:
|
|
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
|
}
|