@fiale-plus/pi-rogue-bundle 0.1.14 → 0.1.16

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 CHANGED
@@ -4,7 +4,9 @@
4
4
 
5
5
  It stitches together (and bundles for a true single-package install):
6
6
 
7
+ - `@fiale-plus/pi-core` (shared contracts/helpers)
7
8
  - `@fiale-plus/pi-rogue-advisor` (logic; direct releases paused)
9
+ - `@fiale-plus/pi-rogue-context-broker` (beta context-broker runtime; disabled by default)
8
10
  - `@fiale-plus/pi-rogue-orchestration` (logic; direct releases paused)
9
11
 
10
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.
@@ -26,12 +28,16 @@ npm install
26
28
  ## Scope boundaries
27
29
 
28
30
  - **Lab / internal helpers are excluded from this bundle.**
31
+ - The beta context-broker runtime is bundled for opt-in experiments but is not registered/enabled by default.
32
+ - Opt-in consumers can import the runtime through the bundle subpath: `@fiale-plus/pi-rogue-bundle/context-broker`.
33
+ - Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi to register the beta `/context` command surface.
29
34
  - `@fiale-plus/pi-rogue-bundle` is the only published surface for the logic.
30
35
  - 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.
31
36
 
32
37
  ## Command surface
33
38
 
