@litfamily/litopencode 1.0.18 → 1.0.20

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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README-Ko-KR.md +56 -86
  3. package/README.md +57 -86
  4. package/dist/activation-routing.js +3 -1
  5. package/dist/cli/args.js +5 -1
  6. package/dist/cli/auto-update.js +1 -1
  7. package/dist/cli/doctor.js +7 -5
  8. package/dist/cli/host-limits.js +2 -5
  9. package/dist/cli/install-config.d.ts +4 -0
  10. package/dist/cli/install-config.js +27 -1
  11. package/dist/cli/install-report.js +4 -1
  12. package/dist/cli/install.js +89 -28
  13. package/dist/cli/json.d.ts +1 -0
  14. package/dist/cli/json.js +10 -4
  15. package/dist/cli/jsonc.d.ts +17 -0
  16. package/dist/cli/jsonc.js +174 -0
  17. package/dist/cli/loop.d.ts +1 -1
  18. package/dist/cli/loop.js +81 -1
  19. package/dist/cli/motion-runtime.d.ts +13 -1
  20. package/dist/cli/motion-runtime.js +89 -2
  21. package/dist/cli/plugin-mutation.js +11 -6
  22. package/dist/cli/tui-config.d.ts +52 -0
  23. package/dist/cli/tui-config.js +198 -0
  24. package/dist/cli/types.d.ts +4 -0
  25. package/dist/cli.js +8 -3
  26. package/dist/deliverable-hedge-guard.js +2 -2
  27. package/dist/tui/mission-band.d.ts +47 -0
  28. package/dist/tui/mission-band.js +290 -0
  29. package/dist/tui/mission-model.d.ts +139 -0
  30. package/dist/tui/mission-model.js +318 -0
  31. package/dist/tui/mission-motion.d.ts +66 -0
  32. package/dist/tui/mission-motion.js +147 -0
  33. package/dist/tui/mission-skills.d.ts +38 -0
  34. package/dist/tui/mission-skills.js +81 -0
  35. package/dist/tui/mission-source.d.ts +68 -0
  36. package/dist/tui/mission-source.js +112 -0
  37. package/dist/tui/mission-text.d.ts +106 -0
  38. package/dist/tui/mission-text.js +613 -0
  39. package/dist/tui.d.ts +6 -0
  40. package/dist/tui.js +342 -0
  41. package/docs/assets/readme/badge-version.svg +1 -1
  42. package/docs/reference-Ko-KR.md +91 -1
  43. package/docs/reference.md +100 -1
  44. package/package.json +21 -2
  45. package/skills/lit-pptx/scripts/lib/template-registry.js +1 -1
  46. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +0 -6
  47. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +0 -6
  48. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +0 -6
  49. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +0 -6
  50. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +0 -6
  51. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +0 -6
  52. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +0 -6
  53. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +0 -6
  54. package/skills/managed-skill-manifest.json +9 -9
  55. package/tools/check-pack-payload.mjs +20 -5
  56. package/tools/version-manifests.json +18 -6
