@litfamily/litopencode 1.0.20 → 1.0.22

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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/dist/agents/registry.js +2 -2
  5. package/dist/cli/doctor.d.ts +5 -0
  6. package/dist/cli/doctor.js +27 -6
  7. package/dist/cli/install-config.d.ts +6 -0
  8. package/dist/cli/install-config.js +32 -2
  9. package/dist/cli/install.js +4 -1
  10. package/dist/cli/model-catalog.d.ts +6 -1
  11. package/dist/cli/model-catalog.js +32 -15
  12. package/dist/cli/model-routing.js +14 -9
  13. package/dist/cli/types.d.ts +1 -1
  14. package/dist/config.d.ts +2 -0
  15. package/dist/config.js +10 -2
  16. package/dist/model-route-policy.d.ts +9 -1
  17. package/dist/model-route-policy.js +46 -47
  18. package/dist/tui/mission-band.d.ts +13 -0
  19. package/dist/tui/mission-band.js +63 -25
  20. package/dist/tui/mission-model.d.ts +69 -3
  21. package/dist/tui/mission-model.js +155 -29
  22. package/dist/tui/mission-motion.d.ts +10 -1
  23. package/dist/tui/mission-motion.js +17 -7
  24. package/dist/tui/mission-source.d.ts +36 -6
  25. package/dist/tui/mission-source.js +167 -13
  26. package/dist/tui/mission-text.d.ts +34 -9
  27. package/dist/tui/mission-text.js +169 -74
  28. package/dist/tui.js +38 -5
  29. package/docs/assets/readme/badge-version.svg +1 -1
  30. package/docs/migration.md +1 -1
  31. package/docs/reference-Ko-KR.md +81 -26
  32. package/docs/reference.md +92 -28
  33. package/package.json +1 -1
  34. package/skills/lit-pptx/package-lock.json +167 -119
  35. package/skills/lit-pptx/package.json +1 -1
  36. package/skills/managed-skill-manifest.json +4 -4
  37. package/skills/readme-studio/templates/hyperframes/package-lock.json +169 -121
  38. package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
  39. package/tools/check-pack-payload.mjs +2 -2
@@ -1,8 +1,14 @@
1
1
  import { effectiveAgentConfig } from "./config.js";
2
- import { defaultOpenAiRoutes, newGenerationOpenAiModels, openAiModelEfforts, previousGenerationOpenAiModels, previousGpt6OpenAiModels, stripFastSuffix } from "./cli/model-catalog.js";
3
- const safeLunaEfforts = new Set(openAiModelEfforts(previousGenerationOpenAiModels.luna));
4
- const safeTerraEfforts = new Set(openAiModelEfforts(previousGenerationOpenAiModels.terra));
2
+ import { defaultOpenAiRoutes, newGenerationOpenAiModels, openAiModelEfforts, previousGenerationOpenAiModels, previousGpt6OpenAiModels, replacedRetiredEffort, stripFastSuffix } from "./cli/model-catalog.js";
5
3
  const safeAstraEfforts = new Set(openAiModelEfforts(defaultOpenAiRoutes.lead.model));
4
+ // The managed rows whose effort is checked; other GPT-5.6 ids stay user-owned as before.
5
+ const guardedModels = new Set([
6
+ newGenerationOpenAiModels.sol,
7
+ previousGpt6OpenAiModels.sol,
8
+ newGenerationOpenAiModels.luna,
9
+ previousGenerationOpenAiModels.luna,
10
+ previousGenerationOpenAiModels.terra
11
+ ]);
6
12
  function dispatchedModel(route) {
7
13
  if (route.model === undefined)
8
14
  return undefined;
@@ -53,41 +59,17 @@ export function diagnoseModelRoutes(routes) {
53
59
  }
54
60
  continue;
55
61
  }
