@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.
- package/CHANGELOG.md +18 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/dist/agents/registry.js +2 -2
- package/dist/cli/doctor.d.ts +5 -0
- package/dist/cli/doctor.js +27 -6
- package/dist/cli/install-config.d.ts +6 -0
- package/dist/cli/install-config.js +32 -2
- package/dist/cli/install.js +4 -1
- package/dist/cli/model-catalog.d.ts +6 -1
- package/dist/cli/model-catalog.js +32 -15
- package/dist/cli/model-routing.js +14 -9
- package/dist/cli/types.d.ts +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +10 -2
- package/dist/model-route-policy.d.ts +9 -1
- package/dist/model-route-policy.js +46 -47
- package/dist/tui/mission-band.d.ts +13 -0
- package/dist/tui/mission-band.js +63 -25
- package/dist/tui/mission-model.d.ts +69 -3
- package/dist/tui/mission-model.js +155 -29
- package/dist/tui/mission-motion.d.ts +10 -1
- package/dist/tui/mission-motion.js +17 -7
- package/dist/tui/mission-source.d.ts +36 -6
- package/dist/tui/mission-source.js +167 -13
- package/dist/tui/mission-text.d.ts +34 -9
- package/dist/tui/mission-text.js +169 -74
- package/dist/tui.js +38 -5
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/migration.md +1 -1
- package/docs/reference-Ko-KR.md +81 -26
- package/docs/reference.md +92 -28
- package/package.json +1 -1
- package/skills/lit-pptx/package-lock.json +167 -119
- package/skills/lit-pptx/package.json +1 -1
- package/skills/managed-skill-manifest.json +4 -4
- package/skills/readme-studio/templates/hyperframes/package-lock.json +169 -121
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- 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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
if ((route.variant
|
|
85
|
-
(
|
|
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: "
|
|
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
|
|
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
|
|
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. */
|
package/dist/tui/mission-band.js
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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((
|
|
152
|
-
if (index > 0)
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 {
|
|
249
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|