@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,19 +1,22 @@
|
|
|
1
1
|
// The mission view model for the OpenCode TUI. It reads only what LitOpenCode already leaves behind:
|
|
2
2
|
// the `metadata.litopencode` marks the plugin attaches to user-message parts on activation and on
|
|
3
|
-
// commands, the host todo list
|
|
4
|
-
//
|
|
3
|
+
// commands, the host todo list and running tool, the evidence ledger in
|
|
4
|
+
// .litopencode/litgoal/lit-loop/goals.json, the bounded work record work-schema-3.json with its plan
|
|
5
|
+
// checklist, and litresearch run folders. No new state is written for the TUI.
|
|
5
6
|
export const contextWarnPercent = 75;
|
|
6
7
|
const timelineLimit = 4;
|
|
7
8
|
const staleGoalSlackMs = 60_000;
|
|
9
|
+
export const familySteps = Object.freeze({
|
|
10
|
+
main: Object.freeze(["interview", "plan", "work", "review", "done"]),
|
|
11
|
+
loop: Object.freeze(["interview", "plan", "work", "review", "done"]),
|
|
12
|
+
research: Object.freeze(["decompose", "wave", "expand", "verify", "synthesis"])
|
|
13
|
+
});
|
|
8
14
|
export const idleMission = Object.freeze({
|
|
9
15
|
active: false,
|
|
10
16
|
discipline: "",
|
|
17
|
+
family: "main",
|
|
11
18
|
phase: "plan",
|
|
12
|
-
steps: Object.freeze(
|
|
13
|
-
{ id: "plan", state: "todo" },
|
|
14
|
-
{ id: "work", state: "todo" },
|
|
15
|
-
{ id: "review", state: "todo" }
|
|
16
|
-
]),
|
|
19
|
+
steps: Object.freeze(familySteps.main.map((id) => Object.freeze({ id, state: "todo" }))),
|
|
17
20
|
blockers: Object.freeze([]),
|
|
18
21
|
warnings: Object.freeze([]),
|
|
19
22
|
timeline: Object.freeze([]),
|
|
@@ -46,29 +49,108 @@ export function readLitMark(metadata, at) {
|
|
|
46
49
|
function stepOf(name) {
|
|
47
50
|
if (name === "")
|
|
48
51
|
return undefined;
|
|
52
|
+
if (name === "deep-interview" || name.endsWith("-interview"))
|
|
53
|
+
return "interview";
|
|
49
54
|
if (name === "review-work" || name.includes("review"))
|
|
50
55
|
return "review";
|
|
51
|
-
if (name === "lit-plan" || name.endsWith("-plan") || name === "lit-crucible"
|
|
56
|
+
if (name === "lit-plan" || name.endsWith("-plan") || name === "lit-crucible")
|
|
52
57
|
return "plan";
|
|
53
58
|
return "work";
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
function isResearchName(name) {
|
|
61
|
+
return name === "litresearch" || name === "lit-research";
|
|
62
|
+
}
|
|
63
|
+
/** The name a stage shows: the loop counts criteria at its work stage and a research wave carries its number. */
|
|
64
|
+
export function stepName(id, view) {
|
|
65
|
+
if (id === "work" && view.family === "loop")
|
|
66
|
+
return "criterion";
|
|
67
|
+
if (id === "wave")
|
|
68
|
+
return `wave ${Math.max(1, view.wave ?? 1)}`;
|
|
69
|
+
return id;
|
|
70
|
+
}
|
|
71
|
+
/** Index of the stage that is running (or flagged); the step count once complete; -1 while idle. */
|
|
72
|
+
export function currentStepIndex(view) {
|
|
73
|
+
if (!view.active)
|
|
74
|
+
return -1;
|
|
75
|
+
const index = view.steps.findIndex((step) => step.state === "now" || step.state === "warn");
|
|
76
|
+
return index >= 0 ? index : view.complete ? view.steps.length : -1;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Counts a plan's checkbox rows at column zero (`- [ ] 1. title — …`). The first open row, cut at its
|
|
80
|
+
* em dash, becomes the next item. Indented rows are not tasks.
|
|
81
|
+
*/
|
|
82
|
+
export function planChecklist(text) {
|
|
83
|
+
let done = 0;
|
|
84
|
+
let total = 0;
|
|
85
|
+
let next;
|
|
86
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
87
|
+
const match = /^- \[( |x|X)\] +(.*)$/u.exec(line);
|
|
88
|
+
if (match === null)
|
|
89
|
+
continue;
|
|
90
|
+
total += 1;
|
|
91
|
+
if (match[1] !== " ")
|
|
92
|
+
done += 1;
|
|
93
|
+
else if (next === undefined)
|
|
94
|
+
next = match[2].split(" — ")[0].trim();
|
|
95
|
+
}
|
|
96
|
+
if (total === 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
return { done, total, ...(next === undefined || next === "" ? {} : { next }) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lenient read of work-schema-3.json for one session: undefined when it is absent, another session's,
|
|
102
|
+
* cancelled, or not the schema-3 shape. Iterations count the recorded `work.progressed` turns.
|
|
103
|
+
*/
|
|
104
|
+
export function readWorkState(raw, sessionID) {
|
|
105
|
+
if (!isRecord(raw) || raw.schemaVersion !== 3 || raw.sessionID !== sessionID)
|
|
106
|
+
return undefined;
|
|
107
|
+
const status = text(raw.status);
|
|
108
|
+
if (status === "" || status === "cancelled")
|
|
109
|
+
return undefined;
|
|
110
|
+
const history = Array.isArray(raw.history) ? raw.history.filter(isRecord) : [];
|
|
111
|
+
const plan = isRecord(raw.plan) ? text(raw.plan.path) : "";
|
|
112
|
+
return { status, iterations: history.filter((entry) => entry.type === "work.progressed").length, planPath: plan };
|
|
113
|
+
}
|
|
114
|
+
// The ledger CLI accepts `passed`, `failed` and `completed` as spellings of its own statuses; a goals.json
|
|
115
|
+
// the agent writes by hand also marks a finished criterion `completed` or `done`.
|
|
116
|
+
const criterionStatusSpellings = { passed: "pass", completed: "pass", complete: "pass", done: "pass", failed: "fail" };
|
|
117
|
+
const goalStatusSpellings = { completed: "complete", done: "complete" };
|
|
118
|
+
/**
|
|
119
|
+
* Picks the goal out of goals.json: the ledger's active goal, else what an agent writes by hand —
|
|
120
|
+
* a goal nested under `goal`, the last entry of a `goals` list, or one goal object at the top.
|
|
121
|
+
*/
|
|
122
|
+
function pickGoal(raw) {
|
|
123
|
+
const hasCriteria = (value) => isRecord(value) && Array.isArray(value.criteria);
|
|
124
|
+
if (Array.isArray(raw.goals)) {
|
|
125
|
+
if (raw.activeGoalId !== undefined) {
|
|
126
|
+
const active = raw.goals.find((candidate) => isRecord(candidate) && candidate.id === raw.activeGoalId);
|
|
127
|
+
return isRecord(active) ? active : undefined;
|
|
128
|
+
}
|
|
129
|
+
return raw.goals.filter(hasCriteria).at(-1);
|
|
130
|
+
}
|
|
131
|
+
if (hasCriteria(raw.goal))
|
|
132
|
+
return raw.goal;
|
|
133
|
+
return hasCriteria(raw) ? raw : undefined;
|
|
134
|
+
}
|
|
135
|
+
/** Lenient read of goals.json: the goal it names, or undefined when there is none. */
|
|
56
136
|
export function readActiveGoal(raw) {
|
|
57
|
-
if (!isRecord(raw)
|
|
137
|
+
if (!isRecord(raw))
|
|
58
138
|
return undefined;
|
|
59
|
-
const goal =
|
|
60
|
-
if (
|
|
139
|
+
const goal = pickGoal(raw);
|
|
140
|
+
if (goal === undefined)
|
|
61
141
|
return undefined;
|
|
62
142
|
const list = (value) => (Array.isArray(value) ? value.filter(isRecord) : []);
|
|
143
|
+
const status = text(goal.status);
|
|
63
144
|
return {
|
|
145
|
+
gated: goal !== raw && Array.isArray(raw.goals) && raw.activeGoalId !== undefined,
|
|
64
146
|
title: text(goal.title) || text(goal.objective),
|
|
65
|
-
status:
|
|
147
|
+
status: goalStatusSpellings[status] ?? status,
|
|
66
148
|
createdAt: text(goal.createdAt),
|
|
67
149
|
updatedAt: text(goal.updatedAt),
|
|
68
150
|
criteria: list(goal.criteria).map((criterion) => ({
|
|
69
151
|
id: text(criterion.id),
|
|
70
152
|
scenario: text(criterion.scenario),
|
|
71
|
-
status: text(criterion.status),
|
|
153
|
+
status: criterionStatusSpellings[text(criterion.status)] ?? text(criterion.status),
|
|
72
154
|
evidence: list(criterion.evidence).map((entry) => ({ kind: text(entry.kind), ref: text(entry.ref), at: text(entry.at) }))
|
|
73
155
|
})),
|
|
74
156
|
checkpoints: list(goal.checkpoints).map((entry) => ({ summary: text(entry.summary), at: text(entry.at) })),
|
|
@@ -131,16 +213,27 @@ export function buildMissionView(input) {
|
|
|
131
213
|
const planSaved = planMark !== undefined && planMark.blocked !== true;
|
|
132
214
|
const planBlocked = planMark?.blocked === true;
|
|
133
215
|
const discipline = latest?.mode && latest.mode !== "bounded-authority-lifecycle" ? latest.mode : latest?.command ?? input.agent ?? "lit";
|
|
216
|
+
// A research run counts when this session started it or wrote to it; an older folder is earlier work.
|
|
217
|
+
const research = input.research !== undefined && !(input.research.touchedAt < marks[0].at - staleGoalSlackMs) ? input.research : undefined;
|
|
218
|
+
const researching = isResearchName(discipline) || (research !== undefined && !research.reported);
|
|
219
|
+
const work = input.work;
|
|
134
220
|
const counted = input.todos.filter((todo) => todo.status !== "cancelled");
|
|
221
|
+
const planList = work?.plan;
|
|
135
222
|
const progress = counted.length > 0
|
|
136
223
|
? { done: counted.filter((todo) => todo.status === "completed").length, total: counted.length }
|
|
137
|
-
:
|
|
138
|
-
? { done:
|
|
139
|
-
: undefined
|
|
224
|
+
: planList !== undefined
|
|
225
|
+
? { done: planList.done, total: planList.total }
|
|
226
|
+
: goal !== undefined && goal.criteria.length > 0
|
|
227
|
+
? { done: goal.criteria.filter((criterion) => criterion.status === "pass").length, total: goal.criteria.length }
|
|
228
|
+
: undefined;
|
|
229
|
+
const countsTasks = counted.length > 0 || planList !== undefined;
|
|
140
230
|
const nextTodo = counted.find((todo) => todo.status === "in_progress") ?? counted.find((todo) => todo.status === "pending");
|
|
141
231
|
const nextCriterion = goal?.criteria.find((criterion) => criterion.status !== "pass");
|
|
232
|
+
const family = researching ? "research" : !countsTasks && goal !== undefined && goal.criteria.length > 0 && /lit-?loop|litgoal/u.test(discipline) ? "loop" : "main";
|
|
142
233
|
const blockers = (goal?.reviewBlockers ?? []).filter((blocker) => !blocker.resolved);
|
|
143
|
-
const complete =
|
|
234
|
+
const complete = researching
|
|
235
|
+
? research?.reported === true
|
|
236
|
+
: goal?.status === "complete" || (progress !== undefined && progress.total > 0 && progress.done === progress.total && goal === undefined && countsTasks);
|
|
144
237
|
let phase = latest === undefined ? agentStep ?? "work" : stepOf(latest.command ?? latest.mode ?? "") ?? "work";
|
|
145
238
|
if (latest?.operation === "save-plan" && !planBlocked)
|
|
146
239
|
phase = "work";
|
|
@@ -148,6 +241,8 @@ export function buildMissionView(input) {
|
|
|
148
241
|
const workComplete = progress === undefined || progress.done === progress.total;
|
|
149
242
|
if (blockers.length > 0 || goal?.status === "review_blocked")
|
|
150
243
|
phase = workComplete ? "review" : phase === "review" ? "review" : "work";
|
|
244
|
+
if (researching)
|
|
245
|
+
phase = research?.stage ?? "decompose";
|
|
151
246
|
if (complete)
|
|
152
247
|
phase = "done";
|
|
153
248
|
const warnings = [];
|
|
@@ -168,23 +263,51 @@ export function buildMissionView(input) {
|
|
|
168
263
|
if (typeof input.contextPercent === "number" && input.contextPercent >= contextWarnPercent) {
|
|
169
264
|
warnings.push({ kind: "context", text: `ctx ${Math.round(input.contextPercent)}%` });
|
|
170
265
|
}
|
|
171
|
-
const order = [
|
|
172
|
-
const position =
|
|
173
|
-
const
|
|
266
|
+
const order = familySteps[family];
|
|
267
|
+
const position = complete ? order.length : order.indexOf(phase);
|
|
268
|
+
const visited = (id) => {
|
|
269
|
+
if (id === "interview")
|
|
270
|
+
return agentStep === "interview" || marks.some((mark) => stepOf(mark.command ?? mark.mode ?? "") === "interview");
|
|
271
|
+
if (id === "plan")
|
|
272
|
+
return planMark !== undefined || work !== undefined || agentStep === "plan" || marks.some((mark) => stepOf(mark.command ?? mark.mode ?? "") === "plan");
|
|
273
|
+
// Finished todos alone prove no review; a review command, a review blocker or the goal ledger's
|
|
274
|
+
// completion gate does.
|
|
275
|
+
if (id === "review")
|
|
276
|
+
return (goal?.status === "complete" && goal.gated) || blockers.length > 0 || (goal?.reviewBlockers.length ?? 0) > 0 || marks.some((mark) => stepOf(mark.command ?? mark.mode ?? "") === "review");
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
174
279
|
const steps = order.map((id, index) => {
|
|
175
280
|
if (index < position)
|
|
176
|
-
return { id, state: id
|
|
281
|
+
return { id, state: visited(id) ? "done" : "skip" };
|
|
177
282
|
if (index > position)
|
|
178
283
|
return { id, state: "todo" };
|
|
179
284
|
return { id, state: warnings.length > 0 ? "warn" : "now" };
|
|
180
285
|
});
|
|
286
|
+
const iteration = work !== undefined && work.iterations > 0 ? work.iterations : family === "loop" && goal !== undefined && goal.checkpoints.length > 0 ? goal.checkpoints.length : undefined;
|
|
287
|
+
const nextPlan = nextTodo === undefined ? planList?.next : undefined;
|
|
288
|
+
const detail = family === "research"
|
|
289
|
+
? research === undefined ? undefined : `${research.claims} claims · ${research.verified} verified`
|
|
290
|
+
: countsTasks && progress !== undefined && progress.done < progress.total ? `task ${progress.done + 1}` : undefined;
|
|
291
|
+
const live = input.live !== undefined && (input.sessionStatus === "busy" || input.sessionStatus === "retry") ? input.live : undefined;
|
|
181
292
|
return {
|
|
182
293
|
active: true,
|
|
183
294
|
discipline,
|
|
295
|
+
family,
|
|
184
296
|
phase,
|
|
185
297
|
steps,
|
|
186
|
-
...(progress === undefined ? {} : { progress }),
|
|
187
|
-
...(
|
|
298
|
+
...(family === "research" || progress === undefined ? {} : { progress }),
|
|
299
|
+
...(family === "research"
|
|
300
|
+
? {}
|
|
301
|
+
: nextTodo !== undefined
|
|
302
|
+
? { next: nextTodo.content }
|
|
303
|
+
: nextPlan !== undefined
|
|
304
|
+
? { next: nextPlan }
|
|
305
|
+
: nextCriterion !== undefined ? { next: `${nextCriterion.id} ${nextCriterion.scenario}`.trim() } : {}),
|
|
306
|
+
...(detail === undefined ? {} : { detail }),
|
|
307
|
+
...(iteration === undefined ? {} : { iteration }),
|
|
308
|
+
...(planList === undefined ? {} : { planTasks: planList.total }),
|
|
309
|
+
...(family === "research" && research !== undefined && research.waves > 0 ? { wave: research.waves } : {}),
|
|
310
|
+
...(live === undefined ? {} : { live }),
|
|
188
311
|
...(goal === undefined
|
|
189
312
|
? {}
|
|
190
313
|
: {
|
|
@@ -287,14 +410,17 @@ export function missionRowKeys(view) {
|
|
|
287
410
|
const keys = ["h1", "h2", "b1"];
|
|
288
411
|
if (view.goal !== undefined)
|
|
289
412
|
keys.push("title");
|
|
290
|
-
keys.push(
|
|
413
|
+
view.steps.forEach((step, index) => keys.push(...(index === 0 ? [] : [`link:${index}`]), `step:${step.id}`));
|
|
291
414
|
if (view.progress !== undefined)
|
|
292
415
|
keys.push("bar");
|
|
293
|
-
const
|
|
294
|
-
|
|
416
|
+
const line = (view.next !== undefined || view.detail !== undefined) && !view.complete;
|
|
417
|
+
const live = view.live !== undefined && !view.complete;
|
|
418
|
+
if (line || live || view.goal !== undefined || view.warnings.length > 0)
|
|
295
419
|
keys.push("b2");
|
|
296
|
-
if (
|
|
297
|
-
keys.push("
|
|
420
|
+
if (line)
|
|
421
|
+
keys.push("detail");
|
|
422
|
+
if (live)
|
|
423
|
+
keys.push("live");
|
|
298
424
|
if (view.goal !== undefined)
|
|
299
425
|
keys.push("crit");
|
|
300
426
|
view.warnings.forEach((warning) => keys.push(`warn:${warning.kind}:${warning.text}`));
|
|
@@ -41,6 +41,10 @@ export type MotionState = {
|
|
|
41
41
|
readonly warnFrom: number;
|
|
42
42
|
readonly warnTo: number;
|
|
43
43
|
readonly celebrate: boolean;
|
|
44
|
+
/** The stage index the map moved forward from; the connectors below it fill and the chip flashes once. */
|
|
45
|
+
readonly stageFrom?: number;
|
|
46
|
+
/** The stage that was running when the mission completed; it and the stages after it flip to done in turn. */
|
|
47
|
+
readonly celebrateFrom?: number;
|
|
44
48
|
};
|
|
45
49
|
export type MotionFrame = {
|
|
46
50
|
/** Milliseconds since the last change; Infinity once settled or when motion is off. */
|
|
@@ -52,6 +56,11 @@ export type MotionFrame = {
|
|
|
52
56
|
readonly warn: number;
|
|
53
57
|
/** Milliseconds into the completion moment while it plays, otherwise undefined. */
|
|
54
58
|
readonly celebrate?: number;
|
|
59
|
+
readonly celebrateFrom?: number;
|
|
60
|
+
/** While a stage change plays: the stage index the map moved forward from. */
|
|
61
|
+
readonly stageFrom?: number;
|
|
62
|
+
/** The clock this frame was sampled at, for the live step's elapsed time. */
|
|
63
|
+
readonly now?: number;
|
|
55
64
|
readonly moving: boolean;
|
|
56
65
|
};
|
|
57
66
|
export declare const idleMotion: MotionState;
|
|
@@ -63,4 +72,4 @@ export declare function rowEntryStart(state: Pick<MotionState, "entering" | "chi
|
|
|
63
72
|
*/
|
|
64
73
|
export declare function planMotion(prev: MissionView | undefined, next: MissionView, now: number, state: MotionState, enabled?: boolean): MotionState;
|
|
65
74
|
export declare function sampleMotion(state: MotionState, now: number): MotionFrame;
|
|
66
|
-
export declare function restingFrameFor(view: MissionView): MotionFrame;
|
|
75
|
+
export declare function restingFrameFor(view: MissionView, now?: number): MotionFrame;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Motion for the mission chip and panel. Everything here is a pure function of a clock value, so the
|
|
2
2
|
// TUI only runs a frame timer while one of these tracks is still moving and stays static otherwise.
|
|
3
|
-
import { missionRowKeys } from "./mission-model.js";
|
|
3
|
+
import { currentStepIndex, missionRowKeys } from "./mission-model.js";
|
|
4
4
|
export const motionTiming = Object.freeze({
|
|
5
5
|
stagger: 60,
|
|
6
6
|
entry: 160,
|
|
@@ -96,6 +96,8 @@ function settleTime(state) {
|
|
|
96
96
|
end = Math.max(end, state.barDelay + motionTiming.fill);
|
|
97
97
|
if (state.celebrate)
|
|
98
98
|
end = Math.max(end, motionTiming.complete);
|
|
99
|
+
if (state.stageFrom !== undefined)
|
|
100
|
+
end = Math.max(end, motionTiming.shimmer);
|
|
99
101
|
return end;
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
@@ -110,8 +112,12 @@ export function planMotion(prev, next, now, state, enabled = true) {
|
|
|
110
112
|
const current = sampleMotion(state, now);
|
|
111
113
|
const appearing = prev === undefined || !prev.active;
|
|
112
114
|
const before = new Set(appearing ? [] : missionRowKeys(prev));
|
|
113
|
-
|
|
115
|
+
// The live step changes with every tool call, so its row appears in place instead of sliding in.
|
|
116
|
+
const entering = missionRowKeys(next).filter((key) => !before.has(key) && key !== "live");
|
|
114
117
|
const goalArrived = !appearing && prev?.goal === undefined && next.goal !== undefined;
|
|
118
|
+
const from = appearing || prev === undefined ? -1 : currentStepIndex(prev);
|
|
119
|
+
const to = currentStepIndex(next);
|
|
120
|
+
const celebrate = !appearing && prev?.complete !== true && next.complete;
|
|
115
121
|
return {
|
|
116
122
|
changeAt: now,
|
|
117
123
|
entering,
|
|
@@ -121,12 +127,14 @@ export function planMotion(prev, next, now, state, enabled = true) {
|
|
|
121
127
|
barDelay: goalArrived || appearing ? motionTiming.fillDelayOnGoal : 0,
|
|
122
128
|
warnFrom: appearing ? nextWarn : current.warn,
|
|
123
129
|
warnTo: nextWarn,
|
|
124
|
-
celebrate
|
|
130
|
+
celebrate,
|
|
131
|
+
...(from >= 0 && to > from && prev?.family === next.family ? { stageFrom: from } : {}),
|
|
132
|
+
...(celebrate && from >= 0 ? { celebrateFrom: from } : {})
|
|
125
133
|
};
|
|
126
134
|
}
|
|
127
135
|
export function sampleMotion(state, now) {
|
|
128
136
|
if (state.changeAt === undefined) {
|
|
129
|
-
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: state.barTo, warn: state.warnTo, moving: false };
|
|
137
|
+
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: state.barTo, warn: state.warnTo, now, moving: false };
|
|
130
138
|
}
|
|
131
139
|
const t = now - state.changeAt;
|
|
132
140
|
const moving = t < settleTime(state);
|
|
@@ -138,10 +146,12 @@ export function sampleMotion(state, now) {
|
|
|
138
146
|
chipEnter: moving && state.chipEnter,
|
|
139
147
|
barRatio: clamp01(state.barFrom + (state.barTo - state.barFrom) * fill),
|
|
140
148
|
warn: clamp01(state.warnFrom + (state.warnTo - state.warnFrom) * fade),
|
|
141
|
-
...(moving && state.celebrate ? { celebrate: t } : {}),
|
|
149
|
+
...(moving && state.celebrate ? { celebrate: t, ...(state.celebrateFrom === undefined ? {} : { celebrateFrom: state.celebrateFrom }) } : {}),
|
|
150
|
+
...(moving && state.stageFrom !== undefined ? { stageFrom: state.stageFrom } : {}),
|
|
151
|
+
now,
|
|
142
152
|
moving
|
|
143
153
|
};
|
|
144
154
|
}
|
|
145
|
-
export function restingFrameFor(view) {
|
|
146
|
-
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: ratio(view), warn: warnTarget(view), moving: false };
|
|
155
|
+
export function restingFrameFor(view, now) {
|
|
156
|
+
return { t: Number.POSITIVE_INFINITY, entering: [], chipEnter: false, barRatio: ratio(view), warn: warnTarget(view), ...(now === undefined ? {} : { now }), moving: false };
|
|
147
157
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MissionInput, type TodoInput } from "./mission-model.ts";
|
|
1
|
+
import { type LiveStep, type MissionInput, type ResearchInput, type TodoInput, type WorkInput } from "./mission-model.ts";
|
|
2
2
|
import { type SkillChip } from "./mission-skills.ts";
|
|
3
3
|
type MessageLike = {
|
|
4
4
|
readonly id: string;
|
|
@@ -47,22 +47,52 @@ export type MissionStateSource = {
|
|
|
47
47
|
};
|
|
48
48
|
part(messageID: string): ReadonlyArray<{
|
|
49
49
|
readonly type?: string;
|
|
50
|
+
readonly tool?: unknown;
|
|
51
|
+
readonly state?: unknown;
|
|
50
52
|
readonly metadata?: unknown;
|
|
51
53
|
}>;
|
|
52
54
|
};
|
|
53
55
|
/** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
|
|
54
56
|
export declare function missionProjectRoot(paths: MissionStateSource["path"]): string | undefined;
|
|
57
|
+
/** Reads goals.json under `root`, refusing symbolic links and oversized files. */
|
|
58
|
+
export declare function createLedgerReader(): (root: string | undefined) => unknown;
|
|
55
59
|
/**
|
|
56
|
-
* Reads
|
|
57
|
-
*
|
|
60
|
+
* Reads this session's bounded work from work-schema-3.json and the checklist of the plan it names.
|
|
61
|
+
* The plan must be a regular file inside `root`; anything else leaves the checklist out.
|
|
58
62
|
*/
|
|
59
|
-
export declare function
|
|
60
|
-
|
|
63
|
+
export declare function createWorkReader(): (root: string | undefined, sessionID: string) => WorkInput | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* What a litresearch run folder says so far. The skill keeps a root-owned journal.md, an append-only
|
|
66
|
+
* claim-graph.jsonl and a final report.md; the stage is read from them: a report is synthesis, a
|
|
67
|
+
* claim with a gate result is verify, a journal whose last heading names EXPAND is expand, claims are
|
|
68
|
+
* a wave (numbered by the highest `wave` field or `## Wave` heading), and nothing yet is decompose.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readResearchRun(journal: string, claimGraph: string, reported: boolean, touchedAt: number): ResearchInput;
|
|
71
|
+
/**
|
|
72
|
+
* The most recently written run under .litopencode/litgoal/lit-loop/research/, or undefined when
|
|
73
|
+
* there is none. Symbolic links are skipped and only the three known files are read.
|
|
74
|
+
*/
|
|
75
|
+
export declare function readLatestResearchRun(root: string | undefined): ResearchInput | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The tool the session's latest assistant message is running: its name and `time.start` from the
|
|
78
|
+
* running tool part. The input is never read.
|
|
79
|
+
*/
|
|
80
|
+
export declare function readLiveStep(state: MissionStateSource, messages: ReadonlyArray<MessageLike>): LiveStep | undefined;
|
|
81
|
+
export type MissionRecords = {
|
|
82
|
+
readonly work?: WorkInput;
|
|
83
|
+
readonly research?: ResearchInput;
|
|
84
|
+
};
|
|
85
|
+
export declare function readMissionInput(state: MissionStateSource, sessionID: string, ledger: unknown, now: number, records?: MissionRecords): MissionInput;
|
|
61
86
|
export type CardInput = {
|
|
62
87
|
readonly chips: readonly SkillChip[];
|
|
88
|
+
readonly busy: boolean;
|
|
63
89
|
readonly contextPercent: number | null;
|
|
64
90
|
readonly branch?: string;
|
|
65
91
|
};
|
|
66
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* What the band card shows beside the mission: the session's skill chips, whether a turn is running,
|
|
94
|
+
* context use and git branch. OpenCode keeps each session's last `session.status` ({ type: "busy" },
|
|
95
|
+
* "retry" or "idle"); a session it has not seen run has none, which counts as not running.
|
|
96
|
+
*/
|
|
67
97
|
export declare function readCardInput(state: MissionStateSource, sessionID: string): CardInput;
|
|
68
98
|
export {};
|