@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,127 @@
|
|
|
1
|
+
// Pure formatting for the TUI: everything the sidebar, pill, banner and
|
|
2
|
+
// dashboard panel show is computed here from a GoalView, so it can be tested
|
|
3
|
+
// without a terminal.
|
|
4
|
+
import type { GoalView } from "../rpc"
|
|
5
|
+
import { tracerLines } from "./tracer"
|
|
6
|
+
|
|
7
|
+
export type Tone = "base" | "muted" | "success" | "warning" | "error" | "info"
|
|
8
|
+
/** T073: a row the width truncates names the panel tab holding its full
|
|
9
|
+
* text — the compact card's expand affordance is click-through, not a hint. */
|
|
10
|
+
export type Line = { text: string; tone: Tone; bold?: boolean; tab?: "now" | "progress" | "decisions" | "goals" }
|
|
11
|
+
|
|
12
|
+
export const STATUS: Record<string, { icon: string; label: string; tone: Tone }> = {
|
|
13
|
+
running: { icon: "▶", label: "running", tone: "success" },
|
|
14
|
+
waiting: { icon: "◷", label: "waiting", tone: "info" },
|
|
15
|
+
verifying: { icon: "◐", label: "verifying", tone: "info" },
|
|
16
|
+
paused: { icon: "⏸", label: "paused", tone: "warning" },
|
|
17
|
+
blocked: { icon: "⛔", label: "blocked", tone: "error" },
|
|
18
|
+
needs_review: { icon: "⚑", label: "needs review", tone: "warning" },
|
|
19
|
+
budget_limited: { icon: "⊘", label: "budget used", tone: "warning" },
|
|
20
|
+
complete: { icon: "✓", label: "complete", tone: "success" },
|
|
21
|
+
failed: { icon: "✗", label: "failed", tone: "error" },
|
|
22
|
+
aborted: { icon: "■", label: "aborted", tone: "muted" },
|
|
23
|
+
superseded: { icon: "⇢", label: "superseded", tone: "muted" },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const statusOf = (s: string) => STATUS[s] ?? { icon: "·", label: s, tone: "muted" as Tone }
|
|
27
|
+
|
|
28
|
+
export const fit = (text: string, width: number) => {
|
|
29
|
+
const flat = text.replace(/\s+/g, " ").trim()
|
|
30
|
+
return flat.length <= width ? flat : `${flat.slice(0, Math.max(1, width - 1))}…`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function fmtDuration(ms: number): string {
|
|
34
|
+
const s = Math.max(0, Math.floor(ms / 1000))
|
|
35
|
+
if (s < 60) return `${s}s`
|
|
36
|
+
const m = Math.floor(s / 60)
|
|
37
|
+
if (m < 60) return `${m}m`
|
|
38
|
+
const h = Math.floor(m / 60)
|
|
39
|
+
return `${h}h${m % 60 ? ` ${String(m % 60).padStart(2, "0")}m` : ""}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const fmtTokens = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n))
|
|
43
|
+
|
|
44
|
+
export const elapsed = (view: GoalView, now: number) => view.activeMs + (view.activeSince !== null ? Math.max(0, now - view.activeSince) : 0)
|
|
45
|
+
|
|
46
|
+
export function bar(done: number, total: number, width = 10): string {
|
|
47
|
+
if (total <= 0) return ""
|
|
48
|
+
const filled = Math.round((done / total) * width)
|
|
49
|
+
return "■".repeat(filled) + "□".repeat(width - filled)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const proven = (view: GoalView) => view.criteria.filter((c) => c.status === "pass").length
|
|
53
|
+
|
|
54
|
+
function usageParts(view: GoalView, now: number): { time: string; spend?: string } {
|
|
55
|
+
const limit = (name: string) => view.budget.find((b) => b.name === name)?.limit
|
|
56
|
+
const time = `turn ${view.turn}${limit("turns") ? `/${limit("turns")}` : ""} · ${fmtDuration(elapsed(view, now))}${limit("wall") ? `/${fmtDuration(limit("wall")!)}` : ""}`
|
|
57
|
+
const spend: string[] = []
|
|
58
|
+
if (view.usage.tokens || limit("tokens")) spend.push(`${fmtTokens(view.usage.tokens)}${limit("tokens") ? `/${fmtTokens(limit("tokens")!)}` : ""} tok`)
|
|
59
|
+
if (view.usage.cost || limit("cost")) spend.push(`$${view.usage.cost.toFixed(2)}${limit("cost") ? `/$${limit("cost")}` : ""}`)
|
|
60
|
+
return { time, ...(spend.length ? { spend: spend.join(" · ") } : {}) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const usageLine = (view: GoalView, now: number) => {
|
|
64
|
+
const u = usageParts(view, now)
|
|
65
|
+
return u.spend ? `${u.time} · ${u.spend}` : u.time
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const clock = (t: number) => new Date(t).toTimeString().slice(0, 8)
|
|
69
|
+
|
|
70
|
+
export function pillText(view: GoalView, now: number): string {
|
|
71
|
+
const st = statusOf(view.status)
|
|
72
|
+
const steps = view.steps ?? []
|
|
73
|
+
const plan = steps.length ? `${steps.filter((s) => s.status === "done").length}/${steps.length} plan · ` : ""
|
|
74
|
+
return `◎ ${st.icon} ${plan}${proven(view)}/${view.criteria.length} proven · t${view.turn} · ${fmtDuration(elapsed(view, now))}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const RESUME = "/goal resume"
|
|
78
|
+
|
|
79
|
+
/** T071: completion silence read as broken. Every terminal surface states
|
|
80
|
+
* that the loop has stopped and where the follow-ups live. */
|
|
81
|
+
export function endStateCopy(status: string, slug: string): { text: string; tone: Tone } | undefined {
|
|
82
|
+
switch (status) {
|
|
83
|
+
case "complete":
|
|
84
|
+
return { text: `◎ ✓ complete — the loop has stopped. Follow-ups: the Goal panel (F9) and .opencode/goals/${slug}/`, tone: "success" }
|
|
85
|
+
case "failed":
|
|
86
|
+
return { text: `◎ ✗ failed — the loop has stopped. What happened: the Goal panel (F9) and .opencode/goals/${slug}/`, tone: "error" }
|
|
87
|
+
case "aborted":
|
|
88
|
+
return { text: `◎ ■ aborted — the loop has stopped. History stays in .opencode/goals/${slug}/`, tone: "muted" }
|
|
89
|
+
default:
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Shown above the composer only when the owner is needed. */
|
|
95
|
+
export function bannerText(view: GoalView): { text: string; tone: Tone } | undefined {
|
|
96
|
+
const st = statusOf(view.status)
|
|
97
|
+
// T071: terminal states say the loop stopped and where follow-ups live —
|
|
98
|
+
// silence after complete read as broken.
|
|
99
|
+
const terminal = endStateCopy(view.status, view.slug)
|
|
100
|
+
if (terminal && view.status !== "running") return terminal
|
|
101
|
+
if (view.awaitingUser && ["running", "waiting", "verifying"].includes(view.status)) return { text: "◎ The goal is waiting for your answer.", tone: "warning" }
|
|
102
|
+
switch (view.status) {
|
|
103
|
+
case "paused":
|
|
104
|
+
return { text: `◎ ${st.icon} Goal paused — ${view.reason ?? ""} · ${RESUME}`, tone: "warning" }
|
|
105
|
+
case "blocked":
|
|
106
|
+
return { text: `◎ ${st.icon} Goal blocked — ${view.blocker?.reason ?? view.reason ?? ""} · fix it, then ${RESUME}`, tone: "error" }
|
|
107
|
+
case "needs_review":
|
|
108
|
+
return { text: `◎ ${st.icon} Goal needs review — ${view.reason ?? ""} · /goal approve <id> or ${RESUME}`, tone: "warning" }
|
|
109
|
+
case "budget_limited":
|
|
110
|
+
return { text: `◎ ${st.icon} Goal stopped at its budget — raise budget in goal.yaml and /goal start, or /goal abort`, tone: "warning" }
|
|
111
|
+
default:
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function statusText(view: GoalView, now: number): string {
|
|
117
|
+
const st = statusOf(view.status)
|
|
118
|
+
const lines = [`${view.title} — ${st.icon} ${st.label}${view.reason ? ` (${view.reason})` : ""}`, `Outcome: ${view.outcome}`, usageLine(view, now), ""]
|
|
119
|
+
for (const c of view.criteria) lines.push(`${c.status === "pass" ? "✓" : c.status === "fail" ? "✗" : c.status === "claimed" ? "◐" : "·"} ${c.id}${c.invariant ? " (invariant)" : ""} ${c.statement}${c.detail && c.status !== "pass" ? `\n ${c.detail.split("\n")[0]}` : ""}`)
|
|
120
|
+
if (view.steps.length) {
|
|
121
|
+
lines.push("")
|
|
122
|
+
for (const s of view.steps) lines.push(`${s.status === "done" ? "✓" : s.status === "active" ? "▸" : "·"} ${s.id} ${s.title}`)
|
|
123
|
+
}
|
|
124
|
+
if (view.progress) lines.push("", `Last progress: ${view.progress.note}${view.progress.next ? ` → ${view.progress.next}` : ""}`)
|
|
125
|
+
if (view.verdict) lines.push("", `Last verdict (turn ${view.verdict.turn}): ${view.verdict.passed ? "passed" : "failed"}`, ...view.verdict.lines.map((l) => ` ${l.split("\n")[0]}`))
|
|
126
|
+
return lines.join("\n")
|
|
127
|
+
}
|