56
- if (modelId === newGenerationOpenAiModels.sol || modelId === previousGpt6OpenAiModels.sol || modelId === newGenerationOpenAiModels.luna) {
57
- const supported = new Set(openAiModelEfforts(modelId));
58
- const variantUnsafe = route.variant !== undefined && !supported.has(route.variant);
59
- const reasoningUnsafe = route.reasoningEffort !== undefined && !supported.has(route.reasoningEffort);
60
- if (variantUnsafe || reasoningUnsafe || (route.variant === undefined && route.reasoningEffort === undefined)) {
61
- diagnostics.push({ agentId, code: "model_effort_unsupported", model, effort });
62
- }
63
- if (route.variant !== undefined && route.reasoningEffort !== undefined && route.variant !== route.reasoningEffort) {
64
- diagnostics.push({ agentId, code: "model_effort_conflict", model, effort });
65
- }
66
- continue;
67
- }
68
- if (modelId === previousGenerationOpenAiModels.luna) {
69
- const hasXhigh = route.variant === "xhigh" || route.reasoningEffort === "xhigh";
70
- if ((route.variant !== undefined && route.variant !== "xhigh" && !safeLunaEfforts.has(route.variant)) ||
71
- (route.reasoningEffort !== undefined && route.reasoningEffort !== "xhigh" && !safeLunaEfforts.has(route.reasoningEffort)) ||
72
- (route.variant === undefined && route.reasoningEffort === undefined)) {
73
- diagnostics.push({ agentId, code: "luna_effort_below_high", model, effort });
74
- }
75
- if (hasXhigh)
76
- diagnostics.push({ agentId, code: "luna_xhigh_forbidden", model, effort });
77
- if (route.variant !== undefined && route.reasoningEffort !== undefined && route.variant !== route.reasoningEffort) {
78
- diagnostics.push({ agentId, code: "luna_effort_conflict", model, effort });
79
- }
62
+ // Each guarded row accepts exactly the efforts OpenCode offers for it.
63
+ if (!guardedModels.has(modelId))
80
64
  continue;
81
- }
82
- if (modelId !== previousGenerationOpenAiModels.terra)
83
- continue;
84
- if ((route.variant !== undefined && !safeTerraEfforts.has(route.variant)) ||
85
- (route.reasoningEffort !== undefined && !safeTerraEfforts.has(route.reasoningEffort)) ||
86
- (route.variant === undefined && route.reasoningEffort === undefined)) {
87
- diagnostics.push({ agentId, code: "terra_effort_below_high", model, effort });
65
+ const supported = new Set(openAiModelEfforts(modelId));
66
+ const variantUnsafe = route.variant !== undefined && !supported.has(route.variant);
67
+ const reasoningUnsafe = route.reasoningEffort !== undefined && !supported.has(route.reasoningEffort);
68
+ if (variantUnsafe || reasoningUnsafe || (route.variant === undefined && route.reasoningEffort === undefined)) {
69
+ diagnostics.push({ agentId, code: "model_effort_unsupported", model, effort });
88
70
  }
89
71
  if (route.variant !== undefined && route.reasoningEffort !== undefined && route.variant !== route.reasoningEffort) {
90
- diagnostics.push({ agentId, code: "terra_effort_conflict", model, effort });
72
+ diagnostics.push({ agentId, code: "model_effort_conflict", model, effort });
91
73
  }
92
74
  }
93
75
  return diagnostics;
