@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
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @fiale-plus/pi-rogue
2
+
3
+ `@fiale-plus/pi-rogue` is the **single consolidated public artefact** for Pi-Rogue.
4
+
5
+ It stitches together (and bundles for a true single-package install):
6
+
7
+ - `@fiale-plus/pi-core` (shared contracts/helpers)
8
+ - `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
9
+ - `@fiale-plus/pi-rogue-context-broker` (context-broker runtime; registered by default with an env kill switch)
10
+ - `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
11
+
12
+ Direct installs of the advisor/orchestration packages are paused (marked private). All users and future releases go through the bundle. See `docs/release.md` and root `AGENTS.md` / `README.md` for the release policy.
13
+
14
+ ## Install (recommended)
15
+
16
+ ```bash
17
+ pi install npm:@fiale-plus/pi-rogue
18
+ ```
19
+
20
+ For local monorepo dev:
21
+
22
+ ```bash
23
+ # from repo root
24
+ npm install
25
+ # then use workspace packages as needed (e.g. for testing changes to advisor/orch before a bundle release)
26
+ ```
27
+
28
+ ## Scope boundaries
29
+
30
+ - **Lab / internal helpers are excluded from this bundle.**
31
+ - The context-broker runtime is bundled and registered by default in the bundle.
32
+ - Consumers can import the runtime through the bundle subpath: `@fiale-plus/pi-rogue/context-broker`.
33
+ - Set `PI_CONTEXT_BROKER_ENABLED=false` before starting Pi to disable the `/context` command surface and prompt-load rewriting.
34
+ - Optional durable broker storage can be enabled with `PI_CONTEXT_BROKER_DURABLE=true` or `PI_CONTEXT_BROKER_STORE_DIR=/path/to/store`; it defaults to SQLite/FTS and supports `PI_CONTEXT_BROKER_BACKEND=jsonl` for the legacy JSONL/blob backend.
35
+ - `@fiale-plus/pi-rogue` is the only published surface for the logic.
36
+ - Internal helper packages (`@fiale-plus/pi-rogue-guardrails`, `@fiale-plus/pi-rogue-brain`, `@fiale-plus/pi-rogue-repo-arch`) are maintained separately in the lab section and not published.
37
+
38
+ ## Command surface
39
+
40
+ - Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
41
+ - Context broker: enabled by default; `PI_CONTEXT_BROKER_ENABLED=false` disables `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, `/context export <handle>`, and `/context prune` with autocomplete.
42
+
43
+ ## Status
44
+
45
+ - **Published:** yes (single artefact)
46
+ - The advisor and orchestration packages continue to receive code changes in this repo; they ship inside bundle releases via `bundledDependencies`.
47
+
48
+ ## Release notes
49
+
50
+ Only `pi-rogue-<semver>` tags/releases are produced. See `docs/release.md` for the full clean policy and checklist.
@@ -0,0 +1,13 @@
1
+ # Pi-Rogue Core
2
+
3
+ Shared helpers for the Pi-Rogue workspace.
4
+
5
+ Includes shared bounded context broker contracts:
6
+
7
+ - `BoundedContextBroker`
8
+ - `ContextArtifact` / `ContextArtifactInput`
9
+ - lookup, retention, and status type definitions
10
+
11
+ The executable in-memory implementation lives in `@fiale-plus/pi-rogue-context-broker`.
12
+
13
+ Install locally from this repo root: `npm install`
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@fiale-plus/pi-core",
3
+ "version": "0.1.0",
4
+ "description": "Shared helpers for the Pi-Rogue workspace.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package"
9
+ ],
10
+ "scripts": {
11
+ "test": "cd ../.. && vitest run packages/core/src/*.test.ts"
12
+ },
13
+ "private": true,
14
+ "main": "./src/index.ts",
15
+ "exports": {
16
+ ".": "./src/index.ts"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }
@@ -0,0 +1,109 @@
1
+ export type ContextArtifactKind =
2
+ | "tool_output"
3
+ | "diff"
4
+ | "file_snapshot"
5
+ | "subagent_result"
6
+ | "advisor_brief"
7
+ | "memory_note";
8
+
9
+ export type ContextArtifactTier = "hot" | "warm" | "cold";
10
+
11
+ export interface ContextArtifactInput {
12
+ sessionId: string;
13
+ kind: ContextArtifactKind;
14
+ payload: string | Buffer;
15
+ summary?: string;
16
+ tags?: string[];
17
+ paths?: string[];
18
+ command?: string;
19
+ branch?: string;
20
+ tier?: ContextArtifactTier;
21
+ ttlMs?: number;
22
+ pinned?: boolean;
23
+ parentIds?: string[];
24
+ createdAt?: number;
25
+ }
26
+
27
+ export interface ContextArtifact {
28
+ id: string;
29
+ handle: string;
30
+ sessionId: string;
31
+ kind: ContextArtifactKind;
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ bytes: number;
35
+ sha256: string;
36
+ payload: string;
37
+ summary: string;
38
+ tags: string[];
39
+ paths: string[];
40
+ command?: string;
41
+ branch?: string;
42
+ tier: ContextArtifactTier;
43
+ expiresAt?: number;
44
+ pinned: boolean;
45
+ parentIds: string[];
46
+ }
47
+
48
+ export interface ContextLookupQuery {
49
+ id?: string;
50
+ handle?: string;
51
+ sessionId?: string;
52
+ kind?: ContextArtifactKind;
53
+ tag?: string;
54
+ path?: string;
55
+ commandPrefix?: string;
56
+ branch?: string;
57
+ tier?: ContextArtifactTier;
58
+ text?: string;
59
+ limit?: number;
60
+ }
61
+
62
+ export interface ContextBrokerStatus {
63
+ records: number;
64
+ bytes: number;
65
+ pinnedRecords: number;
66
+ pinnedBytes: number;
67
+ hotRecords: number;
68
+ hotBytes: number;
69
+ warmRecords: number;
70
+ warmBytes: number;
71
+ coldRecords: number;
72
+ coldBytes: number;
73
+ maxRecords: number;
74
+ maxBytes: number;
75
+ }
76
+
77
+ export interface ContextBrokerOptions {
78
+ maxRecords?: number;
79
+ maxBytes?: number;
80
+ globalMaxRecords?: number;
81
+ globalMaxBytes?: number;
82
+ defaultTtlMs?: number;
83
+ hotTtlMs?: number;
84
+ warmTtlMs?: number;
85
+ coldTtlMs?: number;
86
+ hotMaxRecords?: number;
87
+ warmMaxRecords?: number;
88
+ coldMaxRecords?: number;
89
+ hotMaxBytes?: number;
90
+ warmMaxBytes?: number;
91
+ coldMaxBytes?: number;
92
+ summaryBytes?: number;
93
+ briefBytes?: number;
94
+ }
95
+
96
+ export interface ContextPurgeOptions {
97
+ sessionId?: string;
98
+ keepPinned?: boolean;
99
+ }
100
+
101
+ export interface BoundedContextBroker {
102
+ publish(input: ContextArtifactInput): ContextArtifact;
103
+ lookup(query?: ContextLookupQuery): ContextArtifact[];
104
+ pin(idOrHandle: string, pinned?: boolean): ContextArtifact | null;
105
+ prune(now?: number): ContextBrokerStatus;
106
+ purge(options?: ContextPurgeOptions): ContextBrokerStatus;
107
+ status(): ContextBrokerStatus;
108
+ renderBrief(query?: ContextLookupQuery & { budgetBytes?: number }): string;
109
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./context-broker.js";
2
+ export * from "./paths.js";
3
+ export * from "./risk.js";
4
+ export * from "./storage.js";
5
+ export * from "./text.js";
@@ -0,0 +1,36 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+
5
+ const ROOT_DIR = join(homedir(), ".pi", "agent", "fiale-plus");
6
+
7
+ export function appDir(): string {
8
+ mkdirSync(ROOT_DIR, { recursive: true });
9
+ return ROOT_DIR;
10
+ }
11
+
12
+ export function featureDir(feature: string): string {
13
+ const dir = join(appDir(), feature);
14
+ mkdirSync(dir, { recursive: true });
15
+ return dir;
16
+ }
17
+
18
+ export function sessionKey(ctx: any): string {
19
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
20
+ if (!sessionFile) return "session";
21
+ return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
22
+ }
23
+
24
+ export function sessionDir(feature: string, ctx: any): string {
25
+ const dir = join(featureDir(feature), sessionKey(ctx));
26
+ mkdirSync(dir, { recursive: true });
27
+ return dir;
28
+ }
29
+
30
+ export function featureFile(feature: string, filename: string): string {
31
+ return join(featureDir(feature), filename);
32
+ }
33
+
34
+ export function sessionFile(feature: string, ctx: any, filename: string): string {
35
+ return join(sessionDir(feature, ctx), filename);
36
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { scanShellCommand, type RiskScan } from "./risk.js";
3
+
4
+ describe("scanShellCommand", () => {
5
+ it("returns safe for empty command", () => {
6
+ const result = scanShellCommand("");
7
+ expect(result.safe).toBe(true);
8
+ expect(result.severity).toBe("safe");
9
+ });
10
+
11
+ it("returns safe for a benign command", () => {
12
+ const result = scanShellCommand("echo hello world");
13
+ expect(result.safe).toBe(true);
14
+ expect(result.severity).toBe("safe");
15
+ });
16
+
17
+ it("detects rm as dangerous", () => {
18
+ const result = scanShellCommand("rm -rf /tmp/foo");
19
+ expect(result.safe).toBe(false);
20
+ expect(result.severity).toBe("danger");
21
+ expect(result.findings.some((f) => f.id === "rm")).toBe(true);
22
+ });
23
+
24
+ it("detects rm only as a word boundary (not inside other words)", () => {
25
+ const result = scanShellCommand("program --remove-all");
26
+ expect(result.safe).toBe(true);
27
+ });
28
+
29
+ it("detects sudo as warning", () => {
30
+ const result = scanShellCommand("sudo apt update");
31
+ expect(result.safe).toBe(false);
32
+ expect(result.severity).toBe("warn");
33
+ expect(result.findings.some((f) => f.id === "sudo")).toBe(true);
34
+ });
35
+
36
+ it("detects chmod -R as dangerous", () => {
37
+ const result = scanShellCommand("chmod -R 777 /etc");
38
+ expect(result.safe).toBe(false);
39
+ expect(result.severity).toBe("danger");
40
+ expect(result.findings.some((f) => f.id === "chmod-r")).toBe(true);
41
+ });
42
+
43
+ it("detects chown as dangerous", () => {
44
+ const result = scanShellCommand("chown root:root /var");
45
+ expect(result.safe).toBe(false);
46
+ expect(result.severity).toBe("danger");
47
+ });
48
+
49
+ it("detects git push --force as dangerous", () => {
50
+ const result = scanShellCommand("git push --force origin main");
51
+ expect(result.safe).toBe(false);
52
+ expect(result.severity).toBe("danger");
53
+ expect(result.findings.some((f) => f.id === "force-push")).toBe(true);
54
+ });
55
+
56
+ it("detects git push --force-with-lease as dangerous", () => {
57
+ const result = scanShellCommand("git push --force-with-lease origin main");
58
+ expect(result.safe).toBe(false);
59
+ expect(result.severity).toBe("danger");
60
+ expect(result.findings.some((f) => f.id === "force-push")).toBe(true);
61
+ });
62
+
63
+ it("detects curl | sh as dangerous", () => {
64
+ const result = scanShellCommand("curl https://example.com/install.sh | sh");
65
+ expect(result.safe).toBe(false);
66
+ expect(result.severity).toBe("danger");
67
+ expect(result.findings.some((f) => f.id === "curl-shell")).toBe(true);
68
+ });
69
+
70
+ it("detects curl | bash as dangerous", () => {
71
+ const result = scanShellCommand("curl -fsSL https://example.com/install.sh | bash");
72
+ expect(result.safe).toBe(false);
73
+ expect(result.severity).toBe("danger");
74
+ expect(result.findings.some((f) => f.id === "curl-shell")).toBe(true);
75
+ });
76
+
77
+ it("detects mkfs as dangerous", () => {
78
+ const result = scanShellCommand("mkfs.ext4 /dev/sda1");
79
+ expect(result.safe).toBe(false);
80
+ expect(result.severity).toBe("danger");
81
+ });
82
+
83
+ it("detects dd if= as dangerous", () => {
84
+ const result = scanShellCommand("dd if=/dev/zero of=/tmp/out bs=1M count=10");
85
+ expect(result.safe).toBe(false);
86
+ expect(result.severity).toBe("danger");
87
+ });
88
+
89
+ it("detects shutdown as dangerous", () => {
90
+ const result = scanShellCommand("shutdown -h now");
91
+ expect(result.safe).toBe(false);
92
+ expect(result.severity).toBe("danger");
93
+ });
94
+
95
+ it("detects reboot as dangerous", () => {
96
+ const result = scanShellCommand("reboot");
97
+ expect(result.safe).toBe(false);
98
+ expect(result.severity).toBe("danger");
99
+ });
100
+
101
+ it("scans extra fragments", () => {
102
+ const result = scanShellCommand("docker exec -it container bash", ["docker exec"]);
103
+ expect(result.safe).toBe(false);
104
+ expect(result.findings.some((f) => f.id === "extra:docker exec")).toBe(true);
105
+ });
106
+
107
+ it("returns warn severity when only warnings are found", () => {
108
+ const result = scanShellCommand("sudo echo hello");
109
+ expect(result.safe).toBe(false);
110
+ expect(result.severity).toBe("warn");
111
+ });
112
+
113
+ it("returns danger severity when danger items are found even with warnings", () => {
114
+ const result = scanShellCommand("sudo rm /tmp/foo");
115
+ expect(result.safe).toBe(false);
116
+ expect(result.severity).toBe("danger");
117
+ });
118
+
119
+ it("handles case-insensitive matching", () => {
120
+ const result = scanShellCommand("RM -rf /");
121
+ expect(result.safe).toBe(false);
122
+ expect(result.findings.some((f) => f.id === "rm")).toBe(true);
123
+ });
124
+
125
+ it("ignores empty extra fragments", () => {
126
+ const result = scanShellCommand("echo foo", ["", " "]);
127
+ expect(result.safe).toBe(true);
128
+ });
129
+ });
@@ -0,0 +1,97 @@
1
+ export interface RiskFinding {
2
+ id: string;
3
+ label: string;
4
+ severity: "warn" | "danger";
5
+ }
6
+
7
+ export interface RiskScan {
8
+ safe: boolean;
9
+ severity: "safe" | "warn" | "danger";
10
+ findings: RiskFinding[];
11
+ reason: string;
12
+ }
13
+
14
+ const DEFAULT_PATTERNS: RiskFinding[] = [
15
+ { id: "rm", label: "rm", severity: "danger" },
16
+ { id: "sudo", label: "sudo", severity: "warn" },
17
+ { id: "chmod-r", label: "chmod -R", severity: "danger" },
18
+ { id: "chown", label: "chown", severity: "danger" },
19
+ { id: "mkfs", label: "mkfs", severity: "danger" },
20
+ { id: "dd", label: "dd if=", severity: "danger" },
21
+ { id: "shutdown", label: "shutdown", severity: "danger" },
22
+ { id: "reboot", label: "reboot", severity: "danger" },
23
+ { id: "force-push", label: "git push --force", severity: "danger" },
24
+ { id: "curl-shell", label: "curl | sh", severity: "danger" },
25
+ { id: "wget-shell", label: "wget | sh", severity: "danger" },
26
+ ];
27
+
28
+ function contains(command: string, fragment: string): boolean {
29
+ return command.toLowerCase().includes(fragment.toLowerCase());
30
+ }
31
+
32
+ export function scanShellCommand(command: string, extraFragments: string[] = []): RiskScan {
33
+ const findings: RiskFinding[] = [];
34
+ const text = command.trim();
35
+
36
+ if (!text) {
37
+ return { safe: true, severity: "safe", findings, reason: "Empty command." };
38
+ }
39
+
40
+ for (const pattern of DEFAULT_PATTERNS) {
41
+ switch (pattern.id) {
42
+ case "rm":
43
+ if (/\brm\b/i.test(text)) findings.push(pattern);
44
+ break;
45
+ case "sudo":
46
+ if (/\bsudo\b/i.test(text)) findings.push(pattern);
47
+ break;
48
+ case "chmod-r":
49
+ if (/\bchmod\s+-R\b/i.test(text)) findings.push(pattern);
50
+ break;
51
+ case "chown":
52
+ if (/\bchown\b/i.test(text)) findings.push(pattern);
53
+ break;
54
+ case "mkfs":
55
+ if (/\bmkfs(?:\.[\w-]+)?\b/i.test(text)) findings.push(pattern);
56
+ break;
57
+ case "dd":
58
+ if (/\bdd\s+if=/i.test(text)) findings.push(pattern);
59
+ break;
60
+ case "shutdown":
61
+ if (/\bshutdown\b/i.test(text)) findings.push(pattern);
62
+ break;
63
+ case "reboot":
64
+ if (/\breboot\b/i.test(text)) findings.push(pattern);
65
+ break;
66
+ case "force-push":
67
+ if (/\bgit\s+push\b[\s\S]*--force(?:-with-lease)?/i.test(text)) findings.push(pattern);
68
+ break;
69
+ case "curl-shell":
70
+ if (/\bcurl\b[\s\S]*\|\s*(sh|bash)\b/i.test(text)) findings.push(pattern);
71
+ break;
72
+ case "wget-shell":
73
+ if (/\bwget\b[\s\S]*\|\s*(sh|bash)\b/i.test(text)) findings.push(pattern);
74
+ break;
75
+ }
76
+ }
77
+
78
+ for (const fragment of extraFragments) {
79
+ if (!fragment.trim()) continue;
80
+ if (contains(text, fragment)) {
81
+ findings.push({ id: `extra:${fragment}`, label: fragment, severity: "danger" });
82
+ }
83
+ }
84
+
85
+ if (findings.length === 0) {
86
+ return { safe: true, severity: "safe", findings, reason: "No risky shell fragments found." };
87
+ }
88
+
89
+ const severity = findings.some((finding) => finding.severity === "danger") ? "danger" : "warn";
90
+ const labels = findings.map((finding) => finding.label).join(", ");
91
+ return {
92
+ safe: false,
93
+ severity,
94
+ findings,
95
+ reason: `Detected risky shell fragment(s): ${labels}`,
96
+ };
97
+ }
@@ -0,0 +1,39 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ export function ensureParent(filePath: string): string {
5
+ mkdirSync(dirname(filePath), { recursive: true });
6
+ return filePath;
7
+ }
8
+
9
+ export function readText(filePath: string, fallback = ""): string {
10
+ try {
11
+ return readFileSync(filePath, "utf8");
12
+ } catch {
13
+ return fallback;
14
+ }
15
+ }
16
+
17
+ export function writeText(filePath: string, text: string): void {
18
+ ensureParent(filePath);
19
+ writeFileSync(filePath, text, "utf8");
20
+ }
21
+
22
+ export function appendText(filePath: string, text: string): void {
23
+ ensureParent(filePath);
24
+ appendFileSync(filePath, text, "utf8");
25
+ }
26
+
27
+ export function readJson<T>(filePath: string, fallback: T): T {
28
+ try {
29
+ const raw = readText(filePath).trim();
30
+ if (!raw) return fallback;
31
+ return JSON.parse(raw) as T;
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+
37
+ export function writeJson(filePath: string, value: unknown): void {
38
+ writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
39
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { truncate, safeName } from "./text.js";
3
+
4
+ describe("truncate", () => {
5
+ it("returns full text when within limit", () => {
6
+ expect(truncate("hello", 10)).toBe("hello");
7
+ });
8
+
9
+ it("truncates and appends ellipsis when over limit", () => {
10
+ const result = truncate("hello world this is long", 12);
11
+ expect(result).toHaveLength(12);
12
+ expect(result.endsWith("…")).toBe(true);
13
+ });
14
+
15
+ it("handles empty string", () => {
16
+ expect(truncate("", 10)).toBe("");
17
+ });
18
+ });
19
+
20
+ describe("safeName", () => {
21
+ it("lowercases and replaces spaces with hyphens", () => {
22
+ expect(safeName("My Feature Branch")).toBe("my-feature-branch");
23
+ });
24
+
25
+ it("replaces special characters with hyphens", () => {
26
+ expect(safeName("feature/auth!!@#")).toBe("feature-auth");
27
+ });
28
+
29
+ it("strips leading/trailing hyphens", () => {
30
+ expect(safeName("--main--")).toBe("main");
31
+ });
32
+
33
+ it("falls back to 'main' for empty input", () => {
34
+ expect(safeName("")).toBe("main");
35
+ });
36
+ });
@@ -0,0 +1,14 @@
1
+ export function truncate(text: string, max: number): string {
2
+ if (text.length <= max) return text;
3
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
4
+ }
5
+
6
+ export function safeName(text: string): string {
7
+ return (
8
+ text
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9._-]+/g, "-")
12
+ .replace(/^-+|-+$/g, "") || "main"
13
+ );
14
+ }
@@ -0,0 +1,59 @@
1
+ # @fiale-plus/pi-rogue-advisor
2
+
3
+ > **Releases paused.** This package is now internal. All usage and updates are via the consolidated `@fiale-plus/pi-rogue` 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 the canonical package release.
4
+
5
+ ## What this package is
6
+
7
+ Strategic advisor for Pi sessions with low-overhead preflight/post-review routing, model auto-detection, session memory, and orchestration-managed mid-session check-ins.
8
+
9
+ - SOTA-first model fallback: `gpt-5.5`/`claude-opus-4-6`/`claude-sonnet-4-6` where available.
10
+ - Keeps command-level behavior simple and explicit.
11
+
12
+ ## Install
13
+
14
+ **For users:** Use the canonical package (releases for this package are paused):
15
+
16
+ ```bash
17
+ pi install npm:@fiale-plus/pi-rogue
18
+ ```
19
+
20
+ **For local development (monorepo only):**
21
+
22
+ ```bash
23
+ npm install --workspace packages/advisor
24
+ ```
25
+
26
+ (See root README and docs/release.md for the consolidated policy.)
27
+
28
+ ## Commands
29
+
30
+ | Command | What it does |
31
+ |---|---|
32
+ | `/pi-rogue` | Show the Pi-Rogue cockpit + command pointers |
33
+ | `/advisor` | Show status (`/advisor status`) and quick hint |
34
+ | `/advisor status` | Show mode, review policy, check-in status, model selection, counters |
35
+ | `/advisor on` | Enable advisor (auto mode) |
36
+ | `/advisor off` | Disable advisor |
37
+ | `/advisor mode auto\|manual\|off` | Change routing behavior |
38
+ | `/advisor review light\|strict\|off` | Change review strictness |
39
+ | `/advisor pause <N>` | Pause advisor auto-runs for the next N turns |
40
+ | `/advisor unpause` | Resume advisor auto-runs immediately |
41
+ | `/advisor config` | Show current config |
42
+ | `/advisor model <provider>/<model>` | Set explicit model override |
43
+ | `/advisor <question>` | Get one-shot advisory response |
44
+
45
+ ## Notes on defaults
46
+
47
+ - `mode`: `auto`
48
+ - `review`: `light`
49
+ - `checkins`: `off` (orchestration turns them on when a goal or loop is active)
50
+ - `checkinIntervalMinutes`: `30`
51
+ - `model`: not set (auto-detected)
52
+ - Successful `on_track` review verdicts are recorded silently instead of displayed as follow-up messages.
53
+
54
+ 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. They are lifecycle-managed by orchestration: activating a goal or `/loop` enables them, and clearing either disables them.
55
+
56
+ ## Stability guarantees
57
+
58
+ - No flattening: the advisor remains its own surface and does not hide orchestration commands.
59
+ - Cockpit is simple and explicit: `/pi-rogue` is the top-level status view.
@@ -0,0 +1 @@
1
+ export { default, registerAdvisor } from "../src/extension.js";