popeye-cli 2.1.0 → 2.7.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/dist/adapters/gemini.d.ts +14 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +41 -6
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +14 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +42 -6
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +10 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +44 -5
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.js +1 -1
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +328 -21
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +25 -2
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +21 -6
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +55 -4
- package/dist/generators/doc-parser.js.map +1 -1
- package/dist/generators/templates/fullstack.js +1 -1
- package/dist/generators/templates/website-components.js +1 -1
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +4 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +17 -11
- package/dist/generators/templates/website-config.js.map +1 -1
- package/dist/generators/templates/website-conversion.js +1 -1
- package/dist/generators/templates/website-conversion.js.map +1 -1
- package/dist/generators/templates/website-landing.js +1 -1
- package/dist/generators/templates/website-landing.js.map +1 -1
- package/dist/generators/templates/website-layout.d.ts +36 -4
- package/dist/generators/templates/website-layout.d.ts.map +1 -1
- package/dist/generators/templates/website-layout.js +466 -23
- package/dist/generators/templates/website-layout.js.map +1 -1
- package/dist/generators/templates/website-pricing.js +1 -1
- package/dist/generators/templates/website-pricing.js.map +1 -1
- package/dist/generators/templates/website-sections.js +1 -1
- package/dist/generators/templates/website-sections.js.map +1 -1
- package/dist/generators/templates/website-seo.d.ts.map +1 -1
- package/dist/generators/templates/website-seo.js +4 -1
- package/dist/generators/templates/website-seo.js.map +1 -1
- package/dist/generators/templates/website.d.ts +1 -1
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +1 -1
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-ai.d.ts +52 -0
- package/dist/generators/website-content-ai.d.ts.map +1 -0
- package/dist/generators/website-content-ai.js +141 -0
- package/dist/generators/website-content-ai.js.map +1 -0
- package/dist/generators/website-content-scanner.d.ts +1 -1
- package/dist/generators/website-content-scanner.d.ts.map +1 -1
- package/dist/generators/website-content-scanner.js +98 -1
- package/dist/generators/website-content-scanner.js.map +1 -1
- package/dist/generators/website-context.d.ts +34 -1
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +131 -9
- package/dist/generators/website-context.js.map +1 -1
- package/dist/generators/website-debug.d.ts +12 -0
- package/dist/generators/website-debug.d.ts.map +1 -1
- package/dist/generators/website-debug.js +16 -0
- package/dist/generators/website-debug.js.map +1 -1
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +26 -4
- package/dist/generators/website.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts.map +1 -1
- package/dist/pipeline/artifact-manager.js +3 -0
- package/dist/pipeline/artifact-manager.js.map +1 -1
- package/dist/pipeline/auto-recovery.d.ts +56 -0
- package/dist/pipeline/auto-recovery.d.ts.map +1 -0
- package/dist/pipeline/auto-recovery.js +185 -0
- package/dist/pipeline/auto-recovery.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +39 -0
- package/dist/pipeline/change-request.d.ts.map +1 -1
- package/dist/pipeline/change-request.js +40 -1
- package/dist/pipeline/change-request.js.map +1 -1
- package/dist/pipeline/check-runner.d.ts +30 -1
- package/dist/pipeline/check-runner.d.ts.map +1 -1
- package/dist/pipeline/check-runner.js +122 -1
- package/dist/pipeline/check-runner.js.map +1 -1
- package/dist/pipeline/command-resolver.d.ts.map +1 -1
- package/dist/pipeline/command-resolver.js +33 -2
- package/dist/pipeline/command-resolver.js.map +1 -1
- package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
- package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
- package/dist/pipeline/consensus/arbitrator-query.js +70 -0
- package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
- package/dist/pipeline/consensus/consensus-runner.js +809 -35
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
- package/dist/pipeline/cr-lifecycle.d.ts +42 -0
- package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
- package/dist/pipeline/cr-lifecycle.js +89 -0
- package/dist/pipeline/cr-lifecycle.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +1 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -1
- package/dist/pipeline/gate-engine.js +27 -8
- package/dist/pipeline/gate-engine.js.map +1 -1
- package/dist/pipeline/migration.d.ts.map +1 -1
- package/dist/pipeline/migration.js +3 -26
- package/dist/pipeline/migration.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +311 -16
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
- package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
- package/dist/pipeline/phases/architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/architecture.js +5 -3
- package/dist/pipeline/phases/architecture.js.map +1 -1
- package/dist/pipeline/phases/audit.d.ts.map +1 -1
- package/dist/pipeline/phases/audit.js +5 -3
- package/dist/pipeline/phases/audit.js.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-architecture.js +10 -1
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-master-plan.js +10 -3
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
- package/dist/pipeline/phases/consensus-role-plans.js +10 -1
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
- package/dist/pipeline/phases/done.d.ts.map +1 -1
- package/dist/pipeline/phases/done.js +9 -4
- package/dist/pipeline/phases/done.js.map +1 -1
- package/dist/pipeline/phases/intake.d.ts +1 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -1
- package/dist/pipeline/phases/intake.js +56 -13
- package/dist/pipeline/phases/intake.js.map +1 -1
- package/dist/pipeline/phases/phase-context.d.ts +2 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
- package/dist/pipeline/phases/phase-context.js +3 -1
- package/dist/pipeline/phases/phase-context.js.map +1 -1
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
- package/dist/pipeline/phases/production-gate.js +28 -3
- package/dist/pipeline/phases/production-gate.js.map +1 -1
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
- package/dist/pipeline/phases/qa-validation.js +38 -5
- package/dist/pipeline/phases/qa-validation.js.map +1 -1
- package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
- package/dist/pipeline/phases/recovery-loop.js +200 -6
- package/dist/pipeline/phases/recovery-loop.js.map +1 -1
- package/dist/pipeline/phases/review.d.ts.map +1 -1
- package/dist/pipeline/phases/review.js +58 -28
- package/dist/pipeline/phases/review.js.map +1 -1
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
- package/dist/pipeline/phases/role-planning.js +20 -5
- package/dist/pipeline/phases/role-planning.js.map +1 -1
- package/dist/pipeline/phases/stuck.d.ts.map +1 -1
- package/dist/pipeline/phases/stuck.js +10 -0
- package/dist/pipeline/phases/stuck.js.map +1 -1
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
- package/dist/pipeline/repo-snapshot.js +3 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -1
- package/dist/pipeline/role-execution-adapter.d.ts +2 -1
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
- package/dist/pipeline/role-execution-adapter.js +22 -7
- package/dist/pipeline/role-execution-adapter.js.map +1 -1
- package/dist/pipeline/skill-loader.d.ts +19 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -1
- package/dist/pipeline/skill-loader.js +22 -0
- package/dist/pipeline/skill-loader.js.map +1 -1
- package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
- package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
- package/dist/pipeline/skills/constitution-generator.js +210 -0
- package/dist/pipeline/skills/constitution-generator.js.map +1 -0
- package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
- package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
- package/dist/pipeline/skills/coverage-gate.js +143 -0
- package/dist/pipeline/skills/coverage-gate.js.map +1 -0
- package/dist/pipeline/skills/generator.d.ts +65 -0
- package/dist/pipeline/skills/generator.d.ts.map +1 -0
- package/dist/pipeline/skills/generator.js +221 -0
- package/dist/pipeline/skills/generator.js.map +1 -0
- package/dist/pipeline/skills/role-map.d.ts +38 -0
- package/dist/pipeline/skills/role-map.d.ts.map +1 -0
- package/dist/pipeline/skills/role-map.js +234 -0
- package/dist/pipeline/skills/role-map.js.map +1 -0
- package/dist/pipeline/skills/types.d.ts +47 -0
- package/dist/pipeline/skills/types.d.ts.map +1 -0
- package/dist/pipeline/skills/types.js +5 -0
- package/dist/pipeline/skills/types.js.map +1 -0
- package/dist/pipeline/skills/usage-registry.d.ts +48 -0
- package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
- package/dist/pipeline/skills/usage-registry.js +55 -0
- package/dist/pipeline/skills/usage-registry.js.map +1 -0
- package/dist/pipeline/strategy-context.d.ts +20 -0
- package/dist/pipeline/strategy-context.d.ts.map +1 -0
- package/dist/pipeline/strategy-context.js +55 -0
- package/dist/pipeline/strategy-context.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +30 -5
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
- package/dist/pipeline/type-defs/artifacts.js +5 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -1
- package/dist/pipeline/type-defs/audit.d.ts +28 -13
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.d.ts +19 -8
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
- package/dist/pipeline/type-defs/checks.js +4 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -1
- package/dist/pipeline/type-defs/packets.d.ts +119 -18
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
- package/dist/pipeline/type-defs/packets.js +17 -1
- package/dist/pipeline/type-defs/packets.js.map +1 -1
- package/dist/pipeline/type-defs/state.d.ts +165 -16
- package/dist/pipeline/type-defs/state.d.ts.map +1 -1
- package/dist/pipeline/type-defs/state.js +26 -1
- package/dist/pipeline/type-defs/state.js.map +1 -1
- package/dist/shared/text-utils.d.ts +23 -0
- package/dist/shared/text-utils.d.ts.map +1 -0
- package/dist/shared/text-utils.js +66 -0
- package/dist/shared/text-utils.js.map +1 -0
- package/dist/shared/website-strategy-format.d.ts +18 -0
- package/dist/shared/website-strategy-format.d.ts.map +1 -0
- package/dist/shared/website-strategy-format.js +47 -0
- package/dist/shared/website-strategy-format.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +57 -8
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +1 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +1 -1
- package/dist/types/workflow.d.ts +447 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +3 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +6 -3
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +1 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +2 -29
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +3 -2
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +51 -6
- package/src/adapters/grok.ts +51 -6
- package/src/adapters/openai.ts +53 -5
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/interactive.ts +337 -20
- package/src/generators/all.ts +25 -2
- package/src/generators/doc-parser.ts +75 -15
- package/src/generators/templates/fullstack.ts +1 -1
- package/src/generators/templates/website-components.ts +1 -1
- package/src/generators/templates/website-config.ts +23 -11
- package/src/generators/templates/website-conversion.ts +1 -1
- package/src/generators/templates/website-landing.ts +1 -1
- package/src/generators/templates/website-layout.ts +491 -23
- package/src/generators/templates/website-pricing.ts +1 -1
- package/src/generators/templates/website-sections.ts +1 -1
- package/src/generators/templates/website-seo.ts +4 -1
- package/src/generators/templates/website.ts +3 -0
- package/src/generators/website-content-ai.ts +186 -0
- package/src/generators/website-content-scanner.ts +113 -1
- package/src/generators/website-context.ts +151 -12
- package/src/generators/website-debug.ts +26 -0
- package/src/generators/website.ts +28 -3
- package/src/pipeline/artifact-manager.ts +3 -0
- package/src/pipeline/auto-recovery.ts +283 -0
- package/src/pipeline/change-request.ts +63 -1
- package/src/pipeline/check-runner.ts +141 -2
- package/src/pipeline/command-resolver.ts +34 -2
- package/src/pipeline/consensus/arbitrator-query.ts +101 -0
- package/src/pipeline/consensus/consensus-runner.ts +1099 -42
- package/src/pipeline/cr-lifecycle.ts +103 -0
- package/src/pipeline/gate-engine.ts +36 -8
- package/src/pipeline/migration.ts +5 -30
- package/src/pipeline/orchestrator.ts +367 -16
- package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
- package/src/pipeline/phases/architecture.ts +6 -3
- package/src/pipeline/phases/audit.ts +6 -3
- package/src/pipeline/phases/consensus-architecture.ts +10 -1
- package/src/pipeline/phases/consensus-master-plan.ts +10 -3
- package/src/pipeline/phases/consensus-role-plans.ts +10 -1
- package/src/pipeline/phases/done.ts +15 -4
- package/src/pipeline/phases/intake.ts +67 -14
- package/src/pipeline/phases/phase-context.ts +6 -1
- package/src/pipeline/phases/production-gate.ts +41 -3
- package/src/pipeline/phases/qa-validation.ts +51 -5
- package/src/pipeline/phases/recovery-loop.ts +229 -7
- package/src/pipeline/phases/review.ts +73 -30
- package/src/pipeline/phases/role-planning.ts +23 -5
- package/src/pipeline/phases/stuck.ts +10 -0
- package/src/pipeline/repo-snapshot.ts +3 -0
- package/src/pipeline/role-execution-adapter.ts +30 -4
- package/src/pipeline/skill-loader.ts +33 -0
- package/src/pipeline/skills/constitution-generator.ts +236 -0
- package/src/pipeline/skills/coverage-gate.ts +199 -0
- package/src/pipeline/skills/generator.ts +287 -0
- package/src/pipeline/skills/role-map.ts +248 -0
- package/src/pipeline/skills/types.ts +53 -0
- package/src/pipeline/skills/usage-registry.ts +87 -0
- package/src/pipeline/strategy-context.ts +60 -0
- package/src/pipeline/type-defs/artifacts.ts +5 -0
- package/src/pipeline/type-defs/checks.ts +4 -0
- package/src/pipeline/type-defs/packets.ts +18 -1
- package/src/pipeline/type-defs/state.ts +26 -1
- package/src/shared/text-utils.ts +70 -0
- package/src/shared/website-strategy-format.ts +56 -0
- package/src/state/index.ts +60 -8
- package/src/types/consensus.ts +1 -0
- package/src/types/workflow.ts +6 -0
- package/src/upgrade/handlers.ts +9 -3
- package/src/workflow/consensus.ts +1 -0
- package/src/workflow/website-strategy.ts +2 -36
- package/src/workflow/website-updater.ts +4 -2
- package/tests/adapters/gemini.test.ts +165 -0
- package/tests/adapters/grok.test.ts +137 -0
- package/tests/adapters/openai.test.ts +128 -0
- package/tests/generators/doc-parser.test.ts +88 -9
- package/tests/generators/quality-gate.test.ts +19 -3
- package/tests/generators/website-components.test.ts +34 -0
- package/tests/generators/website-content-ai.test.ts +308 -0
- package/tests/generators/website-content-scanner.test.ts +86 -0
- package/tests/generators/website-context.test.ts +3 -2
- package/tests/integration/smokestack-scaffold.test.ts +385 -0
- package/tests/pipeline/auto-recovery.test.ts +337 -0
- package/tests/pipeline/change-request.test.ts +70 -0
- package/tests/pipeline/command-resolver.test.ts +42 -0
- package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
- package/tests/pipeline/consensus-runner.test.ts +1333 -10
- package/tests/pipeline/consensus-scoring.test.ts +602 -18
- package/tests/pipeline/gate-engine.test.ts +34 -0
- package/tests/pipeline/install-check.test.ts +261 -0
- package/tests/pipeline/migration.test.ts +4 -3
- package/tests/pipeline/orchestrator.test.ts +1506 -15
- package/tests/pipeline/packets/builders.test.ts +29 -6
- package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
- package/tests/pipeline/pipeline-persistence.test.ts +230 -0
- package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
- package/tests/pipeline/role-execution-adapter.test.ts +88 -0
- package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
- package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
- package/tests/pipeline/skills/generator.test.ts +213 -0
- package/tests/pipeline/skills/role-map.test.ts +198 -0
- package/tests/pipeline/skills/usage-registry.test.ts +114 -0
- package/tests/pipeline/strategy-context.test.ts +148 -0
- package/tests/shared/text-utils.test.ts +155 -0
- package/tests/state/progress-analysis.test.ts +375 -0
- package/tests/upgrade/handlers.test.ts +33 -2
- package/tests/workflow/consensus.test.ts +6 -0
- package/tsconfig.json +1 -1
|
@@ -6,36 +6,336 @@
|
|
|
6
6
|
* 1. Independent Review (DEFAULT): N reviewers review simultaneously,
|
|
7
7
|
* no reviewer sees other reviewers' output.
|
|
8
8
|
* 2. Iterative Consensus (optional): for recovery plan iteration.
|
|
9
|
+
*
|
|
10
|
+
* v2.1: Vote normalization pipeline, tag reclassification, hard-blocker
|
|
11
|
+
* detection, config-driven arbitration, reviewer rubric.
|
|
9
12
|
*/
|
|
10
13
|
import { createHash } from 'node:crypto';
|
|
14
|
+
import logging from 'node:console';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { z } from 'zod';
|
|
11
18
|
import { buildConsensusPacket } from '../packets/consensus-packet-builder.js';
|
|
19
|
+
import { isNoneVariant } from '../../shared/text-utils.js';
|
|
20
|
+
import { queryProvider } from './arbitrator-query.js';
|
|
12
21
|
// Re-use existing consensus infrastructure
|
|
13
22
|
import { iterateUntilConsensus } from '../../workflow/consensus.js';
|
|
23
|
+
const logger = logging;
|
|
24
|
+
// ─── Hard Blocker Patterns ───────────────────────────────
|
|
25
|
+
// Module-level const so both containsHardBlockerPatterns() and
|
|
26
|
+
// the forced-REJECT block in normalizeVoteBlockers() can reference it.
|
|
27
|
+
const HARD_BLOCKER_PATTERNS = [
|
|
28
|
+
/\bsql injection\b/i,
|
|
29
|
+
/\bxss\b/i,
|
|
30
|
+
/\bsecurity vulnerabilit(?:y|ies)\b/i,
|
|
31
|
+
/\b(?:build|tests?)\s+(?:is|are\s+)?failing\b/i,
|
|
32
|
+
/\bfails?\s+(?:in\s+)?(?:ci|pipeline|compilation)\b/i,
|
|
33
|
+
/\bdata loss\b/i,
|
|
34
|
+
/\bcritical\s+(?:bug|defect|error)\b/i,
|
|
35
|
+
];
|
|
36
|
+
function stripTag(s) {
|
|
37
|
+
return s.replace(/^\[(BLOCKER|REQUIRED|SUGGESTION)\]\s*/i, '');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pool ALL issue lists, classify by tag prefix.
|
|
41
|
+
* Untagged items retain their origin field for downstream routing.
|
|
42
|
+
*/
|
|
43
|
+
function parseTaggedIssues(blockingIssues, requiredChanges, suggestions) {
|
|
44
|
+
const result = {
|
|
45
|
+
blockers: [], required: [], suggestions: [], untagged: [],
|
|
46
|
+
};
|
|
47
|
+
function classify(items, origin) {
|
|
48
|
+
for (const issue of items) {
|
|
49
|
+
const trimmed = issue.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
if (/^\[BLOCKER\]/i.test(trimmed))
|
|
53
|
+
result.blockers.push(stripTag(trimmed));
|
|
54
|
+
else if (/^\[REQUIRED\]/i.test(trimmed))
|
|
55
|
+
result.required.push(stripTag(trimmed));
|
|
56
|
+
else if (/^\[SUGGESTION\]/i.test(trimmed))
|
|
57
|
+
result.suggestions.push(stripTag(trimmed));
|
|
58
|
+
else
|
|
59
|
+
result.untagged.push({ text: trimmed, origin });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
classify(blockingIssues, 'blocking');
|
|
63
|
+
classify(requiredChanges, 'required');
|
|
64
|
+
classify(suggestions, 'suggestion');
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// ─── Normalization Helpers ───────────────────────────────
|
|
68
|
+
const cleanText = (s) => stripTag(s.trim());
|
|
69
|
+
const cleanList = (arr) => {
|
|
70
|
+
const out = [];
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (const raw of arr) {
|
|
73
|
+
const s = cleanText(raw);
|
|
74
|
+
if (!s || isNoneVariant(s) || seen.has(s))
|
|
75
|
+
continue;
|
|
76
|
+
seen.add(s);
|
|
77
|
+
out.push(s);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
};
|
|
81
|
+
function containsHardBlockerPatterns(issues) {
|
|
82
|
+
return issues.some(issue => HARD_BLOCKER_PATTERNS.some(p => p.test(issue)));
|
|
83
|
+
}
|
|
84
|
+
// ─── Vote Normalization Pipeline ─────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Normalize votes: pool → classify by tag → detect hard blockers → route by vote → dedup.
|
|
87
|
+
* Called after collecting votes, before buildConsensusPacket().
|
|
88
|
+
* Idempotent: running twice produces the same result.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeVoteBlockers(votes) {
|
|
91
|
+
const summary = {
|
|
92
|
+
tagged_blockers_demoted_to_suggestions: 0,
|
|
93
|
+
tagged_blockers_demoted_to_required: 0,
|
|
94
|
+
untagged_from_blocking_routed_to_required: 0,
|
|
95
|
+
forced_rejects: 0,
|
|
96
|
+
};
|
|
97
|
+
const normalized = votes.map((v) => {
|
|
98
|
+
// Step 1: Pool ALL issue lists, classify by tag
|
|
99
|
+
const tagged = parseTaggedIssues(v.blocking_issues.filter(i => !isNoneVariant(i)), (v.required_changes ?? []), v.suggestions);
|
|
100
|
+
// Step 2: Contradiction guard — scan ALL pooled text for hard blockers
|
|
101
|
+
const hasTaggedBlocker = tagged.blockers.length > 0;
|
|
102
|
+
const allPooledText = [
|
|
103
|
+
...tagged.blockers, ...tagged.required, ...tagged.suggestions,
|
|
104
|
+
...tagged.untagged.map(u => u.text),
|
|
105
|
+
].map(cleanText);
|
|
106
|
+
const hasHardPattern = containsHardBlockerPatterns(allPooledText);
|
|
107
|
+
// v2.4.4: Vote-aware contradiction guard
|
|
108
|
+
// Principle:
|
|
109
|
+
// APPROVE + any hard pattern anywhere = genuinely inconsistent -> force REJECT
|
|
110
|
+
// CONDITIONAL = force REJECT only if [BLOCKER] tag OR hard pattern in blocker-origin text
|
|
111
|
+
// REJECT = already reject, no forcing needed
|
|
112
|
+
const hasHardPatternAnywhere = hasHardPattern; // already computed above (allPooledText)
|
|
113
|
+
const blockerOriginText = [
|
|
114
|
+
...tagged.blockers,
|
|
115
|
+
...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
|
|
116
|
+
].map(cleanText);
|
|
117
|
+
const hasHardPatternInBlockers = containsHardBlockerPatterns(blockerOriginText);
|
|
118
|
+
const forceReject = (v.vote === 'APPROVE' && (hasTaggedBlocker || hasHardPatternAnywhere)) ||
|
|
119
|
+
(v.vote === 'CONDITIONAL' && (hasTaggedBlocker || hasHardPatternInBlockers));
|
|
120
|
+
if (forceReject) {
|
|
121
|
+
summary.forced_rejects++;
|
|
122
|
+
// Debug logging for forced-reject diagnosis
|
|
123
|
+
logger.log(`[consensus] Forced REJECT: vote=${v.vote} reviewer=${v.reviewer_id} ` +
|
|
124
|
+
`hasTaggedBlocker=${hasTaggedBlocker} hasHardPatternAnywhere=${hasHardPatternAnywhere} ` +
|
|
125
|
+
`hasHardPatternInBlockers=${hasHardPatternInBlockers}`);
|
|
126
|
+
// Build minimal hard-blocker set: tagged blockers + any text matching patterns
|
|
127
|
+
const hardBlockers = [
|
|
128
|
+
...tagged.blockers,
|
|
129
|
+
...tagged.untagged.map(u => u.text).filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
130
|
+
...tagged.required.filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
131
|
+
...tagged.suggestions.filter(t => HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
132
|
+
];
|
|
133
|
+
// Non-hard items go to required_changes
|
|
134
|
+
const nonHard = [
|
|
135
|
+
...tagged.required.filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
136
|
+
...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
|
|
137
|
+
...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text)
|
|
138
|
+
.filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
139
|
+
];
|
|
140
|
+
const nonHardSuggestions = [
|
|
141
|
+
...tagged.suggestions.filter(t => !HARD_BLOCKER_PATTERNS.some(p => p.test(t))),
|
|
142
|
+
...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
|
|
143
|
+
];
|
|
144
|
+
return {
|
|
145
|
+
...v,
|
|
146
|
+
vote: 'REJECT',
|
|
147
|
+
blocking_issues: cleanList(hardBlockers),
|
|
148
|
+
required_changes: cleanList(nonHard),
|
|
149
|
+
suggestions: cleanList(nonHardSuggestions),
|
|
150
|
+
reviewer_inconsistency: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Step 3: Vote-consistent routing for untagged items
|
|
154
|
+
switch (v.vote) {
|
|
155
|
+
case 'APPROVE': {
|
|
156
|
+
// APPROVE = execution-ready. Tagged blockers → suggestions. All untagged → suggestions.
|
|
157
|
+
summary.tagged_blockers_demoted_to_suggestions += tagged.blockers.length;
|
|
158
|
+
return {
|
|
159
|
+
...v,
|
|
160
|
+
blocking_issues: [],
|
|
161
|
+
required_changes: cleanList([...tagged.required]),
|
|
162
|
+
suggestions: cleanList([
|
|
163
|
+
...tagged.suggestions,
|
|
164
|
+
...tagged.blockers,
|
|
165
|
+
...tagged.untagged.map(u => u.text),
|
|
166
|
+
]),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case 'CONDITIONAL': {
|
|
170
|
+
// CONDITIONAL: tagged blockers → required_changes, untagged-from-blocking → required_changes
|
|
171
|
+
summary.tagged_blockers_demoted_to_required += tagged.blockers.length;
|
|
172
|
+
summary.untagged_from_blocking_routed_to_required += tagged.untagged.filter(u => u.origin === 'blocking').length;
|
|
173
|
+
return {
|
|
174
|
+
...v,
|
|
175
|
+
blocking_issues: [],
|
|
176
|
+
required_changes: cleanList([
|
|
177
|
+
...tagged.required,
|
|
178
|
+
...tagged.blockers,
|
|
179
|
+
...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
|
|
180
|
+
...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
|
|
181
|
+
]),
|
|
182
|
+
suggestions: cleanList([
|
|
183
|
+
...tagged.suggestions,
|
|
184
|
+
...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
|
|
185
|
+
]),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case 'REJECT': {
|
|
189
|
+
// REJECT: untagged-from-blocking stays as blockers
|
|
190
|
+
return {
|
|
191
|
+
...v,
|
|
192
|
+
blocking_issues: cleanList([
|
|
193
|
+
...tagged.blockers,
|
|
194
|
+
...tagged.untagged.filter(u => u.origin === 'blocking').map(u => u.text),
|
|
195
|
+
]),
|
|
196
|
+
required_changes: cleanList([
|
|
197
|
+
...tagged.required,
|
|
198
|
+
...tagged.untagged.filter(u => u.origin === 'required').map(u => u.text),
|
|
199
|
+
]),
|
|
200
|
+
suggestions: cleanList([
|
|
201
|
+
...tagged.suggestions,
|
|
202
|
+
...tagged.untagged.filter(u => u.origin === 'suggestion').map(u => u.text),
|
|
203
|
+
]),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return { votes: normalized, summary };
|
|
209
|
+
}
|
|
210
|
+
// ─── Vote Mapping ────────────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Floor confidence score for CONDITIONAL votes.
|
|
213
|
+
* Matches adapter rubric: 80-94% = "minor revisions needed".
|
|
214
|
+
*/
|
|
215
|
+
export const DEFAULT_CONDITIONAL_FLOOR = 0.80;
|
|
216
|
+
/**
|
|
217
|
+
* Map a reviewer's confidence score (0-1) to a structured vote.
|
|
218
|
+
* Threshold-aware: APPROVE = meets gate bar, CONDITIONAL = iterate, REJECT = major rework.
|
|
219
|
+
*/
|
|
220
|
+
export function mapVote(confidence, threshold, conditionalFloor = DEFAULT_CONDITIONAL_FLOOR) {
|
|
221
|
+
const c = Math.max(0, Math.min(1, confidence));
|
|
222
|
+
const t = Math.max(0, Math.min(1, threshold));
|
|
223
|
+
const f = Math.max(0, Math.min(t, conditionalFloor));
|
|
224
|
+
if (c >= t)
|
|
225
|
+
return 'APPROVE';
|
|
226
|
+
if (c >= f)
|
|
227
|
+
return 'CONDITIONAL';
|
|
228
|
+
return 'REJECT';
|
|
229
|
+
}
|
|
230
|
+
// ─── Vote Disagreement Detection ─────────────────────────
|
|
231
|
+
/**
|
|
232
|
+
* Check if votes have meaningful disagreement (not unanimous).
|
|
233
|
+
*/
|
|
234
|
+
export function hasVoteDisagreement(votes) {
|
|
235
|
+
if (votes.length <= 1)
|
|
236
|
+
return false;
|
|
237
|
+
const uniqueVotes = new Set(votes.map(v => v.vote));
|
|
238
|
+
return uniqueVotes.size > 1;
|
|
239
|
+
}
|
|
14
240
|
const DEFAULT_PROVIDERS = [
|
|
15
241
|
{ provider: 'openai', model: 'gpt-4.1', temperature: 0.3 },
|
|
16
242
|
{ provider: 'gemini', model: 'gemini-2.5-flash', temperature: 0.3 },
|
|
17
243
|
];
|
|
244
|
+
// ─── Plan Content Loader ─────────────────────────────────
|
|
245
|
+
/** Max plan content chars to embed in prompt (50K ~ safe for all providers). */
|
|
246
|
+
const MAX_PLAN_CONTENT_CHARS = 50_000;
|
|
247
|
+
/**
|
|
248
|
+
* Safely load plan content from disk.
|
|
249
|
+
* - Path traversal guard: resolved path must start with projectDir.
|
|
250
|
+
* - Size cap: truncates with marker if content exceeds MAX_PLAN_CONTENT_CHARS.
|
|
251
|
+
*/
|
|
252
|
+
export function loadPlanContent(projectDir, artifactPath) {
|
|
253
|
+
if (!artifactPath) {
|
|
254
|
+
logger.warn('[consensus] No master plan path in packet references');
|
|
255
|
+
return { content: '', truncated: false };
|
|
256
|
+
}
|
|
257
|
+
const resolvedProject = resolve(projectDir);
|
|
258
|
+
const fullPath = resolve(projectDir, artifactPath);
|
|
259
|
+
// Path traversal guard: resolved path must be inside projectDir
|
|
260
|
+
if (!fullPath.startsWith(resolvedProject + '/') && fullPath !== resolvedProject) {
|
|
261
|
+
logger.warn(`[consensus] Path traversal blocked: ${artifactPath} resolved to ${fullPath}`);
|
|
262
|
+
return { content: '', truncated: false };
|
|
263
|
+
}
|
|
264
|
+
if (!existsSync(fullPath)) {
|
|
265
|
+
logger.warn(`[consensus] Plan artifact not found at ${fullPath}`);
|
|
266
|
+
return { content: '', truncated: false };
|
|
267
|
+
}
|
|
268
|
+
let content = readFileSync(fullPath, 'utf-8');
|
|
269
|
+
let truncated = false;
|
|
270
|
+
if (content.length > MAX_PLAN_CONTENT_CHARS) {
|
|
271
|
+
content = content.slice(0, MAX_PLAN_CONTENT_CHARS)
|
|
272
|
+
+ '\n\n[TRUNCATED -- plan exceeds 50K chars. Review based on visible content.]';
|
|
273
|
+
truncated = true;
|
|
274
|
+
logger.warn(`[consensus] Plan content truncated to ${MAX_PLAN_CONTENT_CHARS} chars`);
|
|
275
|
+
}
|
|
276
|
+
logger.log(`[consensus] Loaded plan content from ${artifactPath} (${content.length} chars${truncated ? ', truncated' : ''})`);
|
|
277
|
+
return { content, truncated };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Determine whether arbitration should be triggered and why.
|
|
281
|
+
* Pure function — no side effects, easily unit-testable.
|
|
282
|
+
*/
|
|
283
|
+
export function getArbitrationTrigger(votes, weightedScore, threshold) {
|
|
284
|
+
if (hasVoteDisagreement(votes))
|
|
285
|
+
return 'DISAGREEMENT';
|
|
286
|
+
if (weightedScore >= (threshold - 0.10))
|
|
287
|
+
return 'BORDERLINE_SCORE';
|
|
288
|
+
const avgConfidence = votes.reduce((s, v) => s + v.confidence, 0) / votes.length;
|
|
289
|
+
const allConditional = votes.every(v => v.vote === 'CONDITIONAL');
|
|
290
|
+
const totalRequired = votes.reduce((sum, v) => sum + (v.required_changes?.length ?? 0), 0);
|
|
291
|
+
if (allConditional && avgConfidence >= 0.94 && totalRequired <= 3)
|
|
292
|
+
return 'ALL_CONDITIONAL';
|
|
293
|
+
return 'NONE';
|
|
294
|
+
}
|
|
18
295
|
// ─── Consensus Runner ────────────────────────────────────
|
|
19
296
|
export class ConsensusRunner {
|
|
20
297
|
config;
|
|
298
|
+
arbitrationAttempted = new Set();
|
|
21
299
|
constructor(config) {
|
|
22
300
|
this.config = config;
|
|
23
301
|
}
|
|
24
302
|
/** Run structured consensus on a plan packet */
|
|
25
|
-
async runStructuredConsensus(planPacket, gateDefinition) {
|
|
303
|
+
async runStructuredConsensus(planPacket, gateDefinition, options) {
|
|
26
304
|
const rules = {
|
|
27
305
|
threshold: gateDefinition.consensusThreshold ?? this.config.threshold,
|
|
28
306
|
quorum: this.config.quorum,
|
|
29
307
|
min_reviewers: gateDefinition.minReviewers ?? this.config.minReviewers,
|
|
30
308
|
};
|
|
309
|
+
// v2.4.4: Dev-time warning when version is missing or stuck at 1
|
|
310
|
+
if (planPacket.metadata.version === undefined || planPacket.metadata.version <= 1) {
|
|
311
|
+
logger.warn(`[consensus] Phase ${planPacket.metadata.phase}: version=${planPacket.metadata.version ?? 'undefined'} ` +
|
|
312
|
+
`— ensure this is intentional (not a missing recoveryCount passthrough)`);
|
|
313
|
+
}
|
|
314
|
+
// v2.2.1: Record REVIEWER skill usage if loader available
|
|
315
|
+
if (this.config.skillLoader && this.config.skillUsageRegistry) {
|
|
316
|
+
const { meta } = this.config.skillLoader.loadSkillWithMeta('REVIEWER');
|
|
317
|
+
this.config.skillUsageRegistry.record('REVIEWER', planPacket.metadata.phase, 'review_prompt', meta.source, meta.version);
|
|
318
|
+
}
|
|
319
|
+
// Load actual plan content from disk for inclusion in review prompt
|
|
320
|
+
const { content: planContent } = loadPlanContent(this.config.projectDir, planPacket.references.master_plan?.path);
|
|
321
|
+
const revisionDirective = options?.revisionDirective;
|
|
31
322
|
let votes;
|
|
32
323
|
if (this.config.mode === 'independent') {
|
|
33
|
-
votes = await this.runIndependentReview(planPacket);
|
|
324
|
+
votes = await this.runIndependentReview(planPacket, planContent, revisionDirective);
|
|
34
325
|
}
|
|
35
326
|
else {
|
|
36
|
-
votes = await this.runIterativeReview(planPacket);
|
|
327
|
+
votes = await this.runIterativeReview(planPacket, planContent, revisionDirective);
|
|
328
|
+
}
|
|
329
|
+
// v2.1: Normalize votes before scoring
|
|
330
|
+
const { votes: normalizedVotes, summary: normSummary } = normalizeVoteBlockers(votes);
|
|
331
|
+
if (normSummary.forced_rejects > 0) {
|
|
332
|
+
logger.warn(`[consensus] Normalization forced ${normSummary.forced_rejects} vote(s) to REJECT due to blocker/pattern contradiction`);
|
|
37
333
|
}
|
|
38
|
-
|
|
334
|
+
logger.log(`[consensus] Normalization: ${JSON.stringify(normSummary)}`);
|
|
335
|
+
for (const v of normalizedVotes) {
|
|
336
|
+
logger.log(`[consensus] Normalized: ${v.reviewer_id} vote=${v.vote} conf=${v.confidence.toFixed(3)} blockers=${v.blocking_issues.length}`);
|
|
337
|
+
}
|
|
338
|
+
// Build consensus packet from normalized votes
|
|
39
339
|
const packet = buildConsensusPacket({
|
|
40
340
|
planPacketRef: {
|
|
41
341
|
artifact_id: planPacket.metadata.packet_id,
|
|
@@ -44,19 +344,91 @@ export class ConsensusRunner {
|
|
|
44
344
|
version: planPacket.metadata.version,
|
|
45
345
|
type: 'consensus',
|
|
46
346
|
},
|
|
47
|
-
votes,
|
|
347
|
+
votes: normalizedVotes,
|
|
48
348
|
rules,
|
|
349
|
+
normalizationMoves: normSummary,
|
|
49
350
|
});
|
|
351
|
+
logger.log(`[consensus] Result: weighted_score=${packet.consensus_result.weighted_score.toFixed(3)} score=${packet.consensus_result.score.toFixed(3)} status=${packet.final_status}`);
|
|
352
|
+
// v2.4.2: Attempt arbitration for REJECTED packets if enabled
|
|
353
|
+
if (packet.final_status === 'REJECTED'
|
|
354
|
+
&& this.config.enableArbitration
|
|
355
|
+
&& !this.arbitrationAttempted.has(`${planPacket.metadata.phase}@v${planPacket.metadata.version}`)) {
|
|
356
|
+
const arbitrationTrigger = getArbitrationTrigger(normalizedVotes, packet.consensus_result.weighted_score, rules.threshold);
|
|
357
|
+
const shouldArbitrate = arbitrationTrigger !== 'NONE';
|
|
358
|
+
if (shouldArbitrate) {
|
|
359
|
+
logger.log(`[consensus] Arbitration triggered: reason=${arbitrationTrigger} weighted_score=${packet.consensus_result.weighted_score.toFixed(3)}`);
|
|
360
|
+
this.arbitrationAttempted.add(`${planPacket.metadata.phase}@v${planPacket.metadata.version}`);
|
|
361
|
+
const arbResult = await this.callArbitrator(planPacket, normalizedVotes, rules, planContent);
|
|
362
|
+
if (arbResult?.approved) {
|
|
363
|
+
// v2.2.1: Record ARBITRATOR skill usage
|
|
364
|
+
if (this.config.skillLoader && this.config.skillUsageRegistry) {
|
|
365
|
+
const { meta } = this.config.skillLoader.loadSkillWithMeta('ARBITRATOR');
|
|
366
|
+
this.config.skillUsageRegistry.record('ARBITRATOR', planPacket.metadata.phase, 'arbitration_prompt', meta.source, meta.version);
|
|
367
|
+
}
|
|
368
|
+
// Rebuild with arbitration
|
|
369
|
+
return buildConsensusPacket({
|
|
370
|
+
planPacketRef: {
|
|
371
|
+
artifact_id: planPacket.metadata.packet_id,
|
|
372
|
+
path: '',
|
|
373
|
+
sha256: '',
|
|
374
|
+
version: planPacket.metadata.version,
|
|
375
|
+
type: 'consensus',
|
|
376
|
+
},
|
|
377
|
+
votes: normalizedVotes,
|
|
378
|
+
rules,
|
|
379
|
+
arbitratorResult: {
|
|
380
|
+
decision: arbResult.reasoning,
|
|
381
|
+
merged_patch: arbResult.suggestedChanges?.join('\n'),
|
|
382
|
+
},
|
|
383
|
+
normalizationMoves: normSummary,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// v2.2.1: Record ARBITRATOR skill usage if arbitration occurred (legacy path)
|
|
389
|
+
if (packet.final_status === 'ARBITRATED' && this.config.skillLoader && this.config.skillUsageRegistry) {
|
|
390
|
+
const { meta } = this.config.skillLoader.loadSkillWithMeta('ARBITRATOR');
|
|
391
|
+
this.config.skillUsageRegistry.record('ARBITRATOR', planPacket.metadata.phase, 'arbitration_prompt', meta.source, meta.version);
|
|
392
|
+
}
|
|
393
|
+
// v2.4.2: Diagnostic logging at high version counts
|
|
394
|
+
if (planPacket.metadata.version >= 3) {
|
|
395
|
+
logger.warn(`[consensus] High iteration count: phase=${planPacket.metadata.phase} version=${planPacket.metadata.version} `
|
|
396
|
+
+ `weighted_score=${packet.consensus_result.weighted_score.toFixed(3)} `
|
|
397
|
+
+ `has_true_blockers=${packet.consensus_result.has_true_blockers} `
|
|
398
|
+
+ `status=${packet.final_status}`);
|
|
399
|
+
}
|
|
50
400
|
return packet;
|
|
51
401
|
}
|
|
52
402
|
/** Independent review: spawn N reviewers, each reviews independently */
|
|
53
|
-
async runIndependentReview(planPacket) {
|
|
54
|
-
|
|
403
|
+
async runIndependentReview(planPacket, planContent, revisionDirective) {
|
|
404
|
+
let providers = [...(this.config.reviewerProviders ?? DEFAULT_PROVIDERS)];
|
|
405
|
+
// v2.4.2: Escalation — add tie-breaking reviewer on high iteration count.
|
|
406
|
+
// Only select from configured providers (arbitrator config is a valid source).
|
|
407
|
+
if (planPacket.metadata.version >= 3 && providers.length < 3) {
|
|
408
|
+
const existingNames = new Set(providers.map(p => p.provider));
|
|
409
|
+
// Build candidates from: arbitrator provider + all configured reviewers (deduplicated)
|
|
410
|
+
const candidates = new Set();
|
|
411
|
+
if (this.config.arbitratorProvider)
|
|
412
|
+
candidates.add(this.config.arbitratorProvider.provider);
|
|
413
|
+
for (const p of this.config.reviewerProviders ?? DEFAULT_PROVIDERS)
|
|
414
|
+
candidates.add(p.provider);
|
|
415
|
+
// Pick first configured provider not already reviewing
|
|
416
|
+
const PREFERRED_ORDER = ['grok', 'openai', 'gemini'];
|
|
417
|
+
const tieBreaker = PREFERRED_ORDER.find(p => candidates.has(p) && !existingNames.has(p));
|
|
418
|
+
if (tieBreaker) {
|
|
419
|
+
const model = getModelForProvider(this.config.consensusConfig, tieBreaker);
|
|
420
|
+
providers.push({ provider: tieBreaker, model, temperature: 0.3 });
|
|
421
|
+
logger.log(`[consensus] Escalation: added ${tieBreaker}/${model} as tie-breaking reviewer (v${planPacket.metadata.version})`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
logger.warn(`[consensus] Escalation: no additional provider available. ` +
|
|
425
|
+
`configured=${[...candidates].join(',')} ` +
|
|
426
|
+
`in_use=${[...existingNames].join(',')}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
55
429
|
const numReviewers = Math.max(this.config.minReviewers, providers.length);
|
|
56
|
-
|
|
57
|
-
const prompt = buildReviewPrompt(planPacket);
|
|
430
|
+
const prompt = buildReviewPrompt(planPacket, planContent, revisionDirective);
|
|
58
431
|
const promptHash = createHash('sha256').update(prompt).digest('hex');
|
|
59
|
-
// Spawn reviewers in parallel
|
|
60
432
|
const reviewPromises = [];
|
|
61
433
|
for (let i = 0; i < numReviewers; i++) {
|
|
62
434
|
const provider = providers[i % providers.length];
|
|
@@ -65,22 +437,22 @@ export class ConsensusRunner {
|
|
|
65
437
|
return Promise.all(reviewPromises);
|
|
66
438
|
}
|
|
67
439
|
/** Iterative review: wraps existing iterateUntilConsensus */
|
|
68
|
-
async runIterativeReview(planPacket) {
|
|
69
|
-
const prompt = buildReviewPrompt(planPacket);
|
|
440
|
+
async runIterativeReview(planPacket, planContent, revisionDirective) {
|
|
441
|
+
const prompt = buildReviewPrompt(planPacket, planContent, revisionDirective);
|
|
70
442
|
try {
|
|
71
443
|
const result = await iterateUntilConsensus(prompt, `Phase: ${planPacket.metadata.phase}`, {
|
|
72
444
|
projectDir: this.config.projectDir,
|
|
73
445
|
config: this.config.consensusConfig,
|
|
74
446
|
});
|
|
75
|
-
|
|
447
|
+
const iterativeConfidence = (result.finalScore ?? 50) / 100;
|
|
76
448
|
const vote = {
|
|
77
449
|
reviewer_id: 'iterative-reviewer',
|
|
78
450
|
provider: 'openai',
|
|
79
451
|
model: this.config.consensusConfig?.openaiModel ?? 'gpt-4.1',
|
|
80
452
|
temperature: this.config.consensusConfig?.temperature ?? 0.3,
|
|
81
453
|
prompt_hash: createHash('sha256').update(prompt).digest('hex'),
|
|
82
|
-
vote:
|
|
83
|
-
confidence:
|
|
454
|
+
vote: mapVote(iterativeConfidence, this.config.threshold),
|
|
455
|
+
confidence: iterativeConfidence,
|
|
84
456
|
blocking_issues: result.finalConcerns ?? [],
|
|
85
457
|
suggestions: result.finalRecommendations ?? [],
|
|
86
458
|
evidence_refs: [],
|
|
@@ -102,21 +474,35 @@ export class ConsensusRunner {
|
|
|
102
474
|
}];
|
|
103
475
|
}
|
|
104
476
|
}
|
|
105
|
-
/**
|
|
477
|
+
/**
|
|
478
|
+
* Spawn a single independent reviewer.
|
|
479
|
+
* Governance rule: vote is ALWAYS derived from confidence via mapVote().
|
|
480
|
+
* The LLM's explicit vote is advisory only — logged for debugging.
|
|
481
|
+
*/
|
|
106
482
|
async spawnSingleReviewer(prompt, promptHash, provider, reviewerId) {
|
|
107
483
|
try {
|
|
108
484
|
const result = await this.callProviderForReview(prompt, provider);
|
|
485
|
+
// Governance: always derive vote from confidence, never trust LLM's explicit vote
|
|
486
|
+
const derived = mapVote(result.confidence, this.config.threshold);
|
|
487
|
+
const modelVote = result.modelVote ?? null;
|
|
488
|
+
const reviewer_inconsistency = modelVote !== null && modelVote !== derived;
|
|
489
|
+
if (reviewer_inconsistency) {
|
|
490
|
+
logger.log(`[consensus] ${provider.provider}: model said ${modelVote} but confidence ${result.confidence.toFixed(3)} -> derived ${derived}`);
|
|
491
|
+
}
|
|
492
|
+
logger.log(`[consensus] ${provider.provider}/${provider.model}: vote=${derived} confidence=${result.confidence.toFixed(3)} modelVote=${modelVote ?? 'none'} blockers=${result.blockingIssues.length}`);
|
|
109
493
|
return {
|
|
110
494
|
reviewer_id: reviewerId,
|
|
111
495
|
provider: provider.provider,
|
|
112
496
|
model: provider.model,
|
|
113
497
|
temperature: provider.temperature,
|
|
114
498
|
prompt_hash: promptHash,
|
|
115
|
-
vote:
|
|
499
|
+
vote: derived,
|
|
116
500
|
confidence: result.confidence,
|
|
117
501
|
blocking_issues: result.blockingIssues,
|
|
502
|
+
required_changes: result.requiredChanges ?? [],
|
|
118
503
|
suggestions: result.suggestions,
|
|
119
504
|
evidence_refs: [],
|
|
505
|
+
reviewer_inconsistency,
|
|
120
506
|
};
|
|
121
507
|
}
|
|
122
508
|
catch {
|
|
@@ -134,48 +520,349 @@ export class ConsensusRunner {
|
|
|
134
520
|
};
|
|
135
521
|
}
|
|
136
522
|
}
|
|
137
|
-
/**
|
|
523
|
+
/**
|
|
524
|
+
* Call the appropriate provider adapter for a review.
|
|
525
|
+
* Uses requestRawReview() to bypass adapter prompt wrapping/parsing —
|
|
526
|
+
* the runner owns the prompt and parses the raw LLM response itself.
|
|
527
|
+
*/
|
|
138
528
|
async callProviderForReview(prompt, provider) {
|
|
529
|
+
let raw;
|
|
139
530
|
switch (provider.provider) {
|
|
140
531
|
case 'openai': {
|
|
141
|
-
const {
|
|
142
|
-
|
|
532
|
+
const { requestRawReview } = await import('../../adapters/openai.js');
|
|
533
|
+
raw = await requestRawReview(prompt, {
|
|
143
534
|
openaiModel: provider.model,
|
|
144
535
|
temperature: provider.temperature,
|
|
145
536
|
});
|
|
146
|
-
|
|
537
|
+
break;
|
|
147
538
|
}
|
|
148
539
|
case 'gemini': {
|
|
149
|
-
const {
|
|
150
|
-
|
|
540
|
+
const { requestRawReview } = await import('../../adapters/gemini.js');
|
|
541
|
+
raw = await requestRawReview(prompt, {
|
|
151
542
|
model: provider.model,
|
|
152
543
|
temperature: provider.temperature,
|
|
153
544
|
});
|
|
154
|
-
|
|
545
|
+
break;
|
|
155
546
|
}
|
|
156
547
|
case 'grok': {
|
|
157
|
-
const {
|
|
158
|
-
|
|
548
|
+
const { requestRawReview } = await import('../../adapters/grok.js');
|
|
549
|
+
raw = await requestRawReview(prompt, {
|
|
159
550
|
model: provider.model,
|
|
160
551
|
temperature: provider.temperature,
|
|
161
552
|
});
|
|
162
|
-
|
|
553
|
+
break;
|
|
163
554
|
}
|
|
164
555
|
default:
|
|
165
556
|
throw new Error(`Unknown provider: ${provider.provider}`);
|
|
166
557
|
}
|
|
558
|
+
logger.log(`[consensus] raw(${provider.provider}/${provider.model}): ${raw.slice(0, 500)}`);
|
|
559
|
+
return parseRawReviewResponse(raw);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Call arbitrator provider for tie-breaking (v2.1).
|
|
563
|
+
* v2.4.2: Rotates arbitrator away from dissenting reviewers to prevent
|
|
564
|
+
* systematic failure (e.g., Gemini rejects as reviewer + as arbitrator).
|
|
565
|
+
*/
|
|
566
|
+
async callArbitrator(planPacket, votes, _rules, planContent) {
|
|
567
|
+
let provider = this.config.arbitratorProvider;
|
|
568
|
+
if (!provider)
|
|
569
|
+
return null;
|
|
570
|
+
// v2.4.2: Rotate arbitrator away from dissenting reviewers
|
|
571
|
+
const dissentingProviders = new Set(votes.filter(v => v.vote === 'REJECT').map(v => v.provider));
|
|
572
|
+
if (dissentingProviders.has(provider.provider)) {
|
|
573
|
+
const configuredProviders = new Set((this.config.reviewerProviders ?? DEFAULT_PROVIDERS).map(p => p.provider));
|
|
574
|
+
if (this.config.arbitratorProvider)
|
|
575
|
+
configuredProviders.add(this.config.arbitratorProvider.provider);
|
|
576
|
+
const ARBITRATOR_FALLBACK_ORDER = ['openai', 'grok', 'gemini'];
|
|
577
|
+
const alternate = ARBITRATOR_FALLBACK_ORDER.find(p => !dissentingProviders.has(p) && configuredProviders.has(p));
|
|
578
|
+
if (alternate && alternate !== provider.provider) {
|
|
579
|
+
const model = getModelForProvider(this.config.consensusConfig, alternate);
|
|
580
|
+
logger.log(`[consensus] Arbitrator rotation: ${provider.provider} is a dissenter, switching to ${alternate}/${model}`);
|
|
581
|
+
provider = { provider: alternate, model, temperature: 0.2 };
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
logger.warn(`[consensus] Arbitrator rotation: no configured non-dissenter provider available, keeping ${provider.provider}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const prompt = buildArbitrationPrompt(planPacket, votes, planContent);
|
|
589
|
+
// v2.6.0: Use shared queryProvider for adapter wiring + timeout
|
|
590
|
+
const raw = await queryProvider(prompt, provider);
|
|
591
|
+
if (!raw)
|
|
592
|
+
return null;
|
|
593
|
+
logger.log(`[consensus] arbitrator raw(${provider.provider}/${provider.model}): ${raw.slice(0, 500)}`);
|
|
594
|
+
// v2.4.3: Dedicated arbitrator response parser (not reviewer schema)
|
|
595
|
+
const parsed = parseArbitratorResponse(raw);
|
|
596
|
+
logger.log(`[consensus] Arbitrator decision: approved=${parsed.approved} ` +
|
|
597
|
+
`suggestedChanges=${parsed.suggestedChanges.length}`);
|
|
598
|
+
return {
|
|
599
|
+
approved: parsed.approved,
|
|
600
|
+
score: parsed.approved ? 90 : 10,
|
|
601
|
+
analysis: raw.slice(0, 2000),
|
|
602
|
+
criticalConcerns: [],
|
|
603
|
+
minorConcerns: [],
|
|
604
|
+
subjectiveConcerns: [],
|
|
605
|
+
reasoning: parsed.reasoning || raw.slice(0, 2000),
|
|
606
|
+
suggestedChanges: parsed.suggestedChanges,
|
|
607
|
+
rawResponse: raw,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
logger.warn(`[consensus] Arbitration call failed: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// ─── JSON-first Response Parsing ─────────────────────────
|
|
617
|
+
/**
|
|
618
|
+
* Zod schema for structured JSON review responses from the LLM.
|
|
619
|
+
*/
|
|
620
|
+
const ReviewResponseSchema = z.object({
|
|
621
|
+
vote: z.enum(['APPROVE', 'CONDITIONAL', 'REJECT']),
|
|
622
|
+
confidence: z.number().min(0).max(1),
|
|
623
|
+
blocking_issues: z.array(z.string()).default([]),
|
|
624
|
+
required_changes: z.array(z.string()).default([]),
|
|
625
|
+
suggestions: z.array(z.string()).default([]),
|
|
626
|
+
analysis: z.string().optional(),
|
|
627
|
+
});
|
|
628
|
+
/**
|
|
629
|
+
* Parse raw LLM response text into a ProviderReviewResult.
|
|
630
|
+
* Strategy 1: Try JSON parse first (expected format).
|
|
631
|
+
* Strategy 2: Regex fallback for free-form text responses.
|
|
632
|
+
*
|
|
633
|
+
* @param raw - Raw text from the LLM
|
|
634
|
+
* @returns Parsed review result with confidence, issues, and advisory vote
|
|
635
|
+
*/
|
|
636
|
+
export function parseRawReviewResponse(raw) {
|
|
637
|
+
const jsonResult = tryParseJSON(raw);
|
|
638
|
+
const result = jsonResult ?? parseRegexFallback(raw);
|
|
639
|
+
// Correct confidence if vote and confidence are semantically contradictory
|
|
640
|
+
const { confidence, wasContradiction, original } = correctConfidenceContradiction(result.modelVote ?? null, result.confidence);
|
|
641
|
+
if (wasContradiction) {
|
|
642
|
+
logger.warn(`[consensus] Confidence contradiction corrected: vote=${result.modelVote} `
|
|
643
|
+
+ `conf=${original.toFixed(3)} -> corrected=${confidence.toFixed(3)}`);
|
|
167
644
|
}
|
|
645
|
+
return { ...result, confidence };
|
|
168
646
|
}
|
|
169
|
-
|
|
647
|
+
/**
|
|
648
|
+
* Attempt to parse a JSON response, optionally wrapped in markdown code fences.
|
|
649
|
+
*/
|
|
650
|
+
function tryParseJSON(raw) {
|
|
651
|
+
// Extract JSON from response (may be wrapped in markdown code fences)
|
|
652
|
+
const jsonMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
653
|
+
const candidate = (jsonMatch ? jsonMatch[1] : raw).trim();
|
|
654
|
+
if (!candidate)
|
|
655
|
+
return null;
|
|
656
|
+
try {
|
|
657
|
+
const parsed = JSON.parse(candidate);
|
|
658
|
+
const validated = ReviewResponseSchema.safeParse(parsed);
|
|
659
|
+
if (!validated.success)
|
|
660
|
+
return null;
|
|
661
|
+
const d = validated.data;
|
|
662
|
+
return {
|
|
663
|
+
confidence: d.confidence,
|
|
664
|
+
blockingIssues: d.blocking_issues,
|
|
665
|
+
suggestions: d.suggestions,
|
|
666
|
+
requiredChanges: d.required_changes,
|
|
667
|
+
modelVote: d.vote,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Regex fallback parser for free-form text responses.
|
|
676
|
+
* Extracts vote, confidence, and issue lists from unstructured text.
|
|
677
|
+
*/
|
|
678
|
+
function parseRegexFallback(raw) {
|
|
679
|
+
// Extract vote (advisory only)
|
|
680
|
+
const voteMatch = raw.match(/\bVOTE:\s*(APPROVE|REJECT|CONDITIONAL)\b/i)
|
|
681
|
+
|| raw.match(/\b(APPROVE|REJECT|CONDITIONAL)\b/i);
|
|
682
|
+
const modelVote = voteMatch
|
|
683
|
+
? voteMatch[1].toUpperCase()
|
|
684
|
+
: null;
|
|
685
|
+
// Extract confidence (0-1 scale) — try multiple patterns
|
|
686
|
+
// Note: JSON keys have quotes ("confidence": 0.88), so patterns must handle optional quotes
|
|
687
|
+
let confidence = 0;
|
|
688
|
+
const confPatterns = [
|
|
689
|
+
/"?CONFIDENCE"?\s*:\s*(\d+\.?\d*)/i,
|
|
690
|
+
/"?[Cc]onfidence"?\s*(?:score)?[:\s]+(\d+\.?\d*)/,
|
|
691
|
+
/(\d+\.?\d*)\s*\/\s*1(?:\.0)?/,
|
|
692
|
+
];
|
|
693
|
+
for (const pattern of confPatterns) {
|
|
694
|
+
const match = raw.match(pattern);
|
|
695
|
+
if (match) {
|
|
696
|
+
const val = parseFloat(match[1]);
|
|
697
|
+
confidence = val > 1 ? val / 100 : val;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Fallback: CONSENSUS: XX% format (legacy adapter format)
|
|
702
|
+
if (confidence === 0) {
|
|
703
|
+
const consensusMatch = raw.match(/CONSENSUS:\s*(\d+)%/i);
|
|
704
|
+
if (consensusMatch)
|
|
705
|
+
confidence = parseInt(consensusMatch[1], 10) / 100;
|
|
706
|
+
}
|
|
707
|
+
// Extract issues — handle flexible section headings and tagged items
|
|
708
|
+
const blockingIssues = extractTaggedList(raw, 'BLOCKER')
|
|
709
|
+
.concat(extractSectionList(raw, 'BLOCKING.?ISSUES'));
|
|
710
|
+
const requiredChanges = extractTaggedList(raw, 'REQUIRED')
|
|
711
|
+
.concat(extractSectionList(raw, 'REQUIRED.?CHANGES'));
|
|
712
|
+
const suggestions = extractTaggedList(raw, 'SUGGESTION')
|
|
713
|
+
.concat(extractSectionList(raw, 'SUGGESTIONS', 'CONCERNS', 'RECOMMENDATIONS'));
|
|
170
714
|
return {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
715
|
+
confidence: Math.max(0, Math.min(1, confidence)),
|
|
716
|
+
blockingIssues: dedup(blockingIssues),
|
|
717
|
+
suggestions: dedup(suggestions),
|
|
718
|
+
requiredChanges: dedup(requiredChanges),
|
|
719
|
+
modelVote,
|
|
175
720
|
};
|
|
176
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Extract items prefixed with [TAG] from raw text.
|
|
724
|
+
* E.g. "[BLOCKER] SQL injection vulnerability" → "SQL injection vulnerability"
|
|
725
|
+
*/
|
|
726
|
+
function extractTaggedList(raw, tag) {
|
|
727
|
+
const regex = new RegExp(`\\[${tag}\\]\\s*:?\\s*(.+)`, 'gi');
|
|
728
|
+
const items = [];
|
|
729
|
+
let m;
|
|
730
|
+
while ((m = regex.exec(raw)) !== null)
|
|
731
|
+
items.push(m[1].trim());
|
|
732
|
+
return items;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Extract bullet items from a named section (flexible headings).
|
|
736
|
+
* Handles "BLOCKING ISSUES:", "BLOCKING_ISSUES:", "Blocking Issues:", etc.
|
|
737
|
+
*/
|
|
738
|
+
function extractSectionList(raw, ...patterns) {
|
|
739
|
+
for (const pat of patterns) {
|
|
740
|
+
const regex = new RegExp(`${pat}[:\\s]*\\n([\\s\\S]*?)(?=\\n(?:[A-Z][A-Z_\\s]+:|##)|$)`, 'i');
|
|
741
|
+
const match = raw.match(regex);
|
|
742
|
+
if (match) {
|
|
743
|
+
return match[1]
|
|
744
|
+
.split('\n')
|
|
745
|
+
.map(l => l.replace(/^[\s]*[-*]\s*/, '').replace(/^\d+\.\s*/, '').trim())
|
|
746
|
+
.filter(l => l.length > 0 && !/^none$/i.test(l));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Deduplicate a string array (case-insensitive).
|
|
753
|
+
*/
|
|
754
|
+
function dedup(items) {
|
|
755
|
+
const seen = new Set();
|
|
756
|
+
return items.filter(i => {
|
|
757
|
+
const key = i.toLowerCase().trim();
|
|
758
|
+
if (seen.has(key))
|
|
759
|
+
return false;
|
|
760
|
+
seen.add(key);
|
|
761
|
+
return true;
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
// ─── Confidence Contradiction Correction ─────────────────
|
|
765
|
+
/**
|
|
766
|
+
* Correct confidence when it contradicts the model's explicit vote.
|
|
767
|
+
*
|
|
768
|
+
* The prompt defines confidence as "plan quality score" (0-1) and
|
|
769
|
+
* assigns ranges: REJECT < 0.80, CONDITIONAL 0.80-0.94, APPROVE >= 0.95.
|
|
770
|
+
* Some models confuse this with "assessment certainty" and return e.g.
|
|
771
|
+
* REJECT + 0.99 ("99% sure it's bad"). This function inverts such
|
|
772
|
+
* contradictions so mapVote() receives a semantically correct input.
|
|
773
|
+
*
|
|
774
|
+
* Correction is SYMMETRIC across all three bands:
|
|
775
|
+
* - REJECT + conf >= 0.80 -> invert: min(0.79, 1 - conf)
|
|
776
|
+
* - CONDITIONAL + conf >= 0.95 -> snap to midpoint 0.87
|
|
777
|
+
* - CONDITIONAL + conf < 0.80 -> snap to midpoint 0.87
|
|
778
|
+
* - APPROVE + conf < 0.80 -> invert: max(0.95, 1 - conf)
|
|
779
|
+
* - APPROVE + conf in [0.80, 0.95) -> snap to 0.95
|
|
780
|
+
*
|
|
781
|
+
* If modelVote is null (regex fallback couldn't find a vote), no correction.
|
|
782
|
+
*/
|
|
783
|
+
export function correctConfidenceContradiction(modelVote, rawConfidence) {
|
|
784
|
+
if (modelVote === null) {
|
|
785
|
+
return { confidence: rawConfidence, wasContradiction: false, original: rawConfidence };
|
|
786
|
+
}
|
|
787
|
+
const c = Math.max(0, Math.min(1, rawConfidence));
|
|
788
|
+
// REJECT + confidence >= 0.80: model confused "certainty" with "quality"
|
|
789
|
+
// Invert, cap at 0.79 (top of REJECT range)
|
|
790
|
+
if (modelVote === 'REJECT' && c >= 0.80) {
|
|
791
|
+
const corrected = Math.min(0.79, 1.0 - c);
|
|
792
|
+
return { confidence: corrected, wasContradiction: true, original: c };
|
|
793
|
+
}
|
|
794
|
+
// CONDITIONAL outside its range [0.80, 0.95): snap to midpoint 0.87
|
|
795
|
+
if (modelVote === 'CONDITIONAL' && (c >= 0.95 || c < 0.80)) {
|
|
796
|
+
return { confidence: 0.87, wasContradiction: true, original: c };
|
|
797
|
+
}
|
|
798
|
+
// APPROVE + confidence < 0.80: model confused semantics
|
|
799
|
+
// Invert, floor at 0.95 (bottom of APPROVE range)
|
|
800
|
+
if (modelVote === 'APPROVE' && c < 0.80) {
|
|
801
|
+
const corrected = Math.max(0.95, 1.0 - c);
|
|
802
|
+
return { confidence: corrected, wasContradiction: true, original: c };
|
|
803
|
+
}
|
|
804
|
+
// APPROVE + confidence in [0.80, 0.95): slightly off, snap to 0.95
|
|
805
|
+
if (modelVote === 'APPROVE' && c < 0.95) {
|
|
806
|
+
return { confidence: 0.95, wasContradiction: true, original: c };
|
|
807
|
+
}
|
|
808
|
+
return { confidence: c, wasContradiction: false, original: c };
|
|
809
|
+
}
|
|
810
|
+
// ─── Arbitrator Response Parser (v2.4.3) ─────────────────
|
|
811
|
+
/**
|
|
812
|
+
* Zod schema for arbitrator JSON responses.
|
|
813
|
+
* Accepts both camelCase and snake_case for suggestedChanges.
|
|
814
|
+
*/
|
|
815
|
+
const ArbitratorResponseSchema = z.object({
|
|
816
|
+
approved: z.boolean(),
|
|
817
|
+
reasoning: z.string().optional(),
|
|
818
|
+
suggestedChanges: z.array(z.string()).default([]),
|
|
819
|
+
suggested_changes: z.array(z.string()).default([]),
|
|
820
|
+
});
|
|
821
|
+
/**
|
|
822
|
+
* Parse raw arbitrator response into a structured result.
|
|
823
|
+
* Strategy 1: JSON parse (optionally wrapped in code fences).
|
|
824
|
+
* Strategy 2: Regex fallback for free-form text.
|
|
825
|
+
*
|
|
826
|
+
* @param raw - Raw text from the arbitrator LLM
|
|
827
|
+
* @returns Parsed result with approved boolean, reasoning, and suggested changes
|
|
828
|
+
*/
|
|
829
|
+
export function parseArbitratorResponse(raw) {
|
|
830
|
+
// Strategy 1: JSON parse (with optional code fence wrapping)
|
|
831
|
+
const jsonMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
832
|
+
const candidate = (jsonMatch ? jsonMatch[1] : raw).trim();
|
|
833
|
+
try {
|
|
834
|
+
const parsed = JSON.parse(candidate);
|
|
835
|
+
const validated = ArbitratorResponseSchema.safeParse(parsed);
|
|
836
|
+
if (validated.success) {
|
|
837
|
+
const data = validated.data;
|
|
838
|
+
return {
|
|
839
|
+
approved: data.approved,
|
|
840
|
+
reasoning: data.reasoning ?? '',
|
|
841
|
+
suggestedChanges: [
|
|
842
|
+
...(data.suggestedChanges ?? []),
|
|
843
|
+
...(data.suggested_changes ?? []),
|
|
844
|
+
],
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch { /* fall through to regex */ }
|
|
849
|
+
// Strategy 2: Regex fallback for free-form text
|
|
850
|
+
let approved = false;
|
|
851
|
+
const approvedMatch = raw.match(/approved\s*[:=]\s*(true|false)/i) ??
|
|
852
|
+
raw.match(/\b(approve|approved|accept|accepted)\b/i) ??
|
|
853
|
+
raw.match(/\b(reject|rejected|deny|denied)\b/i);
|
|
854
|
+
if (approvedMatch) {
|
|
855
|
+
const val = approvedMatch[1].toLowerCase();
|
|
856
|
+
approved = ['true', 'approve', 'approved', 'accept', 'accepted'].includes(val);
|
|
857
|
+
}
|
|
858
|
+
const changes = [];
|
|
859
|
+
const changeMatches = raw.matchAll(/(?:^|\n)\s*[-*\d.]+\s+(.+)/g);
|
|
860
|
+
for (const m of changeMatches)
|
|
861
|
+
changes.push(m[1].trim());
|
|
862
|
+
return { approved, reasoning: raw.slice(0, 2000), suggestedChanges: changes };
|
|
863
|
+
}
|
|
177
864
|
// ─── Prompt Builder ──────────────────────────────────────
|
|
178
|
-
export function buildReviewPrompt(planPacket) {
|
|
865
|
+
export function buildReviewPrompt(planPacket, planContent, revisionDirective) {
|
|
179
866
|
const lines = [
|
|
180
867
|
`# Independent Plan Review`,
|
|
181
868
|
``,
|
|
@@ -190,16 +877,99 @@ export function buildReviewPrompt(planPacket) {
|
|
|
190
877
|
...planPacket.constraints.map((c) => `- [${c.type}] ${c.description}`),
|
|
191
878
|
``,
|
|
192
879
|
];
|
|
880
|
+
// Render plan content (loaded from disk by caller)
|
|
881
|
+
if (planContent && planContent.trim().length > 0) {
|
|
882
|
+
lines.push(`## Plan Content`, ``, planContent, ``);
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
lines.push(`## Plan Content`, ``, `[WARNING: Plan content could not be loaded. Review based on metadata only.]`, ``);
|
|
886
|
+
}
|
|
193
887
|
if (planPacket.open_questions?.length) {
|
|
194
888
|
lines.push(`## Open Questions`);
|
|
195
889
|
lines.push(...planPacket.open_questions.map((q) => `- ${q}`));
|
|
196
890
|
lines.push('');
|
|
197
891
|
}
|
|
198
|
-
lines.push(`## Review Instructions`, ``, `You are an independent reviewer. Evaluate this plan for:`, `1. Completeness — are all required artifacts defined?`, `2. Consistency — do acceptance criteria match constraints?`, `3. Feasibility — can this be implemented as described?`, `4. Constitution compliance — does it follow governance rules?`, ``, `
|
|
892
|
+
lines.push(`## Review Instructions`, ``, `You are an independent reviewer. Evaluate this plan for:`, `1. Completeness — are all required artifacts defined?`, `2. Consistency — do acceptance criteria match constraints?`, `3. Feasibility — can this be implemented as described?`, `4. Constitution compliance — does it follow governance rules?`, ``, `## Scoring Guide`, ``, `The "confidence" field represents your assessment of PLAN QUALITY, NOT how certain you are about your review.`, `It answers: "How ready is this plan for execution on a scale of 0.00 to 1.00?"`, ``, `- confidence 0.95-1.00 (vote APPROVE): The plan is EXECUTION-READY as-is.`, `- confidence 0.80-0.94 (vote CONDITIONAL): The plan needs specific changes before execution.`, `- confidence below 0.80 (vote REJECT): The plan has fundamental issues.`, ``, `CRITICAL: Your vote and confidence MUST be consistent:`, ` - REJECT requires confidence below 0.80`, ` - CONDITIONAL requires confidence between 0.80 and 0.94`, ` - APPROVE requires confidence 0.95 or above`, `Do NOT use confidence to express how certain you are of your assessment.`, `A REJECT with confidence 0.99 is INVALID -- it implies the plan is 99% ready while rejecting it.`, `Mismatched vote+confidence will be auto-corrected by the system.`, ``, `IMPORTANT: "Execution-ready" means a competent developer could implement this plan successfully, not that the plan is theoretically perfect. Reserve CONDITIONAL for changes that would cause implementation to fail or produce incorrect results, not style preferences.`, ``, `## Output Format for Issues`, `- Prefix blocking issues with [BLOCKER]: items that MUST be fixed before approval`, `- Prefix required changes with [REQUIRED]: items that need changes but are not deal-breakers`, `- Prefix suggestions with [SUGGESTION]: nice-to-have improvements`, ``, `IMPORTANT: If your vote is APPROVE or CONDITIONAL, do NOT list [BLOCKER] items.`, `[BLOCKER] items are only valid with a REJECT vote.`, ``, `## Response Format`, ``, `Return ONLY a JSON object matching this schema:`, ``, '```json', `{`, ` "vote": "APPROVE" | "CONDITIONAL" | "REJECT",`, ` "confidence": 0.00, // Plan quality score, NOT review certainty`, ` "blocking_issues": ["[BLOCKER] ..."],`, ` "required_changes": ["[REQUIRED] ..."],`, ` "suggestions": ["[SUGGESTION] ..."],`, ` "analysis": "Your detailed analysis here"`, `}`, '```', ``, `### Examples of VALID responses:`, `- APPROVE with confidence 0.97: "Plan is solid, minor style nits only"`, `- CONDITIONAL with confidence 0.85: "Need to add error handling for X"`, `- REJECT with confidence 0.45: "Missing entire auth layer, unclear data model"`, ``, `### Examples of INVALID responses (will be auto-corrected):`, `- REJECT with confidence 0.99: This means "plan is 99% ready" while rejecting it`, `- APPROVE with confidence 0.60: This means "plan has issues" while approving it`, ``, `Confidence = plan quality score (NOT review certainty):`, `- 0.95-1.00: APPROVE range -- plan is execution-ready`, `- 0.80-0.94: CONDITIONAL range -- specific changes needed`, `- Below 0.80: REJECT range -- fundamental issues`, ``, `Your vote and confidence MUST fall in the same range. Mismatches will be auto-corrected.`, ``, `If vote is APPROVE: blocking_issues and required_changes must be empty arrays.`, `If vote is CONDITIONAL: blocking_issues must be empty, use required_changes.`, `If vote is REJECT: use blocking_issues for critical issues.`);
|
|
893
|
+
// v2.4.2: Add revision notice + prior feedback for plan revisions
|
|
894
|
+
if (planPacket.metadata.version > 1) {
|
|
895
|
+
lines.push(``, `## Revision Notice`, ``, `This is revision ${planPacket.metadata.version} of the plan.`, `Prioritize verifying whether prior issues have been adequately addressed.`, `Also flag any new *critical* issues you discover.`, ``);
|
|
896
|
+
}
|
|
897
|
+
if (revisionDirective && revisionDirective.trim().length > 0) {
|
|
898
|
+
const trimmed = revisionDirective.trim();
|
|
899
|
+
const capped = trimmed.length > 2000
|
|
900
|
+
? trimmed.slice(0, 2000) + '\n\n[TRUNCATED -- full directive exceeds 2000 chars]'
|
|
901
|
+
: trimmed;
|
|
902
|
+
lines.push(`## Prior Feedback (Must Address)`, ``, capped, ``, `Confirm each item above is addressed or explain why it is not applicable.`, ``);
|
|
903
|
+
}
|
|
199
904
|
return lines.join('\n');
|
|
200
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Build arbitration prompt with reviewer feedback context.
|
|
908
|
+
*/
|
|
909
|
+
function buildArbitrationPrompt(planPacket, votes, planContent) {
|
|
910
|
+
const voteSummary = votes.map((v, i) => {
|
|
911
|
+
const parts = [
|
|
912
|
+
`### Reviewer ${i + 1} (${v.provider}/${v.model})`,
|
|
913
|
+
`Vote: ${v.vote} (confidence: ${v.confidence.toFixed(2)})`,
|
|
914
|
+
];
|
|
915
|
+
if (v.blocking_issues.length > 0) {
|
|
916
|
+
parts.push(`Blocking: ${v.blocking_issues.join('; ')}`);
|
|
917
|
+
}
|
|
918
|
+
if (v.required_changes?.length) {
|
|
919
|
+
parts.push(`Required changes: ${v.required_changes.join('; ')}`);
|
|
920
|
+
}
|
|
921
|
+
if (v.suggestions.length > 0) {
|
|
922
|
+
parts.push(`Suggestions: ${v.suggestions.join('; ')}`);
|
|
923
|
+
}
|
|
924
|
+
return parts.join('\n');
|
|
925
|
+
}).join('\n\n');
|
|
926
|
+
const planSection = (planContent && planContent.trim().length > 0)
|
|
927
|
+
? [`## Plan Content`, ``, planContent, ``]
|
|
928
|
+
: [`## Plan Content`, ``, `[WARNING: Plan content could not be loaded.]`, ``];
|
|
929
|
+
return [
|
|
930
|
+
`# Arbitration Request`,
|
|
931
|
+
``,
|
|
932
|
+
`## Phase: ${planPacket.metadata.phase}`,
|
|
933
|
+
`## Plan Version: ${planPacket.metadata.version}`,
|
|
934
|
+
``,
|
|
935
|
+
...planSection,
|
|
936
|
+
`## Reviewer Votes`,
|
|
937
|
+
voteSummary,
|
|
938
|
+
``,
|
|
939
|
+
`## Instructions`,
|
|
940
|
+
`The reviewers above could not reach consensus. As arbitrator:`,
|
|
941
|
+
`1. Analyze the disagreement points`,
|
|
942
|
+
`2. Determine if the plan is execution-ready with minor amendments`,
|
|
943
|
+
`3. If approving, provide specific suggestedChanges that address each required_change`,
|
|
944
|
+
`4. If the issues are fundamental, do NOT approve`,
|
|
945
|
+
``,
|
|
946
|
+
`Provide your decision as: approved (true/false), reasoning, and suggestedChanges array.`,
|
|
947
|
+
].join('\n');
|
|
948
|
+
}
|
|
201
949
|
// ─── Factory ─────────────────────────────────────────────
|
|
202
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Helper to resolve model string for a given provider from consensus config.
|
|
952
|
+
*/
|
|
953
|
+
export function getModelForProvider(config, provider) {
|
|
954
|
+
if (!config)
|
|
955
|
+
return provider === 'openai' ? 'gpt-4.1' : provider === 'gemini' ? 'gemini-2.5-flash' : 'grok-3';
|
|
956
|
+
switch (provider) {
|
|
957
|
+
case 'openai': return config.openaiModel ?? 'gpt-4.1';
|
|
958
|
+
case 'gemini': return config.geminiModel ?? 'gemini-2.5-flash';
|
|
959
|
+
case 'grok': return config.grokModel ?? 'grok-3';
|
|
960
|
+
default: return 'gpt-4.1';
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
export function createConsensusRunner(projectDir, consensusConfig, skillLoader, skillUsageRegistry) {
|
|
964
|
+
// Wire arbitration from consensus config
|
|
965
|
+
const enableArbitration = consensusConfig?.enableArbitration !== false;
|
|
966
|
+
const arbitratorProvider = enableArbitration
|
|
967
|
+
? {
|
|
968
|
+
provider: consensusConfig?.arbitrator ?? 'gemini',
|
|
969
|
+
model: getModelForProvider(consensusConfig, consensusConfig?.arbitrator ?? 'gemini'),
|
|
970
|
+
temperature: 0.2,
|
|
971
|
+
}
|
|
972
|
+
: undefined;
|
|
203
973
|
return new ConsensusRunner({
|
|
204
974
|
mode: 'independent',
|
|
205
975
|
minReviewers: 2,
|
|
@@ -207,6 +977,10 @@ export function createConsensusRunner(projectDir, consensusConfig) {
|
|
|
207
977
|
quorum: 2,
|
|
208
978
|
projectDir,
|
|
209
979
|
consensusConfig,
|
|
980
|
+
arbitratorProvider,
|
|
981
|
+
enableArbitration,
|
|
982
|
+
skillLoader,
|
|
983
|
+
skillUsageRegistry,
|
|
210
984
|
});
|
|
211
985
|
}
|
|
212
986
|
//# sourceMappingURL=consensus-runner.js.map
|