@@ -114,22 +96,12 @@ export class UnsafeModelRouteError extends Error {
114
96
  const summary = diagnostics
115
97
  .map((diagnostic) => {
116
98
  switch (diagnostic.code) {
117
- case "luna_effort_below_high":
118
- return `${diagnostic.agentId}: Luna requires high or max (${diagnostic.model}, ${diagnostic.effort})`;
119
- case "luna_xhigh_forbidden":
120
- return `${diagnostic.agentId}: Luna plus xhigh is forbidden (${diagnostic.model}, ${diagnostic.effort})`;
121
- case "luna_effort_conflict":
122
- return `${diagnostic.agentId}: Luna effort fields conflict (${diagnostic.model}, ${diagnostic.effort})`;
123
- case "terra_effort_below_high":
124
- return `${diagnostic.agentId}: TERRA requires high or xhigh (${diagnostic.model}, ${diagnostic.effort})`;
125
- case "terra_effort_conflict":
126
- return `${diagnostic.agentId}: TERRA effort fields conflict (${diagnostic.model}, ${diagnostic.effort})`;
127
99
  case "astra_effort_unsupported":
128
- return `${diagnostic.agentId}: Astra requires low, medium, high, xhigh, or max (${diagnostic.model}, ${diagnostic.effort})`;
100
+ return `${diagnostic.agentId}: Astra requires ${[...safeAstraEfforts].join(", ")} (${diagnostic.model}, ${diagnostic.effort})`;
129
101
  case "astra_effort_conflict":
130
102
  return `${diagnostic.agentId}: Astra effort fields conflict (${diagnostic.model}, ${diagnostic.effort})`;
131
103
  case "model_effort_unsupported":
132
- return `${diagnostic.agentId}: ${diagnostic.model} accepts a supported catalog effort (${diagnostic.model}, ${diagnostic.effort})`;
104
+ return `${diagnostic.agentId}: ${diagnostic.model} accepts ${openAiModelEfforts(unqualifiedModel(diagnostic.model)).join(", ")} (${diagnostic.model}, ${diagnostic.effort})`;
133
105
  case "model_effort_conflict":
134
106
  return `${diagnostic.agentId}: ${diagnostic.model} effort fields conflict (${diagnostic.model}, ${diagnostic.effort})`;
135
107
  }
@@ -144,3 +116,30 @@ export function assertSafeEffectiveAgentRoutes(config, agentIds) {
144
116
  if (diagnostics.length > 0)
145
117
  throw new UnsafeModelRouteError(diagnostics);
146
118
  }
119
+ function isRouteRecord(value) {
120
+ return typeof value === "object" && value !== null && !Array.isArray(value);
121
+ }
122
+ /** The saved routes in a litopencode.json object that still name an effort OpenCode no longer offers. */
123
+ export function retiredEffortRoutes(config) {
124
+ const found = [];
125
+ if (!isRouteRecord(config))
126
+ return found;
127
+ for (const section of ["categories", "agents"]) {
128
+ const entries = config[section];
129
+ if (!isRouteRecord(entries))
130
+ continue;
131
+ for (const [id, route] of Object.entries(entries)) {
132
+ if (!isRouteRecord(route) || typeof route.model !== "string")
133
+ continue;
134
+ const provider = typeof route.provider === "string" ? route.provider : undefined;
135
+ const effort = (value) => (typeof value === "string" ? value : undefined);
136
+ if (replacedRetiredEffort(provider, route.model, effort(route.variant)) === undefined && replacedRetiredEffort(provider, route.model, effort(route.reasoningEffort)) === undefined)
137
+ continue;
138
+ found.push({ section, id, model: route.model.slice(route.model.indexOf("/") + 1) });
139
+ }
140
+ }
141
+ return found;
142
+ }
143
+ export function retiredEffortWarning(route) {
144
+ return `ultra is no longer offered by OpenCode for ${route.model} (${route.section}.${route.id}); the next install moves it to max.`;
145
+ }
@@ -11,6 +11,17 @@ export type BandOptions = {
11
11
  * `band: { card: true }` adds the detail card under the status card. Anything else keeps the defaults.
12
12
  */
13
13
  export declare function readBandOptions(options: unknown): BandOptions;
14
+ export type NotifyOptions = {
15
+ readonly enabled: boolean;
16
+ readonly desktop: boolean;
17
+ readonly sound: boolean;
18
+ };
19
+ /**
20
+ * Reads the finish alert switch from the plugin's tui.json options. It is off unless `notify` is true
21
+ * (desktop notification and the host's `done` sound) or an object that turns either part off with
22
+ * `desktop: false` or `sound: false`.
23
+ */
24
+ export declare function readNotifyOptions(options: unknown): NotifyOptions;
14
25
  /**
15
26
  * The skill chips row, exactly `width` cells: bright accent pills for the current request first, then
16
27
  * dim recent names, and `+N` for the ones that do not fit. The label goes before any chip does.
@@ -25,6 +36,8 @@ export type CardInputs = {
25
36
  readonly view: MissionView;
26
37
  readonly frame: MotionFrame;
27
38
  readonly chips: readonly SkillChip[];
39
+ /** Whether the session's turn is running; false lets a lit card rest at idle. Unknown keeps it working. */
40
+ readonly busy?: boolean;
28
41
  readonly shimmer?: ChipShimmer;
29
42
  readonly figures: CardFigures;
30
43
  /** The project's last goal, shown on the track while the session has no lit mission. */
@@ -4,8 +4,8 @@
4
4
  // It reuses the chip's pills, the panel's bar and the shared motion frame, so the chip, the panel and
5
5
  // the band always move together.
6
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";
7
+ import { contextWarnPercent, currentStepIndex, missionStatus, stepName } from "./mission-model.js";
8
+ import { cellWidth, chipSparkCells, chipSparkWidth, crumb, fade, lineWidth, linkColor, liveSegments, missionPanel, progressBar, shimmerAt, shine, stageFlash, statusSegments, truncateCells } from "./mission-text.js";
9
9
  /**
10
10
  * Reads the band switches from the plugin's tui.json options: `band: false` turns the band off,
11
11
  * `band: { card: true }` adds the detail card under the status card. Anything else keeps the defaults.
@@ -17,12 +17,31 @@ export function readBandOptions(options) {
17
17
  const card = typeof band === "object" && band !== null && band.card === true;
18
18
  return { enabled: true, card };
19
19
  }
20
+ /**
21
+ * Reads the finish alert switch from the plugin's tui.json options. It is off unless `notify` is true
22
+ * (desktop notification and the host's `done` sound) or an object that turns either part off with
23
+ * `desktop: false` or `sound: false`.
24
+ */
25
+ export function readNotifyOptions(options) {
26
+ const notify = typeof options === "object" && options !== null ? options.notify : undefined;
27
+ if (notify === true)
28
+ return { enabled: true, desktop: true, sound: true };
29
+ if (typeof notify !== "object" || notify === null)
30
+ return { enabled: false, desktop: false, sound: false };
31
+ const record = notify;
32
+ const desktop = record.desktop !== false;
33
+ const sound = record.sound !== false;
34
+ return { enabled: desktop || sound, desktop, sound };
35
+ }
20
36
  const minimumText = 12;
21
37
  function elapsedText(minutes) {
22
38
  const total = Math.max(0, minutes ?? 0);
23
39
  return total < 60 ? `${total}m` : `${Math.floor(total / 60)}h${String(total % 60).padStart(2, "0")}`;
24
40
  }
25
- /** The text after the bar: a warning (⚑ and words) replaces the next task; a finished mission shows COMPLETE. */
41
+ /**
42
+ * The text after the bar: a warning (⚑ and words) replaces the live step and the next task; a finished
43
+ * mission shows COMPLETE. Otherwise the live step, then the next task when it still has room.
44
+ */
26
45
  function trailing(view, frame, palette, room) {
27
46
  if (view.complete) {
28
47
  // Before COMPLETE pops in, the slot stays empty rather than flashing the header's "● active".
@@ -37,9 +56,12 @@ function trailing(view, frame, palette, room) {
37
56
  const color = warning.kind === "retry" ? mixHex(palette.warning, palette.error, 0.5) : palette.warning;
38
57
  return [{ text: " ⚑ ", fg: color, bold: true }, { text: truncateCells(warning.text, room - 4), fg: color }];
39
58
  }
40
- if (view.next === undefined)
41
- return [];
42
- return [{ text: " ▸ ", fg: palette.muted }, { text: truncateCells(view.next, room - 4), fg: palette.text }];
59
+ const live = liveSegments(view, frame, palette, Math.min(28, room - 2));
60
+ const lead = live.length === 0 ? [] : [{ text: " ", fg: palette.muted }, ...live];
61
+ const left = room - lineWidth(lead);
62
+ if (view.next === undefined || left < minimumText)
63
+ return lead;
64
+ return [...lead, { text: " ▸ ", fg: palette.muted }, { text: truncateCells(view.next, left - 4), fg: palette.text }];
43
65
  }
44
66
  /** Cuts or pads a line to exactly `width` cells, keeping wide glyphs whole. */
45
67
  function fitLine(line, width, palette) {
@@ -82,28 +104,35 @@ function ruled(lead, left, right, tail, width, border) {
82
104
  return undefined;
83
105
  return [{ text: lead, fg: border }, ...l, { text: "─".repeat(fill), fg: border }, ...r, { text: tail, fg: border }];
84
106
  }
85
- function borderColor(view, frame, palette) {
86
- if (!view.active)
107
+ /**
108
+ * A lit session whose turn has ended rests: nothing is running, so the card stops showing a lit step.
109
+ * A finished goal and an open warning still show, since they are news after the turn.
110
+ */
111
+ function resting(view, busy) {
112
+ return view.active && busy === false && !view.complete && view.warnings.length === 0;
113
+ }
114
+ function borderColor(view, frame, palette, rest) {
115
+ if (!view.active || rest)
87
116
  return palette.borderActive;
88
117
  const enter = frame.chipEnter ? ease.outCubic(seg(frame.t, 0, motionTiming.entry)) : 1;
89
118
  const done = view.complete ? (frame.celebrate === undefined ? 1 : ease.inOutCubic(seg(frame.celebrate, 0, motionTiming.crossfade))) : 0;
90
119
  return mixHex(palette.borderActive, mixHex(mixHex(palette.accent, palette.warning, frame.warn), palette.success, done), enter);
91
120
  }
92
121
  /** The phase on the right of the title: always a glyph plus words. */
93
- function phaseLabel(view, frame, palette) {
94
- if (!view.active)
122
+ function phaseLabel(view, frame, palette, rest) {
123
+ if (!view.active || rest)
95
124
  return { text: "○ IDLE", fg: palette.muted };
96
125
  const status = missionStatus(view);
97
126
  if (status.tone === "success")
98
127
  return { text: "✓ COMPLETE", fg: palette.success, bold: true };
99
128
  if (status.tone === "warning")
100
129
  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 };
130
+ return { text: `● ${stepName(view.phase, view).toUpperCase()}`, fg: palette.info, bold: true };
102
131
  }
103
- function topLine(view, frame, palette, width, border) {
132
+ function topLine(view, frame, palette, width, border, rest) {
104
133
  const brand = [{ text: "🔥 ", fg: palette.text }, { text: "LIT", fg: shine(palette.primary, palette, 0.5, shimmerAt(frame), 0.6), bold: true }];
105
134
  const name = view.active ? view.discipline : "ready";
106
- const right = [phaseLabel(view, frame, palette)];
135
+ const right = [phaseLabel(view, frame, palette, rest)];
107
136
  const withName = (room) => [...brand, { text: " · ", fg: palette.muted }, { text: truncateCells(name, room), fg: view.active ? palette.text : palette.muted }];
108
137
  const nameRoom = width - cellWidth("╭─") - lineWidth(withName(0)) - 2 - (lineWidth(right) + 2) - cellWidth("─╮") - 1;
109
138
  const attempts = [];
@@ -141,17 +170,22 @@ function bottomLine(view, figures, palette, width, border) {
141
170
  }
142
171
  return fitLine([{ text: "╰" + "─".repeat(Math.max(0, width)), fg: border }], width, palette);
143
172
  }
173
+ /** The whole stage map as a breadcrumb, `plan ✓ ━ WORK 1/3 ─ review ─ done`, or only the running stage when tight. */
144
174
  function trackPills(view, frame, palette, tight) {
145
175
  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
- }
176
+ const reached = currentStepIndex(view);
177
+ const current = Math.min(view.steps.length - 1, Math.max(0, reached));
178
+ const flash = stageFlash(frame);
179
+ if (tight)
180
+ return crumb(view, current, frame, palette, 0, flash).map((segment) => fade(segment, alpha(0), palette));
150
181
  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));
182
+ view.steps.forEach((_step, index) => {
183
+ if (index > 0) {
184
+ const color = linkColor(view, index, frame, palette, palette.borderSubtle);
185
+ out.push(fade({ text: color === palette.borderSubtle ? " ─ " : " ━ ", fg: color }, alpha(index), palette));
186
+ }
187
+ for (const segment of crumb(view, index, frame, palette, 0, index === current && reached >= 0 ? flash : -1))
188
+ out.push(fade(segment, alpha(index), palette));
155
189
  });
156
190
  return out;
157
191
  }
@@ -179,7 +213,8 @@ function trackRow(view, frame, palette, width, last) {
179
213
  for (const plan of trackPlans) {
180
214
  const pills = trackPills(view, frame, palette, plan.tight);
181
215
  const bar = plan.bar > 0 && view.progress !== undefined ? [gap, ...progressBar(frame.barRatio, plan.bar, palette, sweep)] : [];
182
- const goal = view.goal;
216
+ // In the criterion loop the stage itself counts criteria, so the count is not repeated.
217
+ const goal = view.family === "loop" ? undefined : view.goal;
183
218
  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
219
  const fixed = lineWidth(pills) + lineWidth(bar) + lineWidth(criteria);
185
220
  if (fixed > width)
@@ -245,12 +280,15 @@ export function skillRow(chips, shimmer, palette, width) {
245
280
  * the prompt never moves. Each row sheds its own low-priority parts as the terminal narrows.
246
281
  */
247
282
  export function statusCard(input, palette, width) {
248
- const { view, frame } = input;
249
- const border = borderColor(view, frame, palette);
283
+ const { frame } = input;
284
+ const rest = resting(input.view, input.busy);
285
+ // At rest the track keeps what is done and unlights the step that was running.
286
+ const view = rest ? { ...input.view, steps: input.view.steps.map((step) => (step.state === "now" ? { ...step, state: "todo" } : step)) } : input.view;
287
+ const border = borderColor(view, frame, palette, rest);
250
288
  const inner = width - 4;
251
289
  const row = (content) => (inner < 0 ? fitLine([], width, palette) : [{ text: "│ ", fg: border }, ...fitLine(content, inner, palette), { text: " │", fg: border }]);
252
290
  return [
253
- topLine(view, frame, palette, width, border),
291
+ topLine(view, frame, palette, width, border, rest),
254
292
  row(trackRow(view, frame, palette, Math.max(0, inner), input.last)),
255
293
  row(skillRow(input.chips, input.shimmer, palette, Math.max(0, inner))),
256
294
  bottomLine(view, input.figures, palette, width, border)
@@ -9,6 +9,34 @@ export type TodoInput = {
9
9
  readonly content: string;
10
10
  readonly status: string;
11
11
  };
12
+ /** The plan file's `- [ ]` / `- [x]` rows: how many are ticked, how many exist, and the first open one. */
13
+ export type PlanChecklist = {
14
+ readonly done: number;
15
+ readonly total: number;
16
+ readonly next?: string;
17
+ };
18
+ /** This session's bounded work from work-schema-3.json: its status, recorded progress turns and plan. */
19
+ export type WorkInput = {
20
+ readonly status: string;
21
+ readonly iterations: number;
22
+ readonly plan?: PlanChecklist;
23
+ };
24
+ export type ResearchStage = "decompose" | "wave" | "expand" | "verify" | "synthesis";
25
+ /** The latest litresearch run folder: what its journal, claim graph and report say so far. */
26
+ export type ResearchInput = {
27
+ readonly stage: ResearchStage;
28
+ readonly waves: number;
29
+ readonly claims: number;
30
+ readonly verified: number;
31
+ readonly reported: boolean;
32
+ /** Last write to the run folder, in ms; a run older than the session's first lit turn is earlier work. */
33
+ readonly touchedAt: number;
34
+ };
35
+ /** The tool the session is running now: its name and start time only, never its input. */
36
+ export type LiveStep = {
37
+ readonly tool: string;
38
+ readonly start: number;
39
+ };
12
40
  export type MissionInput = {
13
41
  readonly marks: readonly LitMark[];
14
42
  readonly agent?: string;
@@ -19,9 +47,17 @@ export type MissionInput = {
19
47
  readonly pendingPermissions?: number;
20
48
  readonly pendingQuestions?: number;
21
49
  readonly contextPercent?: number | null;
50
+ readonly work?: WorkInput;
51
+ readonly research?: ResearchInput;
52
+ readonly live?: LiveStep;
22
53
  readonly now: number;
23
54
  };
24
- export type StepId = "plan" | "work" | "review";
55
+ /**
56
+ * Which stage table the map follows: the main chain, lit-loop's criterion loop (the same stages with
57
+ * the work stage counting criteria), or a litresearch run.
58
+ */
59
+ export type MissionFamily = "main" | "loop" | "research";
60
+ export type StepId = "interview" | "plan" | "work" | "review" | "done" | ResearchStage;
25
61
  export type StepState = "done" | "skip" | "now" | "todo" | "warn";
26
62
  export type MissionStep = {
27
63
  readonly id: StepId;
@@ -50,13 +86,22 @@ export type MissionEvent = {
50
86
  export type MissionView = {
51
87
  readonly active: boolean;
52
88
  readonly discipline: string;
53
- readonly phase: StepId | "done";
89
+ readonly family: MissionFamily;
90
+ readonly phase: StepId;
54
91
  readonly steps: readonly MissionStep[];
55
92
  readonly progress?: {
56
93
  readonly done: number;
57
94
  readonly total: number;
58
95
  };
59
96
  readonly next?: string;
97
+ /** The lead of the detail line: `task 3`, or a research run's claim counts. */
98
+ readonly detail?: string;
99
+ /** Recorded turns of the current loop: bounded work progress, or lit-loop goal checkpoints. */
100
+ readonly iteration?: number;
101
+ /** How many tasks the saved plan holds, shown beside the plan stage. */
102
+ readonly planTasks?: number;
103
+ readonly wave?: number;
104
+ readonly live?: LiveStep;
60
105
  readonly goal?: MissionGoal;
61
106
  readonly blockers: readonly {
62
107
  readonly id: string;
@@ -77,9 +122,28 @@ export type MissionNotice = {
77
122
  readonly celebrate?: boolean;
78
123
  };
79
124
  export declare const contextWarnPercent = 75;
125
+ export declare const familySteps: Readonly<Record<MissionFamily, readonly StepId[]>>;
80
126
  export declare const idleMission: MissionView;
81
127
  /** Reads one part's `metadata.litopencode` mark. Anything without that shape is not a mark. */
82
128
  export declare function readLitMark(metadata: unknown, at: number): LitMark | undefined;
129
+ /** The name a stage shows: the loop counts criteria at its work stage and a research wave carries its number. */
130
+ export declare function stepName(id: StepId, view: Pick<MissionView, "family" | "wave">): string;
131
+ /** Index of the stage that is running (or flagged); the step count once complete; -1 while idle. */
132
+ export declare function currentStepIndex(view: Pick<MissionView, "steps" | "complete" | "active">): number;
133
+ /**
134
+ * Counts a plan's checkbox rows at column zero (`- [ ] 1. title — …`). The first open row, cut at its
135
+ * em dash, becomes the next item. Indented rows are not tasks.
136
+ */
137
+ export declare function planChecklist(text: string): PlanChecklist | undefined;
138
+ /**
139
+ * Lenient read of work-schema-3.json for one session: undefined when it is absent, another session's,
140
+ * cancelled, or not the schema-3 shape. Iterations count the recorded `work.progressed` turns.
141
+ */
142
+ export declare function readWorkState(raw: unknown, sessionID: string): {
143
+ readonly status: string;
144
+ readonly iterations: number;
145
+ readonly planPath: string;
146
+ } | undefined;
83
147
  type LedgerCriterion = {
84
148
  readonly id: string;
85
149
  readonly scenario: string;
@@ -106,8 +170,10 @@ type LedgerGoal = {
106
170
  readonly resolved: boolean;
107
171
  readonly needsUserDecision: boolean;
108
172
  }[];
173
+ /** Written by the ledger CLI, whose completion gate checks evidence; a hand-written goal passed none. */
174
+ readonly gated: boolean;
109
175
  };
110
- /** Lenient read of goals.json: the active goal, or undefined when the file is absent or not the ledger shape. */
176
+ /** Lenient read of goals.json: the goal it names, or undefined when there is none. */
111
177
  export declare function readActiveGoal(raw: unknown): LedgerGoal | undefined;
112
178
  export declare function formatAgo(at: string, now: number): string;
113
179
  export declare function buildMissionView(input: MissionInput): MissionView;