@prevalentware/opencode-goal-plugin 0.1.11 → 0.1.13

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 CHANGED
@@ -6,7 +6,7 @@ This plugin adds:
6
6
 
7
7
  - `/goal <objective>` as an OpenCode command for TUI, desktop, and web.
8
8
  - A sidebar goal indicator with status, elapsed time, and objective.
9
- - Agent tools: `get_goal`, `create_goal`, `update_goal`, and `clear_goal`.
9
+ - Agent tools: `get_goal`, `create_goal`, `set_goal`, `update_goal`, and `clear_goal`.
10
10
  - Goal close evidence: `complete` requires verified evidence, and `unmet` requires a concrete blocker.
11
11
  - Persistent per-session goal state.
12
12
  - Optional automatic continuation on `session.idle`.
@@ -85,6 +85,8 @@ Use `/goal <objective>` in a fresh OpenCode chat to create a long-running goal:
85
85
 
86
86
  Bare `/goal` reports the current goal state. `/goal clear` clears the goal. The TUI also includes a `Goal` command-palette entry for viewing, refreshing, or clearing the current goal state without creating a new goal.
87
87
 
88
+ You can also ask the agent to formulate the objective and call `set_goal` itself, for example: "set your own goal to finish this refactor safely." The tool uses the agent-written objective but still only creates a goal when explicitly requested.
89
+
88
90
  When writing the objective, include the scope, non-goals, and verification path when they matter. The agent is reminded to audit real files, command output, tests, or PR state before closing the goal.
89
91
 
90
92
  The `update_goal` tool can close a goal in two ways:
package/dist/server.js CHANGED
@@ -6,6 +6,38 @@ import { z } from "zod";
6
6
  import { homedir } from "os";
7
7
  import { dirname, join } from "path";
