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 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, agent-facing tools, a deterministic compaction summary, and npm Trusted Publishing CI. All changes are additive and backward-compatible; older state files load unchanged.
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
- - **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 to explicit user requests; `update_goal` supports objective edits and complete/blocked/paused/resumed transitions. Registration is skipped gracefully when the package is absent or with `registerTools: false`. New `buildAgentToolHandlers`. Implements megalist items 7.1 and 7.2.
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.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"
@@ -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,