@@ -0,0 +1,290 @@
1
+ // The band: a status card in OpenCode's full-width app_bottom slot that stays on screen for the
2
+ // whole session, plus an opt-in detail card. Its border carries the LIT mark, the discipline and the
3
+ // phase; row one is the phase track, row two the skill chips, and the bottom border the live figures.
4
+ // It reuses the chip's pills, the panel's bar and the shared motion frame, so the chip, the panel and
5
+ // the band always move together.
6
+ import { ease, mixHex, motionTiming, seg } from "./mission-motion.js";
7
+ import { contextWarnPercent, missionStatus } from "./mission-model.js";
8
+ import { cellWidth, chipSparkCells, chipSparkWidth, fade, lineWidth, missionPanel, progressBar, shimmerAt, shine, statusSegments, stepPill, truncateCells } from "./mission-text.js";
9
+ /**
10
+ * Reads the band switches from the plugin's tui.json options: `band: false` turns the band off,
11
+ * `band: { card: true }` adds the detail card under the status card. Anything else keeps the defaults.
12
+ */
13
+ export function readBandOptions(options) {
14
+ const band = typeof options === "object" && options !== null ? options.band : undefined;
15
+ if (band === false)
16
+ return { enabled: false, card: false };
17
+ const card = typeof band === "object" && band !== null && band.card === true;
18
+ return { enabled: true, card };
19
+ }
20
+ const minimumText = 12;
21
+ function elapsedText(minutes) {
22
+ const total = Math.max(0, minutes ?? 0);
23
+ return total < 60 ? `${total}m` : `${Math.floor(total / 60)}h${String(total % 60).padStart(2, "0")}`;
24
+ }
25
+ /** The text after the bar: a warning (⚑ and words) replaces the next task; a finished mission shows COMPLETE. */
26
+ function trailing(view, frame, palette, room) {
27
+ if (view.complete) {
28
+ // Before COMPLETE pops in, the slot stays empty rather than flashing the header's "● active".
29
+ if (frame.celebrate !== undefined && frame.celebrate < motionTiming.popAt)
30
+ return [];
31
+ const status = statusSegments(view, frame, palette);
32
+ const lead = Math.max(1, 2 + status.offset);
33
+ return [{ text: " ".repeat(lead), fg: palette.muted }, ...status.segments];
34
+ }
35
+ const warning = view.warnings[0];
36
+ if (warning !== undefined) {
37
+ const color = warning.kind === "retry" ? mixHex(palette.warning, palette.error, 0.5) : palette.warning;
38
+ return [{ text: " ⚑ ", fg: color, bold: true }, { text: truncateCells(warning.text, room - 4), fg: color }];
39
+ }
40
+ if (view.next === undefined)
41
+ return [];
42
+ return [{ text: " ▸ ", fg: palette.muted }, { text: truncateCells(view.next, room - 4), fg: palette.text }];
43
+ }
44
+ /** Cuts or pads a line to exactly `width` cells, keeping wide glyphs whole. */
45
+ function fitLine(line, width, palette) {
46
+ const out = [];
47
+ let used = 0;
48
+ for (const segment of line) {
49
+ const room = width - used;
50
+ if (room <= 0)
51
+ break;
52
+ if (cellWidth(segment.text) <= room) {
53
+ out.push(segment);
54
+ used += cellWidth(segment.text);
55
+ continue;
56
+ }
57
+ let text = "";
58
+ let taken = 0;
59
+ for (const char of segment.text) {
60
+ const w = cellWidth(char);
61
+ if (taken + w > room)
62
+ break;
63
+ text += char;
64
+ taken += w;
65
+ }
66
+ if (text !== "")
67
+ out.push({ ...segment, text });
68
+ used += taken;
69
+ break;
70
+ }
71
+ if (used < width)
72
+ out.push({ text: " ".repeat(width - used), fg: palette.muted });
73
+ return out;
74
+ }
75
+ /** A border row: corner, optional left label, rule, optional right label, corner; undefined when it does not fit. */
76
+ function ruled(lead, left, right, tail, width, border) {
77
+ const space = { text: " ", fg: border };
78
+ const l = left.length > 0 ? [space, ...left, space] : [];
79
+ const r = right.length > 0 ? [space, ...right, space] : [];
80
+ const fill = width - cellWidth(lead) - lineWidth(l) - lineWidth(r) - cellWidth(tail);
81
+ if (fill < 1)
82
+ return undefined;
83
+ return [{ text: lead, fg: border }, ...l, { text: "─".repeat(fill), fg: border }, ...r, { text: tail, fg: border }];
84
+ }
85
+ function borderColor(view, frame, palette) {
86
+ if (!view.active)
87
+ return palette.borderActive;
88
+ const enter = frame.chipEnter ? ease.outCubic(seg(frame.t, 0, motionTiming.entry)) : 1;
89
+ const done = view.complete ? (frame.celebrate === undefined ? 1 : ease.inOutCubic(seg(frame.celebrate, 0, motionTiming.crossfade))) : 0;
90
+ return mixHex(palette.borderActive, mixHex(mixHex(palette.accent, palette.warning, frame.warn), palette.success, done), enter);
91
+ }
92
+ /** The phase on the right of the title: always a glyph plus words. */
93
+ function phaseLabel(view, frame, palette) {
94
+ if (!view.active)
95
+ return { text: "○ IDLE", fg: palette.muted };
96
+ const status = missionStatus(view);
97
+ if (status.tone === "success")
98
+ return { text: "✓ COMPLETE", fg: palette.success, bold: true };
99
+ if (status.tone === "warning")
100
+ return { text: `⚑ ${status.text}`, fg: mixHex(palette.info, palette.warning, Math.max(frame.warn, 0.5)), bold: true };
101
+ return { text: `● ${view.phase.toUpperCase()}`, fg: palette.info, bold: true };
102
+ }
103
+ function topLine(view, frame, palette, width, border) {
104
+ const brand = [{ text: "🔥 ", fg: palette.text }, { text: "LIT", fg: shine(palette.primary, palette, 0.5, shimmerAt(frame), 0.6), bold: true }];
105
+ const name = view.active ? view.discipline : "ready";
106
+ const right = [phaseLabel(view, frame, palette)];
107
+ const withName = (room) => [...brand, { text: " · ", fg: palette.muted }, { text: truncateCells(name, room), fg: view.active ? palette.text : palette.muted }];
108
+ const nameRoom = width - cellWidth("╭─") - lineWidth(withName(0)) - 2 - (lineWidth(right) + 2) - cellWidth("─╮") - 1;
109
+ const attempts = [];
110
+ if (nameRoom >= Math.min(4, cellWidth(name)))
111
+ attempts.push([withName(nameRoom), right]);
112
+ attempts.push([brand, right], [brand, []]);
113
+ for (const [left, label] of attempts) {
114
+ const line = ruled("╭─", left, label, "─╮", width, border);
115
+ if (line !== undefined)
116
+ return line;
117
+ }
118
+ return fitLine([{ text: "╭" + "─".repeat(Math.max(0, width)), fg: border }], width, palette);
119
+ }
120
+ /** The figures in the bottom border, most important first; the last ones go first when room runs out. */
121
+ function figureParts(view, figures, palette) {
122
+ const parts = [];
123
+ if (figures.contextPercent !== null) {
124
+ const percent = Math.round(figures.contextPercent);
125
+ parts.push({ text: `ctx ${percent}%`, fg: percent >= contextWarnPercent ? palette.warning : palette.muted });
126
+ }
127
+ if (view.active)
128
+ parts.push({ text: `⟳ ${elapsedText(view.elapsedMinutes)}${figures.inLane ? " · in lane" : ""}`, fg: palette.muted });
129
+ if (figures.branch !== undefined)
130
+ parts.push({ text: `⎇ ${truncateCells(figures.branch, 24)}`, fg: palette.muted });
131
+ return parts;
132
+ }
133
+ function bottomLine(view, figures, palette, width, border) {
134
+ const parts = figureParts(view, figures, palette);
135
+ for (let count = parts.length; count >= 0; count -= 1) {
136
+ const right = [];
137
+ parts.slice(0, count).forEach((part, index) => right.push(...(index === 0 ? [] : [{ text: " · ", fg: palette.muted }]), part));
138
+ const line = ruled("╰", [], right, "─╯", width, border);
139
+ if (line !== undefined)
140
+ return line;
141
+ }
142
+ return fitLine([{ text: "╰" + "─".repeat(Math.max(0, width)), fg: border }], width, palette);
143
+ }
144
+ function trackPills(view, frame, palette, tight) {
145
+ const alpha = (index) => (frame.chipEnter ? ease.outCubic(seg(frame.t, index * motionTiming.stagger, motionTiming.entry)) : 1);
146
+ if (tight) {
147
+ const current = view.steps.find((step) => step.state === "now" || step.state === "warn") ?? view.steps[view.complete ? 2 : view.active ? 1 : 0];
148
+ return [fade(stepPill(current, view, frame, palette), alpha(0), palette)];
149
+ }
150
+ const out = [];
151
+ view.steps.forEach((step, index) => {
152
+ if (index > 0)
153
+ out.push(fade({ text: " ━ ", fg: view.steps[index - 1].state === "done" ? palette.success : palette.borderSubtle }, alpha(index), palette));
154
+ out.push(fade(stepPill(step, view, frame, palette), alpha(index), palette));
155
+ });
156
+ return out;
157
+ }
158
+ /** The text after the track in an idle session: the project's last goal, or how to start. */
159
+ function idleText(last, palette, room) {
160
+ if (last === undefined || last.title === "")
161
+ return [{ text: " ", fg: palette.muted }, { text: truncateCells("add lit to a prompt to start", room - 2), fg: palette.muted }];
162
+ const mark = last.complete ? { text: ` ✓ ${last.passed}/${last.total}`, fg: palette.success } : { text: ` ● ${last.passed}/${last.total}`, fg: palette.info };
163
+ const head = { text: " last: ", fg: palette.muted };
164
+ return [head, { text: truncateCells(last.title, room - cellWidth(head.text) - cellWidth(mark.text)), fg: palette.text }, mark];
165
+ }
166
+ // Shedding order as the row narrows: the next-task text, bar width, the bar, the criteria count and
167
+ // then the step labels. Each plan is tried in turn and the first that fits wins.
168
+ const trackPlans = [
169
+ { tight: false, bar: 16, criteria: true, text: true },
170
+ { tight: false, bar: 16, criteria: true, text: false },
171
+ { tight: false, bar: 8, criteria: true, text: false },
172
+ { tight: false, bar: 0, criteria: true, text: false },
173
+ { tight: false, bar: 0, criteria: false, text: false },
174
+ { tight: true, bar: 0, criteria: false, text: false }
175
+ ];
176
+ function trackRow(view, frame, palette, width, last) {
177
+ const sweep = shimmerAt(frame);
178
+ const gap = { text: " ", fg: palette.muted };
179
+ for (const plan of trackPlans) {
180
+ const pills = trackPills(view, frame, palette, plan.tight);
181
+ const bar = plan.bar > 0 && view.progress !== undefined ? [gap, ...progressBar(frame.barRatio, plan.bar, palette, sweep)] : [];
182
+ const goal = view.goal;
183
+ const criteria = plan.criteria && goal !== undefined ? [gap, { text: `${goal.passed}/${goal.total} criteria`, fg: goal.total > 0 && goal.passed === goal.total ? palette.success : palette.muted }] : [];
184
+ const fixed = lineWidth(pills) + lineWidth(bar) + lineWidth(criteria);
185
+ if (fixed > width)
186
+ continue;
187
+ const room = width - fixed;
188
+ const after = plan.text ? (view.active ? trailing(view, frame, palette, room) : idleText(last, palette, room)) : [];
189
+ const keepText = plan.text && after.length > 0 && (view.complete ? lineWidth(after) <= room : room >= minimumText);
190
+ if (plan.text && after.length > 0 && !keepText)
191
+ continue;
192
+ const body = [...pills, ...bar, ...criteria, ...(keepText ? after : [])];
193
+ // Sparks sit after the text, in the free space, so the pills never shift while they play.
194
+ const sparks = frame.celebrate !== undefined && lineWidth(body) + chipSparkWidth <= width ? chipSparkCells(frame.celebrate, palette) ?? [] : [];
195
+ return fitLine([...body, ...sparks], width, palette);
196
+ }
197
+ return fitLine(trackPills(view, frame, palette, true), width, palette);
198
+ }
199
+ function chipSegments(chip, shimmer, palette) {
200
+ const text = ` ${chip.name} `;
201
+ if (!chip.current)
202
+ return [{ text, fg: palette.muted, ...(palette.element === undefined ? {} : { bg: palette.element }) }];
203
+ const sweep = shimmer?.names.includes(chip.name) === true ? shimmer.sweep : -1;
204
+ if (sweep < 0)
205
+ return [{ text, fg: palette.onFill, bg: palette.accent, bold: true }];
206
+ const chars = [...text];
207
+ return chars.map((char, index) => ({ text: char, fg: palette.onFill, bg: shine(palette.accent, palette, index / (chars.length - 1), sweep, 0.3), bold: true }));
208
+ }
209
+ /**
210
+ * The skill chips row, exactly `width` cells: bright accent pills for the current request first, then
211
+ * dim recent names, and `+N` for the ones that do not fit. The label goes before any chip does.
212
+ */
213
+ export function skillRow(chips, shimmer, palette, width) {
214
+ const label = { text: "skills ", fg: palette.muted };
215
+ if (chips.length === 0)
216
+ return fitLine([label, { text: " none yet", fg: palette.muted }], width, palette);
217
+ const pills = chips.map((chip) => chipSegments(chip, shimmer, palette));
218
+ const space = { text: " ", fg: palette.muted };
219
+ for (const lead of [[label], []]) {
220
+ const all = lineWidth(lead) + pills.reduce((total, pill, index) => total + lineWidth(pill) + (index === 0 ? 0 : 1), 0);
221
+ let shown = pills.length;
222
+ if (all > width) {
223
+ let used = lineWidth(lead);
224
+ shown = 0;
225
+ for (const pill of pills) {
226
+ const next = used + (shown === 0 ? 0 : 1) + lineWidth(pill);
227
+ if (next + cellWidth(` +${pills.length - shown - 1}`) > width)
228
+ break;
229
+ used = next;
230
+ shown += 1;
231
+ }
232
+ }
233
+ if (shown === 0 && lead.length > 0)
234
+ continue;
235
+ const out = [...lead];
236
+ pills.slice(0, shown).forEach((pill, index) => out.push(...(index === 0 ? [] : [space]), ...pill));
237
+ if (shown < pills.length)
238
+ out.push({ text: `${shown === 0 ? "" : " "}+${pills.length - shown}`, fg: palette.muted });
239
+ return fitLine(out, width, palette);
240
+ }
241
+ return fitLine([{ text: `+${pills.length}`, fg: palette.muted }], width, palette);
242
+ }
243
+ /**
244
+ * The status card: four rows of exactly `width` cells whether the session is idle or working, so
245
+ * the prompt never moves. Each row sheds its own low-priority parts as the terminal narrows.
246
+ */
247
+ export function statusCard(input, palette, width) {
248
+ const { view, frame } = input;
249
+ const border = borderColor(view, frame, palette);
250
+ const inner = width - 4;
251
+ const row = (content) => (inner < 0 ? fitLine([], width, palette) : [{ text: "│ ", fg: border }, ...fitLine(content, inner, palette), { text: " │", fg: border }]);
252
+ return [
253
+ topLine(view, frame, palette, width, border),
254
+ row(trackRow(view, frame, palette, Math.max(0, inner), input.last)),
255
+ row(skillRow(input.chips, input.shimmer, palette, Math.max(0, inner))),
256
+ bottomLine(view, input.figures, palette, width, border)
257
+ ];
258
+ }
259
+ const cardRows = 4;
260
+ /**
261
+ * The opt-in detail card under the status card. It keeps a fixed four rows whenever a goal exists, warnings
262
+ * first, so turning a warning on or off never moves the prompt.
263
+ */
264
+ export function bandCard(view, frame, palette, width) {
265
+ if (!view.active || (view.goal === undefined && view.warnings.length === 0))
266
+ return undefined;
267
+ const panel = missionPanel(view, frame, palette);
268
+ if (panel === undefined)
269
+ return undefined;
270
+ const inner = Math.max(8, width - 4);
271
+ const rows = [];
272
+ for (const warning of view.warnings.slice(0, 2)) {
273
+ rows.push([{ text: "⚑ ", fg: palette.warning, bold: true }, { text: truncateCells(warning.text, inner - 2), fg: palette.warning }]);
274
+ }
275
+ if (view.next !== undefined && !view.complete)
276
+ rows.push([{ text: "next ", fg: palette.muted }, { text: truncateCells(`▸ ${view.next}`, inner - 6), fg: palette.text }]);
277
+ if (view.goal !== undefined) {
278
+ const criteria = view.goal.criteria.map((criterion) => `${criterion.status === "pass" ? "✓" : criterion.status === "fail" ? "✗" : "○"}${criterion.id}`).join(" ");
279
+ const title = truncateCells(view.goal.title, Math.max(4, inner - 8 - cellWidth(criteria)));
280
+ rows.push([
281
+ { text: "goal ", fg: palette.muted },
282
+ { text: title, fg: palette.text, bold: true },
283
+ { text: " ", fg: palette.muted },
284
+ { text: truncateCells(criteria, Math.max(0, inner - 8 - cellWidth(title))), fg: palette.muted }
285
+ ]);
286
+ }
287
+ while (rows.length < cardRows)
288
+ rows.push([]);
289
+ return { title: panel.title, border: panel.border, rows: rows.slice(0, cardRows) };
290
+ }
@@ -0,0 +1,139 @@
1
+ export type LitMark = {
2
+ readonly at: number;
3
+ readonly mode?: string;
4
+ readonly command?: string;
5
+ readonly operation?: string;
6
+ readonly blocked?: boolean;
7
+ };
8
+ export type TodoInput = {
9
+ readonly content: string;
10
+ readonly status: string;
11
+ };
12
+ export type MissionInput = {
13
+ readonly marks: readonly LitMark[];
14
+ readonly agent?: string;
15
+ readonly todos: readonly TodoInput[];
16
+ readonly ledger?: unknown;
17
+ readonly sessionStatus?: string;
18
+ readonly retryMessage?: string;
19
+ readonly pendingPermissions?: number;
20
+ readonly pendingQuestions?: number;
21
+ readonly contextPercent?: number | null;
22
+ readonly now: number;
23
+ };
24
+ export type StepId = "plan" | "work" | "review";
25
+ export type StepState = "done" | "skip" | "now" | "todo" | "warn";
26
+ export type MissionStep = {
27
+ readonly id: StepId;
28
+ readonly state: StepState;
29
+ };
30
+ export type MissionWarning = {
31
+ readonly kind: "blocker" | "decision" | "retry" | "permission" | "question" | "context" | "plan";
32
+ readonly text: string;
33
+ };
34
+ export type MissionCriterion = {
35
+ readonly id: string;
36
+ readonly status: string;
37
+ };
38
+ export type MissionGoal = {
39
+ readonly title: string;
40
+ readonly status: string;
41
+ readonly passed: number;
42
+ readonly total: number;
43
+ readonly criteria: readonly MissionCriterion[];
44
+ };
45
+ export type MissionEvent = {
46
+ readonly ago: string;
47
+ readonly kind: string;
48
+ readonly label: string;
49
+ };
50
+ export type MissionView = {
51
+ readonly active: boolean;
52
+ readonly discipline: string;
53
+ readonly phase: StepId | "done";
54
+ readonly steps: readonly MissionStep[];
55
+ readonly progress?: {
56
+ readonly done: number;
57
+ readonly total: number;
58
+ };
59
+ readonly next?: string;
60
+ readonly goal?: MissionGoal;
61
+ readonly blockers: readonly {
62
+ readonly id: string;
63
+ readonly detail: string;
64
+ readonly needsUserDecision: boolean;
65
+ }[];
66
+ readonly warnings: readonly MissionWarning[];
67
+ readonly timeline: readonly MissionEvent[];
68
+ readonly planSaved: boolean;
69
+ readonly complete: boolean;
70
+ /** Whole minutes since this session's first lit turn; only an active view carries it. */
71
+ readonly elapsedMinutes?: number;
72
+ };
73
+ export type MissionNotice = {
74
+ readonly variant: "info" | "success" | "warning" | "error";
75
+ readonly title: string;
76
+ readonly message: string;
77
+ readonly celebrate?: boolean;
78
+ };
79
+ export declare const contextWarnPercent = 75;
80
+ export declare const idleMission: MissionView;
81
+ /** Reads one part's `metadata.litopencode` mark. Anything without that shape is not a mark. */
82
+ export declare function readLitMark(metadata: unknown, at: number): LitMark | undefined;
83
+ type LedgerCriterion = {
84
+ readonly id: string;
85
+ readonly scenario: string;
86
+ readonly status: string;
87
+ readonly evidence: readonly {
88
+ readonly kind: string;
89
+ readonly ref: string;
90
+ readonly at: string;
91
+ }[];
92
+ };
93
+ type LedgerGoal = {
94
+ readonly title: string;
95
+ readonly status: string;
96
+ readonly createdAt: string;
97
+ readonly updatedAt: string;
98
+ readonly criteria: readonly LedgerCriterion[];
99
+ readonly checkpoints: readonly {
100
+ readonly summary: string;
101
+ readonly at: string;
102
+ }[];
103
+ readonly reviewBlockers: readonly {
104
+ readonly id: string;
105
+ readonly detail: string;
106
+ readonly resolved: boolean;
107
+ readonly needsUserDecision: boolean;
108
+ }[];
109
+ };
110
+ /** Lenient read of goals.json: the active goal, or undefined when the file is absent or not the ledger shape. */
111
+ export declare function readActiveGoal(raw: unknown): LedgerGoal | undefined;
112
+ export declare function formatAgo(at: string, now: number): string;
113
+ export declare function buildMissionView(input: MissionInput): MissionView;
114
+ /**
115
+ * OpenCode shows one toast at a time, so several gate events from one refresh collapse into the most
116
+ * urgent one (completion first), with the rest counted in its message. The panel still lists them all.
117
+ */
118
+ export declare function leadingNotice(notices: readonly MissionNotice[]): MissionNotice | undefined;
119
+ /** The gate events between two views. A first view never toasts its whole history. */
120
+ export declare function missionNotices(prev: MissionView | undefined, next: MissionView): readonly MissionNotice[];
121
+ /** The short status beside the header: always a glyph plus words, never colour alone. */
122
+ export declare function missionStatus(view: MissionView): {
123
+ readonly glyph: string;
124
+ readonly text: string;
125
+ readonly tone: "info" | "warning" | "success";
126
+ };
127
+ /**
128
+ * Stable keys for the panel rows a view shows. Motion compares two views' keys to decide which rows
129
+ * slide in, so a new goal animates its own rows without replaying the header.
130
+ */
131
+ export declare function missionRowKeys(view: MissionView): readonly string[];
132
+ /** The project's last goal for the home line, or undefined when the ledger holds none. */
133
+ export declare function lastMission(raw: unknown): {
134
+ readonly title: string;
135
+ readonly passed: number;
136
+ readonly total: number;
137
+ readonly complete: boolean;
138
+ } | undefined;
139
+ export {};