@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.
- package/CHANGELOG.md +319 -0
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/cli/ocg.ts +149 -0
- package/dist/cli.js +8708 -0
- package/dist/tui.d.ts +2 -0
- package/dist/tui.js +1920 -0
- package/package.json +79 -4
- package/rpc.ts +2 -0
- package/server.ts +3 -0
- package/skills/write-goal/SKILL.md +211 -0
- package/skills/write-goal/assets/goal.template.yaml +38 -0
- package/skills/write-goal/references/examples.md +283 -0
- package/skills/write-goal/references/goal-schema.md +53 -0
- package/skills/write-goal/references/interview.md +36 -0
- package/src/cli/install.ts +125 -0
- package/src/cli/status.ts +116 -0
- package/src/contract/parse.ts +454 -0
- package/src/contract/render.ts +134 -0
- package/src/contract/types.ts +158 -0
- package/src/engine/amendment.ts +51 -0
- package/src/engine/amendments.ts +72 -0
- package/src/engine/cleo-bridge.ts +339 -0
- package/src/engine/cleo-link.ts +150 -0
- package/src/engine/continue-work.ts +131 -0
- package/src/engine/decisions.ts +44 -0
- package/src/engine/dogfood-record.ts +278 -0
- package/src/engine/handoff.ts +102 -0
- package/src/engine/interrupt-policy.ts +45 -0
- package/src/engine/inventory.ts +70 -0
- package/src/engine/launches.ts +82 -0
- package/src/engine/loop-stop.ts +50 -0
- package/src/engine/loop.ts +109 -0
- package/src/engine/next-work.ts +82 -0
- package/src/engine/owner-intent.ts +71 -0
- package/src/engine/owner-reviews.ts +178 -0
- package/src/engine/peer-announce.ts +67 -0
- package/src/engine/peer-transport.ts +78 -0
- package/src/engine/policy.ts +42 -0
- package/src/engine/registry.ts +119 -0
- package/src/engine/similarity.ts +65 -0
- package/src/engine/stale-decisions.ts +68 -0
- package/src/engine/state.ts +185 -0
- package/src/engine/store.ts +263 -0
- package/src/engine/tick-record.ts +129 -0
- package/src/engine/tick.ts +207 -0
- package/src/rpc.ts +182 -0
- package/src/server/app.ts +4320 -0
- package/src/server/help.ts +50 -0
- package/src/server/index.ts +23 -0
- package/src/server/intent.ts +22 -0
- package/src/server/messages.ts +228 -0
- package/src/summary/audit.ts +76 -0
- package/src/summary/build.ts +314 -0
- package/src/tui/dashboard.ts +322 -0
- package/src/tui/decision-form.ts +12 -0
- package/src/tui/format.ts +127 -0
- package/src/tui/index.tsx +718 -0
- package/src/tui/manager.ts +94 -0
- package/src/tui/navigation.ts +11 -0
- package/src/tui/pointer-select.ts +20 -0
- package/src/tui/tracer.ts +51 -0
- package/src/util/git.ts +102 -0
- package/src/util/ids.ts +20 -0
- package/src/util/shell.ts +42 -0
- package/src/verify/artifact.ts +95 -0
- package/src/verify/pipeline.ts +200 -0
- package/tui.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/goal",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
package/server.ts
ADDED
|
@@ -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.
|