@akalsey/openclaw-goals 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,146 @@
1
+ # OpenClaw Goals
2
+
3
+ Some things worth doing aren't tasks. They're directions — fuzzy, long-running, and valuable even when the path isn't clear. "Our teams aren't scoring OKRs regularly" is a goal, not a ticket.
4
+
5
+ This plugin accepts those kinds of objectives, decomposes them into candidate approaches, tracks incremental progress, and delivers a weekly status update. You stay oriented without managing the detail.
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 track multi-step tasks.
10
+
11
+ ---
12
+
13
+ ## Setup
14
+
15
+ ### Install
16
+
17
+ ```bash
18
+ openclaw plugins install local:/path/to/openclaw-goals
19
+ ```
20
+
21
+ ### Configuration
22
+
23
+ ```json
24
+ {
25
+ "plugins": {
26
+ "goals": {
27
+ "weeklyCheckInDay": "monday",
28
+ "weeklyCheckInTime": "09:00",
29
+ "activeHours": {
30
+ "start": "08:00",
31
+ "end": "20:00",
32
+ "timezone": "America/Los_Angeles"
33
+ },
34
+ "inboxPath": "~/.openclaw/sapience/goals-inbox.md"
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ All settings are optional — defaults above are used if omitted.
41
+
42
+ ### Create the inbox file
43
+
44
+ ```bash
45
+ touch ~/.openclaw/sapience/goals-inbox.md
46
+ ```
47
+
48
+ The plugin won't error if the file is missing, but you need it to submit goals.
49
+
50
+ ### Output files
51
+
52
+ | File | Purpose |
53
+ |------|---------|
54
+ | `~/.openclaw/sapience/goals.json` | All goals with status, approaches, progress, blockers |
55
+ | `~/.openclaw/sapience/goals-inbox.md` | Where you write new goals |
56
+ | `~/.openclaw/sapience/goals-inbox-position.json` | Byte offset tracking — don't edit this |
57
+
58
+ ---
59
+
60
+ ## Submitting a goal
61
+
62
+ Just tell the agent in conversation:
63
+
64
+ > "I want to improve our OKR scoring rate"
65
+ > "Figure out why PostHog costs keep spiking"
66
+ > "Help me build a habit of writing weekly team updates"
67
+
68
+ The agent recognizes long-running objectives and calls `goal_submit` automatically. A `[GOALS: DECOMPOSE]` prompt arrives within seconds with concrete approaches to choose from.
69
+
70
+ ### Via the inbox file (scripting/external use)
71
+
72
+ You can also append goals directly to the inbox file for use from scripts or other tools:
73
+
74
+ ```bash
75
+ echo "Our teams aren't scoring OKRs regularly — improve that" >> ~/.openclaw/sapience/goals-inbox.md
76
+ ```
77
+
78
+ The next cron pass (within 15 minutes) picks it up.
79
+
80
+ ---
81
+
82
+ ## Decomposition
83
+
84
+ When a new goal is detected, the agent delivers a `[GOALS: DECOMPOSE]` prompt to your active session:
85
+
86
+ > "I noticed this goal: 'Our teams aren't scoring OKRs regularly.' Here are 3 approaches I could take…"
87
+
88
+ It presents 2–4 concrete approaches, explains what each would accomplish and what it would need from you, and asks you to pick one (or none).
89
+
90
+ Your selection is recorded as the `active_approach`. Goals without a selected approach stay in `decomposing` status and don't receive weekly updates until you pick one.
91
+
92
+ ---
93
+
94
+ ## Tracking progress
95
+
96
+ Progress is tracked through the weekly status loop — the agent notes what it did toward the goal and what it plans next. You can also add progress notes manually:
97
+
98
+ ```bash
99
+ # Not yet a built-in command — edit goals.json directly for now
100
+ ```
101
+
102
+ To mark a goal complete or pause it, update the `status` field in `~/.openclaw/sapience/goals.json`:
103
+
104
+ ```json
105
+ { "status": "completed" }
106
+ ```
107
+
108
+ Valid statuses: `decomposing` | `active` | `paused` | `completed` | `abandoned`
109
+
110
+ ---
111
+
112
+ ## Weekly status
113
+
114
+ Every Monday at 9am (or your configured day/time), active goals get a `[GOALS: WEEKLY STATUS]` prompt delivered to your session:
115
+
116
+ > "Weekly status for 'Improve OKR scoring rates':
117
+ > - What happened this week: …
118
+ > - What's blocked: …
119
+ > - What I plan next week: …"
120
+
121
+ Each goal gets its own delivery. If nothing happened and nothing is blocked, the agent says so briefly and doesn't pad.
122
+
123
+ The next delivery date is stored per-goal in `goals.json` and rolls forward automatically after each delivery.
124
+
125
+ ---
126
+
127
+ ## Troubleshooting
128
+
129
+ **Goal submitted but no decomposition prompt**
130
+ The cron fires every 15 minutes. Wait for the next pass, or trigger manually:
131
+ ```bash
132
+ openclaw cron run goals-check-pass
133
+ ```
134
+ Also confirm the inbox path matches your config and that the file is readable.
135
+
136
+ **Same goals showing up again after re-install**
137
+ The byte-position tracker (`goals-inbox-position.json`) tracks what's been read. If it's missing, the inbox is read from the beginning. Delete old content from the inbox file, or manually set the position to the file's current byte length.
138
+
139
+ **Weekly status not delivering**
140
+ Check `goals.json` — the goal must have `status: "active"` and `next_status_delivery` must be a past date. If `active_approach` is empty, the goal is still in `decomposing` status and won't get weekly updates.
141
+
142
+ **Too many goals with no progress**
143
+ Goals without active approaches accumulate in the store. Periodically review `goals.json` and mark stale goals as `paused` or `abandoned` to keep the weekly status meaningful.
144
+
145
+ **Goal decomposition is generic / not useful**
146
+ The quality of decomposition depends on how specific the goal statement is. "Improve things" is hard to decompose. "Get weekly OKR scoring rates above 80% by end of Q3" gives the agent something concrete to work with.
package/SKILL.md ADDED
@@ -0,0 +1,32 @@
1
+ # Using openclaw-goals
2
+
3
+ ## When to submit a goal
4
+
5
+ Call `goal_submit` when the user expresses a fuzzy, long-running objective — something that can't be finished in one session and doesn't have an obvious single action:
6
+
7
+ - "I want to improve our OKR scoring rate"
8
+ - "We need to get better signal on what's blocking engineers"
9
+ - "Figure out why our PostHog costs keep spiking"
10
+ - "Help me build a habit of writing weekly team updates"
11
+
12
+ **Don't** submit routine tasks, one-off requests, or things that are already well-defined tickets. Goals are directions, not tasks.
13
+
14
+ ## How to submit
15
+
16
+ Call `goal_submit(description)` with the user's objective as stated — fuzzy language is fine. You don't need to clean it up or restate it formally. The system handles decomposition.
17
+
18
+ After submitting, confirm to the user: "I've recorded that as a goal and will come back with some approaches."
19
+
20
+ ## What happens next
21
+
22
+ Within the next cron pass (up to 15 minutes), a `[GOALS: DECOMPOSE]` prompt will arrive in the session presenting 2–4 concrete approaches. The user picks one and the goal becomes active.
23
+
24
+ ## Weekly check-ins
25
+
26
+ Every Monday (or the configured day), active goals receive a `[GOALS: WEEKLY STATUS]` prompt. You report what happened, what's blocked, and what's planned next. Keep it brief — if nothing happened, say so.
27
+
28
+ ## When NOT to submit
29
+
30
+ - The user is describing a task they want done now → just do it
31
+ - The user mentions something in passing without expressing intent → don't submit without confirming
32
+ - The goal is already in the active list → update it, don't duplicate
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/service.js";
@@ -0,0 +1,52 @@
1
+ export function buildDecompositionPrompt(description) {
2
+ return `[GOALS: DECOMPOSE] A new goal was submitted. Decompose it into candidate approaches and present them to the user for selection.
3
+
4
+ Goal: "${description}"
5
+
6
+ Your job:
7
+ 1. Think about what you could realistically do toward this goal given your available tools and access.
8
+ 2. Identify 2–4 concrete approaches. For each: describe what you'd do, what tools you'd use, what you could accomplish without human input, and what you'd need from the human to make progress.
9
+ 3. Present the approaches to the user and ask them to pick one (or say "none of these").
10
+
11
+ Keep it practical. Only propose approaches you can actually execute with your current tools. Don't promise what you can't deliver.`;
12
+ }
13
+ export function buildWeeklyStatusPrompt(goal) {
14
+ const recentNotes = goal.progress_notes.slice(-3);
15
+ const progressText = recentNotes.length > 0
16
+ ? recentNotes.map(n => `- ${n.timestamp.slice(0, 10)}: ${n.summary}\n (${n.what_changed})`).join("\n")
17
+ : "No progress logged yet.";
18
+ const blockerText = goal.blockers.length > 0
19
+ ? goal.blockers.map(b => `- ${b.description} (waiting on: ${b.waiting_on})`).join("\n")
20
+ : "None.";
21
+ return `[GOALS: WEEKLY STATUS] Deliver a weekly status update for this goal.
22
+
23
+ Goal: "${goal.description}"
24
+ Active approach: ${goal.active_approach || "(not yet selected)"}
25
+ Status: ${goal.status}
26
+
27
+ Recent progress:
28
+ ${progressText}
29
+
30
+ Current blockers:
31
+ ${blockerText}
32
+
33
+ Deliver a brief status update to the user covering:
34
+ - What happened toward this goal this week
35
+ - What's currently blocked and what would unblock it
36
+ - What you plan to try next week
37
+ - Any questions you need answered to make progress
38
+
39
+ Be concise. If there's nothing new to report, say so briefly.`;
40
+ }
41
+ export async function deliverDecomposition(description, api) {
42
+ await api.session.workflow.enqueueNextTurnInjection({
43
+ sessionTarget: "main",
44
+ text: buildDecompositionPrompt(description),
45
+ });
46
+ }
47
+ export async function deliverWeeklyStatus(goal, api) {
48
+ await api.session.workflow.enqueueNextTurnInjection({
49
+ sessionTarget: "main",
50
+ text: buildWeeklyStatusPrompt(goal),
51
+ });
52
+ }
@@ -0,0 +1,42 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ export async function loadGoals(path) {
5
+ try {
6
+ return JSON.parse(await readFile(resolvePath(path), "utf-8"));
7
+ }
8
+ catch {
9
+ return [];
10
+ }
11
+ }
12
+ export async function saveGoals(goals, path) {
13
+ const resolved = resolvePath(path);
14
+ await mkdir(dirname(resolved), { recursive: true });
15
+ await writeFile(resolved, JSON.stringify(goals, null, 2), "utf-8");
16
+ }
17
+ export function addGoal(goals, goal) {
18
+ return [...goals, goal];
19
+ }
20
+ export function updateGoalStatus(goals, id, status) {
21
+ return goals.map(g => g.id === id
22
+ ? { ...g, status, updated_at: new Date().toISOString() }
23
+ : g);
24
+ }
25
+ export function addProgressNote(goals, id, note) {
26
+ return goals.map(g => g.id === id
27
+ ? { ...g, progress_notes: [...g.progress_notes, note], updated_at: new Date().toISOString() }
28
+ : g);
29
+ }
30
+ export function setActiveApproach(goals, id, approach) {
31
+ return goals.map(g => g.id === id
32
+ ? { ...g, active_approach: approach, status: "active", updated_at: new Date().toISOString() }
33
+ : g);
34
+ }
35
+ export function addBlocker(goals, id, blocker) {
36
+ return goals.map(g => g.id === id
37
+ ? { ...g, blockers: [...g.blockers, { ...blocker, since: new Date().toISOString() }], updated_at: new Date().toISOString() }
38
+ : g);
39
+ }
40
+ export function updateNextDelivery(goals, id, nextDelivery) {
41
+ return goals.map(g => g.id === id ? { ...g, next_status_delivery: nextDelivery } : g);
42
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile, writeFile, open, mkdir } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { resolvePath } from "./utils.js";
4
+ export async function loadPosition(posPath) {
5
+ try {
6
+ const data = JSON.parse(await readFile(resolvePath(posPath), "utf-8"));
7
+ return data.position;
8
+ }
9
+ catch {
10
+ return 0;
11
+ }
12
+ }
13
+ export async function savePosition(position, posPath) {
14
+ const resolved = resolvePath(posPath);
15
+ await mkdir(dirname(resolved), { recursive: true });
16
+ await writeFile(resolved, JSON.stringify({ position }, null, 2), "utf-8");
17
+ }
18
+ export async function readNewGoals(inboxPath, posPath) {
19
+ const resolved = resolvePath(inboxPath);
20
+ let fh;
21
+ try {
22
+ fh = await open(resolved, "r");
23
+ const stat = await fh.stat();
24
+ const position = await loadPosition(posPath);
25
+ if (stat.size <= position)
26
+ return { goals: [], newPosition: position };
27
+ const buffer = Buffer.alloc(stat.size - position);
28
+ await fh.read(buffer, 0, buffer.length, position);
29
+ const newText = buffer.toString("utf-8");
30
+ const newPosition = stat.size;
31
+ const goals = newText
32
+ .split("\n")
33
+ .map(l => l.trim())
34
+ .filter(l => l.length > 0 && !l.startsWith("#"));
35
+ return { goals, newPosition };
36
+ }
37
+ catch {
38
+ return { goals: [], newPosition: 0 };
39
+ }
40
+ finally {
41
+ await fh?.close();
42
+ }
43
+ }
@@ -0,0 +1,110 @@
1
+ // src/service.ts
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ import { DEFAULT_CONFIG } from "./types.js";
4
+ import { resolveDataPath, generateId, isWithinActiveHours, nextWeeklyDate } from "./utils.js";
5
+ import { loadGoals, saveGoals, addGoal, updateNextDelivery } from "./goal-store.js";
6
+ import { readNewGoals, savePosition } from "./inbox-reader.js";
7
+ import { deliverDecomposition, deliverWeeklyStatus } from "./delivery.js";
8
+ function mergeConfig(raw, workspaceDir) {
9
+ return {
10
+ ...DEFAULT_CONFIG,
11
+ ...raw,
12
+ activeHours: { ...DEFAULT_CONFIG.activeHours, ...(raw.activeHours ?? {}) },
13
+ output: {
14
+ ...DEFAULT_CONFIG.output,
15
+ ...(raw.output ?? {}),
16
+ goalsPath: resolveDataPath(raw.output?.goalsPath, workspaceDir, DEFAULT_CONFIG.output.goalsPath),
17
+ },
18
+ inboxPath: resolveDataPath(raw.inboxPath, workspaceDir, DEFAULT_CONFIG.inboxPath),
19
+ inboxPositionPath: resolveDataPath(raw.inboxPositionPath, workspaceDir, DEFAULT_CONFIG.inboxPositionPath),
20
+ };
21
+ }
22
+ function isWeeklyCheckInDue(goal) {
23
+ return goal.status === "active" && new Date(goal.next_status_delivery) <= new Date();
24
+ }
25
+ export default definePluginEntry({
26
+ id: "goals",
27
+ name: "Goals",
28
+ description: "Persistent fuzzy goal tracking with weekly status delivery",
29
+ register(api) {
30
+ const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(api.pluginConfig);
31
+ const config = mergeConfig(api.pluginConfig, workspaceDir);
32
+ api.registerTool({
33
+ name: "goal_submit",
34
+ description: "Submit a new long-running goal. Call this when the user expresses a fuzzy objective that spans multiple sessions. Returns the new goal's id.",
35
+ parameters: {
36
+ type: "object",
37
+ properties: {
38
+ description: { type: "string", description: "The goal as stated by the user — fuzzy and long-running is fine" },
39
+ },
40
+ required: ["description"],
41
+ },
42
+ async execute(_id, params) {
43
+ const { description } = params;
44
+ let goals = await loadGoals(config.output.goalsPath);
45
+ const goal = {
46
+ id: generateId(),
47
+ description,
48
+ decomposed_approaches: [],
49
+ active_approach: "",
50
+ status: "decomposing",
51
+ created_at: new Date().toISOString(),
52
+ updated_at: new Date().toISOString(),
53
+ progress_notes: [],
54
+ blockers: [],
55
+ next_status_delivery: nextWeeklyDate(config.weeklyCheckInDay, config.weeklyCheckInTime, config.activeHours.timezone),
56
+ };
57
+ goals = addGoal(goals, goal);
58
+ await saveGoals(goals, config.output.goalsPath);
59
+ await deliverDecomposition(description, api);
60
+ return { content: [{ type: "text", text: JSON.stringify({ id: goal.id }) }] };
61
+ },
62
+ });
63
+ api.registerTool({
64
+ name: "check_goals",
65
+ description: "Check inbox for new goals and deliver weekly status for active goals. Called by the goals cron.",
66
+ parameters: {},
67
+ async execute(_id, _params) {
68
+ if (!isWithinActiveHours(config)) {
69
+ return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
70
+ }
71
+ let goals = await loadGoals(config.output.goalsPath);
72
+ const { goals: newDescriptions, newPosition } = await readNewGoals(config.inboxPath, config.inboxPositionPath);
73
+ for (const description of newDescriptions) {
74
+ const goal = {
75
+ id: generateId(),
76
+ description,
77
+ decomposed_approaches: [],
78
+ active_approach: "",
79
+ status: "decomposing",
80
+ created_at: new Date().toISOString(),
81
+ updated_at: new Date().toISOString(),
82
+ progress_notes: [],
83
+ blockers: [],
84
+ next_status_delivery: nextWeeklyDate(config.weeklyCheckInDay, config.weeklyCheckInTime, config.activeHours.timezone),
85
+ };
86
+ goals = addGoal(goals, goal);
87
+ await deliverDecomposition(description, api);
88
+ }
89
+ if (newDescriptions.length > 0) {
90
+ await savePosition(newPosition, config.inboxPositionPath);
91
+ }
92
+ for (const goal of goals) {
93
+ if (isWeeklyCheckInDue(goal)) {
94
+ await deliverWeeklyStatus(goal, api);
95
+ goals = updateNextDelivery(goals, goal.id, nextWeeklyDate(config.weeklyCheckInDay, config.weeklyCheckInTime, config.activeHours.timezone));
96
+ }
97
+ }
98
+ await saveGoals(goals, config.output.goalsPath);
99
+ return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
100
+ },
101
+ });
102
+ api.session.workflow.scheduleSessionTurn({
103
+ schedule: { cron: config.schedule },
104
+ sessionTarget: "isolated",
105
+ tag: "goals-check-pass",
106
+ systemPrompt: `You are the goals tracking agent. Call check_goals() to process new goals and deliver weekly status updates. Reply SILENT_REPLY_TOKEN after the tool call.`,
107
+ maxTurns: 2,
108
+ });
109
+ },
110
+ });
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_CONFIG = {
2
+ schedule: "*/15 * * * *",
3
+ activeHours: { start: "08:00", end: "20:00", timezone: "America/Los_Angeles" },
4
+ weeklyCheckInDay: "monday",
5
+ weeklyCheckInTime: "09:00",
6
+ inboxPath: "goals/inbox.md",
7
+ inboxPositionPath: "goals/inbox-position.json",
8
+ output: { goalsPath: "goals/goals.json" },
9
+ };
@@ -0,0 +1,38 @@
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 `goal-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
15
+ }
16
+ export function isWithinActiveHours(config) {
17
+ const formatter = new Intl.DateTimeFormat("en-US", {
18
+ timeZone: config.activeHours.timezone,
19
+ hour: "2-digit", minute: "2-digit", hour12: false,
20
+ });
21
+ const [hours, minutes] = formatter.format(new Date()).split(":").map(Number);
22
+ const now = (hours ?? 0) * 60 + (minutes ?? 0);
23
+ const [sh, sm] = config.activeHours.start.split(":").map(Number);
24
+ const [eh, em] = config.activeHours.end.split(":").map(Number);
25
+ return now >= (sh ?? 0) * 60 + (sm ?? 0) && now <= (eh ?? 0) * 60 + (em ?? 0);
26
+ }
27
+ export function nextWeeklyDate(day, time, timezone) {
28
+ const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
29
+ const targetDay = dayNames.indexOf(day.toLowerCase());
30
+ const now = new Date();
31
+ const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "long" });
32
+ const currentDay = dayNames.indexOf(formatter.format(now).toLowerCase());
33
+ const daysUntil = ((targetDay - currentDay) + 7) % 7 || 7;
34
+ const next = new Date(now.getTime() + daysUntil * 24 * 60 * 60 * 1000);
35
+ const [h, m] = time.split(":").map(Number);
36
+ next.setHours(h ?? 9, m ?? 0, 0, 0);
37
+ return next.toISOString();
38
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "goals",
3
+ "name": "Goals",
4
+ "version": "0.1.0",
5
+ "description": "Persistent fuzzy goal tracking: accepts vague objectives, decomposes them into candidate approaches, tracks incremental progress, and delivers weekly status",
6
+ "contracts": {
7
+ "tools": ["goal_submit", "check_goals"]
8
+ },
9
+ "activation": { "onStartup": true },
10
+ "configSchema": {
11
+ "type": "object",
12
+ "additionalProperties": true
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@akalsey/openclaw-goals",
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
+ "SKILL.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "openclaw": {
23
+ "extensions": [
24
+ "./dist/index.js"
25
+ ],
26
+ "compat": {
27
+ "pluginApi": ">=2026.3.24-beta.2",
28
+ "minGatewayVersion": "2026.3.24-beta.2"
29
+ },
30
+ "install": {
31
+ "localPath": "."
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "openclaw": "latest",
37
+ "typescript": "^5.5.0",
38
+ "vitest": "^2.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "openclaw": ">=2026.3.24-beta.2"
42
+ }
43
+ }