@cleocode/goal 0.0.0-stage → 0.9.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +319 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/cli/ocg.ts +149 -0
  5. package/dist/cli.js +8708 -0
  6. package/dist/tui.d.ts +2 -0
  7. package/dist/tui.js +1920 -0
  8. package/package.json +79 -4
  9. package/rpc.ts +2 -0
  10. package/server.ts +3 -0
  11. package/skills/write-goal/SKILL.md +211 -0
  12. package/skills/write-goal/assets/goal.template.yaml +38 -0
  13. package/skills/write-goal/references/examples.md +283 -0
  14. package/skills/write-goal/references/goal-schema.md +53 -0
  15. package/skills/write-goal/references/interview.md +36 -0
  16. package/src/cli/install.ts +125 -0
  17. package/src/cli/status.ts +116 -0
  18. package/src/contract/parse.ts +454 -0
  19. package/src/contract/render.ts +134 -0
  20. package/src/contract/types.ts +158 -0
  21. package/src/engine/amendment.ts +51 -0
  22. package/src/engine/amendments.ts +72 -0
  23. package/src/engine/cleo-bridge.ts +339 -0
  24. package/src/engine/cleo-link.ts +150 -0
  25. package/src/engine/continue-work.ts +131 -0
  26. package/src/engine/decisions.ts +44 -0
  27. package/src/engine/dogfood-record.ts +278 -0
  28. package/src/engine/handoff.ts +102 -0
  29. package/src/engine/interrupt-policy.ts +45 -0
  30. package/src/engine/inventory.ts +70 -0
  31. package/src/engine/launches.ts +82 -0
  32. package/src/engine/loop-stop.ts +50 -0
  33. package/src/engine/loop.ts +109 -0
  34. package/src/engine/next-work.ts +82 -0
  35. package/src/engine/owner-intent.ts +71 -0
  36. package/src/engine/owner-reviews.ts +178 -0
  37. package/src/engine/peer-announce.ts +67 -0
  38. package/src/engine/peer-transport.ts +78 -0
  39. package/src/engine/policy.ts +42 -0
  40. package/src/engine/registry.ts +119 -0
  41. package/src/engine/similarity.ts +65 -0
  42. package/src/engine/stale-decisions.ts +68 -0
  43. package/src/engine/state.ts +185 -0
  44. package/src/engine/store.ts +263 -0
  45. package/src/engine/tick-record.ts +129 -0
  46. package/src/engine/tick.ts +207 -0
  47. package/src/rpc.ts +182 -0
  48. package/src/server/app.ts +4320 -0
  49. package/src/server/help.ts +50 -0
  50. package/src/server/index.ts +23 -0
  51. package/src/server/intent.ts +22 -0
  52. package/src/server/messages.ts +228 -0
  53. package/src/summary/audit.ts +76 -0
  54. package/src/summary/build.ts +314 -0
  55. package/src/tui/dashboard.ts +322 -0
  56. package/src/tui/decision-form.ts +12 -0
  57. package/src/tui/format.ts +127 -0
  58. package/src/tui/index.tsx +718 -0
  59. package/src/tui/manager.ts +94 -0
  60. package/src/tui/navigation.ts +11 -0
  61. package/src/tui/pointer-select.ts +20 -0
  62. package/src/tui/tracer.ts +51 -0
  63. package/src/util/git.ts +102 -0
  64. package/src/util/ids.ts +20 -0
  65. package/src/util/shell.ts +42 -0
  66. package/src/verify/artifact.ts +95 -0
  67. package/src/verify/pipeline.ts +200 -0
  68. package/tui.tsx +2 -0
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@cleocode/goal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.9.0",
4
+ "description": "CLEO Goal: persistent, host-verified goal mode for OpenCode 2 (and any harness through cleo-goal tick), with a structured goal contract and a live TUI.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "provenance": true
10
+ },
11
+ "engines": {
12
+ "node": ">=22.14.0",
13
+ "opencode": ">=2.0.22"
14
+ },
15
+ "scripts": {
16
+ "build": "bun run build:tui && bun run build:cli",
17
+ "build:tui": "bun scripts/build-tui.mjs",
18
+ "check:tui": "bun scripts/build-tui.mjs --check",
19
+ "build:cli": "bun scripts/build-cli.mjs",
20
+ "check:cli": "bun scripts/build-cli.mjs --check",
21
+ "prepublishOnly": "bun run check:tui && bun run check:cli",
22
+ "test": "bun test --timeout 120000",
23
+ "test:host": "bun test test/host --timeout 120000",
24
+ "typecheck": "bun run check:tui && bun run check:cli && tsc --noEmit"
25
+ },
26
+ "devDependencies": {
27
+ "@opencode/client": "2.0.22",
28
+ "@opencode/plugin": "2.0.22",
29
+ "@opencode/theme": "2.0.22",
30
+ "@opentui/core": "0.5.14",
31
+ "@types/bun": "^1.3.0",
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "bin": {
35
+ "cleo-goal": "./dist/cli.js",
36
+ "ocg": "./dist/cli.js"
37
+ },
38
+ "exports": {
39
+ ".": "./server.ts",
40
+ "./server": "./server.ts",
41
+ "./tui": "./dist/tui.js",
42
+ "./rpc": "./rpc.ts"
43
+ },
44
+ "files": [
45
+ "server.ts",
46
+ "tui.tsx",
47
+ "rpc.ts",
48
+ "cli",
49
+ "dist",
50
+ "src",
51
+ "skills",
52
+ "!skills/*/evals",
53
+ "README.md",
54
+ "LICENSE",
55
+ "CHANGELOG.md"
56
+ ],
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/kryptobaseddev/opencode-goal.git"
60
+ },
61
+ "homepage": "https://github.com/kryptobaseddev/opencode-goal#readme",
62
+ "keywords": [
63
+ "opencode",
64
+ "opencode-plugin",
65
+ "goal",
66
+ "goal-mode",
67
+ "autonomous",
68
+ "agent",
69
+ "cleo",
70
+ "cleo-goal",
71
+ "harness"
72
+ ],
73
+ "dependencies": {
74
+ "@opentui/solid": "0.5.14",
75
+ "solid-js": "1.9.15",
76
+ "yaml": "2.8.1"
77
+ },
78
+ "bugs": {
79
+ "url": "https://github.com/kryptobaseddev/opencode-goal/issues"
80
+ }
81
+ }
package/rpc.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Root RPC contract entry (see server.ts).
2
+ export * from "./src/rpc"
package/server.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Root entry: OpenCode resolves <plugin dir>/server for local plugins and the
2
+ // "./server" export for installed packages; both land here.
3
+ export { default } from "./src/server/index"
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: write-goal
3
+ description: 'Interview the owner and write a goal contract (a goal.yaml under .opencode/goals) that an autonomous goal loop can run until the host proves it done — a "grill me" session through the ask tool that turns a rough intention into an end state, binary criteria with real checks, invariants, protected oracles, non-goals, an optional budget and stop rules, then launches it. Use when the user wants to set up, write, draft, refine or start a goal, run something "until it''s done", use goal mode or /goal, make an agent loop on a task unattended, or says "grill me on this", "turn this into a goal", "goal new", "write-goal". In OpenCode, a bare "/goal new" phrasing is command-routed: the plugin attaches this skill explicitly, so no description-based triggering is needed for that phrasing — trigger evals should measure prose phrasings only. Do NOT use for ordinary one-off requests the user did not ask to turn into a goal.'
4
+ license: MIT
5
+ metadata:
6
+ version: "0.2.0"
7
+ last_updated: "2026-10-10 20:10:00"
8
+ author: kryptobaseddev
9
+ tags: "opencode, goal, goal-mode, planning, interview, autonomous-agents"
10
+ ---
11
+
12
+ # write-goal
13
+
14
+ Turn a rough intention into a **goal contract**: a structured `goal.yaml` that says what must become true, how the host proves it, where the work may reach, and when to stop — then launch it. A goal is a completion contract, not a task description. **Proof, not effort.**
15
+
16
+ ## Rules (read first)
17
+
18
+ 1. **Ask, don't narrate.** Every choice you put to the owner goes through the ask tool — `question` in OpenCode, `AskUserQuestion` in Claude Code — with 2-4 concrete options, the recommended option **first** and labelled `(Recommended)`, each description saying what happens and the trade-off. Never list options in prose and ask for a reply. Plain text is only for open-ended input ("what would prove this is done?"), or when no ask tool exists.
19
+ 2. **Facts are your job; decisions are the owner's.** Find commands, files and current results yourself (read the repo, run the checks, use explore subagents). Ask only what the repository cannot tell you. Subagents never ask the owner.
20
+ 3. **Only when asked.** Never wrap an ordinary request in a goal. If a task would suit goal mode, say so once and let the owner choose.
21
+ 4. **The owner's words stay authoritative.** Record them verbatim in `intent.verbatim`. Write in the owner's language.
22
+ 5. **Show the exact contract before launch** and get approval. If the owner wants a looser goal than you recommend, say why once, then write theirs.
23
+ 6. **Budgets are opt-in.** Never invent a turn cap; the goal stops on its proof or an honest blocker. Offer a budget only for open-ended work, framed in tokens or cost.
24
+
25
+ ## Workflow
26
+
27
+ ### 0 · Seed
28
+ Capture the owner's words exactly. Pick a slug (kebab-case, ≤64 chars). Everything goes in `.opencode/goals/<slug>/`.
29
+
30
+ ### 1 · Recon (no questions yet)
31
+ Inspect the project: package scripts, Makefile, CI config, test/lint/typecheck/bench commands, the files the intention touches, `git status`. Run the likely checks once to capture a **baseline** (a bug-fix goal should start red). Write the facts to `.opencode/goals/<slug>/context.md` — facts only, no decisions. **If the goal ships a new version of the tooling that will verify it** (a plugin, a harness, the goal engine itself), plan the reinstall of that new version into the release step — the completing verifier round runs the *installed* engine, not the one just built.
32
+
33
+ ### 2 · Classify
34
+ bugfix · feature · refactor · migration · perf · research · ops · docs. The class suggests criteria:
35
+ - **bugfix**: the failing case captured before, the same case passing after, plus "the rest of the suite SHALL CONTINUE TO pass".
36
+ - **perf**: metric, threshold, method and run count ("p95 < 250ms across 3 runs").
37
+ - **refactor/migration**: behaviour unchanged (invariants), an `absent` check that the old pattern is gone, a `diff` check on what must not move.
38
+ - **feature**: observable behaviour per scenario (EARS: "WHEN … THE system SHALL …").
39
+ - **research/docs**: the decision or artifact it must produce, judged by a `verifier` question or `human` sign-off.
40
+
41
+ ### 3 · Grill in frontier rounds
42
+ Map the decisions as a tree. Each round, ask the **frontier** — every decision whose prerequisites are already settled — in one ask call (≤4 questions), each with your recommended option derived from recon. Fold each answer into the draft before the next round. Typical: 2-3 rounds, ≤10 questions. Stop when the frontier is empty. Branches, in order:
43
+ 1. **Outcome** — the one end state that becomes true.
44
+ 2. **Done when** — which checks prove it (prefer commands that already exist).
45
+ 3. **Evidence standard** — host checks only, or also the independent verifier / owner sign-off.
46
+ 4. **Invariants and oracles** — what must keep passing; which tests/benchmarks the worker may never edit (`protect`).
47
+ 5. **Scope and non-goals** — at least one non-goal.
48
+ 6. **Risk and escalation** — irreversible or destructive actions; when to stop and ask.
49
+ 7. **Budget** (opt-in) and **autonomy** (questions mid-run deferred; owner messages steer or pause).
50
+
51
+ Log every question and answer to `decisions.jsonl` (one JSON object per line: `{"t","q","options","a","by":"owner"|"assumed"}`). Anything you decide without asking becomes an `assumptions` entry with a rationale and `reversible` — never a silent default.
52
+
53
+ ### 4 · Draft `goal.yaml`
54
+ Start from `assets/goal.template.yaml` (or the shape below). Then validate: call `goal_validate({slug})` when the opencode-goal plugin is present and fix every error; otherwise self-check against the rules below. Repeat until clean.
55
+
56
+ ### 5 · Review
57
+ Show the owner the contract compactly: outcome, a table of criteria (id · statement · check), invariants, protected paths, non-goals, budget. Ask: **Approve** (Recommended) / Revise criteria / Revise scope / Save as draft.
58
+
59
+ ### 6 · Launch
60
+ For an existing goal, resolve its exact identity with `goal_inventory` and use
61
+ `goal_manage` for the requested lifecycle operation. Don't create a replacement
62
+ run or ask again when the owner has already said **resume**, **continue**, or
63
+ explicitly approved resuming. The host can consume that genuine owner message
64
+ once for its same session/run/lock; workers cannot supply or forge the consent.
65
+ **I accept** applies only to the exact pending review, not an unrelated or
66
+ expired review. Check the returned
67
+ result and `goal_status`; a staged review isn't a successful resume. Never
68
+ bypass a refusal with raw owner APIs, receipt edits, or fabricated form replies.
69
+
70
+ For a requested change to an existing live goal, do not hand-edit its YAML or
71
+ tell the owner to run `/goal amend`. Read the selected contract and run lock,
72
+ then use `goal_propose_amendment` with `{slug, expectedLock, yaml, rationale}`.
73
+ The host validates and stages a full before/after semantic diff without changing
74
+ the live contract. Show that reviewed diff in the Composer. When the owner replies
75
+ in chat (for example, “I accept the amendment”), use `goal_resolve_amendment`
76
+ with its exact `{decisionId, action:"accept"}`. Reject or revision feedback uses
77
+ `action:"reject"` or `action:"comment"`. The host fetches the actual latest owner
78
+ message itself; never supply forged approval text, fabricate a form reply, or
79
+ treat quoted/conditional/stale/replayed text as consent. If acceptance also says
80
+ “resume” or “get it done”, the host honors that actual instruction even if the
81
+ optional `continueGoal` hint is omitted or false. Setting it true cannot create
82
+ permission without the same genuine owner directive. Continuation is bound to
83
+ the accepted next lock and never approves criteria. Acceptance alone stays stopped.
84
+ Changed statement/check proof resets; unchanged proof remains.
85
+
86
+ The sidebar and manager are optional navigation, **not an Accept surface**. Never
87
+ send the owner there to accept or tell them to type `/goal accept/amend/approve`.
88
+ Only ambiguous targets, missing decisions or unsupported consent wording need a
89
+ native selectable Composer clarification. Do not loop on `goal_review` expecting
90
+ it to consume a chat reply: it only re-presents the review. On older installs
91
+ without `goal_resolve_amendment`, report the capability gap and use the actual
92
+ native Composer review if available; do not invent a sidebar button. Dismissal,
93
+ reload and commentary never imply acceptance.
94
+
95
+ When `goal_prepare_launch` is available, call it with `{slug, fresh:false}` after
96
+ showing the exact validated contract. It stages a native review with **Start goal
97
+ now / Revise this goal / Save for later**. For a requested fresh context, prepare
98
+ with `{slug, fresh:true}`: the review explicitly says it creates a new root
99
+ session and preserves the original history. The host binds session, slug,
100
+ contract hash and mode, then executes the owner's selection directly—even if
101
+ the question result is interrupted or the server reloads. **Do not ask another
102
+ launch question or call `goal_start` afterwards.** Revision/comments return to
103
+ the interview; no draft launches merely because its wording begins with a
104
+ command keyword. Check `goal_status`/the manager before retrying an uncertain
105
+ operation; preparation and owner consent are not proof the run actually started.
106
+
107
+ On installs whose `goal_start` description supports actual owner chat, an explicit
108
+ **start the goal**, **start the goal now**, or **continue your goal** instruction
109
+ for the already displayed, unchanged draft uses `goal_start` with that exact slug
110
+ and reviewed mode. Don't re-ask the interview or keep replacing expired forms.
111
+ The host reads the actual owner history, matches the last displayed contract/hash/
112
+ session/mode, and consumes a new authorization once; the agent supplies no consent
113
+ text. A fresh chat instruction is not replay of an expired form answer. Generic,
114
+ quoted, conditional, ambiguous or spent replies cannot launch. A pending unrelated
115
+ form remains a blocker; dismissing a form alone isn't approval. On older installs,
116
+ report the capability gap rather than replaying a timed choice or fabricating an
117
+ owner API call. Always check `goal_status`: an expired receipt isn't a running goal.
118
+
119
+ If a live goal's `goal.yaml` on disk differs from its locked contract (an edit
120
+ made outside the engine, so staging and acceptance refuse), call
121
+ `goal_recover_divergence` with `{slug, expectedLock}` from any live session of
122
+ that project. The host shows the owner the exact difference and last-writer
123
+ provenance with **Adopt disk version / Restore locked version / Leave
124
+ unchanged**. An unaccepted proposal whose text matches the disk bytes is
125
+ evidence, never consent. Never restore or edit the YAML by hand.
126
+
127
+ Ask about agent autonomy during the interview when the owner wants fewer
128
+ interruptions: `autonomy.amend` lists change classes (`timeout`, `budget`,
129
+ `plan`, `recover`) the agent applies itself, journalled and undoable with
130
+ `/goal revert <slug>`. The owner sets it; the agent can never widen it. Ask
131
+ whether the verifier must read sibling folders; list them under
132
+ `verification.read` (read-only) instead of letting rounds time out.
133
+
134
+ If `goal_manage` returns `requiredReview` for an unresolved amendment, don't request
135
+ another resume choice: the prerequisite prevents it from executing. The author can
136
+ restore that exact review with `goal_review`; a live session may instead stage a
137
+ validated diff against the same current run/lock for a **new** exact owner review.
138
+ Staging doesn't transfer or cancel another author's consent. Only an applied,
139
+ journalled generation retires conflicting unanswered proposals. If the original
140
+ Core session is actually unavailable, use the supported owner-confirmed `attach`
141
+ operation for the stopped run before resuming; mailbox release alone doesn't prove
142
+ Core-session death. No lock/status/receipt edits or forced cross-session acceptance.
143
+
144
+ Older installed plugins without `goal_prepare_launch` use this compatibility flow:
145
+ Ask with these options: **`Start goal now`** (Recommended) / Start in a fresh session / **Start clean** / Save for later. The first option's label must **start with** `Start goal now` — a `(Recommended)` suffix is expected and fine (T068: the engine matches on the prefix, never byte-exact).
146
+ - OpenCode with the plugin: on "Start goal now", call `goal_start({slug})`. On an approved fresh mode, call `goal_start({slug, fresh:true})`; it opens a **new root session**, prompts the kickoff there and preserves the original. The approval lasts 10 minutes and survives restarts. A refusal names missing, mismatched or expired approval: inspect it and present a fresh exact Composer review if needed. Never send the owner to a sidebar or manual command as a workaround.
147
+ - Without a supported fresh-session launch capability, report that limitation explicitly; do not claim a handoff or ask the owner to operate a missing control.
148
+ - Claude Code (no plugin): offer a `/goal` condition rendered from the contract (≤4,000 chars: the end state, the stated check, the constraints that matter).
149
+ - Codex: offer `/goal <end state>. Done when <check>. Scope: <in/out>. If <blocker>, stop and report.`
150
+
151
+ ## The contract (`goal/v1`) — essentials
152
+
153
+ ```yaml
154
+ schema: goal/v1
155
+ id: fix-login-timeout # = directory slug
156
+ title: Login no longer times out # ≤80 chars, shown in the sidebar
157
+ intent:
158
+ verbatim: "login keeps timing out on slow networks, fix it"
159
+ outcome: Login completes on a 3G-throttled connection without a timeout error
160
+ non_goals: [Redesigning the login page]
161
+ criteria: # binary; each must be able to fail
162
+ - id: C1
163
+ statement: WHEN the login e2e test runs under 3G throttling THE login SHALL succeed
164
+ check: {kind: command, run: "npm run test:e2e -- login-slow", expect: {exit: 0}, live: true}
165
+ - id: C2
166
+ statement: 'No hard-coded 5s timeout remains in src/auth'
167
+ check: {kind: absent, pattern: "timeout:\\s*5000", paths: [src/auth]}
168
+ invariants: # must stay true all run; host-checkable kinds only
169
+ - id: I1
170
+ statement: The unit suite SHALL CONTINUE TO pass
171
+ check: {kind: command, run: "npm test"}
172
+ protect: ["test/e2e/**"] # oracles the worker may not edit
173
+ plan: # optional; becomes the sidebar checklist
174
+ - {id: S1, title: Reproduce under throttling, proves: []}
175
+ - {id: S2, title: Fix the timeout handling, proves: [C1, C2], depends_on: [S1]}
176
+ assumptions:
177
+ - {id: A1, text: 3G profile = 400ms RTT / 400kbps, rationale: Chrome DevTools preset, reversible: true}
178
+ stop:
179
+ escalate_when: ["The fix needs a backend API change"]
180
+ ```
181
+
182
+ Check kinds: `command` (`run`, `expect: {exit | stdout_contains | stdout_regex}`, `timeout` s, `runs`, `live`) · `file` (`path`, `exists`) · `contains` (`path`, `text`|`regex`) · `absent` (`pattern`, `paths`) · `diff` (`paths` unchanged since start) · `verifier` (`ask`: judged by a read-only verifier that must quote files) · `human` (`ask`: owner sign-off).
183
+
184
+ Optional blocks: `why`, `scope {in, out}`, `constraints`, `budget {turns, wall: "3h", tokens: "3M", cost_usd}`, `autonomy {questions: defer|allow, on_user_message: steer|pause, on_interrupt: pause|resume-on-message}`, `verification {mode: host|host+verifier|strict, max_rejections}` (default `host+verifier`, 3).
185
+
186
+ Rules the validator enforces: `schema: goal/v1`; `id` equals the directory; `intent.verbatim`, `outcome`, ≥1 `non_goals` and ≥1 essential criterion are required; ids are `C1…`, `I1…`, `S1…`, `A1…` and unique; every criterion has a check of a known kind with its required fields; invariants are host-checkable; plan references exist and have no cycles; budgets parse. Warnings: an outcome that reads as an activity ("keep improving…"), vague words without a number ("fast", "robust", "clean"), no host-checkable essential criterion. **Quote any value containing `: `** with single quotes. Keep every `command` check on a **single line**: YAML folded scalars (`>-`) fold equally-indented lines but keep literal newlines before more-indented ones, so a loop body written on its own line becomes a separate shell command at verify time. Rehearse each command check exactly as stored (extract it from the parsed YAML, run it through the login shell) before claiming.
187
+
188
+ **Version-release criteria never pin an exact version string.** A `contains`/`stdout_contains` needle like `"version": "1.2.0"` goes stale the moment a patch ships mid-run and turns a proven criterion red for no reason (contract drift — the live v0.2 case). Check the semantic prefix instead: a one-line command such as `node -e "process.exit(require('./package.json').version.startsWith('1.2.')?0:1)"`, or a `contains` regex anchored on the major.minor pair.
189
+
190
+ Full field reference: `references/goal-schema.md`. Question bank and recommendation heuristics: `references/interview.md`. Four complete contracts (bugfix, perf, migration, feature): `references/examples.md`.
191
+
192
+ ## Weak → strong
193
+
194
+ - "Find all bugs" → `outcome: Every test in test/auth passes` · C1 `npm test -- test/auth` exits 0 · protect `test/**` · non-goal: no new features · escalate: a fix needs a schema change.
195
+ - "Make it faster" → C1 `node bench/render.mjs` prints `speedup=3` or more across 3 runs (`stdout_regex`) · I1 suite passes · non-goal: no new dependencies.
196
+ - "Clean up the old API" → C1 `absent` pattern `legacyClient\.` in `src/` · I1 build and tests pass · C2 `verifier`: "Does MIGRATION.md list every replaced call?".
197
+
198
+ ## Common mistakes
199
+
200
+ | Mistake | Instead |
201
+ |---|---|
202
+ | Asking in prose, or one question per message | One ask call per frontier round with recommended options |
203
+ | Asking what the repo can answer | Recon first; ask only owner decisions |
204
+ | Effort instead of proof ("keep improving X") | An end state plus a check that can fail |
205
+ | A check the worker can edit | Put the oracle under `protect` |
206
+ | No non-goal | At least one, so the loop does not fill the vacuum |
207
+ | Inventing a budget or turn cap | Leave it out unless the owner wants one |
208
+ | A `command` check spread over indented lines | One line — folded scalars keep newlines before indented lines; rehearse the stored command before claiming |
209
+ | A multi-lookahead `regex` in a `contains` check | The engine matches without dot-all — every `(?=.*phrase)` must be satisfiable on ONE line of the file; prefer one needle per criterion, or a single alternation |
210
+ | Pinning a release with an exact version string (`contains "version": "1.2.0"` or `stdout_contains "1.2.0"`) | A prefix or semantic check — `node -e "process.exit(require('./package.json').version.startsWith('1.2.')?0:1)"` for a command, or a `contains` regex like `"version": "1\.2\.` — patch releases must not turn a passing criterion red (contract drift; the live v0.2 case) |
211
+ | Launching before the owner saw the exact contract | Review step, then the `Start goal now` question |
@@ -0,0 +1,38 @@
1
+ # goal/v1 contract — fill every field marked REQUIRED; delete optional blocks you do not need.
2
+ # Values that contain ": " must be wrapped in single quotes.
3
+ schema: goal/v1
4
+ id: my-goal # REQUIRED: equals the directory name .opencode/goals/my-goal/
5
+ title: One line the owner will recognise # REQUIRED, ≤80 chars
6
+ intent:
7
+ verbatim: "the owner's own words, unedited" # REQUIRED
8
+ outcome: The single end state that is true when this is done # REQUIRED: not an activity
9
+ why: Why it matters (optional)
10
+ scope:
11
+ in: [src/area/**]
12
+ out: [anything outside src/area]
13
+ non_goals: # REQUIRED: at least one
14
+ - Something the loop must not wander into
15
+ constraints:
16
+ - A non-negotiable that rules options out
17
+ criteria: # REQUIRED: at least one essential criterion
18
+ - id: C1
19
+ statement: WHEN the target check runs THE result SHALL pass
20
+ essential: true
21
+ check: {kind: command, run: "npm test", expect: {exit: 0}, timeout: 600, live: true}
22
+ invariants:
23
+ - id: I1
24
+ statement: The existing build SHALL CONTINUE TO succeed
25
+ check: {kind: command, run: "npm run build"}
26
+ protect: # oracles the worker may never edit
27
+ - "test/**"
28
+ assumptions:
29
+ - {id: A1, text: A default chosen without asking, rationale: why it is reasonable, reversible: true}
30
+ plan:
31
+ - {id: S1, title: First concrete step, proves: []}
32
+ - {id: S2, title: Step that makes C1 true, proves: [C1], depends_on: [S1]}
33
+ stop:
34
+ when: Every essential criterion is host-verified and invariants hold
35
+ escalate_when:
36
+ - The change needs an irreversible or destructive action
37
+ autonomy: {questions: defer, on_user_message: steer, on_interrupt: pause}
38
+ verification: {mode: host+verifier, max_rejections: 3}
@@ -0,0 +1,283 @@
1
+ # Worked examples
2
+
3
+ Four complete `goal/v1` contracts, one per common class (bugfix, perf, migration,
4
+ feature). Copy one into `.opencode/goals/<slug>/goal.yaml`, replace the placeholders
5
+ with facts from your recon, and validate (`goal_validate({slug})` or the parser).
6
+
7
+ Every example here parses with the plugin's own `parseContract` — treat them as
8
+ templates that must stay valid, not as prose.
9
+
10
+ ---
11
+
12
+ ## 1 · Bugfix — a timeout that only happens on slow networks
13
+
14
+ ```yaml
15
+ schema: goal/v1
16
+ id: fix-login-timeout
17
+ title: Login no longer times out on 3G
18
+ intent:
19
+ verbatim: 'login keeps timing out on slow networks, fix it'
20
+ outcome: Login completes on a 3G-throttled connection without a timeout error
21
+ non_goals:
22
+ - Redesigning the login page
23
+ - Touching the session refresh flow
24
+ criteria:
25
+ - id: C1
26
+ statement: WHEN the login e2e test runs under 3G throttling THE login SHALL succeed
27
+ check:
28
+ kind: command
29
+ run: npm run test:e2e -- login-slow
30
+ expect: { exit: 0 }
31
+ live: true
32
+ timeout: 300
33
+ - id: C2
34
+ statement: 'No hard-coded 5s timeout remains in src/auth'
35
+ check:
36
+ kind: absent
37
+ pattern: 'timeout:\s*5000'
38
+ paths: [src/auth]
39
+ invariants:
40
+ - id: I1
41
+ statement: The full unit suite SHALL CONTINUE TO pass
42
+ check:
43
+ kind: command
44
+ run: npm test
45
+ expect: { exit: 0 }
46
+ protect: ['test/e2e/**']
47
+ plan:
48
+ - id: S1
49
+ title: Reproduce under throttling
50
+ proves: []
51
+ - id: S2
52
+ title: Fix the timeout handling
53
+ proves: [C1, C2]
54
+ depends_on: [S1]
55
+ assumptions:
56
+ - id: A1
57
+ text: 3G profile is 400ms RTT / 400kbps
58
+ rationale: Chrome DevTools preset
59
+ reversible: true
60
+ stop:
61
+ escalate_when:
62
+ - 'The fix needs a backend API change'
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 2 · Perf — a render pass that must get measurably faster
68
+
69
+ ```yaml
70
+ schema: goal/v1
71
+ id: speed-up-render
72
+ title: Board render is at least 3x faster on 10k rows
73
+ intent:
74
+ verbatim: 'the board takes forever to render with 10k rows, make it at least 3 times faster'
75
+ outcome: The render benchmark reports speedup of 3 or more versus the baseline, with no visual regressions
76
+ non_goals:
77
+ - Rewriting the board in a different framework
78
+ - Changing the rendered markup or styling
79
+ criteria:
80
+ - id: C1
81
+ statement: THE render benchmark SHALL report speedup of 3 or more across 3 runs
82
+ check:
83
+ kind: command
84
+ run: node bench/render.mjs --runs 3
85
+ expect: { stdout_regex: 'speedup=([3-9]|\d{2,})' }
86
+ runs: 3
87
+ - id: C2
88
+ statement: THE board snapshot SHALL be unchanged
89
+ check:
90
+ kind: command
91
+ run: npm test -- board.snapshot
92
+ expect: { exit: 0 }
93
+ invariants:
94
+ - id: I1
95
+ statement: The full test suite SHALL CONTINUE TO pass
96
+ check:
97
+ kind: command
98
+ run: npm test
99
+ expect: { exit: 0 }
100
+ - id: I2
101
+ statement: 'The baseline bench result (bench/baseline.json) SHALL NOT change'
102
+ check:
103
+ kind: diff
104
+ paths: [bench/baseline.json]
105
+ protect: ['bench/**']
106
+ scope:
107
+ in:
108
+ - 'src/board render path only'
109
+ out:
110
+ - 'The data-loading pipeline'
111
+ stop:
112
+ escalate_when:
113
+ - 'Speedup 2x is reachable but 3x needs a data-model change'
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 3 · Migration — retire an old client without behaviour change
119
+
120
+ ```yaml
121
+ schema: goal/v1
122
+ id: drop-legacy-client
123
+ title: Every call site uses the new API client
124
+ intent:
125
+ verbatim: 'remove every legacyClient call and move to the new api client, nothing should behave differently'
126
+ outcome: 'No legacyClient import or call remains in src/, the suite and typecheck pass, and MIGRATION.md documents the replacement for every removed call'
127
+ non_goals:
128
+ - Changing request payloads or endpoints
129
+ - Deleting the legacy client package from package.json
130
+ criteria:
131
+ - id: C1
132
+ statement: 'No legacyClient reference remains in src/'
133
+ check:
134
+ kind: absent
135
+ pattern: 'legacyClient\.'
136
+ paths: [src]
137
+ - id: C2
138
+ statement: The typecheck and full suite pass after the migration
139
+ check:
140
+ kind: command
141
+ run: npm run typecheck && npm test
142
+ expect: { exit: 0 }
143
+ timeout: 600
144
+ - id: C3
145
+ statement: MIGRATION.md lists a replacement for every removed call
146
+ check:
147
+ kind: verifier
148
+ ask: >-
149
+ Read MIGRATION.md and git log for this run. For every legacyClient call site
150
+ removed, does MIGRATION.md name the new-api replacement? Quote one line per
151
+ gap you find.
152
+ invariants:
153
+ - id: I1
154
+ statement: 'The public API surface (api/public.d.ts) SHALL NOT change'
155
+ check:
156
+ kind: diff
157
+ paths: [api/public.d.ts]
158
+ protect: ['api/public.d.ts']
159
+ plan:
160
+ - id: S1
161
+ title: Map every legacyClient call site
162
+ proves: []
163
+ - id: S2
164
+ title: Replace call sites in dependency order
165
+ proves: [C1]
166
+ - id: S3
167
+ title: Document replacements in MIGRATION.md
168
+ proves: [C3]
169
+ depends_on: [S2]
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 4 · Feature — an export button with observable behaviour per scenario
175
+
176
+ ```yaml
177
+ schema: goal/v1
178
+ id: csv-export
179
+ title: Users can export the orders table as CSV
180
+ intent:
181
+ verbatim: 'add an export button to the orders table that downloads a csv'
182
+ outcome: The orders table has an Export CSV button that downloads the currently filtered rows as valid CSV
183
+ non_goals:
184
+ - XLSX or PDF export
185
+ - Server-side async export jobs
186
+ criteria:
187
+ - id: C1
188
+ statement: WHEN the user clicks Export CSV with filters applied THE download SHALL contain exactly the filtered rows in display order
189
+ check:
190
+ kind: command
191
+ run: npm run test:e2e -- orders-export
192
+ expect: { exit: 0 }
193
+ live: true
194
+ - id: C2
195
+ statement: THE csv writer SHALL escape quotes, commas and newlines
196
+ check:
197
+ kind: command
198
+ run: npm test -- csvWriter
199
+ expect: { exit: 0 }
200
+ - id: C3
201
+ statement: The export button is approved design-wise by the owner
202
+ check:
203
+ kind: human
204
+ ask: 'Open the orders page and confirm the Export CSV button matches the design mock'
205
+ invariants:
206
+ - id: I1
207
+ statement: The full test suite SHALL CONTINUE TO pass
208
+ check:
209
+ kind: command
210
+ run: npm test
211
+ expect: { exit: 0 }
212
+ - id: I2
213
+ statement: No new runtime dependency SHALL be added
214
+ check:
215
+ kind: contains
216
+ path: package.json
217
+ text: '"dependencies": {'
218
+ scope:
219
+ in:
220
+ - 'src/orders table, src/lib/csv writer, e2e coverage'
221
+ out:
222
+ - 'Other tables and pages'
223
+ stop:
224
+ escalate_when:
225
+ - 'Exports above 50k rows would need a streaming format change'
226
+ ```
227
+
228
+ ---
229
+
230
+ ## 5 · Release — ship v1.2 and keep the check true across patch releases
231
+
232
+ The trap this example exists for: pinning the release with an exact version
233
+ string. `contains "version": "1.2.0"` passes on release day and then fails
234
+ the moment 1.2.1 ships mid-run — contract drift, red criterion, an owner
235
+ amendment that changes nothing real (this exact case closed the v0.2 dogfood
236
+ run). Check the semantic prefix instead.
237
+
238
+ ```yaml
239
+ schema: goal/v1
240
+ id: ship-v1-2
241
+ title: v1.2 released from a clean tree
242
+ intent:
243
+ verbatim: "cut the 1.2 release"
244
+ outcome: v1.2.x is tagged, the changelog carries it, and a clean checkout of the tag passes its own suite
245
+ non_goals:
246
+ - Rewriting release automation
247
+ - Backporting to 1.1
248
+ criteria:
249
+ - id: C1
250
+ statement: package.json is semantically bumped for the 1.2 line
251
+ check: {kind: command, run: 'node -e "process.exit(require(\"./package.json\").version.startsWith(\"1.2.\")?0:1)"'}
252
+ - id: C2
253
+ statement: CHANGELOG.md carries the 1.2 entry
254
+ check: {kind: contains, path: CHANGELOG.md, regex: "## \\[?1\\.2\\."}
255
+ - id: C3
256
+ statement: the pushed tag installs from git in an isolated environment
257
+ check: {kind: command, run: 'INSTALL_TEST=1 TAG=v1.2.0 bun test test/release/git-install.test.ts', timeout: 600}
258
+ invariants:
259
+ - id: I1
260
+ statement: the suite still passes on the release commit
261
+ check: {kind: command, run: "bun test"}
262
+ protect: ["docs/baselines/**"]
263
+ plan:
264
+ - {id: S1, title: Bump + changelog, proves: [C1, C2]}
265
+ - {id: S2, title: Tag, push, git-install proof, proves: [C3, I1], depends_on: [S1]}
266
+ ```
267
+
268
+ Why each check holds across patch releases: C1 reads the version and asserts
269
+ the `1.2.` prefix — 1.2.0, 1.2.1, 1.2.9 all pass, 1.3.0 fails (the line
270
+ moved on, which SHOULD be red); C2 anchors the changelog heading on the
271
+ major.minor pair; only C3 names an exact tag, because a specific tag is
272
+ genuinely what it verifies.
273
+
274
+ ---
275
+
276
+ ## What makes these pass
277
+
278
+ - Every criterion is **binary and able to fail**: a command that can exit non-zero, a
279
+ pattern that can still be present, a question a verifier can answer "no" to.
280
+ - Invariants pin what must not regress; `protect` pins the files the worker may never
281
+ edit (oracles, baselines, public API surfaces).
282
+ - Each has at least one **non-goal**, so the loop cannot invent scope when it stalls.
283
+ - Budgets are absent on purpose — the goals stop on proof or an honest blocker.