34
- - `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
39
+ - Default: `/advisor`, `/goal`, `/loop`, `/autoresearch`, `/autoresearch-lab` plus status/config/command paths (all provided via the bundle).
40
+ - Opt-in beta: `PI_CONTEXT_BROKER_ENABLED=true` adds `/context status`, `/context brief`, `/context lookup <handle|text>`, `/context pin <handle>`, and `/context prune` with autocomplete.
35
41
 
36
42
  ## Status
37
43
 
@@ -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,81 @@
1
+ export type ContextArtifactKind =
2
+ | "tool_output"
3
+ | "diff"
4
+ | "file_snapshot"
5
+ | "subagent_result"
6
+ | "advisor_brief"
7
+ | "memory_note";
8
+
9
+ export interface ContextArtifactInput {
10
+ sessionId: string;
11
+ kind: ContextArtifactKind;
12
+ payload: string | Buffer;
13
+ summary?: string;
14
+ tags?: string[];
15
+ paths?: string[];
16
+ command?: string;
17
+ branch?: string;
18
+ ttlMs?: number;
19
+ pinned?: boolean;
20
+ parentIds?: string[];
21
+ createdAt?: number;
22
+ }
23
+
24
+ export interface ContextArtifact {
25
+ id: string;
26
+ handle: string;
27
+ sessionId: string;
28
+ kind: ContextArtifactKind;
29
+ createdAt: number;
30
+ updatedAt: number;
31
+ bytes: number;
32
+ sha256: string;
33
+ payload: string;
34
+ summary: string;
35
+ tags: string[];
36
+ paths: string[];
37
+ command?: string;
38
+ branch?: string;
39
+ expiresAt?: number;
40
+ pinned: boolean;
41
+ parentIds: string[];
42
+ }
43
+
44
+ export interface ContextLookupQuery {
45
+ id?: string;
46
+ handle?: string;
47
+ sessionId?: string;
48
+ kind?: ContextArtifactKind;
49
+ tag?: string;
50
+ path?: string;
51
+ commandPrefix?: string;
52
+ branch?: string;
53
+ text?: string;
54
+ limit?: number;
55
+ }
56
+
57
+ export interface ContextBrokerStatus {
58
+ records: number;
59
+ bytes: number;
60
+ pinnedRecords: number;
61
+ pinnedBytes: number;
62
+ maxRecords: number;
63
+ maxBytes: number;
64
+ }
65
+
66
+ export interface ContextBrokerOptions {
67
+ maxRecords?: number;
68
+ maxBytes?: number;
69
+ defaultTtlMs?: number;
70
+ summaryBytes?: number;
71
+ briefBytes?: number;
72
+ }
73
+
74
+ export interface BoundedContextBroker {
75
+ publish(input: ContextArtifactInput): ContextArtifact;
76
+ lookup(query?: ContextLookupQuery): ContextArtifact[];
77
+ pin(idOrHandle: string, pinned?: boolean): ContextArtifact | null;
78
+ prune(now?: number): ContextBrokerStatus;
79
+ status(): ContextBrokerStatus;
80
+ renderBrief(query?: ContextLookupQuery & { budgetBytes?: number }): string;
81
+ }
@@ -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,38 @@
1
+ # Pi-Rogue Context Broker
2
+
3
+ Beta context broker runtime for Pi-Rogue.
4
+
5
+ This package contains the executable in-memory bounded broker implementation:
6
+
7
+ - `createInMemoryContextBroker()` stores artifacts behind stable `ctx://...` handles.
8
+ - Lookups support handle, session, kind, tag, path, command prefix, branch, and text filters.
9
+ - Omitted summaries become metadata-only placeholders, keeping raw payloads out of prompt briefs by default.
10
+ - Pruning enforces per-session record/byte caps, TTL expiry on reads, and pinned-artifact retention.
11
+
12
+ It is intentionally disabled by default in the bundle.
13
+
14
+ ## Opt-in beta extension
15
+
16
+ Set `PI_CONTEXT_BROKER_ENABLED=true` before starting Pi with the bundle installed to enable the beta extension:
17
+
18
+ ```bash
19
+ PI_CONTEXT_BROKER_ENABLED=true pi
20
+ ```
21
+
22
+ When enabled, the bundle registers `/context` commands:
23
+
24
+ - `/context status` — enabled state, record/byte counts, pinned counts.
25
+ - `/context brief` — bounded prompt-safe broker brief with handles and summaries.
26
+ - `/context lookup <handle|text>` — exact handle rehydration or current-session text search.
27
+ - `/context pin <handle>` — protect an artifact from normal TTL/cap pruning.
28
+ - `/context prune` — run TTL/cap pruning immediately.
29
+
30
+ The command includes autocomplete for subcommands and known artifact handles. Exact handle lookup returns clipped payload text; text search returns a smaller clipped excerpt, and truncation is marked explicitly.
31
+
32
+ ## Session behavior and limits
33
+
34
+ - On session start/reload, the beta backfills the current Pi session branch from `toolResult` and prompt-visible `bashExecution` entries.
35
+ - Backfill is idempotent by session entry id, skips malformed entries instead of failing the session, and honors Pi's `excludeFromContext` bash entries.
36
+ - The current implementation remains in-memory. Restarting Pi loses broker state until the current branch is backfilled again.
37
+ - Prompt integration injects only a bounded broker brief and lookup guidance. It does not yet rewrite existing raw tool-result messages out of Pi's transcript context; that deeper prompt-load reduction remains a follow-up.
38
+ - Rollback is immediate: unset `PI_CONTEXT_BROKER_ENABLED` and `/reload` or restart Pi.
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@fiale-plus/pi-rogue-context-broker",
3
+ "version": "0.1.0",
4
+ "description": "Beta context broker runtime for Pi-Rogue. In-memory bounded broker implementation behind explicit opt-in.",
5
+ "private": true,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "pi-package"
10
+ ],
11
+ "scripts": {
12
+ "check": "tsc -p ../../tsconfig.json --noEmit",
13
+ "test": "cd ../.. && vitest run packages/context-broker/src/*.test.ts"
14
+ },
15
+ "main": "./src/index.ts",
16
+ "exports": {
17
+ ".": "./src/index.ts",
18
+ "./extension": "./src/extension.ts"
19
+ },
20
+ "dependencies": {
21
+ "@fiale-plus/pi-core": "^0.1.0"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "package.json"
27
+ ]
28
+ }