opencode-goal-plugin 0.3.0 → 0.4.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/CHANGELOG.md +7 -2
- package/README.md +17 -0
- package/package.json +9 -1
- package/src/goal-plugin.js +264 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.0 — 2026-06-21
|
|
6
|
+
|
|
7
|
+
- **Expose agent-facing goal tools (`get_goal`, `get_goal_history`, `set_goal`, `update_goal`, `clear_goal`)** when the host provides `@opencode-ai/plugin` (a new *optional* peer dependency, loaded via a cached dynamic import so the zero-runtime-dependency posture is preserved). `set_goal` is constrained by its description to explicit user requests (so the agent does not set goals on its own); it accepts optional `maxTurns` / `maxTokens` / `maxDurationMs` overrides plus `successCriteria` / `constraints` / `mode`. `update_goal` supports objective edits and `complete` / `blocked` / `paused` / `resumed` transitions (with `evidence` / `blocker`). Tools create, replace, and clear goals through the **multi-goal registry** (the same `buildGoalState` → `registerSessionGoal` → `focusGoal` path the `/goal` command uses), so tool-created goals persist, appear in `/goal list`, and are driven by the idle handler; `complete` archives with evidence and auto-promotes the next goal in an ordered (sisyphus) sequence. Registration is skipped gracefully when the package is absent or with `registerTools: false`. New `buildAgentToolHandlers` / `buildAgentTools` / `agentToolSessionID` helpers. Implements megalist items 7.1 and 7.2. _(This is the work the 0.3.0 changelog mistakenly listed as already shipped; it is now actually implemented and adapted to the current multi-goal architecture.)_
|
|
8
|
+
- **Fix a goal-registry leak when resuming.** `resetGoalBudget` rotates a goal's `goalId`, but the multi-goal registry is keyed by `goalId`, so resuming and then clearing/replacing left a stale entry behind (visible in `/goal list` and persisted). Both the `/goal resume` command path and the agent `update_goal {status:"resumed"}` path now re-key the registry to the new id (the focused pointer holds the same object). Regression test added for the command path.
|
|
9
|
+
|
|
5
10
|
## 0.3.0 — 2026-06-14
|
|
6
11
|
|
|
7
|
-
> A large feature release. Stronger completion integrity (evidence gate, optional auditor, visible audit messages), durable lifecycle ledger with state reconstruction, multiple goals per session with focus and ordered sisyphus sequences, richer goal schema, more auto-continue guardrails, project-local state with migration,
|
|
12
|
+
> A large feature release. Stronger completion integrity (evidence gate, optional auditor, visible audit messages), durable lifecycle ledger with state reconstruction, multiple goals per session with focus and ordered sisyphus sequences, richer goal schema, more auto-continue guardrails, project-local state with migration, a deterministic compaction summary, and npm Trusted Publishing CI. All changes are additive and backward-compatible; older state files load unchanged.
|
|
8
13
|
|
|
9
14
|
### Completion integrity & audit
|
|
10
15
|
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
### Storage, tools & packaging
|
|
37
42
|
|
|
38
43
|
- **Default goal state to a project-local path, with an env override and migration fallbacks.** State resolves as `stateFilePath` option → `OPENCODE_GOAL_STATE_PATH` env var → project-local `<cwd>/.opencode/goals/state.json` (previously `~/.opencode-goal-plugin/state.json`). When the default path is empty, the plugin migrates forward on first load from the legacy home path and the XDG path, then writes project-local. Explicit option/env paths are literal with no fallback; a present-but-corrupt primary is preserved. New `resolveStateFilePath` / `xdgStateFilePath` / `legacyStateFilePaths` helpers. Home-based fallback paths resolve from an injectable `env.HOME` (falling back to `os.homedir()`), making path resolution deterministic across platforms — `os.homedir()` ignores `$HOME` on macOS. Implements megalist items 6.1 and 6.2.
|
|
39
|
-
- **
|
|
44
|
+
- _**Correction (2026-06-21):** an earlier version of this entry claimed agent-facing goal tools shipped in 0.3.0. They did not — the work was on an unmerged branch (`wr/agent-tools`) and was never included in the 0.3.0 release. The feature now actually ships; see the **Unreleased** section above. Megalist items 7.1 and 7.2._
|
|
40
45
|
- **Add a `Publish` GitHub Actions workflow (`.github/workflows/publish.yml`) for npm Trusted Publishing (OIDC).** On a push to `main` it runs the full check matrix on Node 18/20/22, then publishes via OIDC with no stored `NPM_TOKEN`, using a publish-on-version-change model (only publishes when `package.json`'s version is new). The publish job requires `id-token: write` and is gated behind a `release` environment. First run still requires a human to publish an initial version and configure the npm Trusted Publisher. Implements megalist item 9.1.
|
|
41
46
|
|
|
42
47
|
## 0.2.0 — 2026-06-14
|
package/README.md
CHANGED
|
@@ -256,11 +256,28 @@ Additional plugin-level options:
|
|
|
256
256
|
- `warnTurnsRemaining` / `warnDurationMsRemaining` / `warnTokensRemaining` — thresholds at which the auto-continue prompt appends a "limits are near" warning (default `3` turns, `60000` ms, `25000` context tokens). Lower them to warn closer to the limit, or raise them to warn earlier.
|
|
257
257
|
- `commandName` — the slash command the plugin owns (default `goal`). Set it to e.g. `objective` to drive the workflow with `/objective` instead of `/goal`; a leading slash is tolerated. Remember to register the matching command name in your OpenCode `command` config. User-facing hints (`/goal status`, `/goal resume`, …) follow the configured name.
|
|
258
258
|
- `registerCommand` — whether the plugin installs its `command.execute.before` hook at all (default `true`). Set it to `false` if you only want the auto-continue/persistence behavior driven programmatically and don't want the plugin to own a slash command.
|
|
259
|
+
- `registerTools` — whether the plugin registers the agent-facing goal tools (default `true`). Requires the optional `@opencode-ai/plugin` peer dependency to be present; when it is absent, tool registration is skipped and the command/event hooks still work. Set to `false` to omit the programmatic tool surface entirely. See [Agent tools](#agent-tools-optional).
|
|
259
260
|
- `persistState` — whether to persist active goals and recent goal results to disk.
|
|
260
261
|
- `stateFilePath` — where the persisted state JSON is written. Overrides the default project-local path and the `OPENCODE_GOAL_STATE_PATH` env var. Useful if you want a fixed or ephemeral location. When unset, the default is `<cwd>/.opencode/goals/state.json` (see the persistence section above), and `OPENCODE_GOAL_STATE_PATH` can override it without editing config.
|
|
261
262
|
- `resultRetentionMs` — how long a completed goal summary remains available through `/goal status` after the goal leaves active memory.
|
|
262
263
|
- `maxStoredResults` — maximum number of completed-goal summaries retained in process memory before the oldest ones are evicted.
|
|
263
264
|
|
|
265
|
+
## Agent tools (optional)
|
|
266
|
+
|
|
267
|
+
In addition to the `/goal` command, the plugin can expose the same workflow to the model as callable tools, so the agent can inspect and manage the goal itself. This requires the optional `@opencode-ai/plugin` peer dependency (it provides the `tool` helper and schema). When that package isn't installed the tools are simply not registered and everything else keeps working. Disable them explicitly with `registerTools: false`.
|
|
268
|
+
|
|
269
|
+
Registered tools:
|
|
270
|
+
|
|
271
|
+
- `get_goal` — current goal status (objective, budget usage, latest checkpoint).
|
|
272
|
+
- `get_goal_history` — lifecycle history and latest checkpoint.
|
|
273
|
+
- `set_goal` — set/replace the session goal. Its description constrains the agent to call it **only when the user explicitly asks** to set a goal. Accepts `objective` plus optional `maxTurns`, `maxTokens`, `maxDurationMs`, `successCriteria`, `constraints`, and `mode`.
|
|
274
|
+
- `update_goal` — revise the `objective` and/or set `status` to `complete` / `blocked` / `paused` / `resumed` (with `evidence` for complete or `blocker` for blocked).
|
|
275
|
+
- `clear_goal` — clear the current goal and discard its saved status.
|
|
276
|
+
|
|
277
|
+
These operate on the same per-session multi-goal state as the command path: a tool-set goal persists, shows up in `/goal list`, and is driven by the idle auto-continue; completing a goal in an ordered (sisyphus) sequence auto-promotes the next.
|
|
278
|
+
|
|
279
|
+
> Integration note: the tool execute-context shape (`ctx.sessionID`) and the `tool.schema` surface follow the OpenCode plugin docs. The tool **logic** is unit-tested independently, but the live registration should be confirmed against a real OpenCode run (see the smoke-test checklist).
|
|
280
|
+
|
|
264
281
|
## Audit messages
|
|
265
282
|
|
|
266
283
|
When the assistant marks a goal complete or blocked, the plugin announces the audit instead of doing it silently: an audit-start message ("Auditing goal completion…") and an audit-result message ("completion accepted — goal archived" / "paused as blocked — …"). By default these are delivered through OpenCode's structured log (`client.app.log`, visible to the user). Provide an `auditMessenger(sessionID, text)` plugin option to route them elsewhere (for example into the live conversation once a suitable message API is available), or set `auditMessages: false` to disable them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Session-scoped /goal workflow for OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/goal-plugin.js",
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
"agent",
|
|
33
33
|
"goal"
|
|
34
34
|
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@opencode-ai/plugin": ">=1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@opencode-ai/plugin": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
35
43
|
"license": "MIT",
|
|
36
44
|
"engines": {
|
|
37
45
|
"node": ">=18"
|
package/src/goal-plugin.js
CHANGED
|
@@ -1590,6 +1590,240 @@ function buildGoalState(sessionID, condition, options, meta = {}, lastStatus = "
|
|
|
1590
1590
|
}
|
|
1591
1591
|
}
|
|
1592
1592
|
|
|
1593
|
+
const AGENT_UPDATE_STATUSES = new Set(["complete", "blocked", "paused", "resumed"])
|
|
1594
|
+
|
|
1595
|
+
// Programmatic equivalents of the /goal command, exposed to the agent as tools
|
|
1596
|
+
// (megalist items 7.1 / 7.2). Each handler operates on a session id and mutates
|
|
1597
|
+
// the same in-memory state the command path uses, persisting through the
|
|
1598
|
+
// provided `persist` callback, and returns a human-readable string for the tool
|
|
1599
|
+
// result. Goal creation/replacement routes through the multi-goal registry
|
|
1600
|
+
// (buildGoalState + registerSessionGoal + focusGoal) exactly like the command
|
|
1601
|
+
// path, so tool-created goals persist and are driven by the idle handler.
|
|
1602
|
+
function buildAgentToolHandlers({ defaultGoalOptions, persist }) {
|
|
1603
|
+
async function getGoal(sessionID) {
|
|
1604
|
+
const goal = goalStates.get(sessionID)
|
|
1605
|
+
if (goal) return formatStatus(goal)
|
|
1606
|
+
const lastResult = lastGoalResults.get(sessionID)
|
|
1607
|
+
if (lastResult) return formatGoalResult(lastResult)
|
|
1608
|
+
return "No active goal."
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async function getGoalHistory(sessionID) {
|
|
1612
|
+
const goal = goalStates.get(sessionID)
|
|
1613
|
+
if (goal) {
|
|
1614
|
+
return [
|
|
1615
|
+
`Goal history for: ${goal.condition}`,
|
|
1616
|
+
"",
|
|
1617
|
+
`Latest checkpoint: ${goal.lastCheckpoint?.summary || "none yet"}`,
|
|
1618
|
+
"",
|
|
1619
|
+
formatHistory(goal.history),
|
|
1620
|
+
].join("\n")
|
|
1621
|
+
}
|
|
1622
|
+
const lastResult = lastGoalResults.get(sessionID)
|
|
1623
|
+
if (lastResult) {
|
|
1624
|
+
return [
|
|
1625
|
+
`Last goal history for: ${lastResult.condition}`,
|
|
1626
|
+
"",
|
|
1627
|
+
`Latest checkpoint: ${lastResult.lastCheckpoint?.summary || "none recorded"}`,
|
|
1628
|
+
"",
|
|
1629
|
+
formatHistory(lastResult.history),
|
|
1630
|
+
].join("\n")
|
|
1631
|
+
}
|
|
1632
|
+
return "No goal history recorded yet."
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async function setGoal(sessionID, args = {}) {
|
|
1636
|
+
const objective = typeof args.objective === "string" ? args.objective.trim() : ""
|
|
1637
|
+
if (!objective) return "No objective provided. Pass a non-empty `objective`."
|
|
1638
|
+
|
|
1639
|
+
const options = normalizeOptions({
|
|
1640
|
+
...defaultGoalOptions,
|
|
1641
|
+
...(Number.isFinite(args.maxTurns) ? { maxTurns: args.maxTurns } : {}),
|
|
1642
|
+
...(Number.isFinite(args.maxTokens) ? { maxTokens: args.maxTokens } : {}),
|
|
1643
|
+
...(Number.isFinite(args.maxDurationMs) ? { maxDurationMs: args.maxDurationMs } : {}),
|
|
1644
|
+
})
|
|
1645
|
+
const meta = {
|
|
1646
|
+
successCriteria: typeof args.successCriteria === "string" ? args.successCriteria : "",
|
|
1647
|
+
constraints: typeof args.constraints === "string" ? args.constraints : "",
|
|
1648
|
+
mode: typeof args.mode === "string" ? args.mode : "normal",
|
|
1649
|
+
}
|
|
1650
|
+
const goal = buildGoalState(sessionID, objective, options, meta)
|
|
1651
|
+
pushHistory(
|
|
1652
|
+
goal,
|
|
1653
|
+
"set",
|
|
1654
|
+
`Goal created via agent tool with limits: ${options.maxTurns} auto-continues, ${Math.round(options.maxDurationMs / 1000)}s, ${options.maxTokens.toLocaleString()} context tokens.`,
|
|
1655
|
+
)
|
|
1656
|
+
// Mirror the `/goal <condition>` replace path: discard the focused goal and
|
|
1657
|
+
// its saved result, drop any ordered sequence, then register + focus the new
|
|
1658
|
+
// goal so it persists and the idle handler drives it.
|
|
1659
|
+
sessionOrdered.delete(sessionID)
|
|
1660
|
+
cleanupGoal(sessionID)
|
|
1661
|
+
lastGoalResults.delete(sessionID)
|
|
1662
|
+
registerSessionGoal(goal)
|
|
1663
|
+
focusGoal(sessionID, goal)
|
|
1664
|
+
await persist()
|
|
1665
|
+
return `New active goal: ${goal.condition}`
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async function updateGoal(sessionID, args = {}) {
|
|
1669
|
+
const goal = goalStates.get(sessionID)
|
|
1670
|
+
if (!goal) return "No active goal to update. Use set_goal first."
|
|
1671
|
+
|
|
1672
|
+
const messages = []
|
|
1673
|
+
|
|
1674
|
+
if (typeof args.objective === "string" && args.objective.trim()) {
|
|
1675
|
+
goal.condition = args.objective.trim()
|
|
1676
|
+
goal.stopped = false
|
|
1677
|
+
goal.stopReason = ""
|
|
1678
|
+
goal.blockedReason = ""
|
|
1679
|
+
goal.budgetWrapupSent = false
|
|
1680
|
+
goal.noProgressTurns = 0
|
|
1681
|
+
goal.lastStatus = "Goal objective updated."
|
|
1682
|
+
pushHistory(goal, "edited", `Objective updated to: ${summarizeText(goal.condition, 400)}`)
|
|
1683
|
+
messages.push(`Objective updated: ${goal.condition}`)
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
if (args.status !== undefined) {
|
|
1687
|
+
const status = String(args.status).trim().toLowerCase()
|
|
1688
|
+
if (!AGENT_UPDATE_STATUSES.has(status)) {
|
|
1689
|
+
return `Invalid status: ${args.status} (expected complete, blocked, paused, or resumed).`
|
|
1690
|
+
}
|
|
1691
|
+
if (status === "complete") {
|
|
1692
|
+
const evidence = typeof args.evidence === "string" ? args.evidence.trim() : ""
|
|
1693
|
+
goal.lastStatus = "Goal completed."
|
|
1694
|
+
pushHistory(
|
|
1695
|
+
goal,
|
|
1696
|
+
"completed",
|
|
1697
|
+
evidence ? `Marked complete via tool: ${summarizeText(evidence, 400)}` : "Marked complete via agent tool.",
|
|
1698
|
+
)
|
|
1699
|
+
rememberGoalResult(sessionID, goal, "achieved", "", evidence)
|
|
1700
|
+
cleanupGoal(sessionID)
|
|
1701
|
+
// Advance an ordered (sisyphus) sequence just like the marker path does.
|
|
1702
|
+
if (sessionOrdered.has(sessionID)) promoteNextOrderedGoal(sessionID)
|
|
1703
|
+
await persist()
|
|
1704
|
+
return "Goal marked complete and archived."
|
|
1705
|
+
}
|
|
1706
|
+
if (status === "blocked") {
|
|
1707
|
+
goal.blockedReason = typeof args.blocker === "string" ? args.blocker.trim() : ""
|
|
1708
|
+
goal.stopped = true
|
|
1709
|
+
goal.stopReason = "blocked"
|
|
1710
|
+
goal.lastStatus = "Assistant reported blocked."
|
|
1711
|
+
pushHistory(goal, "blocked", goal.blockedReason || "Marked blocked via agent tool.")
|
|
1712
|
+
messages.push("Goal marked blocked.")
|
|
1713
|
+
} else if (status === "paused") {
|
|
1714
|
+
goal.stopped = true
|
|
1715
|
+
goal.stopReason = "paused"
|
|
1716
|
+
goal.lastStatus = "Goal paused."
|
|
1717
|
+
pushHistory(goal, "paused", "Paused via agent tool.")
|
|
1718
|
+
messages.push("Goal paused.")
|
|
1719
|
+
} else if (status === "resumed") {
|
|
1720
|
+
const previousGoalId = goal.goalId
|
|
1721
|
+
resetGoalBudget(goal)
|
|
1722
|
+
// resetGoalBudget rotates goalId; re-key the registry so the goal stays
|
|
1723
|
+
// findable by its new id (the focused pointer holds the same object).
|
|
1724
|
+
if (goal.goalId !== previousGoalId) {
|
|
1725
|
+
removeSessionGoal(sessionID, previousGoalId)
|
|
1726
|
+
registerSessionGoal(goal)
|
|
1727
|
+
focusGoal(sessionID, goal)
|
|
1728
|
+
}
|
|
1729
|
+
goal.stopped = false
|
|
1730
|
+
goal.stopReason = ""
|
|
1731
|
+
goal.blockedReason = ""
|
|
1732
|
+
goal.lastStatus = "Goal resumed with a fresh local budget."
|
|
1733
|
+
pushHistory(goal, "resumed", "Resumed via agent tool with a fresh local budget window.")
|
|
1734
|
+
messages.push("Goal resumed with fresh limits.")
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (!messages.length) {
|
|
1739
|
+
return "Nothing to update. Provide `objective` and/or `status`."
|
|
1740
|
+
}
|
|
1741
|
+
await persist()
|
|
1742
|
+
return messages.join(" ")
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
async function clearGoal(sessionID) {
|
|
1746
|
+
// Mirror `/goal clear`: drop the ordered flag and the focused goal + result.
|
|
1747
|
+
sessionOrdered.delete(sessionID)
|
|
1748
|
+
cleanupGoal(sessionID)
|
|
1749
|
+
lastGoalResults.delete(sessionID)
|
|
1750
|
+
await persist()
|
|
1751
|
+
return "Goal cleared."
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
return { getGoal, getGoalHistory, setGoal, updateGoal, clearGoal }
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function agentToolSessionID(ctx) {
|
|
1758
|
+
return ctx?.sessionID || ctx?.session_id || ctx?.session?.id || ctx?.sessionId || null
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Cache the optional @opencode-ai/plugin import once. It provides the `tool`
|
|
1762
|
+
// helper and `tool.schema` (zod). It is an optional peer dependency: when it is
|
|
1763
|
+
// not installed (e.g. unit tests, older OpenCode), tool registration is simply
|
|
1764
|
+
// skipped and the command/event hooks still work.
|
|
1765
|
+
let opencodePluginModulePromise
|
|
1766
|
+
async function loadOpencodePluginModule() {
|
|
1767
|
+
if (opencodePluginModulePromise === undefined) {
|
|
1768
|
+
opencodePluginModulePromise = import("@opencode-ai/plugin")
|
|
1769
|
+
.then((mod) => mod)
|
|
1770
|
+
.catch(() => null)
|
|
1771
|
+
}
|
|
1772
|
+
return opencodePluginModulePromise
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function buildAgentTools(toolHelper, handlers) {
|
|
1776
|
+
const schema = toolHelper.schema
|
|
1777
|
+
const run = (handler) => async (args, ctx) => {
|
|
1778
|
+
const sessionID = agentToolSessionID(ctx)
|
|
1779
|
+
if (!sessionID) return "No session id available for the goal tool."
|
|
1780
|
+
return handler(sessionID, args || {})
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
get_goal: toolHelper({
|
|
1784
|
+
description:
|
|
1785
|
+
"Get the status of the current goal for this session (objective, budget usage, last checkpoint).",
|
|
1786
|
+
args: {},
|
|
1787
|
+
execute: run((sessionID) => handlers.getGoal(sessionID)),
|
|
1788
|
+
}),
|
|
1789
|
+
get_goal_history: toolHelper({
|
|
1790
|
+
description: "Get the lifecycle history and latest checkpoint of the current goal for this session.",
|
|
1791
|
+
args: {},
|
|
1792
|
+
execute: run((sessionID) => handlers.getGoalHistory(sessionID)),
|
|
1793
|
+
}),
|
|
1794
|
+
set_goal: toolHelper({
|
|
1795
|
+
description:
|
|
1796
|
+
"Set a new session goal for autonomous auto-continue. ONLY call this when the user explicitly asks you to set, define, or start working toward a goal — never decide to set a goal on your own. Replaces any existing goal.",
|
|
1797
|
+
args: {
|
|
1798
|
+
objective: schema.string(),
|
|
1799
|
+
maxTurns: schema.number().optional(),
|
|
1800
|
+
maxTokens: schema.number().optional(),
|
|
1801
|
+
maxDurationMs: schema.number().optional(),
|
|
1802
|
+
successCriteria: schema.string().optional(),
|
|
1803
|
+
constraints: schema.string().optional(),
|
|
1804
|
+
mode: schema.string().optional(),
|
|
1805
|
+
},
|
|
1806
|
+
execute: run((sessionID, args) => handlers.setGoal(sessionID, args)),
|
|
1807
|
+
}),
|
|
1808
|
+
update_goal: toolHelper({
|
|
1809
|
+
description:
|
|
1810
|
+
"Update the current goal: revise its `objective`, and/or set its `status` to complete, blocked, paused, or resumed. Mark complete only after verifying the objective is truly done; include `evidence` (for complete) or `blocker` (for blocked).",
|
|
1811
|
+
args: {
|
|
1812
|
+
objective: schema.string().optional(),
|
|
1813
|
+
status: schema.string().optional(),
|
|
1814
|
+
evidence: schema.string().optional(),
|
|
1815
|
+
blocker: schema.string().optional(),
|
|
1816
|
+
},
|
|
1817
|
+
execute: run((sessionID, args) => handlers.updateGoal(sessionID, args)),
|
|
1818
|
+
}),
|
|
1819
|
+
clear_goal: toolHelper({
|
|
1820
|
+
description: "Clear the current goal for this session and discard its saved status.",
|
|
1821
|
+
args: {},
|
|
1822
|
+
execute: run((sessionID) => handlers.clearGoal(sessionID)),
|
|
1823
|
+
}),
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1593
1827
|
function formatGoalList(sessionID) {
|
|
1594
1828
|
const goals = listSessionGoals(sessionID)
|
|
1595
1829
|
const focusedId = goalStates.get(sessionID)?.goalId || null
|
|
@@ -1787,6 +2021,8 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1787
2021
|
await persist()
|
|
1788
2022
|
}
|
|
1789
2023
|
|
|
2024
|
+
const agentToolHandlers = buildAgentToolHandlers({ defaultGoalOptions, persist })
|
|
2025
|
+
|
|
1790
2026
|
const hooks = {
|
|
1791
2027
|
"command.execute.before": async (input, output) => {
|
|
1792
2028
|
if (input.command !== commandName) return
|
|
@@ -1872,7 +2108,16 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1872
2108
|
return
|
|
1873
2109
|
}
|
|
1874
2110
|
|
|
2111
|
+
const previousGoalId = goal.goalId
|
|
1875
2112
|
resetGoalBudget(goal)
|
|
2113
|
+
// resetGoalBudget rotates goalId; re-key the multi-goal registry to the
|
|
2114
|
+
// new id so a later clear/replace removes the goal instead of leaking a
|
|
2115
|
+
// stale entry (the focused pointer holds the same object reference).
|
|
2116
|
+
if (goal.goalId !== previousGoalId) {
|
|
2117
|
+
removeSessionGoal(sessionID, previousGoalId)
|
|
2118
|
+
registerSessionGoal(goal)
|
|
2119
|
+
focusGoal(sessionID, goal)
|
|
2120
|
+
}
|
|
1876
2121
|
goal.stopped = false
|
|
1877
2122
|
goal.stopReason = ""
|
|
1878
2123
|
goal.blockedReason = ""
|
|
@@ -2583,6 +2828,22 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
2583
2828
|
delete hooks["command.execute.before"]
|
|
2584
2829
|
}
|
|
2585
2830
|
|
|
2831
|
+
// Register agent-facing tools (megalist 7.1 / 7.2) when @opencode-ai/plugin is
|
|
2832
|
+
// available (it provides the `tool` helper and zod-style schema). Disabled via
|
|
2833
|
+
// `registerTools: false`. When the helper is absent the command/event hooks
|
|
2834
|
+
// still work; only the programmatic tool surface is omitted, preserving the
|
|
2835
|
+
// zero-runtime-dependency posture.
|
|
2836
|
+
if (pluginOptions.registerTools !== false) {
|
|
2837
|
+
const toolModule = await loadOpencodePluginModule()
|
|
2838
|
+
if (toolModule?.tool?.schema) {
|
|
2839
|
+
try {
|
|
2840
|
+
hooks.tool = buildAgentTools(toolModule.tool, agentToolHandlers)
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
await logPluginError(client, "Failed to register goal agent tools", error)
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2586
2847
|
return hooks
|
|
2587
2848
|
}
|
|
2588
2849
|
|
|
@@ -2593,6 +2854,9 @@ export default {
|
|
|
2593
2854
|
|
|
2594
2855
|
export const testInternals = {
|
|
2595
2856
|
activeGoal,
|
|
2857
|
+
agentToolSessionID,
|
|
2858
|
+
buildAgentToolHandlers,
|
|
2859
|
+
buildAgentTools,
|
|
2596
2860
|
listSessionGoals,
|
|
2597
2861
|
formatGoalList,
|
|
2598
2862
|
appendLedgerLine,
|