@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.
Files changed (68) hide show
  1. package/README.md +50 -0
  2. package/node_modules/@fiale-plus/pi-core/README.md +13 -0
  3. package/node_modules/@fiale-plus/pi-core/package.json +25 -0
  4. package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
  5. package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
  6. package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
  7. package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
  8. package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
  9. package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
  10. package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
  11. package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
  18. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
  19. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
  20. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  21. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  22. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
  23. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
  24. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  25. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
  26. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
  27. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  28. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  29. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
  30. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
  31. package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
  32. package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -0
  33. package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
  34. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +749 -0
  35. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
  36. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
  37. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
  38. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
  39. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
  40. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
  41. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  42. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  43. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  44. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  45. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  46. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
  47. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  48. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  49. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  50. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  51. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  52. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  53. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  54. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  55. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  56. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  57. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  58. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  59. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  60. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  61. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  62. package/package.json +51 -0
  63. package/src/context-broker-file.ts +1 -0
  64. package/src/context-broker-sqlite.ts +1 -0
  65. package/src/context-broker.ts +1 -0
  66. package/src/extension.test.ts +68 -0
  67. package/src/extension.ts +27 -0
  68. package/src/index.ts +1 -0
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@fiale-plus/pi-rogue-advisor",
3
+ "version": "0.1.5",
4
+ "description": "Pi-Rogue advisor extension for Pi — multi-model support, SOTA model suggestion, cache-aware session advisory. (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/advisor/src/*.test.ts"
18
+ },
19
+ "main": "./src/index.ts",
20
+ "exports": {
21
+ ".": "./src/index.ts"
22
+ },
23
+ "pi": {
24
+ "extensions": [
25
+ "./advisor"
26
+ ],
27
+ "skills": [
28
+ "./skills"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "^0.74.0",
33
+ "@earendil-works/pi-ai": "^0.74.0",
34
+ "@earendil-works/pi-tui": "^0.74.0"
35
+ },
36
+ "dependencies": {
37
+ "typebox": "^1.1.24"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "files": [
43
+ "advisor",
44
+ "src",
45
+ "skills",
46
+ "assets",
47
+ "README.md",
48
+ "package.json"
49
+ ]
50
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: advisor
3
+ description: Zero-config strategic advisor for Pi. Auto-detects best model, phase-aware routing, preflight + post-review + cache. Use for architecture, tradeoffs, planning.
4
+ ---
5
+
6
+ # Pi-Rogue Advisor
7
+
8
+ Use this skill for non-trivial decisions before/after significant edits.
9
+
10
+ ## Quick start
11
+
12
+ - `/pi-rogue` — open cockpit and command pointers
13
+ - `/advisor status` — show current advisor settings and model route
14
+ - `/advisor <question>` — ask immediate advice
15
+ - Check-ins are lifecycle-managed by orchestration, not by the advisor command surface
16
+
17
+ ## Command surface
18
+
19
+ | Command | What it does |
20
+ |---|---|
21
+ | `/advisor` | Show status + config summary |
22
+ | `/advisor status` | Same as `/advisor` |
23
+ | `/advisor on` | Enable auto mode |
24
+ | `/advisor off` | Disable advisor |
25
+ | `/advisor mode auto\|manual\|off` | Control when advisor auto-runs |
26
+ | `/advisor review light\|strict\|off` | Set review threshold |
27
+ | `/advisor config` | Dump full config |
28
+ | `/advisor pause <N>` | Pause advisor auto-runs for the next N turns |
29
+ | `/advisor unpause` | Resume advisor auto-runs immediately |
30
+ | `/advisor model <provider/model>` | Pin model explicitly |
31
+ | `/advisor <question>` | Run one advisory response |
32
+
33
+ ## Routing and safety
34
+
35
+ - Preflight is heuristics + quick local gate first.
36
+ - Review runs after edits and/or at completion points by policy.
37
+ - No standalone check-in command: check-ins are triggered from goal/loop orchestration cadence (not from advisor internals), using higher/advanced advisor models first with regular model fallback enabled by default.
38
+
39
+ ## Keep scope clear
40
+
41
+ - Successful `on_track` review verdicts are recorded silently instead of displayed as follow-up messages.
42
+ - Goal/loop-managed check-ins gate on session activity and `checkinIntervalMinutes`, avoid overlapping calls, and use higher/advanced advisor models first with regular model fallback enabled by default.
43
+ - The advisor surface is separate from orchestration (`goal`/`loop`/`autoresearch`) and intentionally stays a small command set with explicit entries above.
44
+
45
+ ## Defaults
46
+
47
+ - `mode: auto`
48
+ - `review: light`
49
+ - `checkins: off` by default; orchestration owns cadence and enables them when a goal or loop is active
50
+ - `checkinIntervalMinutes: 30`
51
+ - `model: auto`
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { extractBinaryGateFeatureCounts } from "./binary-gate-features.js";
3
+
4
+ describe("binary gate feature extraction", () => {
5
+ it("emits shared lexical and routing cue features", () => {
6
+ const features = extractBinaryGateFeatureCounts("review the auth migration diff before production deploy?");
7
+
8
+ expect(features.get("cue:question_mark")).toBe(1);
9
+ expect(features.get("cue:question_punct")).toBe(1);
10
+ expect(features.get("cue:imperative")).toBe(1);
11
+ expect(features.get("len_bucket:medium")).toBe(1);
12
+ expect(features.get("complex:auth")).toBe(1);
13
+ expect(features.get("complex:migration")).toBe(1);
14
+ expect(features.get("review:review")).toBe(1);
15
+ expect(features.get("review:diff")).toBe(1);
16
+ expect(features.get("safety:production")).toBe(1);
17
+ expect(features.get("safety:deploy")).toBe(1);
18
+ });
19
+ });
@@ -0,0 +1,248 @@
1
+ const NORMALIZE_REPLACEMENT_PATTERNS = [
2
+ [/https?:\/\/\S+/g, " url "],
3
+ [/[^a-z0-9\s']/g, " "],
4
+ ] as const;
5
+
6
+ function normalizeBinaryGateText(text: string): string {
7
+ return String(text ?? "")
8
+ .toLowerCase()
9
+ .replace(NORMALIZE_REPLACEMENT_PATTERNS[0]![0], NORMALIZE_REPLACEMENT_PATTERNS[0]![1])
10
+ .replace(NORMALIZE_REPLACEMENT_PATTERNS[1]![0], NORMALIZE_REPLACEMENT_PATTERNS[1]![1])
11
+ .replace(/\s+/g, " ")
12
+ .trim();
13
+ }
14
+
15
+ function normalizeBinaryGateTokens(text: string): string[] {
16
+ const norm = normalizeBinaryGateText(text);
17
+ return norm ? norm.split(" ").filter(Boolean) : [];
18
+ }
19
+
20
+ function replaceSpaces(value: string): string {
21
+ return value.replace(/\s+/g, "_");
22
+ }
23
+
24
+ function inc(map: Map<string, number>, key: string, by = 1): void {
25
+ map.set(key, (map.get(key) || 0) + by);
26
+ }
27
+
28
+ export function extractBinaryGateFeatureCounts(text: string): Map<string, number> {
29
+ const counts = new Map<string, number>();
30
+ const toks = normalizeBinaryGateTokens(text);
31
+ const lower = normalizeBinaryGateText(text);
32
+
33
+ for (const n of [1, 2]) {
34
+ if (toks.length >= n) {
35
+ for (let i = 0; i <= toks.length - n; i++) {
36
+ inc(counts, `w${n}:${toks.slice(i, i + n).join("_")}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ const norm = ` ${lower} `;
42
+ for (const n of [3, 4]) {
43
+ if (norm.length >= n) {
44
+ for (let i = 0; i <= norm.length - n; i++) {
45
+ const g = norm.slice(i, i + n);
46
+ if (!/^\s+$/.test(g)) {
47
+ inc(counts, `c${n}:${g}`);
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ if (toks.length > 0) inc(counts, `pref1:${toks[0]}`);
54
+ if (toks.length > 1) inc(counts, `pref2:${toks.slice(0, 2).join("_")}`);
55
+ if (toks.length > 2) inc(counts, `pref3:${toks.slice(0, 3).join("_")}`);
56
+
57
+ if (text.includes("?")) inc(counts, "cue:question_mark");
58
+
59
+ if (toks.length > 0) {
60
+ inc(counts, `len_bucket:${toks.length <= 3 ? "short" : toks.length <= 8 ? "medium" : "long"}`);
61
+ }
62
+
63
+ if (/[\?\!]/.test(text)) {
64
+ inc(counts, "cue:question_punct");
65
+ }
66
+
67
+ const imperative = /^(create|add|make|change|write|fix|update|remove|delete|run|install|set|build|deploy|check|investigate|debug|review|test|refactor|merge|close|open|start|stop|continue|show|list|compact|setup|implement|build|write|create|add|make|refactor|rename|extract|migrate|patch)/i.test(text.trim());
68
+ if (imperative) {
69
+ inc(counts, "cue:imperative");
70
+ }
71
+
72
+ const safetyWords = [
73
+ "rm -rf",
74
+ "sudo",
75
+ "shutdown",
76
+ "reboot",
77
+ "mkfs",
78
+ "chmod -R",
79
+ "chown",
80
+ "git push --force",
81
+ "curl | sh",
82
+ "wget | sh",
83
+ "drop table",
84
+ "delete database",
85
+ "secret",
86
+ "token",
87
+ "credential",
88
+ "password",
89
+ "prod",
90
+ "production",
91
+ "deploy",
92
+ "deploying",
93
+ ];
94
+ for (const safetyWord of safetyWords) {
95
+ if (lower.includes(safetyWord)) {
96
+ inc(counts, `safety:${replaceSpaces(safetyWord)}`);
97
+ }
98
+ }
99
+
100
+ const complexityWords = [
101
+ "architecture",
102
+ "refactor",
103
+ "design",
104
+ "tradeoff",
105
+ "security",
106
+ "auth",
107
+ "migration",
108
+ "performance",
109
+ "scale",
110
+ "scalability",
111
+ "framework",
112
+ "system design",
113
+ "schema",
114
+ "data model",
115
+ "protocol",
116
+ "advisor routing",
117
+ "advisor flow",
118
+ "router logic",
119
+ "call vs skip",
120
+ "skip vs call",
121
+ "compare",
122
+ "recommend",
123
+ "benchmark",
124
+ "evaluate",
125
+ "experiment",
126
+ "train",
127
+ "strategy",
128
+ "choose",
129
+ "make sense",
130
+ "worth",
131
+ "kpi",
132
+ "kpis",
133
+ "how it works",
134
+ "where it comes from",
135
+ "what would you choose",
136
+ "what do you think",
137
+ "next step",
138
+ "pick between",
139
+ "buy",
140
+ "usage",
141
+ "sustained speed",
142
+ "available models",
143
+ "running model kpis",
144
+ ];
145
+ let complexityCount = 0;
146
+ for (const complexityWord of complexityWords) {
147
+ if (lower.includes(complexityWord)) {
148
+ complexityCount++;
149
+ inc(counts, `complex:${replaceSpaces(complexityWord)}`);
150
+ }
151
+ }
152
+ if (complexityCount > 0) {
153
+ inc(counts, `complex_count:${complexityCount}`);
154
+ }
155
+
156
+ const debugWords = ["debug", "bug", "error", "stack trace", "traceback", "fail", "broken", "investigate", "why is", "cannot", "can't", "crash", "regression"];
157
+ for (const debugWord of debugWords) {
158
+ if (lower.includes(debugWord)) {
159
+ inc(counts, `debug:${replaceSpaces(debugWord)}`);
160
+ }
161
+ }
162
+
163
+ const contextWords = ["need more context", "missing context", "clarify", "not enough info", "unspecified", "unknown", "ambiguous"];
164
+ for (const contextWord of contextWords) {
165
+ if (lower.includes(contextWord)) {
166
+ inc(counts, `context:${replaceSpaces(contextWord)}`);
167
+ }
168
+ }
169
+
170
+ const reviewWords = ["review", "check", "verify", "validate", "diff", "pr", "pull request", "feedback"];
171
+ for (const reviewWord of reviewWords) {
172
+ if (lower.includes(reviewWord)) {
173
+ inc(counts, `review:${replaceSpaces(reviewWord)}`);
174
+ }
175
+ }
176
+
177
+ const doneWords = ["done", "complete", "fixed", "implemented", "works", "passing tests", "tests pass", "verified", "looks good", "merged"];
178
+ for (const doneWord of doneWords) {
179
+ if (lower.includes(doneWord)) {
180
+ inc(counts, `done:${replaceSpaces(doneWord)}`);
181
+ }
182
+ }
183
+
184
+ const checkinWords = ["check-in", "checkin", "mid-hour", "alignment", "progress", "status", "stats", "log", "logs"];
185
+ for (const checkinWord of checkinWords) {
186
+ if (lower.includes(checkinWord)) {
187
+ inc(counts, `checkin:${replaceSpaces(checkinWord)}`);
188
+ }
189
+ }
190
+
191
+ const cues = [
192
+ "check",
193
+ "why",
194
+ "what",
195
+ "how",
196
+ "should",
197
+ "status",
198
+ "stats",
199
+ "log",
200
+ "logs",
201
+ "review",
202
+ "diff",
203
+ "pr",
204
+ "build",
205
+ "run",
206
+ "test",
207
+ "deploy",
208
+ "fix",
209
+ "debug",
210
+ "install",
211
+ "configure",
212
+ "plan",
213
+ "continue",
214
+ "resume",
215
+ "compact",
216
+ "research",
217
+ "update",
218
+ "patch",
219
+ "cleanup",
220
+ "remove",
221
+ ];
222
+ const multi = [
223
+ "what is",
224
+ "what's",
225
+ "safe to use",
226
+ "pull request",
227
+ "model family",
228
+ "how does",
229
+ "next step",
230
+ "path forward",
231
+ "should we",
232
+ "what should",
233
+ ];
234
+
235
+ const tokenSet = new Set(toks);
236
+ for (const cue of cues) {
237
+ if (tokenSet.has(cue)) {
238
+ inc(counts, `cue:${cue}`);
239
+ }
240
+ }
241
+ for (const cue of multi) {
242
+ if (lower.includes(cue)) {
243
+ inc(counts, `cue:${replaceSpaces(cue)}`);
244
+ }
245
+ }
246
+
247
+ return counts;
248
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { binaryGatePredict } from "./router.js";
3
+
4
+ describe("binary gate model", () => {
5
+ it("returns a decision when model is available", () => {
6
+ const result = binaryGatePredict("test");
7
+ if (result) {
8
+ expect(["continue", "escalate"]).toContain(result.decision);
9
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
10
+ expect(result.confidence).toBeLessThanOrEqual(1);
11
+ }
12
+ });
13
+
14
+ it("classifies short prompts and returns a valid decision", () => {
15
+ const result = binaryGatePredict("fix typo");
16
+ if (result) {
17
+ expect(["continue", "escalate"]).toContain(result.decision);
18
+ expect(result.confidence).toBeGreaterThan(0.5);
19
+ }
20
+ });
21
+
22
+ it("handles empty text gracefully", () => {
23
+ const result = binaryGatePredict("");
24
+ if (result) {
25
+ expect(["continue", "escalate"]).toContain(result.decision);
26
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
27
+ expect(result.confidence).toBeLessThanOrEqual(1);
28
+ }
29
+ });
30
+
31
+ it("handles very long text without crashing", () => {
32
+ const longText = "a".repeat(10000);
33
+ const result = binaryGatePredict(longText);
34
+ if (result) {
35
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
36
+ expect(result.confidence).toBeLessThanOrEqual(1);
37
+ }
38
+ });
39
+
40
+ it("handles unicode text", () => {
41
+ const result = binaryGatePredict("Привет мир 你好世界 مرحبا بالعالم");
42
+ if (result) {
43
+ expect(["continue", "escalate"]).toContain(result.decision);
44
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
45
+ expect(result.confidence).toBeLessThanOrEqual(1);
46
+ }
47
+ });
48
+
49
+ it("handles special characters and potential injection", () => {
50
+ const result = binaryGatePredict("fix <script>alert('xss')</script> && rm -rf /");
51
+ if (result) {
52
+ expect(["continue", "escalate"]).toContain(result.decision);
53
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
54
+ expect(result.confidence).toBeLessThanOrEqual(1);
55
+ }
56
+ });
57
+
58
+ it("handles URLs", () => {
59
+ const result = binaryGatePredict("check https://example.com/path?query=value&foo=bar");
60
+ if (result) {
61
+ expect(["continue", "escalate"]).toContain(result.decision);
62
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
63
+ expect(result.confidence).toBeLessThanOrEqual(1);
64
+ }
65
+ });
66
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
3
+
4
+ describe("advisor completions", () => {
5
+ it("offers top-level advisor continuations", () => {
6
+ const values = advisorArgumentCompletions("")?.map((i) => i.value);
7
+ expect(values).toEqual(expect.arrayContaining(["status", "config", "model", "review", "pause", "unpause"]));
8
+ expect(values).not.toContain("checkins");
9
+ });
10
+
11
+ it("offers nested review choices", () => {
12
+ const values = advisorArgumentCompletions("review ")?.map((i) => i.value);
13
+ expect(values).toEqual(["light", "strict", "off"]);
14
+ });
15
+
16
+ });
17
+
18
+ describe("pi-rogue cockpit completions", () => {
19
+ it("offers umbrella sections", () => {
20
+ const values = piRogueArgumentCompletions("")?.map((i) => i.value);
21
+ expect(values).toEqual(expect.arrayContaining(["status", "advisor", "orchestration", "help"]));
22
+ });
23
+
24
+ it("fans out to orchestration shortcuts", () => {
25
+ const values = piRogueArgumentCompletions("orchestration ")?.map((i) => i.value);
26
+ expect(values).toEqual(expect.arrayContaining(["goal", "loop", "autoresearch", "autoresearch-lab"]));
27
+ });
28
+ });
@@ -0,0 +1,79 @@
1
+ type CompletionItem = { value: string; label: string; description?: string };
2
+
3
+ function item(value: string, description?: string): CompletionItem {
4
+ return { value, label: value, ...(description ? { description } : {}) };
5
+ }
6
+
7
+ function complete(values: Array<[string, string?]>, prefix: string): CompletionItem[] | null {
8
+ const q = prefix.trimStart().toLowerCase();
9
+ const items = values.map(([value, description]) => item(value, description));
10
+ const filtered = q
11
+ ? items.filter((i) => i.value.startsWith(q))
12
+ : items;
13
+ return filtered.length > 0 ? filtered : null;
14
+ }
15
+
16
+ function completionsForPrefix(prefix: string, topLevel: Array<[string, string?]>, nested: Record<string, Array<[string, string?]>>): CompletionItem[] | null {
17
+ const q = prefix.trimStart().toLowerCase();
18
+ if (!q) return complete(topLevel, q);
19
+
20
+ const [head, ...rest] = q.split(/\s+/);
21
+ if (!head) return complete(topLevel, q);
22
+
23
+ if (rest.length === 0) {
24
+ const top = complete(topLevel, head);
25
+ if (top) return top;
26
+ }
27
+
28
+ const next = nested[head];
29
+ if (next) {
30
+ return complete(next, rest.join(" "));
31
+ }
32
+
33
+ return complete(topLevel, q);
34
+ }
35
+
36
+ const advisorTopLevel: Array<[string, string?]> = [
37
+ ["status", "show status and configuration"],
38
+ ["config", "show full config"],
39
+ ["on", "enable auto mode"],
40
+ ["off", "disable advisor"],
41
+ ["mode", "set auto/manual/off"],
42
+ ["review", "set light/strict/off"],
43
+ ["pause", "pause advisor auto-runs for N turns"],
44
+ ["unpause", "resume advisor auto-runs immediately"],
45
+ ["model", "set or inspect model override"],
46
+ ];
47
+
48
+ const advisorNested: Record<string, Array<[string, string?]>> = {
49
+ mode: [["auto"], ["manual"], ["off"]],
50
+ review: [["light"], ["strict"], ["off"]],
51
+ model: [["auto"], ["openai-codex/gpt-5.5"], ["anthropic/claude-opus-4-6"]],
52
+ };
53
+
54
+ const piRogueTopLevel: Array<[string, string?]> = [
55
+ ["status", "show cockpit"],
56
+ ["advisor", "advisor status"],
57
+ ["orchestration", "goal/loop/autoresearch shortcuts"],
58
+ ["help", "show cockpit help"],
59
+ ];
60
+
61
+ const piRogueNested: Record<string, Array<[string, string?]>> = {
62
+ advisor: advisorTopLevel,
63
+ orchestration: [
64
+ ["goal", "goal commands"],
65
+ ["loop", "loop commands"],
66
+ ["autoresearch", "solo research flow"],
67
+ ["autoresearch-lab", "parallel research flow"],
68
+ ["status", "show all surfaces"],
69
+ ],
70
+ help: [["advisor"], ["orchestration"], ["status"]],
71
+ };
72
+
73
+ export function advisorArgumentCompletions(prefix: string): CompletionItem[] | null {
74
+ return completionsForPrefix(prefix, advisorTopLevel, advisorNested);
75
+ }
76
+
77
+ export function piRogueArgumentCompletions(prefix: string): CompletionItem[] | null {
78
+ return completionsForPrefix(prefix, piRogueTopLevel, piRogueNested);
79
+ }