@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# goal/v1 field reference
|
|
2
|
+
|
|
3
|
+
The plugin's parser (`src/contract/parse.ts` in opencode-goal) is the source of truth; this mirrors it.
|
|
4
|
+
|
|
5
|
+
## Top level
|
|
6
|
+
|
|
7
|
+
| Field | Required | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `schema` | yes | exactly `goal/v1` |
|
|
10
|
+
| `id` | yes | kebab-case slug, ≤64 chars, equals the directory name |
|
|
11
|
+
| `title` | yes | ≤80 chars recommended (sidebar) |
|
|
12
|
+
| `intent.verbatim` | yes | the owner's words, never rewritten |
|
|
13
|
+
| `outcome` | yes | one end state; warned if it reads as an activity or uses vague words without a number |
|
|
14
|
+
| `why` | no | |
|
|
15
|
+
| `scope.in` / `scope.out` | no | globs or plain descriptions |
|
|
16
|
+
| `non_goals` | yes (≥1) | |
|
|
17
|
+
| `constraints` | no | |
|
|
18
|
+
| `criteria` | yes (≥1 essential) | ids `C1…` |
|
|
19
|
+
| `invariants` | no | ids `I1…`, host-checkable kinds only, always essential |
|
|
20
|
+
| `protect` | no | globs the worker may not edit; enforced before edits and re-checked at verification |
|
|
21
|
+
| `assumptions` | no | ids `A1…`; `status` assumed (default) / confirmed / vetoed |
|
|
22
|
+
| `plan` | no | ids `S1…`; `proves` must reference criteria; `depends_on` must reference steps; no cycles |
|
|
23
|
+
| `budget` | no | `turns` (int), `wall` ("90m", "3h", "1h30m"), `tokens` (400k, "3M"; spend across the run), `cost_usd` |
|
|
24
|
+
| `stop.when`, `stop.escalate_when` | no | shown to the worker; escalation means `goal_block` |
|
|
25
|
+
| `autonomy` | no | `questions` defer (default) / allow · `on_user_message` steer (default) / pause · `on_interrupt` pause (default) / resume-on-message · `amend`: owner-set policy, a list of change classes the agent may apply to its own contract without asking — `timeout` (a check's timeout only), `budget`, `plan` (plan steps and assumptions), `recover` (adopt an in-policy out-of-band disk edit). Default `[]` = owner reviews every change. Criteria statements/checks, protect, scope, verification, stop rules and the policy itself are never agent-applicable. |
|
|
26
|
+
| `verification` | no | `mode` host / host+verifier (default) / strict · `max_rejections` 1-10 (default 3) · `read`: extra folders or files outside the project the verifier may READ (plain paths, relative to the project root; never writable; everything else outside the project stays denied) |
|
|
27
|
+
| `execution.agent` | no | informational in v0.1 |
|
|
28
|
+
| `meta` | no | free-form |
|
|
29
|
+
|
|
30
|
+
## Criterion
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- id: C1
|
|
34
|
+
statement: WHEN … THE … SHALL … # binary, specific
|
|
35
|
+
essential: true # default true; optional criteria never block completion
|
|
36
|
+
check: { kind: …, … }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| `check.kind` | Fields | Passes when |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `command` | `run` (required), `expect.exit` (default 0), `expect.stdout_contains`, `expect.stdout_regex`, `timeout` seconds (default 300), `runs` (1-10, all must pass), `live` (re-run by the host after turns that changed files) | every run meets every expectation; runs in the owner's login shell at the project root |
|
|
42
|
+
| `file` | `path`, `exists` (default true) | the path exists (or not) |
|
|
43
|
+
| `contains` | `path`, `text` or `regex` | the file contains it |
|
|
44
|
+
| `absent` | `pattern` (extended regex), `paths` | `git grep` finds no match (goal state excluded) |
|
|
45
|
+
| `diff` | `paths` (globs) | none changed since the run started |
|
|
46
|
+
| `verifier` | `ask` | the read-only verifier answers proven with quotes the host finds verbatim in the files |
|
|
47
|
+
| `human` | `ask` | the owner approves it (`/goal approve C3`) |
|
|
48
|
+
|
|
49
|
+
## Validator messages
|
|
50
|
+
|
|
51
|
+
Errors: wrong schema · bad or mismatched id · missing title / intent.verbatim / outcome · no non-goals · no criteria · no essential criterion · criterion id not `C<n>` · missing statement · missing or unknown check kind · missing kind fields (run, path, pattern, paths, ask, text|regex) · invalid regex · non-positive timeout · invariant with a non-host kind · duplicate ids · plan id not `S<n>` · unknown `proves`/`depends_on` · dependency cycle · bad budget values · bad autonomy / verification values · invalid YAML (with a hint for unquoted `: `).
|
|
52
|
+
|
|
53
|
+
Warnings: title over 80 chars · outcome reads as an activity · vague words without a number (outcome and statements) · no host-checkable essential criterion.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Interview: frontier rounds
|
|
2
|
+
|
|
3
|
+
## Protocol
|
|
4
|
+
1. After recon, write every open decision as a node; note which decisions depend on which.
|
|
5
|
+
2. A round asks the **frontier**: the decisions whose prerequisites are settled. One ask call, ≤4 questions, 2-4 options each, recommended option first with "(Recommended)", every option describing what happens and its trade-off.
|
|
6
|
+
3. After the answers, update the draft contract and `decisions.jsonl`, then compute the next frontier.
|
|
7
|
+
4. Stop when the frontier is empty: an implementer agent could run the contract without asking a single question.
|
|
8
|
+
5. Anything not worth the owner's time becomes an assumption: `{id, text, rationale, reversible}`.
|
|
9
|
+
|
|
10
|
+
`decisions.jsonl` line: `{"t":"2026-10-02T23:00:00Z","q":"What proves it is done?","options":["npm test exits 0","…"],"a":"npm test exits 0","by":"owner"}`
|
|
11
|
+
|
|
12
|
+
## Question bank (recommendation heuristics in brackets)
|
|
13
|
+
|
|
14
|
+
**Outcome** — "Which end state should be true when this goal finishes?" [the narrowest state that satisfies the owner's words; offer one broader and one narrower alternative]
|
|
15
|
+
|
|
16
|
+
**Done when** — "What proves it?" [existing commands from recon first: the failing test, the suite, the build, a bench; a new check only if none exists]
|
|
17
|
+
|
|
18
|
+
**Evidence standard** — "How strict is completion?" [host checks + independent verifier (Recommended) · host checks only (cheapest; semantic criteria need owner sign-off) · strict (verifier re-confirms host checks)]
|
|
19
|
+
|
|
20
|
+
**Oracles** — "Which files must the worker never change?" [the tests and benchmarks named in Done-when]
|
|
21
|
+
|
|
22
|
+
**Invariants** — "What must keep working the whole time?" [the full test suite, the build, typecheck]
|
|
23
|
+
|
|
24
|
+
**Scope / non-goals** — "Where may the work reach, and what is explicitly out?" [the module recon found; non-goal = the most tempting adjacent change]
|
|
25
|
+
|
|
26
|
+
**Escalation** — "When should it stop and ask instead of pushing on?" [irreversible data changes, public API changes, new dependencies, credentials]
|
|
27
|
+
|
|
28
|
+
**Budget** (only for open-ended goals) — "Cap the spend?" [no cap (Recommended for well-proven goals) · a token cap sized to the work · a wall-clock cap]
|
|
29
|
+
|
|
30
|
+
**Autonomy** — "If you type while it runs?" [steer: your message guides the next turn (Recommended) · pause]
|
|
31
|
+
|
|
32
|
+
**Priority** (ask only when the project already has a live goal — check the registry or `.opencode/goals/*/run.json` for a non-terminal run; a first goal needs no ordering) — "Where does this goal rank against the live one if you must choose where work goes first?" [medium (Recommended: the default lane) · high (first in /goal list, the palette and the start picker; taken ahead of the live goal) · low (lists last; the live goal keeps the attention) · skip (no field — sorts last by recency)]
|
|
33
|
+
|
|
34
|
+
## Launch question
|
|
35
|
+
|
|
36
|
+
Options, in this order: `Start goal now` (Recommended; the label must start with the exact text `goal_start` checks) · Start in a fresh session · Start clean (the engine opens a new session owned by the goal) · Save for later.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `cleo-goal install --project`: register this plugin in the PROJECT's OpenCode
|
|
2
|
+
// v2 config (`.opencode/opencode.json`, key `plugins`). Idempotent. Never the
|
|
3
|
+
// user-global config: `opencode plugin add` only writes the global file, which is
|
|
4
|
+
// why this exists.
|
|
5
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeSync } from "node:fs"
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
8
|
+
|
|
9
|
+
export const INSTALL_SCHEMA_VERSION = 1
|
|
10
|
+
export const PACKAGE_NAME = "@cleocode/goal"
|
|
11
|
+
/** Specs that name this plugin, whatever the version: the npm package and the git repo it was installed from before npm. */
|
|
12
|
+
const IDENTITIES = [PACKAGE_NAME, "github:kryptobaseddev/opencode-goal", "opencode-goal"]
|
|
13
|
+
export const PROJECT_CONFIG = join(".opencode", "opencode.json")
|
|
14
|
+
|
|
15
|
+
type Options = Record<string, unknown>
|
|
16
|
+
export type PluginEntry = string | [string, Options] | { package: string; options?: Options }
|
|
17
|
+
|
|
18
|
+
export type InstallResult = {
|
|
19
|
+
schemaVersion: typeof INSTALL_SCHEMA_VERSION
|
|
20
|
+
action: "installed" | "already-present" | "updated" | "dry-run"
|
|
21
|
+
file: string
|
|
22
|
+
spec: string
|
|
23
|
+
plugins: PluginEntry[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class InstallError extends Error {}
|
|
27
|
+
|
|
28
|
+
const specOf = (entry: PluginEntry): string => (typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : entry.package)
|
|
29
|
+
|
|
30
|
+
/** The spec without its version, tag or ref: `@cleocode/goal@0.8.1` → `@cleocode/goal`. */
|
|
31
|
+
export function identity(spec: string): string {
|
|
32
|
+
const hash = spec.indexOf("#")
|
|
33
|
+
const bare = hash === -1 ? spec : spec.slice(0, hash)
|
|
34
|
+
const at = bare.lastIndexOf("@")
|
|
35
|
+
return at > 0 ? bare.slice(0, at) : bare
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isOurs = (entry: PluginEntry) => IDENTITIES.includes(identity(specOf(entry)))
|
|
39
|
+
|
|
40
|
+
const withSpec = (entry: PluginEntry, spec: string): PluginEntry =>
|
|
41
|
+
typeof entry === "string" ? spec : Array.isArray(entry) ? [spec, entry[1]] : { ...entry, package: spec }
|
|
42
|
+
|
|
43
|
+
const canonical = (path: string) => {
|
|
44
|
+
try {
|
|
45
|
+
return realpathSync(path)
|
|
46
|
+
} catch {
|
|
47
|
+
return resolve(path)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const inside = (child: string, parent: string) => {
|
|
52
|
+
const rel = relative(parent, child)
|
|
53
|
+
return !(rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Refuse anything that is user-global OpenCode config: the home directory as a
|
|
58
|
+
* project, a project under the config dirs, and (T13871) a `.opencode` dir or
|
|
59
|
+
* `opencode.json` that resolves, through a symlink, into a config dir or
|
|
60
|
+
* anywhere outside the project.
|
|
61
|
+
*/
|
|
62
|
+
function assertProjectScoped(root: string, home: string) {
|
|
63
|
+
const r = canonical(root)
|
|
64
|
+
const h = canonical(home)
|
|
65
|
+
if (r === h) throw new InstallError(`refusing to install into the home directory (${h}): its .opencode/ is user-global; run from a project`)
|
|
66
|
+
const globals = [...new Set([canonical(process.env.XDG_CONFIG_HOME || join(h, ".config")), canonical(join(h, ".config")), canonical(join(h, ".opencode"))])]
|
|
67
|
+
const underGlobal = (path: string) => globals.find((g) => inside(path, g))
|
|
68
|
+
const g = underGlobal(r)
|
|
69
|
+
if (g) throw new InstallError(`refusing to install under ${g}: that is user-global config; run from a project`)
|
|
70
|
+
for (const target of [join(root, ".opencode"), join(root, PROJECT_CONFIG)]) {
|
|
71
|
+
if (!existsSync(target)) continue
|
|
72
|
+
const real = canonical(target)
|
|
73
|
+
const global = underGlobal(real)
|
|
74
|
+
if (global) throw new InstallError(`refusing to write ${target}: it resolves to ${real}, inside user-global ${global}`)
|
|
75
|
+
if (!inside(real, r)) throw new InstallError(`refusing to write ${target}: it resolves to ${real}, outside the project ${r}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readPlugins(path: string): { config: Record<string, unknown>; plugins: PluginEntry[] } {
|
|
80
|
+
if (!existsSync(path)) return { config: {}, plugins: [] }
|
|
81
|
+
let config: unknown
|
|
82
|
+
try {
|
|
83
|
+
config = JSON.parse(readFileSync(path, "utf8"))
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new InstallError(`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}); left untouched`)
|
|
86
|
+
}
|
|
87
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new InstallError(`${path} is not a JSON object; left untouched`)
|
|
88
|
+
const record = config as Record<string, unknown>
|
|
89
|
+
const plugins = record.plugins ?? []
|
|
90
|
+
if (!Array.isArray(plugins)) throw new InstallError(`${path}: "plugins" is not an array; left untouched`)
|
|
91
|
+
return { config: record, plugins: plugins as PluginEntry[] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function writeAtomic(path: string, text: string) {
|
|
95
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`
|
|
96
|
+
const fd = openSync(tmp, "wx", 0o644)
|
|
97
|
+
try {
|
|
98
|
+
writeSync(fd, text)
|
|
99
|
+
fsyncSync(fd)
|
|
100
|
+
} finally {
|
|
101
|
+
closeSync(fd)
|
|
102
|
+
}
|
|
103
|
+
renameSync(tmp, path)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Add (or move to `spec`) this plugin in `<root>/.opencode/opencode.json`. */
|
|
107
|
+
export function installProject(root: string, spec: string, options: { dryRun?: boolean; home?: string } = {}): InstallResult {
|
|
108
|
+
assertProjectScoped(root, options.home ?? homedir())
|
|
109
|
+
const path = join(root, PROJECT_CONFIG)
|
|
110
|
+
if (!existsSync(path) && existsSync(`${path}c`)) {
|
|
111
|
+
throw new InstallError(`${PROJECT_CONFIG}c exists; add "${spec}" to its "plugins" by hand (JSONC is not rewritten, to keep its comments)`)
|
|
112
|
+
}
|
|
113
|
+
const { config, plugins } = readPlugins(path)
|
|
114
|
+
const ours = plugins.findIndex(isOurs)
|
|
115
|
+
const next = ours === -1 ? [...plugins, spec] : plugins.map((entry, i) => (i === ours ? withSpec(entry, spec) : entry)).filter((entry, i) => i <= ours || !isOurs(entry))
|
|
116
|
+
const action = ours === -1 ? "installed" : specOf(plugins[ours]!) === spec && plugins.filter(isOurs).length === 1 ? "already-present" : "updated"
|
|
117
|
+
const result = { schemaVersion: INSTALL_SCHEMA_VERSION, file: PROJECT_CONFIG, spec, plugins: action === "already-present" ? plugins : next } as const
|
|
118
|
+
if (options.dryRun) return { ...result, action: "dry-run" }
|
|
119
|
+
if (action !== "already-present") {
|
|
120
|
+
mkdirSync(join(root, ".opencode"), { recursive: true })
|
|
121
|
+
// A symlinked opencode.json inside the project is written through, never replaced by a file.
|
|
122
|
+
writeAtomic(existsSync(path) ? canonical(path) : path, `${JSON.stringify({ ...config, plugins: next }, null, 2)}\n`)
|
|
123
|
+
}
|
|
124
|
+
return { ...result, action }
|
|
125
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `cleo-goal status --json`: the read contract CLEO parses (schemaVersion 1).
|
|
2
|
+
// CLEO never reads run.json directly; this is the only shape it relies on, so
|
|
3
|
+
// a field change here is a schemaVersion bump.
|
|
4
|
+
import { parseContract } from "../contract/parse"
|
|
5
|
+
import type { Contract } from "../contract/types"
|
|
6
|
+
import { budgetUse, isTerminal, type RunState, type Status } from "../engine/state"
|
|
7
|
+
import type { Store } from "../engine/store"
|
|
8
|
+
|
|
9
|
+
export const STATUS_SCHEMA_VERSION = 1
|
|
10
|
+
|
|
11
|
+
export type StatusCriterion = {
|
|
12
|
+
id: string
|
|
13
|
+
statement: string
|
|
14
|
+
status: "unknown" | "pass" | "fail" | "claimed"
|
|
15
|
+
essential: boolean
|
|
16
|
+
invariant: boolean
|
|
17
|
+
by: string | null
|
|
18
|
+
detail: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type GoalStatusJson = {
|
|
22
|
+
schemaVersion: typeof STATUS_SCHEMA_VERSION
|
|
23
|
+
goal: string | null
|
|
24
|
+
runId: string | null
|
|
25
|
+
title: string
|
|
26
|
+
status: Status | "none"
|
|
27
|
+
priority?: "low" | "medium" | "high"
|
|
28
|
+
turn: number
|
|
29
|
+
reason: string | null
|
|
30
|
+
criteria: StatusCriterion[]
|
|
31
|
+
proven: number
|
|
32
|
+
total: number
|
|
33
|
+
budget: { ratio: number; parts: Array<{ name: string; used: number; limit: number }> }
|
|
34
|
+
verdict: { passed: boolean; lines: string[]; turn: number } | null
|
|
35
|
+
blocker: { key: string; count: number; reason: string } | null
|
|
36
|
+
updatedAt: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NONE: GoalStatusJson = {
|
|
40
|
+
schemaVersion: STATUS_SCHEMA_VERSION,
|
|
41
|
+
goal: null,
|
|
42
|
+
runId: null,
|
|
43
|
+
title: "",
|
|
44
|
+
status: "none",
|
|
45
|
+
turn: 0,
|
|
46
|
+
reason: null,
|
|
47
|
+
criteria: [],
|
|
48
|
+
proven: 0,
|
|
49
|
+
total: 0,
|
|
50
|
+
budget: { ratio: 0, parts: [] },
|
|
51
|
+
verdict: null,
|
|
52
|
+
blocker: null,
|
|
53
|
+
updatedAt: 0,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The run to report: the newest live run, else the newest finished one. */
|
|
57
|
+
export function selectRun(store: Store): RunState | undefined {
|
|
58
|
+
const runs = store
|
|
59
|
+
.slugs()
|
|
60
|
+
.map((slug) => store.readRun(slug))
|
|
61
|
+
.filter((run): run is RunState => Boolean(run))
|
|
62
|
+
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
|
|
63
|
+
return runs.find((run) => !isTerminal(run.status)) ?? runs[0]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The contract generation the run locked (the disk copy may have moved on). */
|
|
67
|
+
function lockedContract(store: Store, run: RunState): Contract | undefined {
|
|
68
|
+
const disk = store.readContract(run.slug)
|
|
69
|
+
if (disk?.contract && disk.lock === run.lock) return disk.contract
|
|
70
|
+
try {
|
|
71
|
+
return parseContract(store.lockedText(run.slug, run.runId, run.lock), { slug: run.slug }).contract ?? disk?.contract
|
|
72
|
+
} catch {
|
|
73
|
+
return disk?.contract
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Build the status reply for the project the store belongs to. */
|
|
78
|
+
export function goalStatus(store: Store, now = Date.now()): GoalStatusJson {
|
|
79
|
+
const run = selectRun(store)
|
|
80
|
+
if (!run) return NONE
|
|
81
|
+
const contract = lockedContract(store, run)
|
|
82
|
+
const declared = contract
|
|
83
|
+
? [...contract.criteria.map((c) => ({ c, invariant: false })), ...contract.invariants.map((c) => ({ c, invariant: true }))]
|
|
84
|
+
: []
|
|
85
|
+
const criteria: StatusCriterion[] = declared.map(({ c, invariant }) => {
|
|
86
|
+
const state = run.criteria[c.id]
|
|
87
|
+
return {
|
|
88
|
+
id: c.id,
|
|
89
|
+
statement: c.statement,
|
|
90
|
+
status: state?.status ?? "unknown",
|
|
91
|
+
essential: c.essential,
|
|
92
|
+
invariant,
|
|
93
|
+
by: state?.by ?? null,
|
|
94
|
+
detail: state?.detail ?? null,
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
const states = Object.values(run.criteria).filter((c) => c && typeof c === "object")
|
|
98
|
+
const budget = contract ? budgetUse(run, contract, now) : { ratio: 0, parts: [] }
|
|
99
|
+
return {
|
|
100
|
+
schemaVersion: STATUS_SCHEMA_VERSION,
|
|
101
|
+
goal: run.slug,
|
|
102
|
+
runId: run.runId,
|
|
103
|
+
title: run.title,
|
|
104
|
+
status: run.status,
|
|
105
|
+
...(run.priority ? { priority: run.priority } : {}),
|
|
106
|
+
turn: run.turn,
|
|
107
|
+
reason: run.reason ?? null,
|
|
108
|
+
criteria,
|
|
109
|
+
proven: states.filter((c) => c.status === "pass").length,
|
|
110
|
+
total: states.length,
|
|
111
|
+
budget: { ratio: budget.ratio, parts: budget.parts.map((p) => ({ name: p.name, used: p.used, limit: p.limit })) },
|
|
112
|
+
verdict: run.verdict ? { passed: run.verdict.passed, lines: run.verdict.lines, turn: run.verdict.turn } : null,
|
|
113
|
+
blocker: run.blocker ? { key: run.blocker.key, count: run.blocker.count, reason: run.blocker.reason } : null,
|
|
114
|
+
updatedAt: run.updatedAt,
|
|
115
|
+
}
|
|
116
|
+
}
|