@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 +139 -0
- package/dist/index.js +1 -0
- package/dist/src/calibration-bridge.js +39 -0
- package/dist/src/feedback-parser.js +73 -0
- package/dist/src/log-writer.js +22 -0
- package/dist/src/service.js +48 -0
- package/dist/src/types.js +5 -0
- package/dist/src/utils.js +15 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +42 -0
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,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
|
+
}
|