@akalsey/sapience-feedback 0.1.1

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,141 @@
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
+ None required. This plugin works standalone.
18
+
19
+ If `sapience` is also installed, the calibration profile at `~/.openclaw/sapience/calibration.json` feeds directly into autonomy routing. Without sapience, the profile is still written but nothing reads it.
20
+
21
+ ### Install
22
+
23
+ ```bash
24
+ openclaw plugins install npm:@akalsey/sapience-feedback
25
+ ```
26
+
27
+ ### Configuration
28
+
29
+ ```json
30
+ {
31
+ "plugins": {
32
+ "sapience-feedback": {
33
+ "logPath": "~/.openclaw/sapience/feedback.md",
34
+ "calibrationPath": "~/.openclaw/sapience/calibration.json",
35
+ "memoryEnabled": true
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ All settings are optional — defaults are used if omitted.
42
+
43
+ ---
44
+
45
+ ## What it detects
46
+
47
+ The plugin scans every message you send for three types of signals:
48
+
49
+ ### Corrections
50
+
51
+ Phrases that express "don't do that" or "do it differently":
52
+
53
+ - `don't update OKRs for other teams without asking`
54
+ - `stop doing that`
55
+ - `never push to main without a PR`
56
+ - `use the company template, not the default`
57
+ - `you shouldn't have done that without checking`
58
+
59
+ **Effect:** Confidence on the matching domain/action-class drops by 0.3.
60
+
61
+ ### Confirmations
62
+
63
+ Phrases that express "yes, that was right":
64
+
65
+ - `yes exactly`
66
+ - `good call`
67
+ - `perfect, keep doing that`
68
+ - `that's exactly right`
69
+
70
+ **Effect:** Confidence on the matching domain/action-class increases by 0.1.
71
+
72
+ ### Tier adjustments
73
+
74
+ Explicit instructions about how much autonomy you want:
75
+
76
+ - `just do it` / `you don't need to ask` → bumps toward **Act**
77
+ - `always ask me first` / `ask me before touching X` → bumps toward **Ask**
78
+
79
+ **Effect:** Tier for matching domain/action-class is updated directly.
80
+
81
+ ---
82
+
83
+ ## Domain detection
84
+
85
+ The plugin extracts domains from context in your message:
86
+
87
+ | Text contains | Domain |
88
+ |---------------|--------|
89
+ | `github`, `PR`, `push` | `github` |
90
+ | `Salesforce` | `salesforce` |
91
+ | `PostHog` | `posthog` |
92
+ | `Slack` | `slack` |
93
+ | `slides`, `deck` | `slides` |
94
+ | `OKR` | `okr-system` |
95
+ | `Linear` | `linear` |
96
+ | (nothing matched) | `general` |
97
+
98
+ 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`).
99
+
100
+ ---
101
+
102
+ ## Reading the feedback log
103
+
104
+ ```bash
105
+ cat ~/.openclaw/sapience/feedback.md
106
+ ```
107
+
108
+ Each entry shows:
109
+ - Signal type (correction / confirmation / tier_adjustment)
110
+ - Domain and action class affected
111
+ - The original message
112
+ - Tier adjustment, if any
113
+ - Meta-pointer written to memory, for corrections
114
+
115
+ ---
116
+
117
+ ## Meta-memory pointers
118
+
119
+ For corrections, the plugin calls `api.memory.add` to write a behavioral reminder directly into OpenClaw's native memory:
120
+
121
+ > "Before working on github / github/action: check feedback log — correction recorded: 'don't push to main without a PR'"
122
+
123
+ Future sessions surface this pointer automatically through OpenClaw's standard memory system. No separate memory plugin is required — memory writes go through the same API OpenClaw itself uses.
124
+
125
+ To disable memory writes, set `memoryEnabled: false` in config.
126
+
127
+ ---
128
+
129
+ ## Troubleshooting
130
+
131
+ **Feedback not being detected**
132
+ 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.
133
+
134
+ **Calibration not updating**
135
+ 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 `sapience` when it first routes a proposal in that domain.
136
+
137
+ **Domain matching to "general" when it shouldn't**
138
+ Add more specific keywords to your feedback message. "Don't do that" → "Don't update the Salesforce contact without asking."
139
+
140
+ **Too many false positives in the feedback log**
141
+ 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,56 @@
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: "sapience-feedback",
21
+ name: "Sapience 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
+ try {
31
+ const signals = parseMessage(message.content);
32
+ for (const signal of signals) {
33
+ const metaPointer = signal.type === "correction" ? buildMetaPointer(signal) : undefined;
34
+ const entry = {
35
+ id: generateId(),
36
+ detected_at: new Date().toISOString(),
37
+ signal,
38
+ meta_pointer: metaPointer,
39
+ };
40
+ await appendFeedback(entry, config.logPath);
41
+ await applyFeedbackToProfile(signal, config.calibrationPath);
42
+ if (metaPointer && config.memoryEnabled) {
43
+ await api.memory?.add({
44
+ content: metaPointer,
45
+ metadata: { tags: ["feedback", "behavioral-correction", signal.domain], source: "feedback" },
46
+ });
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // don't let feedback processing errors disrupt the session
52
+ }
53
+ });
54
+ }
55
+ },
56
+ });
@@ -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": "sapience-feedback",
3
+ "name": "Sapience 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/sapience-feedback",
3
+ "version": "0.1.1",
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
+ }