@fiale-plus/pi-rogue 0.2.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/README.md +50 -0
- package/node_modules/@fiale-plus/pi-core/README.md +13 -0
- package/node_modules/@fiale-plus/pi-core/package.json +25 -0
- package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
- package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
- package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
- package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
- package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
- package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
- package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
- package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +749 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
- package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
- package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
- package/package.json +51 -0
- package/src/context-broker-file.ts +1 -0
- package/src/context-broker-sqlite.ts +1 -0
- package/src/context-broker.ts +1 -0
- package/src/extension.test.ts +68 -0
- package/src/extension.ts +27 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { appendText, featureFile, truncate } from "./internal.js";
|
|
6
|
+
import { extractBinaryGateFeatureCounts } from "./binary-gate-features.js";
|
|
7
|
+
|
|
8
|
+
export type AdvisorPhase = "preflight" | "review" | "closeout";
|
|
9
|
+
export type PreflightLabel = "continue" | "escalate_to_advisor" | "need_more_context" | "low_confidence";
|
|
10
|
+
export type ReviewLabel = "on_track" | "course_correct" | "not_done" | "abstain";
|
|
11
|
+
export type RouterSource = "heuristic" | "model" | "llm";
|
|
12
|
+
export type PreflightPolicy = "off" | "light" | "full" | "direct";
|
|
13
|
+
export type ReviewPolicy = "off" | "light" | "strict";
|
|
14
|
+
|
|
15
|
+
export interface AdvisorRouteInput {
|
|
16
|
+
phase: AdvisorPhase;
|
|
17
|
+
text: string;
|
|
18
|
+
brief?: string;
|
|
19
|
+
fileChanged?: boolean;
|
|
20
|
+
failed?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AdvisorRouteDecision {
|
|
24
|
+
phase: AdvisorPhase;
|
|
25
|
+
label: PreflightLabel | ReviewLabel;
|
|
26
|
+
confidence: number;
|
|
27
|
+
reason: string;
|
|
28
|
+
source: RouterSource;
|
|
29
|
+
preflight: PreflightPolicy;
|
|
30
|
+
review: ReviewPolicy;
|
|
31
|
+
escalate: boolean;
|
|
32
|
+
safety: boolean;
|
|
33
|
+
promptHash: string;
|
|
34
|
+
promptSummary: string;
|
|
35
|
+
briefSummary?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RouterResponse {
|
|
39
|
+
label: PreflightLabel | ReviewLabel;
|
|
40
|
+
confidence?: number;
|
|
41
|
+
reason?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ROUTER_LOG_PATH = featureFile("advisor", "evals/advisor-router.jsonl");
|
|
45
|
+
const ROUTER_VERSION = 1;
|
|
46
|
+
|
|
47
|
+
// ── Binary gate model (trained from local session data) ──────────────────
|
|
48
|
+
const BINARY_GATE_PATH = featureFile("advisor", "binary-gate-model.json");
|
|
49
|
+
const BINARY_GATE_SOURCE_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../assets/binary-gate-model.json");
|
|
50
|
+
const BINARY_GATE_THRESHOLD = 0.55;
|
|
51
|
+
|
|
52
|
+
interface BinaryGateModel {
|
|
53
|
+
kind: string;
|
|
54
|
+
labels: string[];
|
|
55
|
+
features: string[];
|
|
56
|
+
idf: number[];
|
|
57
|
+
bias: number[];
|
|
58
|
+
weights: number[][];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let _binaryGateCache: BinaryGateModel | null | undefined = undefined;
|
|
62
|
+
|
|
63
|
+
function ensureBinaryGateSeeded(): void {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(BINARY_GATE_SOURCE_PATH)) return;
|
|
66
|
+
const sourceStat = statSync(BINARY_GATE_SOURCE_PATH);
|
|
67
|
+
if (existsSync(BINARY_GATE_PATH)) {
|
|
68
|
+
const installedStat = statSync(BINARY_GATE_PATH);
|
|
69
|
+
if (installedStat.mtimeMs >= sourceStat.mtimeMs && installedStat.size === sourceStat.size) return;
|
|
70
|
+
}
|
|
71
|
+
mkdirSync(dirname(BINARY_GATE_PATH), { recursive: true });
|
|
72
|
+
copyFileSync(BINARY_GATE_SOURCE_PATH, BINARY_GATE_PATH);
|
|
73
|
+
} catch {
|
|
74
|
+
// best effort: if the seed copy fails, fall back to the installed path if present
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadBinaryGate(): BinaryGateModel | null {
|
|
79
|
+
if (_binaryGateCache !== undefined) return _binaryGateCache;
|
|
80
|
+
try {
|
|
81
|
+
ensureBinaryGateSeeded();
|
|
82
|
+
if (!existsSync(BINARY_GATE_PATH)) return null;
|
|
83
|
+
_binaryGateCache = JSON.parse(readFileSync(BINARY_GATE_PATH, "utf8")) as BinaryGateModel;
|
|
84
|
+
if (_binaryGateCache.kind !== "binary-logreg-v1") { _binaryGateCache = null; return null; }
|
|
85
|
+
return _binaryGateCache;
|
|
86
|
+
} catch { _binaryGateCache = null; return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function binaryGateFeatures(text: string, model: BinaryGateModel) {
|
|
90
|
+
const counts = extractBinaryGateFeatureCounts(text);
|
|
91
|
+
const index = new Map(model.features.map((f, i) => [f, i]));
|
|
92
|
+
const pairs: Array<[number, number]> = [];
|
|
93
|
+
let nrm = 0;
|
|
94
|
+
for (const [feature, tf] of counts) {
|
|
95
|
+
const idx = index.get(feature);
|
|
96
|
+
if (idx === undefined) continue;
|
|
97
|
+
const value = (1 + Math.log(tf)) * model.idf[idx];
|
|
98
|
+
pairs.push([idx, value]);
|
|
99
|
+
nrm += value * value;
|
|
100
|
+
}
|
|
101
|
+
const scale = nrm > 0 ? 1 / Math.sqrt(nrm) : 1;
|
|
102
|
+
pairs.sort((a, b) => a[0] - b[0]);
|
|
103
|
+
return { I: pairs.map(([i]) => i), V: pairs.map(([, v]) => v * scale) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function binaryGatePredict(text: string): { decision: "continue" | "escalate"; confidence: number } | null {
|
|
107
|
+
const model = loadBinaryGate();
|
|
108
|
+
if (!model) return null;
|
|
109
|
+
const vec = binaryGateFeatures(text, model);
|
|
110
|
+
const scores = model.bias.slice();
|
|
111
|
+
for (let c = 0; c < model.weights.length; c++) {
|
|
112
|
+
let score = scores[c]; const w = model.weights[c];
|
|
113
|
+
for (let i = 0; i < vec.I.length; i++) score += w[vec.I[i]] * vec.V[i];
|
|
114
|
+
scores[c] = score;
|
|
115
|
+
}
|
|
116
|
+
const maxS = Math.max(...scores);
|
|
117
|
+
const exps = scores.map((v) => Math.exp(v - maxS));
|
|
118
|
+
const sum = exps.reduce((a, b) => a + b, 0) || 1;
|
|
119
|
+
const probs = exps.map((v) => v / sum);
|
|
120
|
+
const idx = probs[0] >= probs[1] ? 0 : 1;
|
|
121
|
+
return { decision: model.labels[idx] as "continue" | "escalate", confidence: probs[idx] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const QUICK_EDIT_RE = /\b(quick edit|small edit|tiny edit|rename|format(?:ting)?|lint|style|doc(?:s)?|comment|typo|readme|spell|spacing|cleanup|one[- ]?liner)\b/i;
|
|
125
|
+
const ROUTINE_CLEANUP_RE = /\b(routine docs?|docs? and formatting|formatting cleanup|generated changes|large diff|docs?\/formatting)\b/i;
|
|
126
|
+
const COMPLEX_RE = /\b(architecture|architectural|refactor|design|trade[- ]?off|concurrency|security|auth|migration|performance|scale|scalability|framework|system design|schema|data model|protocol|advisor routing|advisor flow|router logic|call vs skip|skip vs call|compare|recommend|benchmark|evaluate|experiment|train|strategy|choose|make sense|worth(?: it)?|kpi|kpis|how it works|where it comes from|what would you choose|what do you think|next step|pick between|buy|usage|sustained speed|available models|running model kpis)\b/i;
|
|
127
|
+
const DEBUG_RE = /\b(debug|bug|error|stack trace|traceback|fail(?:ed|ure)?|broken|investigate|why is|cannot|can't|crash|regression)\b/i;
|
|
128
|
+
const CONTEXT_RE = /\b(need more context|missing context|clarify|not enough info|unspecified|unknown|ambiguous)\b/i;
|
|
129
|
+
const SAFETY_RE = /\b(rm\s+-rf|sudo\b|shutdown\b|reboot\b|mkfs(?:\.[\w-]+)?\b|chmod\s+-R\b|chown\b|git\s+push\b[\s\S]*--force(?:-with-lease)?|curl\b[\s\S]*\|\s*(?:sh|bash)\b|wget\b[\s\S]*\|\s*(?:sh|bash)\b|drop\s+table\b|delete\s+database\b|credential\b|password\b|secret\b)\b/i;
|
|
130
|
+
const COMPACTION_RE = /\b(compact(?:ed|ion)?|missing history|history might flip|prior constraint|resume(?:d)? after compaction)\b/i;
|
|
131
|
+
const REASSURANCE_RE = /\b(reassurance|confidence|increase confidence|already know the likely answer|just for reassurance|main model already gives a solid answer|solid answer)\b/i;
|
|
132
|
+
const CHECKPOINT_RE = /\b(checkpoint|multi-step implementation|clearer boundary|interrupt now|wait until there is a clearer boundary|mid implementation)\b/i;
|
|
133
|
+
const CHEAP_SIGNAL_RE = /\b(cheap extra signal|exact diff plus exact error|exact diff|exact error|recent error in the session history|recent history)\b/i;
|
|
134
|
+
const CLOSEOUT_RE = /\b(closeout|on[- ]?track|course[- ]?correct|not[- ]?done|should this be marked|mostly done|mostly complete|needs changes before closeout|needs changes|needs correction|review judgment)\b/i;
|
|
135
|
+
const REVIEW_NEEDS_WORK_RE = /\b(todo|wip|incomplete|missing|broken|fails?|error|bug|revise|adjust|fix(?:ed)?\s+needed|not done|still open|needs changes|needs correction|course[- ]?correct)\b/i;
|
|
136
|
+
const DONE_RE = /\b(done|complete(?:d)?|fixed|implemented|works?|passing tests?|tests pass|verified|looks good|merged)\b/i;
|
|
137
|
+
|
|
138
|
+
function squish(text: unknown, max = 220): string {
|
|
139
|
+
const value = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
140
|
+
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hashText(...parts: string[]): string {
|
|
144
|
+
return createHash("sha256").update(parts.join("||")).digest("hex").slice(0, 16);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clampConfidence(value: unknown, fallback: number): number {
|
|
148
|
+
const n = Number(value);
|
|
149
|
+
if (!Number.isFinite(n)) return fallback;
|
|
150
|
+
return Math.max(0, Math.min(1, n));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizePhaseLabel(phase: AdvisorPhase, label: unknown): PreflightLabel | ReviewLabel | null {
|
|
154
|
+
const value = String(label ?? "").trim();
|
|
155
|
+
if (phase === "preflight") {
|
|
156
|
+
return value === "continue" || value === "escalate_to_advisor" || value === "need_more_context" || value === "low_confidence"
|
|
157
|
+
? value
|
|
158
|
+
: null;
|
|
159
|
+
}
|
|
160
|
+
return value === "on_track" || value === "course_correct" || value === "not_done" || value === "abstain"
|
|
161
|
+
? value
|
|
162
|
+
: null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function preflightPolicy(label: PreflightLabel): PreflightPolicy {
|
|
166
|
+
switch (label) {
|
|
167
|
+
case "continue": return "off";
|
|
168
|
+
case "escalate_to_advisor": return "full";
|
|
169
|
+
case "need_more_context": return "direct";
|
|
170
|
+
case "low_confidence": return "light";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reviewPolicy(label: ReviewLabel): ReviewPolicy {
|
|
175
|
+
switch (label) {
|
|
176
|
+
case "on_track": return "off";
|
|
177
|
+
case "course_correct": return "light";
|
|
178
|
+
case "not_done": return "strict";
|
|
179
|
+
case "abstain": return "off";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const TOKEN_ACTION_RE = /^(?:revoke|revok|rotate|rotat|reset|invalidat|regenerat|regenerate|exfiltrate|exfiltrat|expos|expose|hardcod|hardcode|paste|share|send|commit|storing|store|stored|stor|delete|delet|remove|remov|print|dump|disclos|disclose|copi|copy|export|import|leak)(?:ed|ing|s|es|e|ion|ions)?$/;
|
|
184
|
+
const TOKEN_DIRECT_ACTION_RE = /^(?:revoke|revok|rotate|rotat|reset|invalidat|regenerat|regenerate|exfiltrate|exfiltrat|expos|expose|hardcod|hardcode|paste|share|send|delet|delete|copi|copy|export|import|remove|stor|store|commit|print|dump|disclos|disclose|leak)(?:ed|ing|s|es|e)?$/;
|
|
185
|
+
const TOKEN_DIRECT_ACTION_REQUIRES_CONTEXT_PREFIXES = ["copy", "export", "import", "remove", "store"];
|
|
186
|
+
const TOKEN_CONTEXT_PREFIXES = ["api", "access", "hf", "hugging", "face", "github", "gitlab", "secret", "credential", "personal", "pat", "oauth", "bearer", "auth", "env", "environment", "dotenv", "openai", "anthropic", "azure", "aws", "gcp", "service", "compromis", "compromised", "stale", "leaked", "exposed", "exposure", "key"];
|
|
187
|
+
const HISTORICAL_TOKEN_RE = /\b(previously|prior|history|historical|thread|earlier)\b/i;
|
|
188
|
+
|
|
189
|
+
function isSafetySensitive(text: string): boolean {
|
|
190
|
+
const lower = String(text ?? "").toLowerCase();
|
|
191
|
+
const hasTokenWord = /\btokens?\b/i.test(lower);
|
|
192
|
+
if (!SAFETY_RE.test(text) && !hasTokenWord) return false;
|
|
193
|
+
|
|
194
|
+
const hasNonTokenSafetySignal = /\b(rm\s+-rf|sudo|shutdown|reboot|mkfs|chown|chmod\s+-R|git\s+push\b[\s\S]*--force(?:-with-lease)?|curl\b[\s\S]*\|\s*(?:sh|bash)\b|wget\b[\s\S]*\|\s*(?:sh|bash)\b|drop\s+table|delete\s+database|credential|password|secret)\b/i.test(lower);
|
|
195
|
+
if (SAFETY_RE.test(text) && hasNonTokenSafetySignal) return true;
|
|
196
|
+
|
|
197
|
+
if (!hasTokenWord) return true;
|
|
198
|
+
|
|
199
|
+
const hasTokenAction = hasTokenCredentialAction(lower);
|
|
200
|
+
if (!hasTokenAction) return false;
|
|
201
|
+
|
|
202
|
+
const historicalTokenMention = isHistoricalTokenMention(lower);
|
|
203
|
+
if (!historicalTokenMention) return true;
|
|
204
|
+
|
|
205
|
+
return hasActiveHistoricalTokenAction(lower);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function hasTokenCredentialAction(lower: string): boolean {
|
|
209
|
+
return detectTokenCredentialAction(lower).hasAny;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function hasActiveHistoricalTokenAction(lower: string): boolean {
|
|
213
|
+
return detectTokenCredentialAction(lower).hasActiveDirect;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function detectTokenCredentialAction(lower: string): { hasAny: boolean; hasActiveDirect: boolean } {
|
|
217
|
+
if (!/\btokens?\b/i.test(lower)) return { hasAny: false, hasActiveDirect: false };
|
|
218
|
+
|
|
219
|
+
const words = lower.match(/[a-z0-9_]+/g) ?? [];
|
|
220
|
+
const tokenIndexes = words.reduce<number[]>((acc, word, index) => {
|
|
221
|
+
if (/^tokens?$/.test(word)) acc.push(index);
|
|
222
|
+
return acc;
|
|
223
|
+
}, []);
|
|
224
|
+
if (!tokenIndexes.length) return { hasAny: false, hasActiveDirect: false };
|
|
225
|
+
|
|
226
|
+
const wordWindow = 8;
|
|
227
|
+
const tokenOnlyWord = tokenIndexes.length === 1 && words.length === 1;
|
|
228
|
+
if (tokenOnlyWord) return { hasAny: false, hasActiveDirect: false };
|
|
229
|
+
|
|
230
|
+
let hasAny = false;
|
|
231
|
+
let hasActiveDirect = false;
|
|
232
|
+
|
|
233
|
+
for (const tokenIndex of tokenIndexes) {
|
|
234
|
+
const start = Math.max(0, tokenIndex - wordWindow);
|
|
235
|
+
const end = Math.min(words.length, tokenIndex + wordWindow + 1);
|
|
236
|
+
let hasContextualAction = false;
|
|
237
|
+
let hasContext = false;
|
|
238
|
+
let hasPotentialActiveContextualAction = false;
|
|
239
|
+
|
|
240
|
+
for (let i = start; i < end; i++) {
|
|
241
|
+
const word = words[i];
|
|
242
|
+
const isRotationNoun = word === "rotation";
|
|
243
|
+
if (TOKEN_DIRECT_ACTION_RE.test(word)) {
|
|
244
|
+
const requiresContext = TOKEN_DIRECT_ACTION_REQUIRES_CONTEXT_PREFIXES.some((prefix) => word.startsWith(prefix));
|
|
245
|
+
if (requiresContext) {
|
|
246
|
+
hasContextualAction = true;
|
|
247
|
+
if (!word.endsWith("ed")) hasPotentialActiveContextualAction = true;
|
|
248
|
+
if (hasContext && !word.endsWith("ed")) {
|
|
249
|
+
hasAny = true;
|
|
250
|
+
hasActiveDirect = true;
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
hasAny = true;
|
|
256
|
+
if (!word.endsWith("ed")) hasActiveDirect = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (isRotationNoun) {
|
|
260
|
+
hasContextualAction = true;
|
|
261
|
+
if (hasContext) {
|
|
262
|
+
hasAny = true;
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (TOKEN_ACTION_RE.test(word)) hasContextualAction = true;
|
|
267
|
+
if (TOKEN_CONTEXT_PREFIXES.some((prefix) => word.startsWith(prefix))) hasContext = true;
|
|
268
|
+
if (hasContext && hasContextualAction) {
|
|
269
|
+
hasAny = true;
|
|
270
|
+
if (hasPotentialActiveContextualAction) hasActiveDirect = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { hasAny, hasActiveDirect };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isHistoricalTokenMention(lower: string): boolean {
|
|
279
|
+
if (!/\btokens?\b/i.test(lower)) return false;
|
|
280
|
+
if (!HISTORICAL_TOKEN_RE.test(lower)) return false;
|
|
281
|
+
return /\b(?:hf|hugging\s*face)\b/i.test(lower);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function hasQuickEditSignal(text: string): boolean {
|
|
285
|
+
return QUICK_EDIT_RE.test(text);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function hasRoutineCleanupSignal(text: string): boolean {
|
|
289
|
+
return ROUTINE_CLEANUP_RE.test(text);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function hasComplexSignal(text: string): boolean {
|
|
293
|
+
return COMPLEX_RE.test(text) || DEBUG_RE.test(text);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasCompactionLowRiskSignal(text: string): boolean {
|
|
297
|
+
return COMPACTION_RE.test(text) && /\blow[- ]?risk\b/i.test(text);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasReassuranceOnlySignal(text: string): boolean {
|
|
301
|
+
return REASSURANCE_RE.test(text) && !/\b(material|flip|decision|risk|uncertain|flip the decision)\b/i.test(text);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hasCheckpointSignal(text: string): boolean {
|
|
305
|
+
return CHECKPOINT_RE.test(text) && !/\b(risky|security|irreversible|unknown dependency|hidden dependency)\b/i.test(text);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasCheapSignalMaterialSignal(text: string): boolean {
|
|
309
|
+
return CHEAP_SIGNAL_RE.test(text) && /\b(materially|flip the decision|decision-changing|materially changes|could change|main model is useless|main model already gives a solid answer|solid answer)\b/i.test(text);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hasCheapSignalIrrelevantSignal(text: string): boolean {
|
|
313
|
+
return CHEAP_SIGNAL_RE.test(text) && /\b(should not change|shouldn'?t change|not materially|already gives a solid answer|solid answer)\b/i.test(text);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function needsContext(text: string): boolean {
|
|
317
|
+
return CONTEXT_RE.test(text) || text.trim().length < 18 || text.trim().split(/\s+/).length < 4;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function reviewSignals(input: AdvisorRouteInput): { label: ReviewLabel; confidence: number; reason: string } {
|
|
321
|
+
const text = `${input.text}\n${input.brief ?? ""}`.trim();
|
|
322
|
+
if (input.failed) {
|
|
323
|
+
return { label: "not_done", confidence: 0.95, reason: "Turn reported failure." };
|
|
324
|
+
}
|
|
325
|
+
if (input.phase === "closeout" && CLOSEOUT_RE.test(text)) {
|
|
326
|
+
return { label: /\bnot[- ]?done\b/i.test(text) ? "not_done" : "course_correct", confidence: 0.86, reason: "Closeout judgment requested." };
|
|
327
|
+
}
|
|
328
|
+
if (REVIEW_NEEDS_WORK_RE.test(text)) {
|
|
329
|
+
return { label: /\b(not done|incomplete|wip|todo|still open)\b/i.test(text) ? "not_done" : "course_correct", confidence: 0.83, reason: "Needs-work signal detected." };
|
|
330
|
+
}
|
|
331
|
+
if (input.fileChanged && DONE_RE.test(text)) {
|
|
332
|
+
return { label: "on_track", confidence: 0.84, reason: "Change looks complete." };
|
|
333
|
+
}
|
|
334
|
+
if (DONE_RE.test(text)) {
|
|
335
|
+
return { label: "on_track", confidence: 0.72, reason: "Completion signal detected." };
|
|
336
|
+
}
|
|
337
|
+
return { label: "abstain", confidence: 0.56, reason: "Insufficient review signal." };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function preflightSignals(input: AdvisorRouteInput): { label: PreflightLabel; confidence: number; reason: string; safety: boolean } {
|
|
341
|
+
const text = `${input.text}\n${input.brief ?? ""}`.trim();
|
|
342
|
+
if (isSafetySensitive(text)) {
|
|
343
|
+
return { label: "escalate_to_advisor", confidence: 0.98, reason: "Safety-sensitive keywords detected.", safety: true };
|
|
344
|
+
}
|
|
345
|
+
if (hasRoutineCleanupSignal(text) || (hasQuickEditSignal(text) && !hasComplexSignal(text))) {
|
|
346
|
+
return { label: "continue", confidence: 0.9, reason: "Small-edit or routine-cleanup signal detected.", safety: false };
|
|
347
|
+
}
|
|
348
|
+
if (hasCompactionLowRiskSignal(text)) {
|
|
349
|
+
return { label: "continue", confidence: 0.84, reason: "Low-risk compaction boundary; advisor not needed.", safety: false };
|
|
350
|
+
}
|
|
351
|
+
if (hasReassuranceOnlySignal(text) || hasCheckpointSignal(text) || hasCheapSignalIrrelevantSignal(text)) {
|
|
352
|
+
return { label: "continue", confidence: 0.82, reason: "Main model should handle this without advisor.", safety: false };
|
|
353
|
+
}
|
|
354
|
+
if (hasCheapSignalMaterialSignal(text) || /\b(advisor-router|advisor flow|advisor routing|router logic|call vs skip|skip vs call|compare|recommend|benchmark|evaluate|experiment|train|research)\b/i.test(text)) {
|
|
355
|
+
return { label: "escalate_to_advisor", confidence: 0.88, reason: "Advisor-specific or decision-changing work detected.", safety: false };
|
|
356
|
+
}
|
|
357
|
+
if (hasComplexSignal(text)) {
|
|
358
|
+
return { label: "escalate_to_advisor", confidence: 0.88, reason: "Complex or high-uncertainty work detected.", safety: false };
|
|
359
|
+
}
|
|
360
|
+
if (needsContext(text)) {
|
|
361
|
+
return { label: "need_more_context", confidence: 0.66, reason: "Prompt is too underspecified.", safety: false };
|
|
362
|
+
}
|
|
363
|
+
return { label: "low_confidence", confidence: 0.54, reason: "No strong routing signal.", safety: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function heuristicRoute(input: AdvisorRouteInput): AdvisorRouteDecision {
|
|
367
|
+
if (input.phase === "review" || input.phase === "closeout") {
|
|
368
|
+
const result = reviewSignals(input);
|
|
369
|
+
return {
|
|
370
|
+
phase: input.phase,
|
|
371
|
+
label: result.label,
|
|
372
|
+
confidence: result.confidence,
|
|
373
|
+
reason: result.reason,
|
|
374
|
+
source: "heuristic",
|
|
375
|
+
preflight: "off",
|
|
376
|
+
review: reviewPolicy(result.label),
|
|
377
|
+
escalate: result.label !== "on_track" && result.label !== "abstain",
|
|
378
|
+
safety: false,
|
|
379
|
+
promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
|
|
380
|
+
promptSummary: squish(input.text, 220),
|
|
381
|
+
briefSummary: input.brief ? squish(input.brief, 220) : undefined,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = preflightSignals(input);
|
|
386
|
+
return {
|
|
387
|
+
phase: input.phase,
|
|
388
|
+
label: result.label,
|
|
389
|
+
confidence: result.confidence,
|
|
390
|
+
reason: result.reason,
|
|
391
|
+
source: "heuristic",
|
|
392
|
+
preflight: preflightPolicy(result.label),
|
|
393
|
+
review: result.label === "continue" ? "off" : result.label === "escalate_to_advisor" ? "light" : "light",
|
|
394
|
+
escalate: result.label === "escalate_to_advisor",
|
|
395
|
+
safety: result.safety,
|
|
396
|
+
promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
|
|
397
|
+
promptSummary: squish(input.text, 220),
|
|
398
|
+
briefSummary: input.brief ? squish(input.brief, 220) : undefined,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function shouldQueryClassifier(route: AdvisorRouteDecision): boolean {
|
|
403
|
+
if (route.safety) return false;
|
|
404
|
+
if (route.phase === "preflight") {
|
|
405
|
+
return route.label === "low_confidence" || route.label === "need_more_context" || route.confidence < 0.7;
|
|
406
|
+
}
|
|
407
|
+
return route.label === "abstain" || route.confidence < 0.68;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function buildRouterPrompt(input: AdvisorRouteInput): string {
|
|
411
|
+
const phase = input.phase;
|
|
412
|
+
const labels = phase === "preflight"
|
|
413
|
+
? "continue | escalate_to_advisor | need_more_context | low_confidence"
|
|
414
|
+
: "on_track | course_correct | not_done | abstain";
|
|
415
|
+
|
|
416
|
+
return [
|
|
417
|
+
"You are a routing classifier for a coding assistant. Return ONLY valid JSON.",
|
|
418
|
+
`Phase: ${phase}`,
|
|
419
|
+
`Allowed labels: ${labels}`,
|
|
420
|
+
"Format: {\"label\":\"...\",\"confidence\":0-1,\"reason\":\"...\"}",
|
|
421
|
+
`Task: ${squish(input.text, 800)}`,
|
|
422
|
+
input.brief ? `Recent context: ${squish(input.brief, 500)}` : "",
|
|
423
|
+
input.fileChanged !== undefined ? `File changed: ${String(input.fileChanged)}` : "",
|
|
424
|
+
input.failed !== undefined ? `Failed: ${String(input.failed)}` : "",
|
|
425
|
+
phase === "preflight"
|
|
426
|
+
? [
|
|
427
|
+
"Guidance: continue for tiny edits and direct answers; escalate_to_advisor for architecture, design, tradeoffs, security, or high uncertainty; need_more_context when underspecified; low_confidence when mixed signals.",
|
|
428
|
+
].join(" ")
|
|
429
|
+
: [
|
|
430
|
+
"Guidance: on_track for clearly complete work; course_correct for partial work that needs changes; not_done when incomplete or failing; abstain when there is not enough signal.",
|
|
431
|
+
].join(" "),
|
|
432
|
+
].filter(Boolean).join("\n");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function parseRouterResponse(phase: AdvisorPhase, text: string): RouterResponse | null {
|
|
436
|
+
const raw = String(text ?? "").trim();
|
|
437
|
+
if (!raw) return null;
|
|
438
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
|
|
439
|
+
try {
|
|
440
|
+
const parsed = JSON.parse(cleaned) as { label?: unknown; confidence?: unknown; reason?: unknown; decision?: unknown };
|
|
441
|
+
const label = normalizePhaseLabel(phase, parsed.label ?? parsed.decision);
|
|
442
|
+
if (!label) return null;
|
|
443
|
+
return {
|
|
444
|
+
label,
|
|
445
|
+
confidence: clampConfidence(parsed.confidence, 0.5),
|
|
446
|
+
reason: typeof parsed.reason === "string" && parsed.reason.trim() ? parsed.reason.trim() : "Classifier returned no reason.",
|
|
447
|
+
};
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function mergeClassifierDecision(input: AdvisorRouteInput, decision: RouterResponse, source: RouterSource): AdvisorRouteDecision {
|
|
454
|
+
if (input.phase === "review" || input.phase === "closeout") {
|
|
455
|
+
const label = normalizePhaseLabel(input.phase, decision.label) as ReviewLabel | null;
|
|
456
|
+
const finalLabel: ReviewLabel = label ?? reviewSignals(input).label;
|
|
457
|
+
const confidence = clampConfidence(decision.confidence, 0.5);
|
|
458
|
+
return {
|
|
459
|
+
phase: input.phase,
|
|
460
|
+
label: finalLabel,
|
|
461
|
+
confidence,
|
|
462
|
+
reason: decision.reason?.trim() || "Classifier returned no reason.",
|
|
463
|
+
source,
|
|
464
|
+
preflight: "off",
|
|
465
|
+
review: reviewPolicy(finalLabel),
|
|
466
|
+
escalate: finalLabel !== "on_track" && finalLabel !== "abstain",
|
|
467
|
+
safety: false,
|
|
468
|
+
promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
|
|
469
|
+
promptSummary: squish(input.text, 220),
|
|
470
|
+
briefSummary: input.brief ? squish(input.brief, 220) : undefined,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const label = normalizePhaseLabel(input.phase, decision.label) as PreflightLabel | null;
|
|
475
|
+
const finalLabel: PreflightLabel = label ?? preflightSignals(input).label;
|
|
476
|
+
const confidence = clampConfidence(decision.confidence, 0.5);
|
|
477
|
+
return {
|
|
478
|
+
phase: input.phase,
|
|
479
|
+
label: finalLabel,
|
|
480
|
+
confidence,
|
|
481
|
+
reason: decision.reason?.trim() || "Classifier returned no reason.",
|
|
482
|
+
source,
|
|
483
|
+
preflight: preflightPolicy(finalLabel),
|
|
484
|
+
review: finalLabel === "continue" ? "off" : finalLabel === "escalate_to_advisor" ? "light" : "light",
|
|
485
|
+
escalate: finalLabel === "escalate_to_advisor",
|
|
486
|
+
safety: isSafetySensitive(`${input.text}\n${input.brief ?? ""}`),
|
|
487
|
+
promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
|
|
488
|
+
promptSummary: squish(input.text, 220),
|
|
489
|
+
briefSummary: input.brief ? squish(input.brief, 220) : undefined,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function routeLogEntry(route: AdvisorRouteDecision): Record<string, unknown> {
|
|
494
|
+
return {
|
|
495
|
+
at: new Date().toISOString(),
|
|
496
|
+
version: ROUTER_VERSION,
|
|
497
|
+
phase: route.phase,
|
|
498
|
+
label: route.label,
|
|
499
|
+
confidence: route.confidence,
|
|
500
|
+
reason: truncate(route.reason, 240),
|
|
501
|
+
source: route.source,
|
|
502
|
+
safety: route.safety,
|
|
503
|
+
escalate: route.escalate,
|
|
504
|
+
preflight: route.preflight,
|
|
505
|
+
review: route.review,
|
|
506
|
+
promptHash: route.promptHash,
|
|
507
|
+
prompt: route.promptSummary,
|
|
508
|
+
brief: route.briefSummary,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function appendRouteLog(route: AdvisorRouteDecision): void {
|
|
513
|
+
appendText(ROUTER_LOG_PATH, `${JSON.stringify(routeLogEntry(route))}\n`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export type AdvisorDisplayDecision = "continue" | "review" | "defer";
|
|
517
|
+
export type AdvisorDisplayTag = "advisor:model" | "advisor:rules" | "advisor:llm";
|
|
518
|
+
|
|
519
|
+
function displayDecision(route: AdvisorRouteDecision): AdvisorDisplayDecision {
|
|
520
|
+
if (route.phase === "preflight") {
|
|
521
|
+
switch (route.label as PreflightLabel) {
|
|
522
|
+
case "continue": return "continue";
|
|
523
|
+
case "escalate_to_advisor": return "review";
|
|
524
|
+
case "need_more_context": return "defer";
|
|
525
|
+
case "low_confidence": return "defer";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
switch (route.label as ReviewLabel) {
|
|
530
|
+
case "on_track": return "continue";
|
|
531
|
+
case "course_correct": return "review";
|
|
532
|
+
case "not_done": return "review";
|
|
533
|
+
case "abstain": return "defer";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function displayTag(route: AdvisorRouteDecision | AdvisorDisplayTag): AdvisorDisplayTag {
|
|
538
|
+
if (typeof route === "string") return route;
|
|
539
|
+
switch (route.source) {
|
|
540
|
+
case "model": return "advisor:model";
|
|
541
|
+
case "llm": return "advisor:llm";
|
|
542
|
+
default: return "advisor:rules";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function formatAdvisorDisplay(tag: AdvisorDisplayTag, decision: AdvisorDisplayDecision, explanation: string): string {
|
|
547
|
+
const text = squish(explanation || "no extra detail", 140).toLowerCase();
|
|
548
|
+
return `[${tag}: ${decision}, reason: ${text}]`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function routeNote(route: AdvisorRouteDecision): string {
|
|
552
|
+
const explanation = route.reason || (route.phase === "preflight"
|
|
553
|
+
? route.label === "continue"
|
|
554
|
+
? "routine work can continue without advisor attention"
|
|
555
|
+
: route.label === "escalate_to_advisor"
|
|
556
|
+
? "complex or high-risk work needs advisor review"
|
|
557
|
+
: route.label === "need_more_context"
|
|
558
|
+
? "more context is needed before routing confidently"
|
|
559
|
+
: "signal is mixed, so defer the decision"
|
|
560
|
+
: route.label === "on_track"
|
|
561
|
+
? "work looks on track and can continue"
|
|
562
|
+
: route.label === "course_correct"
|
|
563
|
+
? "work is progressing but needs review"
|
|
564
|
+
: route.label === "not_done"
|
|
565
|
+
? "work is incomplete or failing and needs review"
|
|
566
|
+
: "review signal is too weak to decide");
|
|
567
|
+
return formatAdvisorDisplay(displayTag(route), displayDecision(route), explanation);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function mergeReviewPolicy(base: ReviewPolicy, route: ReviewPolicy): ReviewPolicy {
|
|
571
|
+
if (base === "off") return "off";
|
|
572
|
+
if (route === "strict") return "strict";
|
|
573
|
+
if (route === "light") return base === "strict" ? "strict" : "light";
|
|
574
|
+
return base;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function summarizeRoute(route: AdvisorRouteDecision): string {
|
|
578
|
+
const phase = route.phase === "closeout" ? "closeout" : route.phase;
|
|
579
|
+
return `${phase}:${route.label} (${Math.round(route.confidence * 100)}%)`;
|
|
580
|
+
}
|