@akalsey/openclaw-feedback 0.1.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.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # OpenClaw Feedback
2
+
3
+ You give feedback constantly — correcting a format choice, confirming something worked well, redirecting how the agent handled a decision. This plugin catches those signals from your normal chat messages and makes them permanent.
4
+
5
+ A correction becomes a calibration. A confirmation reinforces a pattern. The sapience calibration profile updates in real time so the agent's behavior reflects how you actually want it to operate.
6
+
7
+ This plugin is part of the Sapience Suite that gives your OpenClaw agent genuine agency — not just the ability to execute tasks, but the judgment to know when to act, when to ask, when to propose, and when to say "I'm not sure how you want me to handle this."
8
+
9
+ This plugin can be used without Sapience if all you want to do is have the agent track and incorporate feedback.
10
+
11
+ ---
12
+
13
+ ## Setup
14
+
15
+ ### Prerequisites
16
+
17
+ `openclaw-sapience` must be installed. This plugin writes to the shared calibration profile at `~/.openclaw/sapience/calibration.json`.
18
+
19
+ ### Install
20
+
21
+ ```bash
22
+ openclaw plugins install local:/path/to/openclaw-feedback
23
+ ```
24
+
25
+ ### Configuration
26
+
27
+ ```json
28
+ {
29
+ "plugins": {
30
+ "feedback": {
31
+ "logPath": "~/.openclaw/sapience/feedback.md",
32
+ "calibrationPath": "~/.openclaw/sapience/calibration.json",
33
+ "memoryEnabled": true
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ All settings are optional — defaults are used if omitted.
40
+
41
+ ---
42
+
43
+ ## What it detects
44
+
45
+ The plugin scans every message you send for three types of signals:
46
+
47
+ ### Corrections
48
+
49
+ Phrases that express "don't do that" or "do it differently":
50
+
51
+ - `don't update OKRs for other teams without asking`
52
+ - `stop doing that`
53
+ - `never push to main without a PR`
54
+ - `use the company template, not the default`
55
+ - `you shouldn't have done that without checking`
56
+
57
+ **Effect:** Confidence on the matching domain/action-class drops by 0.3.
58
+
59
+ ### Confirmations
60
+
61
+ Phrases that express "yes, that was right":
62
+
63
+ - `yes exactly`
64
+ - `good call`
65
+ - `perfect, keep doing that`
66
+ - `that's exactly right`
67
+
68
+ **Effect:** Confidence on the matching domain/action-class increases by 0.1.
69
+
70
+ ### Tier adjustments
71
+
72
+ Explicit instructions about how much autonomy you want:
73
+
74
+ - `just do it` / `you don't need to ask` → bumps toward **Act**
75
+ - `always ask me first` / `ask me before touching X` → bumps toward **Ask**
76
+
77
+ **Effect:** Tier for matching domain/action-class is updated directly.
78
+
79
+ ---
80
+
81
+ ## Domain detection
82
+
83
+ The plugin extracts domains from context in your message:
84
+
85
+ | Text contains | Domain |
86
+ |---------------|--------|
87
+ | `github`, `PR`, `push` | `github` |
88
+ | `Salesforce` | `salesforce` |
89
+ | `PostHog` | `posthog` |
90
+ | `Slack` | `slack` |
91
+ | `slides`, `deck` | `slides` |
92
+ | `OKR` | `okr-system` |
93
+ | `Linear` | `linear` |
94
+ | (nothing matched) | `general` |
95
+
96
+ Domain detection is keyword-based, not semantic. Be explicit when giving feedback: "don't update Salesforce records" works better than "don't do that" (which routes to `general`).
97
+
98
+ ---
99
+
100
+ ## Reading the feedback log
101
+
102
+ ```bash
103
+ cat ~/.openclaw/sapience/feedback.md
104
+ ```
105
+
106
+ Each entry shows:
107
+ - Signal type (correction / confirmation / tier_adjustment)
108
+ - Domain and action class affected
109
+ - The original message
110
+ - Tier adjustment, if any
111
+ - Meta-pointer written to memory, for corrections
112
+
113
+ ---
114
+
115
+ ## Meta-memory pointers
116
+
117
+ For corrections, the plugin writes a memory pointer:
118
+
119
+ > "Before working on github / github/action: check feedback log — correction recorded: 'don't push to main without a PR'"
120
+
121
+ This means future sessions will be reminded to check the feedback log before acting in that domain, even if the calibration confidence hasn't yet dropped below the threshold.
122
+
123
+ Requires `memoryEnabled: true` (default) and OpenClaw's built-in memory API.
124
+
125
+ ---
126
+
127
+ ## Troubleshooting
128
+
129
+ **Feedback not being detected**
130
+ The plugin only scans messages you send (role: `user`), not the agent's responses. Make sure you're sending the correction as a chat message, not just thinking it.
131
+
132
+ **Calibration not updating**
133
+ Check that `calibration.json` exists and has an entry for the domain you're correcting. Feedback only updates *existing* entries — it doesn't create new ones. New domains are created by `openclaw-sapience` when it first routes a proposal in that domain.
134
+
135
+ **Domain matching to "general" when it shouldn't**
136
+ Add more specific keywords to your feedback message. "Don't do that" → "Don't update the Salesforce contact without asking."
137
+
138
+ **Too many false positives in the feedback log**
139
+ The pattern matching is intentionally broad. If casual phrases are being captured incorrectly, check the log and note which patterns are misfiring. You can't currently tune the patterns without modifying `src/feedback-parser.ts`.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/service.js";
@@ -0,0 +1,39 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ async function loadProfile(path) {
5
+ try {
6
+ return JSON.parse(await readFile(resolvePath(path), "utf-8"));
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ }
12
+ async function saveProfile(profile, path) {
13
+ const resolved = resolvePath(path);
14
+ await mkdir(dirname(resolved), { recursive: true });
15
+ await writeFile(resolved, JSON.stringify(profile, null, 2), "utf-8");
16
+ }
17
+ export async function applyFeedbackToProfile(signal, calibrationPath) {
18
+ const profile = await loadProfile(calibrationPath);
19
+ if (profile.length === 0)
20
+ return;
21
+ const idx = profile.findIndex(e => e.domain === signal.domain && e.action_class === signal.action_class);
22
+ if (idx === -1)
23
+ return;
24
+ const entry = profile[idx];
25
+ let updated;
26
+ if (signal.type === "confirmation") {
27
+ updated = { ...entry, confidence: Math.min(1, entry.confidence + 0.1), confirmed_count: entry.confirmed_count + 1, last_calibrated: new Date().toISOString() };
28
+ }
29
+ else if (signal.type === "correction") {
30
+ updated = { ...entry, confidence: Math.max(0, entry.confidence - 0.3), corrected_count: entry.corrected_count + 1, last_calibrated: new Date().toISOString() };
31
+ }
32
+ else if (signal.type === "tier_adjustment" && signal.suggested_tier) {
33
+ updated = { ...entry, tier: signal.suggested_tier, last_calibrated: new Date().toISOString() };
34
+ }
35
+ else {
36
+ return;
37
+ }
38
+ await saveProfile(profile.map((e, i) => i === idx ? updated : e), calibrationPath);
39
+ }
@@ -0,0 +1,73 @@
1
+ const CORRECTION_PATTERNS = [
2
+ /\bdon'?t\b.{0,60}\b(do|update|change|delete|send|push|write|modify|set|use)\b/i,
3
+ /\bstop\b.{0,40}\b(doing|that|this)\b/i,
4
+ /\bnever\b.{0,60}\b(do|update|change|delete|send|push|write)\b/i,
5
+ /\bshouldn'?t\s+(have|be)\b/i,
6
+ /\bwrong\s+(format|approach|template|way)\b/i,
7
+ /\b(use|should use|always use)\s+the\s+\w/i,
8
+ ];
9
+ const TIER_UP_PATTERNS = [
10
+ /\bjust\s+do\s+it\b/i,
11
+ /\byou\s+don'?t\s+need\s+to\s+ask\b/i,
12
+ /\bgo\s+ahead\s+without\s+asking\b/i,
13
+ /\bdo\s+it\s+automatically\b/i,
14
+ ];
15
+ const TIER_DOWN_PATTERNS = [
16
+ /\balways\s+ask\s+(me\s+)?(first|before)\b/i,
17
+ /\bask\s+me\s+before\b/i,
18
+ /\bdon'?t\s+do\s+that\s+without\s+(asking|checking)\b/i,
19
+ /\bnext\s+time\s+(check|ask|confirm)\b/i,
20
+ ];
21
+ const CONFIRMATION_PATTERNS = [
22
+ /\byes[\s,]+exactly\b/i,
23
+ /\bgood\s+call\b/i,
24
+ /\bperfect[,.]?\s*(keep|that'?s|yes)?\b/i,
25
+ /\bkeep\s+doing\s+that\b/i,
26
+ /\bthat'?s\s+(exactly\s+)?right\b/i,
27
+ ];
28
+ const DOMAIN_PATTERNS = [
29
+ [/github/i, "github"],
30
+ [/salesforce/i, "salesforce"],
31
+ [/posthog/i, "posthog"],
32
+ [/lovable/i, "lovable"],
33
+ [/slack/i, "slack"],
34
+ [/slides?|deck/i, "slides"],
35
+ [/okr/i, "okr-system"],
36
+ [/linear/i, "linear"],
37
+ ];
38
+ function extractDomain(text) {
39
+ for (const [pattern, domain] of DOMAIN_PATTERNS) {
40
+ if (pattern.test(text))
41
+ return domain;
42
+ }
43
+ return "general";
44
+ }
45
+ export function parseMessage(text) {
46
+ const signals = [];
47
+ const domain = extractDomain(text);
48
+ for (const pattern of TIER_UP_PATTERNS) {
49
+ if (pattern.test(text)) {
50
+ signals.push({ type: "tier_adjustment", domain, action_class: "general", message: text, suggested_tier: "act", raw_text: text });
51
+ return signals;
52
+ }
53
+ }
54
+ for (const pattern of TIER_DOWN_PATTERNS) {
55
+ if (pattern.test(text)) {
56
+ signals.push({ type: "tier_adjustment", domain, action_class: "general", message: text, suggested_tier: "ask", raw_text: text });
57
+ return signals;
58
+ }
59
+ }
60
+ for (const pattern of CORRECTION_PATTERNS) {
61
+ if (pattern.test(text)) {
62
+ signals.push({ type: "correction", domain, action_class: "general", message: text, raw_text: text });
63
+ return signals;
64
+ }
65
+ }
66
+ for (const pattern of CONFIRMATION_PATTERNS) {
67
+ if (pattern.test(text)) {
68
+ signals.push({ type: "confirmation", domain, action_class: "general", message: text, raw_text: text });
69
+ return signals;
70
+ }
71
+ }
72
+ return signals;
73
+ }
@@ -0,0 +1,22 @@
1
+ import { appendFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ export async function appendFeedback(entry, logPath) {
5
+ const resolved = resolvePath(logPath);
6
+ await mkdir(dirname(resolved), { recursive: true });
7
+ const lines = [
8
+ `## ${entry.detected_at}`,
9
+ ``,
10
+ `**Signal type:** ${entry.signal.type}`,
11
+ `**Domain:** ${entry.signal.domain} / ${entry.signal.action_class}`,
12
+ `**Message:** "${entry.signal.message}"`,
13
+ ];
14
+ if (entry.signal.suggested_tier) {
15
+ lines.push(`**Tier adjustment:** → ${entry.signal.suggested_tier}`);
16
+ }
17
+ if (entry.meta_pointer) {
18
+ lines.push(`**Meta-pointer written:** ${entry.meta_pointer}`);
19
+ }
20
+ lines.push(``, `---`, ``);
21
+ await appendFile(resolved, lines.join("\n") + "\n", "utf-8");
22
+ }
@@ -0,0 +1,48 @@
1
+ // src/service.ts
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ import { DEFAULT_CONFIG } from "./types.js";
4
+ import { resolveDataPath, generateId } from "./utils.js";
5
+ import { parseMessage } from "./feedback-parser.js";
6
+ import { appendFeedback } from "./log-writer.js";
7
+ import { applyFeedbackToProfile } from "./calibration-bridge.js";
8
+ function mergeConfig(raw, workspaceDir) {
9
+ return {
10
+ ...DEFAULT_CONFIG,
11
+ ...raw,
12
+ logPath: resolveDataPath(raw.logPath, workspaceDir, DEFAULT_CONFIG.logPath),
13
+ calibrationPath: resolveDataPath(raw.calibrationPath, workspaceDir, DEFAULT_CONFIG.calibrationPath),
14
+ };
15
+ }
16
+ function buildMetaPointer(signal) {
17
+ return `Before working on ${signal.domain} / ${signal.action_class}: check feedback log — correction recorded: "${signal.raw_text.slice(0, 80)}"`;
18
+ }
19
+ export default definePluginEntry({
20
+ id: "feedback",
21
+ name: "Feedback",
22
+ description: "Persists behavioral corrections and confirmations into the sapience calibration profile",
23
+ register(api) {
24
+ const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(api.pluginConfig);
25
+ const config = mergeConfig(api.pluginConfig, workspaceDir);
26
+ if (api.session?.onMessage) {
27
+ api.session.onMessage(async (message) => {
28
+ if (message.role !== "user")
29
+ return;
30
+ const signals = parseMessage(message.content);
31
+ for (const signal of signals) {
32
+ const metaPointer = signal.type === "correction" ? buildMetaPointer(signal) : undefined;
33
+ const entry = {
34
+ id: generateId(),
35
+ detected_at: new Date().toISOString(),
36
+ signal,
37
+ meta_pointer: metaPointer,
38
+ };
39
+ await appendFeedback(entry, config.logPath);
40
+ await applyFeedbackToProfile(signal, config.calibrationPath);
41
+ if (metaPointer && config.memoryEnabled && api.memory?.write) {
42
+ await api.memory.write({ text: metaPointer, type: "behavioral-correction" });
43
+ }
44
+ }
45
+ });
46
+ }
47
+ },
48
+ });
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_CONFIG = {
2
+ logPath: "sapience/feedback.md",
3
+ calibrationPath: "sapience/calibration.json",
4
+ memoryEnabled: true,
5
+ };
@@ -0,0 +1,15 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ export function resolvePath(p) {
4
+ return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
5
+ }
6
+ export function resolveDataPath(override, workspaceDir, defaultRelative) {
7
+ if (!override)
8
+ return join(workspaceDir, defaultRelative);
9
+ if (override.startsWith('/') || override.startsWith('~/'))
10
+ return resolvePath(override);
11
+ return join(workspaceDir, override);
12
+ }
13
+ export function generateId() {
14
+ return `fb-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "feedback",
3
+ "name": "Feedback",
4
+ "version": "0.1.0",
5
+ "description": "Persists behavioral corrections and confirmations from session messages into the sapience calibration profile",
6
+ "activation": { "onStartup": true },
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": true
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@akalsey/openclaw-feedback",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "exports": {
7
+ ".": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "openclaw.plugin.json",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "openclaw": {
22
+ "extensions": [
23
+ "./dist/index.js"
24
+ ],
25
+ "compat": {
26
+ "pluginApi": ">=2026.3.24-beta.2",
27
+ "minGatewayVersion": "2026.3.24-beta.2"
28
+ },
29
+ "install": {
30
+ "localPath": "."
31
+ }
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "openclaw": "latest",
36
+ "typescript": "^5.5.0",
37
+ "vitest": "^2.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "openclaw": ">=2026.3.24-beta.2"
41
+ }
42
+ }