Files
PrismFacet/PLAN.md
zhi 3a36605238 init: PrismFacet project plan
Dynamic system prompt injection plugin for OpenClaw.
Routes agent context through configurable routers, matches
against registered rules, and injects prompt files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 11:02:57 +00:00

6.1 KiB

PrismFacet

Dynamic system prompt injection plugin for OpenClaw. Routes agent context through configurable routers, matches against registered rules, and injects corresponding prompt files into the system prompt.

Concept

Agent session starts
  → before_prompt_build(ctx)
    → for each router:
        router.resolve(ctx) → key        (e.g. "developer")
        lookup rules for router:key       (e.g. "role:developer")
        read prompt file                  (e.g. roles/developer/ROLE.md)
    → append all matched prompts to system prompt

Routers resolve context into a key. They are TypeScript files — drop one into routers/ to register it.

Rules map router:key → prompt file path. Managed via MCP tool at runtime.

Architecture

~/.openclaw/plugins/prism-facet/
├── openclaw.plugin.json          # Plugin manifest
├── package.json
├── index.ts                      # Entry point
├── src/
│   ├── router-loader.ts          # Load/reload routers from routers/
│   ├── rule-store.ts             # CRUD for rules (rules.json)
│   └── prompt-injector.ts        # before_prompt_build hook logic
├── routers/                      # Router functions (drop-in)
│   ├── role.ts                   # Resolves agent role (reads ego.json)
│   ├── position.ts               # Resolves agent position
│   └── agent-id.ts               # Returns agentId directly (no deps)
└── rules.json                    # Persisted rules (managed via tool)

Router Interface

// Every router exports a resolve function
export interface RouterContext {
  agentId: string;
}

export function resolve(ctx: RouterContext): string | Promise<string>;

Example routers:

// routers/agent-id.ts — zero dependencies
export function resolve(ctx: { agentId: string }): string {
  return ctx.agentId;
}
// routers/role.ts — reads ego.json (optional dependency on PaddedCell)
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';

const EGO_PATH = `${homedir()}/.openclaw/ego.json`;

export function resolve(ctx: { agentId: string }): string {
  try {
    const ego = JSON.parse(readFileSync(EGO_PATH, 'utf8'));
    return ego['agent-scope']?.[ctx.agentId]?.role || '';
  } catch {
    return '';
  }
}

Hot Reload

Routers are loaded via dynamic import() with cache-busting. A reload tool clears the module cache and re-imports all routers without restarting the gateway.

Rules

Rules are stored in rules.json:

{
  "role:developer": "/path/to/roles/developer/ROLE.md",
  "role:operator": "/path/to/roles/operator/ROLE.md",
  "position:tech-leader": "/path/to/positions/tech-leader/POSITION.md",
  "agent-id:operator": "/path/to/special/operator-extra.md"
}

Key format: {router-name}:{resolved-key}

MCP Tools

prompt-rules

Subcommand Description
add --router <name> --key <key> --file <path> Register a rule
remove --router <name> --key <key> Remove a rule
list List all rules
test --agent <agent-id> Preview which rules match and what prompts would be injected
reload-routers Hot-reload all router functions

Runtime Flow

1. Gateway starts → plugin loads
2. Load all routers from routers/ via dynamic import()
3. Load rules.json

On each before_prompt_build(event, ctx):
4. For each loaded router:
   a. key = router.resolve(ctx)
   b. If key is empty → skip
   c. ruleKey = "{routerName}:{key}"
   d. If rules[ruleKey] exists → read the prompt file
5. Concatenate all matched prompt contents
6. Return { appendSystemContext: concatenated }

On reload-routers tool call:
7. Clear import cache for all routers
8. Re-import all routers from routers/

Plugin Manifest

{
  "id": "prism-facet",
  "name": "PrismFacet",
  "version": "0.1.0",
  "description": "Dynamic system prompt injection via routers and rules",
  "main": "dist/index.js",
  "configSchema": {
    "type": "object",
    "properties": {
      "routersDir": {
        "type": "string",
        "description": "Directory containing router .ts/.js files"
      },
      "rulesFile": {
        "type": "string",
        "description": "Path to rules.json"
      }
    }
  }
}

Configuration

// openclaw.json
{
  "plugins": {
    "entries": {
      "prism-facet": {
        "enabled": true,
        "hooks": {
          "allowPromptInjection": true
        },
        "config": {
          "routersDir": "~/.openclaw/plugins/prism-facet/routers",
          "rulesFile": "~/.openclaw/plugins/prism-facet/rules.json"
        }
      }
    }
  }
}

Implementation Phases

Phase 1: Core Plugin

  • Plugin scaffold (manifest, package.json, tsconfig)
  • Router loader with dynamic import + cache busting
  • Rule store (CRUD on rules.json)
  • before_prompt_build hook: resolve routers → match rules → inject prompts
  • Built-in routers: agent-id.ts

Phase 2: MCP Tools

  • prompt-rules add/remove/list tool
  • prompt-rules test tool (preview matches for an agent)
  • prompt-rules reload-routers tool

Phase 3: Default Routers

  • role.ts (reads ego.json)
  • position.ts (reads ego.json)
  • Documentation for writing custom routers

Phase 4: Testing & Deployment

  • Test on laptop environment
  • Deploy to T2 (production)
  • Integrate with ClawSkills role/position definitions
  • Verify injection for all active agents

Dependencies

Dependency Required? Notes
OpenClaw v2026.4.9+ Yes before_prompt_build + allowPromptInjection
PaddedCell / ego-mgr No Only needed by role.ts / position.ts routers
Node.js ESM Yes Dynamic import() for router loading

Design Principles

  1. No hard dependencies — Plugin works with just agent-id router. External system routers are optional.
  2. Drop-in extensibility — Add a .ts file to routers/ to add a new routing dimension.
  3. Hot reload — Router changes don't require gateway restart.
  4. Separation of concerns — Routers resolve context → Rules map keys to files → Plugin injects content.