8
8
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
9
+ import { Data, Effect, Schema } from "effect";
10
+
11
+ class StateReadError extends Data.TaggedError("StateReadError") {
12
+ }
13
+
14
+ class StateDecodeError extends Data.TaggedError("StateDecodeError") {
15
+ }
16
+
17
+ class StateWriteError extends Data.TaggedError("StateWriteError") {
18
+ }
19
+ var NullableString = Schema.NullOr(Schema.String);
20
+ var NullableNumber = Schema.NullOr(Schema.Number);
21
+ var GoalSchema = Schema.Struct({
22
+ sessionID: Schema.String,
23
+ objective: Schema.String,
24
+ status: Schema.Literal("active", "paused", "budgetLimited", "complete", "unmet"),
25
+ tokenBudget: NullableNumber,
26
+ tokensUsed: Schema.Number,
27
+ timeUsedSeconds: Schema.Number,
28
+ createdAt: Schema.Number,
29
+ updatedAt: Schema.Number,
30
+ completionEvidence: Schema.optionalWith(NullableString, { default: () => null }),
31
+ blocker: Schema.optionalWith(NullableString, { default: () => null }),
32
+ closedAt: Schema.optionalWith(NullableNumber, { default: () => null }),
33
+ lastAccountedAt: NullableNumber,
34
+ autoTurns: Schema.Number,
35
+ lastContinuationAt: NullableNumber
36
+ });
37
+ var StateSchema = Schema.Struct({
38
+ version: Schema.Literal(1),
39
+ goals: Schema.Record({ key: Schema.String, value: GoalSchema })
40
+ });
9
41
  function defaultStateFile() {
10
42
  const dataHome = process.env.XDG_DATA_HOME || (process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : join(homedir(), ".local", "share"));
11
43
  return join(dataHome, "opencode-goal-plugin", "goals.json");
@@ -19,30 +51,60 @@ function nowSeconds() {
19
51
  function emptyState() {
20
52
  return { version: 1, goals: {} };
21
53
  }
54
+ function isMissingStateFile(error) {
55
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
56
+ }
57
+ function mutableState(state) {
58
+ return JSON.parse(JSON.stringify(state));
59
+ }
60
+ function decodeState(value) {
61
+ return Schema.decodeUnknown(StateSchema)(value).pipe(Effect.map(mutableState), Effect.mapError((cause) => new StateDecodeError({ cause })));
62
+ }
63
+ function readStateEffect() {
64
+ return Effect.tryPromise({
65
+ try: () => readFile(statePath(), "utf8"),
66
+ catch: (cause) => new StateReadError({ cause })
67
+ }).pipe(Effect.flatMap((raw) => Effect.try({
68
+ try: () => JSON.parse(raw),
69
+ catch: (cause) => new StateDecodeError({ cause })
70
+ })), Effect.flatMap(decodeState), Effect.catchAll((error) => error._tag === "StateReadError" && isMissingStateFile(error.cause) ? Effect.succeed(emptyState()) : Effect.fail(error)));
71
+ }
72
+ function writeStateEffect(state) {
73
+ return Effect.tryPromise({
74
+ try: async () => {
75
+ const file = statePath();
76
+ await mkdir(dirname(file), { recursive: true });
77
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
78
+ await writeFile(tmp, JSON.stringify(state, null, 2) + `
79
+ `);
80
+ await rename(tmp, file);
81
+ },
82
+ catch: (cause) => new StateWriteError({ cause })
83
+ });
84
+ }
22
85
  async function readState() {
23
- try {
24
- const raw = await readFile(statePath(), "utf8");
25
- const parsed = JSON.parse(raw);
26
- return parsed && parsed.version === 1 && parsed.goals ? parsed : emptyState();
27
- } catch (error) {
28
- if (error.code === "ENOENT")
29
- return emptyState();
30
- throw error;
31
- }
86
+ return Effect.runPromise(readStateEffect());
32
87
  }
33
- async function writeState(state) {
34
- const file = statePath();
35
- await mkdir(dirname(file), { recursive: true });
36
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
37
- await writeFile(tmp, JSON.stringify(state, null, 2) + `
38
- `);
39
- await rename(tmp, file);
88
+ var mutationQueue = Promise.resolve();
89
+ function enqueueMutation(operation) {
90
+ const current = mutationQueue.then(operation, operation);
91
+ mutationQueue = current.then(() => {
92
+ return;
93
+ }, () => {
94
+ return;
95
+ });
96
+ return current;
40
97
  }
41
98
  async function mutate(fn) {
42
- const state = await readState();
43
- const result = await fn(state);
44
- await writeState(state);
45
- return result;
99
+ return enqueueMutation(() => Effect.runPromise(Effect.gen(function* () {
100
+ const state = yield* readStateEffect();
101
+ const result = yield* Effect.tryPromise({
102
+ try: () => Promise.resolve(fn(state)),
103
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
104
+ });
105
+ yield* writeStateEffect(state);
106
+ return result;
107
+ })));
46
108
  }
47
109
  function validateObjective(objective) {
48
110
  const value = objective.trim();
@@ -250,9 +312,9 @@ Do not rely on intent, partial progress, elapsed effort, memory of earlier work,
250
312
  }
251
313
  function systemReminder(goal) {
252
314
  if (!goal) {
253
- return `OpenCode goal mode is available through get_goal, create_goal, and update_goal tools.
315
+ return `OpenCode goal mode is available through get_goal, create_goal, set_goal, and update_goal tools.
254
316
 
255
- Create a goal only when explicitly requested by the user or system/developer instructions. Do not infer goals from ordinary tasks. When closing a goal, update_goal requires evidence for status "complete" or a blocker for status "unmet".`;
317
+ Create a goal only when explicitly requested by the user or system/developer instructions. Use set_goal when the user asks you to formulate and set your own goal. Do not infer goals from ordinary tasks. When closing a goal, update_goal requires evidence for status "complete" or a blocker for status "unmet".`;
256
318
  }
257
319
  if (goal.status === "active")
258
320
  return continuationPrompt(goal);
@@ -395,6 +457,17 @@ var server = async ({ client }, options) => {
395
457
  return JSON.stringify({ goal }, null, 2);
396
458
  }
397
459
  },
460
+ set_goal: {
461
+ description: "Set a new goal when the user explicitly asks the agent to formulate and set its own goal. The model should write the objective itself based on the user's explicit request. Fails if a non-complete goal exists.",
462
+ args: {
463
+ objective: z.string().min(1).max(4000).describe("The model-formulated concrete objective to start pursuing.")
464
+ },
465
+ async execute(args, context) {
466
+ const input = args;
467
+ const goal = await createGoal(context.sessionID, input.objective);
468
+ return JSON.stringify({ goal }, null, 2);
469
+ }
470
+ },
398
471
  update_goal: {
399
472
  description: "Close the existing goal only after an audit against real evidence. Use status complete only when the objective is achieved and no required work remains, and include evidence. Use status unmet only when the objective cannot be achieved or is blocked, and include the blocker. Do not close a goal merely because work is stopping.",
400
473
  args: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prevalentware/opencode-goal-plugin",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Codex-style long-running goal mode for OpenCode.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -36,7 +36,7 @@
36
36
  ],
37
37
  "scripts": {
38
38
  "clean": "rm -rf dist",
39
- "build": "bun run clean && bun build ./src/server.ts --outdir ./dist --target bun --external @opencode-ai/plugin --external zod",
39
+ "build": "bun run clean && bun build ./src/server.ts --outdir ./dist --target bun --external @opencode-ai/plugin --external effect --external zod",
40
40
  "ci:version": "bun scripts/resolve-ci-version.ts",
41
41
  "lint": "eslint .",
42
42
  "pack:dry-run": "npm pack --dry-run",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@opencode-ai/plugin": "^1.14.39",
49
+ "effect": "^3.21.2",
49
50
  "zod": "^4.1.8"
50
51
  },
51
52
  "devDependencies": {