@fiale-plus/pi-rogue-bundle 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +24 -13
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  7. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  8. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  9. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
  10. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
  11. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
  18. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  19. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  20. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  21. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  22. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  23. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
  24. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  25. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  26. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  27. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  28. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  29. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  30. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  31. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  32. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  33. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  34. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  35. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  36. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  37. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  38. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  39. package/package.json +10 -2
@@ -0,0 +1,516 @@
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
+
7
+ export type AdvisorPhase = "preflight" | "review" | "closeout";
8
+ export type PreflightLabel = "continue" | "escalate_to_advisor" | "need_more_context" | "low_confidence";
9
+ export type ReviewLabel = "on_track" | "course_correct" | "not_done" | "abstain";
10
+ export type RouterSource = "heuristic" | "model" | "llm";
11
+ export type PreflightPolicy = "off" | "light" | "full" | "direct";
12
+ export type ReviewPolicy = "off" | "light" | "strict";
13
+
14
+ export interface AdvisorRouteInput {
15
+ phase: AdvisorPhase;
16
+ text: string;
17
+ brief?: string;
18
+ fileChanged?: boolean;
19
+ failed?: boolean;
20
+ }
21
+
22
+ export interface AdvisorRouteDecision {
23
+ phase: AdvisorPhase;
24
+ label: PreflightLabel | ReviewLabel;
25
+ confidence: number;
26
+ reason: string;
27
+ source: RouterSource;
28
+ preflight: PreflightPolicy;
29
+ review: ReviewPolicy;
30
+ escalate: boolean;
31
+ safety: boolean;
32
+ promptHash: string;
33
+ promptSummary: string;
34
+ briefSummary?: string;
35
+ }
36
+
37
+ export interface RouterResponse {
38
+ label: PreflightLabel | ReviewLabel;
39
+ confidence?: number;
40
+ reason?: string;
41
+ }
42
+
43
+ const ROUTER_LOG_PATH = featureFile("advisor", "evals/advisor-router.jsonl");
44
+ const ROUTER_VERSION = 1;
45
+
46
+ // ── Binary gate model (trained from local session data) ──────────────────
47
+ const BINARY_GATE_PATH = featureFile("advisor", "binary-gate-model.json");
48
+ const BINARY_GATE_SOURCE_PATH = resolve(dirname(fileURLToPath(import.meta.url)), "../assets/binary-gate-model.json");
49
+ const BINARY_GATE_THRESHOLD = 0.55;
50
+
51
+ interface BinaryGateModel {
52
+ kind: string;
53
+ labels: string[];
54
+ features: string[];
55
+ idf: number[];
56
+ bias: number[];
57
+ weights: number[][];
58
+ }
59
+
60
+ let _binaryGateCache: BinaryGateModel | null | undefined = undefined;
61
+
62
+ function ensureBinaryGateSeeded(): void {
63
+ try {
64
+ if (!existsSync(BINARY_GATE_SOURCE_PATH)) return;
65
+ const sourceStat = statSync(BINARY_GATE_SOURCE_PATH);
66
+ if (existsSync(BINARY_GATE_PATH)) {
67
+ const installedStat = statSync(BINARY_GATE_PATH);
68
+ if (installedStat.mtimeMs >= sourceStat.mtimeMs && installedStat.size === sourceStat.size) return;
69
+ }
70
+ mkdirSync(dirname(BINARY_GATE_PATH), { recursive: true });
71
+ copyFileSync(BINARY_GATE_SOURCE_PATH, BINARY_GATE_PATH);
72
+ } catch {
73
+ // best effort: if the seed copy fails, fall back to the installed path if present
74
+ }
75
+ }
76
+
77
+ function loadBinaryGate(): BinaryGateModel | null {
78
+ if (_binaryGateCache !== undefined) return _binaryGateCache;
79
+ try {
80
+ ensureBinaryGateSeeded();
81
+ if (!existsSync(BINARY_GATE_PATH)) return null;
82
+ _binaryGateCache = JSON.parse(readFileSync(BINARY_GATE_PATH, "utf8")) as BinaryGateModel;
83
+ if (_binaryGateCache.kind !== "binary-logreg-v1") { _binaryGateCache = null; return null; }
84
+ return _binaryGateCache;
85
+ } catch { _binaryGateCache = null; return null; }
86
+ }
87
+
88
+ function binaryGateTokens(text: string): string[] {
89
+ const norm = String(text ?? "").toLowerCase()
90
+ .replace(/https?:\/\/\S+/g, " url ")
91
+ .replace(/[^a-z0-9\s']/g, " ")
92
+ .replace(/\s+/g, " ").trim();
93
+ return norm ? norm.split(" ").filter(Boolean) : [];
94
+ }
95
+
96
+ function binaryGateFeatures(text: string, model: BinaryGateModel) {
97
+ const toks = binaryGateTokens(text);
98
+ const lower = String(text ?? "").toLowerCase()
99
+ .replace(/https?:\/\/\S+/g, " url ")
100
+ .replace(/[^a-z0-9\s']/g, " ")
101
+ .replace(/\s+/g, " ").trim();
102
+ const counts = new Map<string, number>();
103
+ const inc = (k: string, b = 1) => counts.set(k, (counts.get(k) || 0) + b);
104
+ for (const n of [1, 2]) {
105
+ if (toks.length >= n) for (let i = 0; i <= toks.length - n; i++)
106
+ inc(`w${n}:${toks.slice(i, i + n).join("_")}`);
107
+ }
108
+ const norm = ` ${lower} `;
109
+ for (const n of [3, 4]) {
110
+ if (norm.length >= n) for (let i = 0; i <= norm.length - n; i++) {
111
+ const g = norm.slice(i, i + n);
112
+ if (!/^\s+$/.test(g)) inc(`c${n}:${g}`);
113
+ }
114
+ }
115
+ if (toks.length > 0) inc(`pref1:${toks[0]}`);
116
+ if (toks.length > 1) inc(`pref2:${toks.slice(0, 2).join("_")}`);
117
+ if (toks.length > 2) inc(`pref3:${toks.slice(0, 3).join("_")}`);
118
+ if (text.includes("?")) inc("cue:question_mark");
119
+ const cues = ["check","why","what","how","should","status","stats","log","logs","review","diff","pr","build","run","test","deploy","fix","debug","install","configure","plan","continue","resume","compact","research","update","patch","cleanup","remove"];
120
+ const multi = ["what is","what's","safe to use","pull request","model family","how does","next step","path forward","should we","what should"];
121
+ const ts = new Set(toks);
122
+ for (const c of cues) if (ts.has(c)) inc(`cue:${c}`);
123
+ for (const c of multi) if (lower.includes(c)) inc(`cue:${c.replace(/\s+/g,"_")}`);
124
+
125
+ const index = new Map(model.features.map((f, i) => [f, i]));
126
+ const pairs: Array<[number, number]> = [];
127
+ let nrm = 0;
128
+ for (const [feature, tf] of counts) {
129
+ const idx = index.get(feature);
130
+ if (idx === undefined) continue;
131
+ const value = (1 + Math.log(tf)) * model.idf[idx];
132
+ pairs.push([idx, value]); nrm += value * value;
133
+ }
134
+ const scale = nrm > 0 ? 1 / Math.sqrt(nrm) : 1;
135
+ pairs.sort((a, b) => a[0] - b[0]);
136
+ return { I: pairs.map(([i]) => i), V: pairs.map(([, v]) => v * scale) };
137
+ }
138
+
139
+ export function binaryGatePredict(text: string): { decision: "continue" | "escalate"; confidence: number } | null {
140
+ const model = loadBinaryGate();
141
+ if (!model) return null;
142
+ const vec = binaryGateFeatures(text, model);
143
+ const scores = model.bias.slice();
144
+ for (let c = 0; c < model.weights.length; c++) {
145
+ let score = scores[c]; const w = model.weights[c];
146
+ for (let i = 0; i < vec.I.length; i++) score += w[vec.I[i]] * vec.V[i];
147
+ scores[c] = score;
148
+ }
149
+ const maxS = Math.max(...scores);
150
+ const exps = scores.map((v) => Math.exp(v - maxS));
151
+ const sum = exps.reduce((a, b) => a + b, 0) || 1;
152
+ const probs = exps.map((v) => v / sum);
153
+ const idx = probs[0] >= probs[1] ? 0 : 1;
154
+ return { decision: model.labels[idx] as "continue" | "escalate", confidence: probs[idx] };
155
+ }
156
+
157
+ 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;
158
+ const ROUTINE_CLEANUP_RE = /\b(routine docs?|docs? and formatting|formatting cleanup|generated changes|large diff|docs?\/formatting)\b/i;
159
+ 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;
160
+ const DEBUG_RE = /\b(debug|bug|error|stack trace|traceback|fail(?:ed|ure)?|broken|investigate|why is|cannot|can't|crash|regression)\b/i;
161
+ const CONTEXT_RE = /\b(need more context|missing context|clarify|not enough info|unspecified|unknown|ambiguous)\b/i;
162
+ 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|secret\b|token\b|credential\b|password\b)\b/i;
163
+ const COMPACTION_RE = /\b(compact(?:ed|ion)?|missing history|history might flip|prior constraint|resume(?:d)? after compaction)\b/i;
164
+ 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;
165
+ const CHECKPOINT_RE = /\b(checkpoint|multi-step implementation|clearer boundary|interrupt now|wait until there is a clearer boundary|mid implementation)\b/i;
166
+ 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;
167
+ 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;
168
+ 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;
169
+ const DONE_RE = /\b(done|complete(?:d)?|fixed|implemented|works?|passing tests?|tests pass|verified|looks good|merged)\b/i;
170
+
171
+ function squish(text: unknown, max = 220): string {
172
+ const value = String(text ?? "").replace(/\s+/g, " ").trim();
173
+ return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
174
+ }
175
+
176
+ function hashText(...parts: string[]): string {
177
+ return createHash("sha256").update(parts.join("||")).digest("hex").slice(0, 16);
178
+ }
179
+
180
+ function clampConfidence(value: unknown, fallback: number): number {
181
+ const n = Number(value);
182
+ if (!Number.isFinite(n)) return fallback;
183
+ return Math.max(0, Math.min(1, n));
184
+ }
185
+
186
+ function normalizePhaseLabel(phase: AdvisorPhase, label: unknown): PreflightLabel | ReviewLabel | null {
187
+ const value = String(label ?? "").trim();
188
+ if (phase === "preflight") {
189
+ return value === "continue" || value === "escalate_to_advisor" || value === "need_more_context" || value === "low_confidence"
190
+ ? value
191
+ : null;
192
+ }
193
+ return value === "on_track" || value === "course_correct" || value === "not_done" || value === "abstain"
194
+ ? value
195
+ : null;
196
+ }
197
+
198
+ function preflightPolicy(label: PreflightLabel): PreflightPolicy {
199
+ switch (label) {
200
+ case "continue": return "off";
201
+ case "escalate_to_advisor": return "full";
202
+ case "need_more_context": return "direct";
203
+ case "low_confidence": return "light";
204
+ }
205
+ }
206
+
207
+ function reviewPolicy(label: ReviewLabel): ReviewPolicy {
208
+ switch (label) {
209
+ case "on_track": return "off";
210
+ case "course_correct": return "light";
211
+ case "not_done": return "strict";
212
+ case "abstain": return "off";
213
+ }
214
+ }
215
+
216
+ function isSafetySensitive(text: string): boolean {
217
+ return SAFETY_RE.test(text);
218
+ }
219
+
220
+ function hasQuickEditSignal(text: string): boolean {
221
+ return QUICK_EDIT_RE.test(text);
222
+ }
223
+
224
+ function hasRoutineCleanupSignal(text: string): boolean {
225
+ return ROUTINE_CLEANUP_RE.test(text);
226
+ }
227
+
228
+ function hasComplexSignal(text: string): boolean {
229
+ return COMPLEX_RE.test(text) || DEBUG_RE.test(text);
230
+ }
231
+
232
+ function hasCompactionLowRiskSignal(text: string): boolean {
233
+ return COMPACTION_RE.test(text) && /\blow[- ]?risk\b/i.test(text);
234
+ }
235
+
236
+ function hasReassuranceOnlySignal(text: string): boolean {
237
+ return REASSURANCE_RE.test(text) && !/\b(material|flip|decision|risk|uncertain|flip the decision)\b/i.test(text);
238
+ }
239
+
240
+ function hasCheckpointSignal(text: string): boolean {
241
+ return CHECKPOINT_RE.test(text) && !/\b(risky|security|irreversible|unknown dependency|hidden dependency)\b/i.test(text);
242
+ }
243
+
244
+ function hasCheapSignalMaterialSignal(text: string): boolean {
245
+ 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);
246
+ }
247
+
248
+ function hasCheapSignalIrrelevantSignal(text: string): boolean {
249
+ 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);
250
+ }
251
+
252
+ function needsContext(text: string): boolean {
253
+ return CONTEXT_RE.test(text) || text.trim().length < 18 || text.trim().split(/\s+/).length < 4;
254
+ }
255
+
256
+ function reviewSignals(input: AdvisorRouteInput): { label: ReviewLabel; confidence: number; reason: string } {
257
+ const text = `${input.text}\n${input.brief ?? ""}`.trim();
258
+ if (input.failed) {
259
+ return { label: "not_done", confidence: 0.95, reason: "Turn reported failure." };
260
+ }
261
+ if (input.phase === "closeout" && CLOSEOUT_RE.test(text)) {
262
+ return { label: /\bnot[- ]?done\b/i.test(text) ? "not_done" : "course_correct", confidence: 0.86, reason: "Closeout judgment requested." };
263
+ }
264
+ if (REVIEW_NEEDS_WORK_RE.test(text)) {
265
+ 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." };
266
+ }
267
+ if (input.fileChanged && DONE_RE.test(text)) {
268
+ return { label: "on_track", confidence: 0.84, reason: "Change looks complete." };
269
+ }
270
+ if (DONE_RE.test(text)) {
271
+ return { label: "on_track", confidence: 0.72, reason: "Completion signal detected." };
272
+ }
273
+ return { label: "abstain", confidence: 0.56, reason: "Insufficient review signal." };
274
+ }
275
+
276
+ function preflightSignals(input: AdvisorRouteInput): { label: PreflightLabel; confidence: number; reason: string; safety: boolean } {
277
+ const text = `${input.text}\n${input.brief ?? ""}`.trim();
278
+ if (isSafetySensitive(text)) {
279
+ return { label: "escalate_to_advisor", confidence: 0.98, reason: "Safety-sensitive keywords detected.", safety: true };
280
+ }
281
+ if (hasRoutineCleanupSignal(text) || (hasQuickEditSignal(text) && !hasComplexSignal(text))) {
282
+ return { label: "continue", confidence: 0.9, reason: "Small-edit or routine-cleanup signal detected.", safety: false };
283
+ }
284
+ if (hasCompactionLowRiskSignal(text)) {
285
+ return { label: "continue", confidence: 0.84, reason: "Low-risk compaction boundary; advisor not needed.", safety: false };
286
+ }
287
+ if (hasReassuranceOnlySignal(text) || hasCheckpointSignal(text) || hasCheapSignalIrrelevantSignal(text)) {
288
+ return { label: "continue", confidence: 0.82, reason: "Main model should handle this without advisor.", safety: false };
289
+ }
290
+ 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)) {
291
+ return { label: "escalate_to_advisor", confidence: 0.88, reason: "Advisor-specific or decision-changing work detected.", safety: false };
292
+ }
293
+ if (hasComplexSignal(text)) {
294
+ return { label: "escalate_to_advisor", confidence: 0.88, reason: "Complex or high-uncertainty work detected.", safety: false };
295
+ }
296
+ if (needsContext(text)) {
297
+ return { label: "need_more_context", confidence: 0.66, reason: "Prompt is too underspecified.", safety: false };
298
+ }
299
+ return { label: "low_confidence", confidence: 0.54, reason: "No strong routing signal.", safety: false };
300
+ }
301
+
302
+ export function heuristicRoute(input: AdvisorRouteInput): AdvisorRouteDecision {
303
+ if (input.phase === "review" || input.phase === "closeout") {
304
+ const result = reviewSignals(input);
305
+ return {
306
+ phase: input.phase,
307
+ label: result.label,
308
+ confidence: result.confidence,
309
+ reason: result.reason,
310
+ source: "heuristic",
311
+ preflight: "off",
312
+ review: reviewPolicy(result.label),
313
+ escalate: result.label !== "on_track" && result.label !== "abstain",
314
+ safety: false,
315
+ promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
316
+ promptSummary: squish(input.text, 220),
317
+ briefSummary: input.brief ? squish(input.brief, 220) : undefined,
318
+ };
319
+ }
320
+
321
+ const result = preflightSignals(input);
322
+ return {
323
+ phase: input.phase,
324
+ label: result.label,
325
+ confidence: result.confidence,
326
+ reason: result.reason,
327
+ source: "heuristic",
328
+ preflight: preflightPolicy(result.label),
329
+ review: result.label === "continue" ? "off" : result.label === "escalate_to_advisor" ? "light" : "light",
330
+ escalate: result.label === "escalate_to_advisor",
331
+ safety: result.safety,
332
+ promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
333
+ promptSummary: squish(input.text, 220),
334
+ briefSummary: input.brief ? squish(input.brief, 220) : undefined,
335
+ };
336
+ }
337
+
338
+ export function shouldQueryClassifier(route: AdvisorRouteDecision): boolean {
339
+ if (route.safety) return false;
340
+ if (route.phase === "preflight") {
341
+ return route.label === "low_confidence" || route.label === "need_more_context" || route.confidence < 0.7;
342
+ }
343
+ return route.label === "abstain" || route.confidence < 0.68;
344
+ }
345
+
346
+ export function buildRouterPrompt(input: AdvisorRouteInput): string {
347
+ const phase = input.phase;
348
+ const labels = phase === "preflight"
349
+ ? "continue | escalate_to_advisor | need_more_context | low_confidence"
350
+ : "on_track | course_correct | not_done | abstain";
351
+
352
+ return [
353
+ "You are a routing classifier for a coding assistant. Return ONLY valid JSON.",
354
+ `Phase: ${phase}`,
355
+ `Allowed labels: ${labels}`,
356
+ "Format: {\"label\":\"...\",\"confidence\":0-1,\"reason\":\"...\"}",
357
+ `Task: ${squish(input.text, 800)}`,
358
+ input.brief ? `Recent context: ${squish(input.brief, 500)}` : "",
359
+ input.fileChanged !== undefined ? `File changed: ${String(input.fileChanged)}` : "",
360
+ input.failed !== undefined ? `Failed: ${String(input.failed)}` : "",
361
+ phase === "preflight"
362
+ ? [
363
+ "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.",
364
+ ].join(" ")
365
+ : [
366
+ "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.",
367
+ ].join(" "),
368
+ ].filter(Boolean).join("\n");
369
+ }
370
+
371
+ export function parseRouterResponse(phase: AdvisorPhase, text: string): RouterResponse | null {
372
+ const raw = String(text ?? "").trim();
373
+ if (!raw) return null;
374
+ const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
375
+ try {
376
+ const parsed = JSON.parse(cleaned) as { label?: unknown; confidence?: unknown; reason?: unknown; decision?: unknown };
377
+ const label = normalizePhaseLabel(phase, parsed.label ?? parsed.decision);
378
+ if (!label) return null;
379
+ return {
380
+ label,
381
+ confidence: clampConfidence(parsed.confidence, 0.5),
382
+ reason: typeof parsed.reason === "string" && parsed.reason.trim() ? parsed.reason.trim() : "Classifier returned no reason.",
383
+ };
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ export function mergeClassifierDecision(input: AdvisorRouteInput, decision: RouterResponse, source: RouterSource): AdvisorRouteDecision {
390
+ if (input.phase === "review" || input.phase === "closeout") {
391
+ const label = normalizePhaseLabel(input.phase, decision.label) as ReviewLabel | null;
392
+ const finalLabel: ReviewLabel = label ?? reviewSignals(input).label;
393
+ const confidence = clampConfidence(decision.confidence, 0.5);
394
+ return {
395
+ phase: input.phase,
396
+ label: finalLabel,
397
+ confidence,
398
+ reason: decision.reason?.trim() || "Classifier returned no reason.",
399
+ source,
400
+ preflight: "off",
401
+ review: reviewPolicy(finalLabel),
402
+ escalate: finalLabel !== "on_track" && finalLabel !== "abstain",
403
+ safety: false,
404
+ promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
405
+ promptSummary: squish(input.text, 220),
406
+ briefSummary: input.brief ? squish(input.brief, 220) : undefined,
407
+ };
408
+ }
409
+
410
+ const label = normalizePhaseLabel(input.phase, decision.label) as PreflightLabel | null;
411
+ const finalLabel: PreflightLabel = label ?? preflightSignals(input).label;
412
+ const confidence = clampConfidence(decision.confidence, 0.5);
413
+ return {
414
+ phase: input.phase,
415
+ label: finalLabel,
416
+ confidence,
417
+ reason: decision.reason?.trim() || "Classifier returned no reason.",
418
+ source,
419
+ preflight: preflightPolicy(finalLabel),
420
+ review: finalLabel === "continue" ? "off" : finalLabel === "escalate_to_advisor" ? "light" : "light",
421
+ escalate: finalLabel === "escalate_to_advisor",
422
+ safety: isSafetySensitive(`${input.text}\n${input.brief ?? ""}`),
423
+ promptHash: hashText(input.phase, input.text, input.brief ?? "", String(input.fileChanged ?? false), String(input.failed ?? false)),
424
+ promptSummary: squish(input.text, 220),
425
+ briefSummary: input.brief ? squish(input.brief, 220) : undefined,
426
+ };
427
+ }
428
+
429
+ export function routeLogEntry(route: AdvisorRouteDecision): Record<string, unknown> {
430
+ return {
431
+ at: new Date().toISOString(),
432
+ version: ROUTER_VERSION,
433
+ phase: route.phase,
434
+ label: route.label,
435
+ confidence: route.confidence,
436
+ reason: truncate(route.reason, 240),
437
+ source: route.source,
438
+ safety: route.safety,
439
+ escalate: route.escalate,
440
+ preflight: route.preflight,
441
+ review: route.review,
442
+ promptHash: route.promptHash,
443
+ prompt: route.promptSummary,
444
+ brief: route.briefSummary,
445
+ };
446
+ }
447
+
448
+ export function appendRouteLog(route: AdvisorRouteDecision): void {
449
+ appendText(ROUTER_LOG_PATH, `${JSON.stringify(routeLogEntry(route))}\n`);
450
+ }
451
+
452
+ export type AdvisorDisplayDecision = "continue" | "review" | "defer";
453
+ export type AdvisorDisplayTag = "advisor:model" | "advisor:rules" | "advisor:llm";
454
+
455
+ function displayDecision(route: AdvisorRouteDecision): AdvisorDisplayDecision {
456
+ if (route.phase === "preflight") {
457
+ switch (route.label as PreflightLabel) {
458
+ case "continue": return "continue";
459
+ case "escalate_to_advisor": return "review";
460
+ case "need_more_context": return "defer";
461
+ case "low_confidence": return "defer";
462
+ }
463
+ }
464
+
465
+ switch (route.label as ReviewLabel) {
466
+ case "on_track": return "continue";
467
+ case "course_correct": return "review";
468
+ case "not_done": return "review";
469
+ case "abstain": return "defer";
470
+ }
471
+ }
472
+
473
+ function displayTag(route: AdvisorRouteDecision | AdvisorDisplayTag): AdvisorDisplayTag {
474
+ if (typeof route === "string") return route;
475
+ switch (route.source) {
476
+ case "model": return "advisor:model";
477
+ case "llm": return "advisor:llm";
478
+ default: return "advisor:rules";
479
+ }
480
+ }
481
+
482
+ export function formatAdvisorDisplay(tag: AdvisorDisplayTag, decision: AdvisorDisplayDecision, explanation: string): string {
483
+ const text = squish(explanation || "no extra detail", 140).toLowerCase();
484
+ return `[${tag}: ${decision}, reason: ${text}]`;
485
+ }
486
+
487
+ export function routeNote(route: AdvisorRouteDecision): string {
488
+ const explanation = route.reason || (route.phase === "preflight"
489
+ ? route.label === "continue"
490
+ ? "routine work can continue without advisor attention"
491
+ : route.label === "escalate_to_advisor"
492
+ ? "complex or high-risk work needs advisor review"
493
+ : route.label === "need_more_context"
494
+ ? "more context is needed before routing confidently"
495
+ : "signal is mixed, so defer the decision"
496
+ : route.label === "on_track"
497
+ ? "work looks on track and can continue"
498
+ : route.label === "course_correct"
499
+ ? "work is progressing but needs review"
500
+ : route.label === "not_done"
501
+ ? "work is incomplete or failing and needs review"
502
+ : "review signal is too weak to decide");
503
+ return formatAdvisorDisplay(displayTag(route), displayDecision(route), explanation);
504
+ }
505
+
506
+ export function mergeReviewPolicy(base: ReviewPolicy, route: ReviewPolicy): ReviewPolicy {
507
+ if (base === "off") return "off";
508
+ if (route === "strict") return "strict";
509
+ if (route === "light") return base === "strict" ? "strict" : "light";
510
+ return base;
511
+ }
512
+
513
+ export function summarizeRoute(route: AdvisorRouteDecision): string {
514
+ const phase = route.phase === "closeout" ? "closeout" : route.phase;
515
+ return `${phase}:${route.label} (${Math.round(route.confidence * 100)}%)`;
516
+ }
@@ -0,0 +1,56 @@
1
+ # @fiale-plus/pi-rogue-orchestration
2
+
3
+ > **Releases paused.** This package is now internal. All usage and updates are via the single consolidated `@fiale-plus/pi-rogue-bundle` artefact (see root README, `docs/release.md`, and AGENTS.md). Direct installs and independent releases are on pause; the package is marked private. Code here continues to evolve and ships inside bundle releases.
4
+
5
+ ## What this package is
6
+
7
+ Session orchestration for Pi-Rogue built around three primitives:
8
+
9
+ 1. `goal` — define and track what success looks like
10
+ 2. `loop` — periodic execution with explicit start/stop
11
+ 3. `autoresearch` / `autoresearch-lab` — goal+loop facades for iterative or parallelized optimization
12
+
13
+ ## Install
14
+
15
+ **For users:** Use the bundle (releases for this package are paused):
16
+
17
+ ```bash
18
+ pi install npm:@fiale-plus/pi-rogue-bundle
19
+ ```
20
+
21
+ **For local development (monorepo only):**
22
+
23
+ ```bash
24
+ npm install --workspace packages/orchestration
25
+ ```
26
+
27
+ (See root README and docs/release.md for the consolidated policy.)
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does |
32
+ |---|---|
33
+ | `/goal set <text>` | Set/update current goal and re-arm check-ins |
34
+ | `/goal show` | Show current goal |
35
+ | `/goal clear` | Clear active goal |
36
+ | `/goal list` | Show recent goal history |
37
+ | `/loop <interval> <instruction>` | Create or reset periodic loop (`1m` minimum) |
38
+ | `/loop status` | Show current loop state |
39
+ | `/loop off` / `clear` / `stop` | Clear loop |
40
+ | `/autoresearch <instruction>` | Start/update solo research flow |
41
+ | `/autoresearch status` | Show autoresearch state |
42
+ | `/autoresearch clear` | Clear solo research + underlying loop |
43
+ | `/autoresearch-lab <instruction>` | Start/update parallel research mode |
44
+ | `/autoresearch-lab status` | Show lab state |
45
+ | `/autoresearch-lab clear` | Clear lab + underlying loop |
46
+
47
+ ## Behavior notes
48
+
49
+ - `loop` supports minimum interval `1m`.
50
+ - `goal` checks are done through assistant loop ticks; `GOAL_DONE` / `GOAL_CONTINUE` are preserved.
51
+ - `autoresearch` and `autoresearch-lab` are thin facades over `/goal + /loop`.
52
+ - A goal or loop activation enables scheduled advisor check-ins; stopping or clearing the active goal/loop disables them again.
53
+ - Check-ins are part of orchestration lifecycle, not a standalone advisor command. They use the advisor interval, higher/advanced advisor models first, and regular model fallback by default.
54
+ - A small repetition guard detects repeated assistant output and nudges the next turn to inspect current state before retrying.
55
+ - There are no hidden flow budgets. Long loops run until `/loop off`, `/goal clear`, or a `GOAL_DONE` response clears the active goal and loop.
56
+ - Stale research state is cleared when `goal` or `loop` are cleared.
@@ -0,0 +1 @@
1
+ export { default, registerAutoresearch, registerGoal, registerLoop, registerOrchestration } from "../src/index.js";
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@fiale-plus/pi-rogue-orchestration",
3
+ "version": "0.1.11",
4
+ "description": "Orchestration controls for Pi-Rogue: scheduled loop, goal resolution, and autoresearch. (Releases paused; consolidated into @fiale-plus/pi-rogue-bundle. Install the bundle instead.)",
5
+ "private": true,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/fiale-plus/pi-rogue.git"
11
+ },
12
+ "keywords": [
13
+ "pi-package"
14
+ ],
15
+ "scripts": {
16
+ "check": "tsc -p ../../tsconfig.json --noEmit",
17
+ "test": "cd ../.. && vitest run packages/orchestration/src/*.test.ts"
18
+ },
19
+ "main": "./orchestration/index.ts",
20
+ "exports": {
21
+ ".": "./orchestration/index.ts"
22
+ },
23
+ "pi": {
24
+ "extensions": [
25
+ "./orchestration"
26
+ ],
27
+ "skills": [
28
+ "./skills"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "^0.74.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "files": [
38
+ "orchestration",
39
+ "src",
40
+ "skills",
41
+ "README.md",
42
+ "package.json"
43
+ ]
44
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: orchestration
3
+ description: Session orchestration for Pi; use when you want to manage loop cadence, goals, or opt-in autoresearch in the current session.
4
+ ---
5
+
6
+ # Pi-Rogue Orchestration
7
+
8
+ Use this skill to run measurable, bounded workflow loops inside a Pi session.
9
+
10
+ ## Command surface
11
+
12
+ | Command | What it does |
13
+ |---|---|
14
+ | `/goal set <text>` | Set or update the current goal |
15
+ | `/goal show` | Show current goal |
16
+ | `/goal clear` | Clear goal |
17
+ | `/goal list` | Show recent goal history |
18
+ | `/loop <interval> <instruction>` | Run periodic checks (`1m` minimum) |
19
+ | `/loop status` | Show current loop |
20
+ | `/loop off` / `/loop clear` / `/loop stop` | Stop and clear loop |
21
+ | `/autoresearch <instruction>` | Solo iterative research on top of `/goal + /loop` |
22
+ | `/autoresearch status` | Show research counters and backing state |
23
+ | `/autoresearch clear` | Clear research and stop backing loop |
24
+ | `/autoresearch-lab <instruction>` | Parallel research mode (lab) |
25
+ | `/autoresearch-lab status` | Show lab state |
26
+ | `/autoresearch-lab clear` | Clear lab and stop backing loop |
27
+
28
+ ## Behavior rules
29
+
30
+ - `loop` is the primitive; `goal` is the execution intent.
31
+ - Goal completion is explicit through `GOAL_DONE` / `GOAL_CONTINUE` in loop checks.
32
+ - `autoresearch` / `autoresearch-lab` are facades over goal+loop.
33
+ - Goal or loop activation enables scheduled advisor check-ins; stopping or clearing either disables them.
34
+ - Check-ins belong to orchestration lifecycle, not the advisor command surface, and use higher/advanced advisor models first, with regular model fallback enabled by default.
35
+ - `autoresearch` enforces multi-cycle + evidence-aware completion.
36
+ - Clearing goal/loop clears stale autoresearch state.
37
+
38
+ ## Safety and agentic flow
39
+
40
+ - Auto-detect opportunities are proposals first, not silent launches.
41
+ - `autoresearch-lab` requires explicit confirmation for escalation.
42
+ - Commands remain distinct:
43
+ - `/autoresearch` = solo optimization
44
+ - `/autoresearch-lab` = parallel lab mode