@akalsey/openclaw-sapience 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,169 @@
1
+ # Sapience Suite for OpenClaw
2
+
3
+ The Sapience Suite transforms OpenClaw from a reactive assistant into a proactive agent with genuine autonomy. It learns when to act, when to propose, when to ask, and when to explore — calibrated to your actual preferences, not a static policy you had to configure upfront.
4
+
5
+ The suite has four plugins that each work independently and compose into a whole:
6
+
7
+ | Plugin | Does |
8
+ |--------|------|
9
+ | `openclaw-proactive-thinking` | Periodic thinking passes; generates observations and proposals |
10
+ | `openclaw-sapience` *(this plugin)* | Routes proposals through autonomy tiers; calibrates to your preferences; delivers weekly digest |
11
+ | `openclaw-feedback` | Captures corrections and confirmations from chat; recalibrates autonomy profile |
12
+ | `openclaw-goals` | Accepts fuzzy long-running goals; decomposes them; tracks progress; weekly status |
13
+
14
+ ## How it works
15
+
16
+ `openclaw-proactive-thinking` runs a thinking pass every 15 minutes and writes proposals to `proposals.jsonl`. `openclaw-sapience` reads that sidecar, routes each proposal through an autonomy decision function, and delivers it to your main session at the right level:
17
+
18
+ - **Act** — high-confidence, reversible, low-blast-radius → done immediately, brief notification
19
+ - **Propose** — worth doing, needs your approval → surfaces it for a yes/no
20
+ - **Ask** — agent can do it but needs one piece of information → asks exactly what's needed
21
+ - **Explore** — the problem is real but the right path is unclear → presents 2–3 options with tradeoffs
22
+ - **Learning** — new domain or low confidence → calibration question before acting
23
+
24
+ The routing decision uses a calibration profile: per-domain, per-action-class entries with a confidence score. Until a domain is calibrated, everything goes through **Learning** mode and will ask you to confirm it's choices before acting.
25
+
26
+ ## Setup
27
+
28
+ ### Prerequisites
29
+
30
+ Install `openclaw-proactive-thinking` first. Sapience reads its output.
31
+
32
+ ### Install order
33
+
34
+ ```bash
35
+ openclaw plugins install local:/path/to/openclaw-proactive-thinking
36
+ openclaw plugins install local:/path/to/openclaw-sapience
37
+ openclaw plugins install local:/path/to/openclaw-feedback # optional
38
+ openclaw plugins install local:/path/to/openclaw-goals # optional
39
+ ```
40
+
41
+ ### Configuration (sapience)
42
+
43
+ ```json
44
+ {
45
+ "plugins": {
46
+ "sapience": {
47
+ "autonomy": {
48
+ "defaultTier": "propose",
49
+ "domainFloors": {
50
+ "github": "propose",
51
+ "salesforce": "ask"
52
+ }
53
+ },
54
+ "learning": {
55
+ "enabled": true,
56
+ "confidenceDropThreshold": 0.4
57
+ },
58
+ "digest": {
59
+ "enabled": true,
60
+ "day": "friday",
61
+ "time": "17:00"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ **`defaultTier`** — What tier to use for uncalibrated actions when learning mode is off. Default: `"propose"`.
69
+
70
+ **`domainFloors`** — Minimum tier for a domain. If calibration says `act` for a domain with floor `propose`, it routes as `propose`. Use this for domains where you never want autonomous action regardless of confidence.
71
+
72
+ **`confidenceDropThreshold`** — Below this confidence, Learning mode fires instead of the calibrated tier. Default: `0.4`.
73
+
74
+ **`digest`** — Weekly summary of what was acted on, what's pending review, and what's planned. Delivered at the configured day and time.
75
+
76
+ ### Output files
77
+
78
+ | File | Purpose |
79
+ |------|---------|
80
+ | `~/.openclaw/sapience/calibration.json` | Autonomy calibration profile (shared with `openclaw-feedback`) |
81
+ | `~/.openclaw/sapience/processed-passes.json` | Tracks which proactive-thinking passes have been routed |
82
+ | `~/.openclaw/sapience/action-log.md` | Log of every Act-tier item delivered |
83
+
84
+ ## Training: calibrating autonomy
85
+
86
+ Calibration is the process of teaching the agent your preferences per domain and action type.
87
+
88
+ ### Learning mode
89
+
90
+ When sapience sees a domain/action-class combination with no calibration data (or low confidence), it fires a **Learning** prompt instead of acting:
91
+
92
+ > "I noticed [item]. My instinct is to surface this as a proposal. Is that the right level of initiative, or would you prefer I handle this differently?"
93
+
94
+ You respond to confirm or redirect. The calibration profile updates accordingly.
95
+
96
+ ### How confidence builds
97
+
98
+ | Event | Effect |
99
+ |-------|--------|
100
+ | You confirm the proposed approach ("yes, that's right") | Confidence +0.1 |
101
+ | You correct the approach ("no, just do it") | Confidence −0.3, tier updated |
102
+ | No feedback | Confidence unchanged |
103
+
104
+ Confidence caps at 1.0 and floors at 0.0. A domain needs roughly 3–5 confirmations to reach the default threshold (0.4) from zero.
105
+
106
+ ### Reading the calibration profile
107
+
108
+ ```bash
109
+ cat ~/.openclaw/sapience/calibration.json
110
+ ```
111
+
112
+ Each entry:
113
+ ```json
114
+ {
115
+ "domain": "github",
116
+ "action_class": "github/action",
117
+ "tier": "propose",
118
+ "confidence": 0.7,
119
+ "confirmed_count": 4,
120
+ "corrected_count": 1,
121
+ "last_calibrated": "2026-05-20T14:00:00Z",
122
+ "notes": ""
123
+ }
124
+ ```
125
+
126
+ ### Resetting a domain
127
+
128
+ Delete the entry from `calibration.json` to reset a domain to Learning mode.
129
+
130
+ ## Day-to-day use
131
+
132
+ Once installed, the suite runs in the background. What you'll see in your sessions:
133
+
134
+ - `[SAPIENCE: PROPOSE]` — a proposal needing your yes/no
135
+ - `[SAPIENCE: ACT]` — notification of something just done
136
+ - `[SAPIENCE: ASK]` — a question needed before proceeding
137
+ - `[SAPIENCE: EXPLORE]` — a problem with options for you to choose from
138
+ - `[SAPIENCE: CALIBRATE]` — a calibration question for a new domain
139
+ - `[SAPIENCE: WEEKLY DIGEST]` — Friday summary of actions, pending items, and plans
140
+
141
+ You don't need to do anything to receive these — they arrive as injected turns in your active session.
142
+
143
+ ### Weekly digest
144
+
145
+ Every Friday at 5pm (or your configured time), the digest summarizes:
146
+ - What was acted on this week
147
+ - Proposals still waiting on your input
148
+ - What's planned for next week
149
+
150
+ ## Troubleshooting
151
+
152
+ **Nothing being delivered to my session**
153
+ Check that proactive-thinking is writing `proposals.jsonl`:
154
+ ```bash
155
+ cat ~/.openclaw/proactive-thinking/proposals.jsonl | tail -1 | python3 -m json.tool
156
+ ```
157
+ If the file is empty or missing, proactive-thinking isn't running. Check its logs first.
158
+
159
+ **Everything is going to Learning mode**
160
+ Expected behavior for the first week or two. Each calibration response builds confidence. If it continues beyond 2–3 weeks for a domain you use daily, check `calibration.json` — entries may not be getting written.
161
+
162
+ **Calibration profile not updating**
163
+ Feedback plugin (`openclaw-feedback`) handles explicit correction/confirmation capture. If it's not installed, calibrations only happen through the Learning mode prompts. Install `openclaw-feedback` for passive capture from chat messages.
164
+
165
+ **`domainFloors` not respected**
166
+ Floors only prevent routing *above* the floor — they don't push Act-tier items down to propose. `"github": "propose"` means github/action can be at most `propose`, `ask`, or `explore`, never `act`. If you're seeing Act-tier github items, check the floor config key matches the domain name exactly (lowercase).
167
+
168
+ **Duplicate deliveries**
169
+ `processed-passes.json` tracks which proactive-thinking passes have been routed. If it's missing or corrupt, passes get re-delivered. Delete it and it will rebuild from the current pass forward.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/service.js";
@@ -0,0 +1,20 @@
1
+ import { appendFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ export async function appendAction(item, note, logPath) {
5
+ const resolved = resolvePath(logPath);
6
+ await mkdir(dirname(resolved), { recursive: true });
7
+ const ts = new Date().toISOString();
8
+ const entry = [
9
+ `## ${ts}`,
10
+ ``,
11
+ `**Action:** ${item.text}`,
12
+ `**Domain/class:** ${item.domain} / ${item.action_class}`,
13
+ `**Tier:** Act (confidence ${item.confidence.toFixed(2)})`,
14
+ `**Note:** ${note}`,
15
+ ``,
16
+ `---`,
17
+ ``,
18
+ ].join("\n");
19
+ await appendFile(resolved, entry, "utf-8");
20
+ }
@@ -0,0 +1,18 @@
1
+ import { getEntry, needsCalibration } from "./calibration.js";
2
+ const TIER_ORDER = ["act", "propose", "ask", "explore"];
3
+ export function routeItem(item, profile, config) {
4
+ const entry = getEntry(profile, item.domain, item.action_class);
5
+ if (config.learning.enabled && needsCalibration(entry, config.learning.confidenceDropThreshold)) {
6
+ return { ...item, tier: "learning", confidence: entry?.confidence ?? 0 };
7
+ }
8
+ let tier = (entry?.tier ?? config.autonomy.defaultTier);
9
+ const confidence = entry?.confidence ?? 0;
10
+ const floor = config.autonomy.domainFloors[item.domain];
11
+ if (floor) {
12
+ const tierIdx = TIER_ORDER.indexOf(tier);
13
+ const floorIdx = TIER_ORDER.indexOf(floor);
14
+ if (tierIdx !== -1 && floorIdx !== -1 && tierIdx < floorIdx)
15
+ tier = floor;
16
+ }
17
+ return { ...item, tier, confidence };
18
+ }
@@ -0,0 +1,53 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ export async function loadProfile(path) {
5
+ try {
6
+ return JSON.parse(await readFile(resolvePath(path), "utf-8"));
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ }
12
+ export 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 function getEntry(profile, domain, action_class) {
18
+ return profile.find(e => e.domain === domain && e.action_class === action_class) ?? null;
19
+ }
20
+ export function needsCalibration(entry, threshold) {
21
+ return !entry || entry.confidence < threshold;
22
+ }
23
+ export function upsertEntry(profile, domain, action_class, update) {
24
+ const idx = profile.findIndex(e => e.domain === domain && e.action_class === action_class);
25
+ const base = idx === -1
26
+ ? {
27
+ domain, action_class, tier: "propose", confidence: 0,
28
+ confirmed_count: 0, corrected_count: 0,
29
+ last_calibrated: new Date().toISOString(), notes: "",
30
+ }
31
+ : profile[idx];
32
+ const updated = { ...base, ...update, last_calibrated: new Date().toISOString() };
33
+ if (idx === -1)
34
+ return [...profile, updated];
35
+ return profile.map((e, i) => i === idx ? updated : e);
36
+ }
37
+ export function applyConfirmation(entry) {
38
+ return {
39
+ ...entry,
40
+ confidence: Math.min(1, entry.confidence + 0.1),
41
+ confirmed_count: entry.confirmed_count + 1,
42
+ last_calibrated: new Date().toISOString(),
43
+ };
44
+ }
45
+ export function applyCorrection(entry, newTier) {
46
+ return {
47
+ ...entry,
48
+ tier: newTier,
49
+ confidence: Math.max(0, entry.confidence - 0.3),
50
+ corrected_count: entry.corrected_count + 1,
51
+ last_calibrated: new Date().toISOString(),
52
+ };
53
+ }
@@ -0,0 +1,54 @@
1
+ import { appendAction } from "./action-log.js";
2
+ export function buildTierPrompt(item) {
3
+ switch (item.tier) {
4
+ case "act":
5
+ return `[SAPIENCE: ACT] High-confidence action — execute immediately without asking the user for approval, then notify them briefly.
6
+
7
+ Action: ${item.text}
8
+ Domain: ${item.domain} / ${item.action_class}
9
+ Confidence: ${(item.confidence * 100).toFixed(0)}%
10
+
11
+ Execute now. After completion, send a brief message: "I just [what you did]. [Reversible/not reversible]."`;
12
+ case "propose":
13
+ return `[SAPIENCE: PROPOSE] A thinking pass identified this as worth doing. Surface it to the user for approval.
14
+
15
+ Proposal: ${item.text}
16
+ Priority: ${item.priority}/5
17
+
18
+ Present this concisely and ask if they'd like you to proceed.`;
19
+ case "ask":
20
+ return `[SAPIENCE: ASK] You're capable of this but need information to proceed. Ask the user for exactly what you need.
21
+
22
+ Action: ${item.text}
23
+ Domain: ${item.domain}
24
+
25
+ State what you can do, then ask the one or two specific questions that would unblock you.`;
26
+ case "explore":
27
+ return `[SAPIENCE: EXPLORE] A problem was identified but the right approach isn't obvious. Present it with options.
28
+
29
+ Problem: ${item.text}
30
+ Priority: ${item.priority}/5
31
+
32
+ Name the problem, offer 2–3 concrete approaches with their tradeoffs, and ask which fits what they're trying to accomplish.`;
33
+ case "learning":
34
+ return `[SAPIENCE: CALIBRATE] This domain/action class hasn't been calibrated yet. Check with the user before routing.
35
+
36
+ Item: ${item.text}
37
+ Domain: ${item.domain} / ${item.action_class}
38
+ Current confidence: ${(item.confidence * 100).toFixed(0)}%
39
+
40
+ Tell the user: "I noticed [item]. My instinct is to [what you'd do at the propose tier]. Is that the right level of initiative, or would you prefer I handle this differently?"`;
41
+ }
42
+ }
43
+ export async function deliverItems(items, api, config) {
44
+ const sorted = [...items].sort((a, b) => (a.tier === "act" ? 0 : 1) - (b.tier === "act" ? 0 : 1));
45
+ for (const item of sorted) {
46
+ if (item.tier === "act") {
47
+ await appendAction(item, "Queued for immediate execution", config.output.actionLogPath);
48
+ }
49
+ await api.session.workflow.enqueueNextTurnInjection({
50
+ sessionTarget: "main",
51
+ text: buildTierPrompt(item),
52
+ });
53
+ }
54
+ }
@@ -0,0 +1,20 @@
1
+ // src/processed-passes.ts
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { dirname } from "path";
4
+ import { resolvePath } from "./utils.js";
5
+ export async function loadProcessedPasses(path) {
6
+ try {
7
+ const data = JSON.parse(await readFile(resolvePath(path), "utf-8"));
8
+ return new Set(data.pass_ids);
9
+ }
10
+ catch {
11
+ return new Set();
12
+ }
13
+ }
14
+ export async function markPassProcessed(passId, path, processed) {
15
+ const updated = new Set([...processed, passId]);
16
+ const resolved = resolvePath(path);
17
+ await mkdir(dirname(resolved), { recursive: true });
18
+ await writeFile(resolved, JSON.stringify({ pass_ids: [...updated] }, null, 2), "utf-8");
19
+ return updated;
20
+ }
@@ -0,0 +1,52 @@
1
+ import { readFile } from "fs/promises";
2
+ import { resolvePath } from "./utils.js";
3
+ const DOMAIN_PATTERNS = [
4
+ [/github/i, "github"],
5
+ [/salesforce/i, "salesforce"],
6
+ [/posthog/i, "posthog"],
7
+ [/lovable/i, "lovable"],
8
+ [/slack/i, "slack"],
9
+ [/google[\s-]?docs?/i, "google-docs"],
10
+ [/slides?|deck/i, "slides"],
11
+ [/okr/i, "okr-system"],
12
+ [/linear/i, "linear"],
13
+ ];
14
+ export function extractDomain(text) {
15
+ for (const [pattern, domain] of DOMAIN_PATTERNS) {
16
+ if (pattern.test(text))
17
+ return domain;
18
+ }
19
+ return "general";
20
+ }
21
+ export function proposalSetToItems(raw) {
22
+ if (raw.nothing_to_report)
23
+ return [];
24
+ const items = [];
25
+ for (const obs of raw.observations) {
26
+ const domain = extractDomain(obs.text + " " + obs.evidence);
27
+ items.push({ id: obs.id, type: "observation", text: obs.text, domain, action_class: "observation", priority: obs.priority, pass_id: raw.pass_id, pass_timestamp: raw.timestamp });
28
+ }
29
+ for (const action of raw.proposed_actions) {
30
+ const domain = extractDomain(action.text + " " + action.rationale);
31
+ items.push({ id: action.id, type: "action", text: action.text, domain, action_class: `${domain}/action`, priority: action.priority, pass_id: raw.pass_id, pass_timestamp: raw.timestamp });
32
+ }
33
+ for (const audit of raw.proposed_audits) {
34
+ const domain = extractDomain(audit.domain + " " + audit.rationale);
35
+ items.push({ id: audit.id, type: "audit", text: `${audit.domain}: ${audit.rationale}`, domain, action_class: `${domain}/audit`, priority: audit.priority, pass_id: raw.pass_id, pass_timestamp: raw.timestamp });
36
+ }
37
+ for (const q of raw.open_questions) {
38
+ const domain = extractDomain(q.text + " " + q.blocking_what);
39
+ items.push({ id: q.id, type: "question", text: q.text, domain, action_class: "question", priority: 3, pass_id: raw.pass_id, pass_timestamp: raw.timestamp });
40
+ }
41
+ return items;
42
+ }
43
+ export async function readUnprocessedPasses(proposalsPath, processedIds) {
44
+ try {
45
+ const content = await readFile(resolvePath(proposalsPath), "utf-8");
46
+ const lines = content.trim().split("\n").filter(Boolean);
47
+ return lines.map(l => JSON.parse(l)).filter(p => !processedIds.has(p.pass_id));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
@@ -0,0 +1,86 @@
1
+ // src/service.ts
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ import { DEFAULT_CONFIG } from "./types.js";
4
+ import { resolveDataPath, isWithinActiveHours } from "./utils.js";
5
+ import { loadProfile, saveProfile, upsertEntry } from "./calibration.js";
6
+ import { routeItem } from "./autonomy.js";
7
+ import { readUnprocessedPasses, proposalSetToItems } from "./proposal-adapter.js";
8
+ import { loadProcessedPasses, markPassProcessed } from "./processed-passes.js";
9
+ import { deliverItems } from "./delivery.js";
10
+ import { isDigestDay, buildDigestPrompt } from "./weekly-digest.js";
11
+ function mergeConfig(raw, workspaceDir) {
12
+ return {
13
+ ...DEFAULT_CONFIG,
14
+ ...raw,
15
+ activeHours: { ...DEFAULT_CONFIG.activeHours, ...(raw.activeHours ?? {}) },
16
+ proactiveThinking: {
17
+ ...DEFAULT_CONFIG.proactiveThinking,
18
+ ...(raw.proactiveThinking ?? {}),
19
+ proposalsPath: resolveDataPath(raw.proactiveThinking?.proposalsPath, workspaceDir, DEFAULT_CONFIG.proactiveThinking.proposalsPath),
20
+ },
21
+ learning: { ...DEFAULT_CONFIG.learning, ...(raw.learning ?? {}) },
22
+ autonomy: { ...DEFAULT_CONFIG.autonomy, ...(raw.autonomy ?? {}) },
23
+ digest: { ...DEFAULT_CONFIG.digest, ...(raw.digest ?? {}) },
24
+ output: {
25
+ ...DEFAULT_CONFIG.output,
26
+ ...(raw.output ?? {}),
27
+ calibrationPath: resolveDataPath(raw.output?.calibrationPath, workspaceDir, DEFAULT_CONFIG.output.calibrationPath),
28
+ actionLogPath: resolveDataPath(raw.output?.actionLogPath, workspaceDir, DEFAULT_CONFIG.output.actionLogPath),
29
+ processedPassesPath: resolveDataPath(raw.output?.processedPassesPath, workspaceDir, DEFAULT_CONFIG.output.processedPassesPath),
30
+ },
31
+ };
32
+ }
33
+ export default definePluginEntry({
34
+ id: "sapience",
35
+ name: "Sapience",
36
+ description: "Autonomy layer: routes proactive-thinking proposals through tier function, calibrates to human preferences, delivers weekly digest",
37
+ register(api) {
38
+ const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(api.pluginConfig);
39
+ const config = mergeConfig(api.pluginConfig, workspaceDir);
40
+ api.registerTool({
41
+ name: "process_proposals",
42
+ description: "Process new proposals from the proactive-thinking log and route them through the autonomy tier function. Called by the sapience cron.",
43
+ parameters: {},
44
+ async execute(_id, _params) {
45
+ if (!isWithinActiveHours(config)) {
46
+ return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
47
+ }
48
+ const [processed, profile] = await Promise.all([
49
+ loadProcessedPasses(config.output.processedPassesPath),
50
+ loadProfile(config.output.calibrationPath),
51
+ ]);
52
+ const newPasses = await readUnprocessedPasses(config.proactiveThinking.proposalsPath, processed);
53
+ let updatedProcessed = processed;
54
+ let updatedProfile = profile;
55
+ for (const pass of newPasses) {
56
+ const items = proposalSetToItems(pass);
57
+ const routed = items.map(item => routeItem(item, updatedProfile, config));
58
+ await deliverItems(routed, api, config);
59
+ updatedProcessed = await markPassProcessed(pass.pass_id, config.output.processedPassesPath, updatedProcessed);
60
+ for (const item of routed) {
61
+ const exists = updatedProfile.find(e => e.domain === item.domain && e.action_class === item.action_class);
62
+ if (!exists) {
63
+ updatedProfile = upsertEntry(updatedProfile, item.domain, item.action_class, {
64
+ tier: config.autonomy.defaultTier,
65
+ confidence: 0,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ await saveProfile(updatedProfile, config.output.calibrationPath);
71
+ if (config.digest.enabled && isDigestDay(config) && newPasses.length === 0) {
72
+ const prompt = await buildDigestPrompt(config);
73
+ await api.session.workflow.enqueueNextTurnInjection({ sessionTarget: "main", text: prompt });
74
+ }
75
+ return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
76
+ },
77
+ });
78
+ api.session.workflow.scheduleSessionTurn({
79
+ schedule: { cron: config.schedule },
80
+ sessionTarget: "isolated",
81
+ tag: "sapience-routing-pass",
82
+ systemPrompt: `You are the sapience routing agent. Call process_proposals() to route new thinking pass proposals. Reply SILENT_REPLY_TOKEN after the tool call.`,
83
+ maxTurns: 2,
84
+ });
85
+ },
86
+ });
@@ -0,0 +1,22 @@
1
+ export const DEFAULT_CONFIG = {
2
+ schedule: "*/15 * * * *",
3
+ activeHours: { start: "08:00", end: "20:00", timezone: "America/Los_Angeles" },
4
+ proactiveThinking: {
5
+ proposalsPath: "proactive-thinking/proposals.jsonl",
6
+ },
7
+ learning: {
8
+ enabled: true,
9
+ recalibrateOnNewDomain: true,
10
+ confidenceDropThreshold: 0.4,
11
+ },
12
+ autonomy: {
13
+ defaultTier: "propose",
14
+ domainFloors: {},
15
+ },
16
+ digest: { enabled: true, day: "friday", time: "17:00" },
17
+ output: {
18
+ calibrationPath: "sapience/calibration.json",
19
+ actionLogPath: "sapience/action-log.md",
20
+ processedPassesPath: "sapience/processed-passes.json",
21
+ },
22
+ };
@@ -0,0 +1,23 @@
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 isWithinActiveHours(config) {
14
+ const formatter = new Intl.DateTimeFormat("en-US", {
15
+ timeZone: config.activeHours.timezone,
16
+ hour: "2-digit", minute: "2-digit", hour12: false,
17
+ });
18
+ const [hours, minutes] = formatter.format(new Date()).split(":").map(Number);
19
+ const now = (hours ?? 0) * 60 + (minutes ?? 0);
20
+ const [sh, sm] = config.activeHours.start.split(":").map(Number);
21
+ const [eh, em] = config.activeHours.end.split(":").map(Number);
22
+ return now >= (sh ?? 0) * 60 + (sm ?? 0) && now <= (eh ?? 0) * 60 + (em ?? 0);
23
+ }
@@ -0,0 +1,42 @@
1
+ import { readFile } from "fs/promises";
2
+ import { resolvePath } from "./utils.js";
3
+ export function isDigestDay(config) {
4
+ const now = new Date();
5
+ const parts = new Intl.DateTimeFormat("en-US", {
6
+ timeZone: config.activeHours.timezone,
7
+ weekday: "long", hour: "2-digit", minute: "2-digit", hour12: false,
8
+ }).formatToParts(now);
9
+ const weekday = parts.find(p => p.type === "weekday")?.value?.toLowerCase() ?? "";
10
+ const hour = parseInt(parts.find(p => p.type === "hour")?.value ?? "0");
11
+ const minute = parseInt(parts.find(p => p.type === "minute")?.value ?? "0");
12
+ const [digestHour, digestMinute] = config.digest.time.split(":").map(Number);
13
+ return weekday === config.digest.day.toLowerCase()
14
+ && hour === (digestHour ?? 17)
15
+ && minute < 30;
16
+ }
17
+ export async function buildDigestPrompt(config) {
18
+ let actionLog = "No actions logged this week.";
19
+ try {
20
+ const raw = await readFile(resolvePath(config.output.actionLogPath), "utf-8");
21
+ actionLog = raw.length > 3000
22
+ ? "...(earlier entries omitted)\n\n" + raw.slice(-3000)
23
+ : raw;
24
+ }
25
+ catch { /* file absent is fine */ }
26
+ return `[SAPIENCE: WEEKLY DIGEST] Build and deliver a weekly summary to the user.
27
+
28
+ ## Action log from this week
29
+ ${actionLog}
30
+
31
+ ## Instructions
32
+
33
+ Deliver a brief weekly summary with these sections:
34
+
35
+ **What I did this week:** List actions actually taken (from the action log above). If nothing was logged, say so.
36
+
37
+ **Pending your review:** Any proposals from this week that are still waiting on human input.
38
+
39
+ **What I plan next week:** Based on any active goals or pending work you're aware of.
40
+
41
+ Keep it concise. This is a status ping, not a report. Omit sections you have nothing meaningful to say about.`;
42
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "sapience",
3
+ "name": "Sapience",
4
+ "version": "0.1.0",
5
+ "description": "Autonomy layer: routes proactive-thinking proposals through tier function, calibrates to human preferences, delivers weekly digest",
6
+ "contracts": {
7
+ "tools": ["process_proposals"]
8
+ },
9
+ "activation": {
10
+ "onStartup": true
11
+ },
12
+ "configSchema": {
13
+ "type": "object",
14
+ "additionalProperties": true
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@akalsey/openclaw-sapience",
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
+ "dependencies": {
34
+ "@sinclair/typebox": "^0.33.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.0.0",
38
+ "openclaw": "latest",
39
+ "typescript": "^5.5.0",
40
+ "vitest": "^2.0.0"
41
+ },
42
+ "peerDependencies": {
43
+ "openclaw": ">=2026.3.24-beta.2"
44
+ }
45
+ }