@litfamily/litopencode 1.0.31 → 1.0.33

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 (282) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README-Ko-KR.md +6 -6
  3. package/README.md +6 -6
  4. package/TRADEMARK.md +12 -0
  5. package/dist/activation-primary-prompts.js +5 -1
  6. package/dist/activation-probe.js +1 -0
  7. package/dist/activation-prompt-utils.d.ts +1 -1
  8. package/dist/activation-reentry.d.ts +20 -0
  9. package/dist/activation-reentry.js +110 -1
  10. package/dist/activation-routing.d.ts +2 -1
  11. package/dist/activation-routing.js +100 -11
  12. package/dist/activation-workflow-prompts.d.ts +4 -0
  13. package/dist/activation-workflow-prompts.js +30 -0
  14. package/dist/activation.d.ts +1 -1
  15. package/dist/activation.js +44 -5
  16. package/dist/adhd-next.d.ts +51 -0
  17. package/dist/adhd-next.js +150 -0
  18. package/dist/agents/defaults.js +1 -1
  19. package/dist/cli/managed-skill-assets.d.ts +1 -1
  20. package/dist/cli/managed-skill-assets.js +7 -1
  21. package/dist/cli/native-skill-integrity.js +1 -1
  22. package/dist/cli.js +7 -1
  23. package/dist/commands.d.ts +28 -1
  24. package/dist/commands.js +58 -5
  25. package/dist/durable-plan.d.ts +5 -1
  26. package/dist/durable-plan.js +14 -2
  27. package/dist/features.d.ts +80 -1
  28. package/dist/features.js +105 -1
  29. package/dist/hooks.js +3 -0
  30. package/dist/ledger.js +12 -1
  31. package/dist/plan-scope-roots.d.ts +23 -0
  32. package/dist/plan-scope-roots.js +116 -0
  33. package/dist/restart-notice.d.ts +7 -0
  34. package/dist/restart-notice.js +40 -0
  35. package/dist/secret-shapes.js +5 -5
  36. package/dist/server.js +4 -0
  37. package/dist/session-liveness.d.ts +20 -0
  38. package/dist/session-liveness.js +99 -0
  39. package/dist/skills.d.ts +22 -1
  40. package/dist/skills.js +36 -0
  41. package/dist/start-work-typed.d.ts +34 -0
  42. package/dist/start-work-typed.js +167 -0
  43. package/dist/state.d.ts +3 -0
  44. package/dist/state.js +3 -0
  45. package/dist/tools.js +22 -2
  46. package/dist/tui/mission-band.d.ts +22 -1
  47. package/dist/tui/mission-band.js +75 -16
  48. package/dist/tui/mission-looks.d.ts +38 -0
  49. package/dist/tui/mission-looks.js +73 -0
  50. package/dist/tui/mission-model.d.ts +2 -0
  51. package/dist/tui/mission-model.js +3 -1
  52. package/dist/tui/mission-rainbow.d.ts +33 -0
  53. package/dist/tui/mission-rainbow.js +90 -0
  54. package/dist/tui/mission-source.d.ts +49 -0
  55. package/dist/tui/mission-source.js +217 -2
  56. package/dist/tui/mission-text.d.ts +2 -0
  57. package/dist/tui/mission-text.js +9 -1
  58. package/dist/tui/mission-threads.d.ts +58 -0
  59. package/dist/tui/mission-threads.js +128 -0
  60. package/dist/tui.js +112 -7
  61. package/dist/uiux-visual-catalog.js +8 -1
  62. package/dist/v2/events.js +3 -0
  63. package/dist/v2/session-hooks.js +38 -5
  64. package/dist/workflow-stack.d.ts +53 -0
  65. package/dist/workflow-stack.js +196 -0
  66. package/docs/assets/readme/badge-version.svg +1 -1
  67. package/docs/assets/skills/lit-schematic.webp +0 -0
  68. package/docs/reference-Ko-KR.md +48 -3
  69. package/docs/reference.md +54 -3
  70. package/package.json +1 -1
  71. package/skills/frontend-ui-ux/SKILL.md +3 -2
  72. package/skills/frontend-ui-ux/kit/components.css +414 -0
  73. package/skills/frontend-ui-ux/kit/fonts.json +22 -0
  74. package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  75. package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  76. package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  77. package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  78. package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  79. package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  80. package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  81. package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  82. package/skills/frontend-ui-ux/kit/ui.js +226 -0
  83. package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
  84. package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
  85. package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
  86. package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
  87. package/skills/frontend-ui-ux/references/house-styles.md +236 -0
  88. package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
  89. package/skills/frontend-ui-ux/references/production.md +3 -1
  90. package/skills/frontend-ui-ux/references/slop-register.md +4 -4
  91. package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  92. package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  93. package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
  94. package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
  95. package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
  96. package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
  97. package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
  98. package/skills/lit-adhd/SKILL.md +209 -0
  99. package/skills/lit-adhd/references/brief-format.md +46 -0
  100. package/skills/lit-adhd/references/card-examples.md +103 -0
  101. package/skills/lit-adhd/references/status-taxonomy.md +34 -0
  102. package/skills/lit-adhd/scripts/rewind.mjs +933 -0
  103. package/skills/lit-diagram-drawer/SKILL.md +4 -3
  104. package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  105. package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  106. package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  107. package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  108. package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  109. package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
  110. package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
  111. package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
  112. package/skills/lit-docx/SKILL.md +2 -1
  113. package/skills/lit-handoff/SKILL.md +2 -0
  114. package/skills/lit-hijack/SKILL.md +422 -0
  115. package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
  116. package/skills/lit-hijack/references/clean-room-build.md +111 -0
  117. package/skills/lit-hijack/references/distil-and-licence.md +100 -0
  118. package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
  119. package/skills/lit-hijack/references/parity-and-report.md +110 -0
  120. package/skills/lit-hijack/scripts/hijack.mjs +993 -0
  121. package/skills/lit-plan/SKILL.md +8 -0
  122. package/skills/lit-pptx/SKILL.md +27 -11
  123. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  124. package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  125. package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  126. package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
  127. package/skills/lit-pptx/references/density-and-fill.md +9 -3
  128. package/skills/lit-pptx/references/direction-step.md +21 -6
  129. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
  130. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
  131. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
  132. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
  133. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
  134. package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  135. package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  136. package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
  137. package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
  138. package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  139. package/skills/lit-pptx/references/layout-families.md +65 -47
  140. package/skills/lit-pptx/references/title-treatments.md +83 -134
  141. package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
  142. package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  143. package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
  144. package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  145. package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
  146. package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
  147. package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  148. package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  149. package/skills/lit-pptx/references/tonalities/night.md +1 -1
  150. package/skills/lit-pptx/references/tonalities/paper.md +2 -2
  151. package/skills/lit-pptx/references/tonalities/riso.md +137 -0
  152. package/skills/lit-pptx/references/tonalities/signal.md +3 -3
  153. package/skills/lit-pptx/references/tonalities/studio.md +7 -6
  154. package/skills/lit-pptx/run.mjs +2 -1
  155. package/skills/lit-pptx/scripts/compile-deck.js +1 -1
  156. package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
  157. package/skills/lit-pptx/scripts/craft_extras.py +56 -3
  158. package/skills/lit-pptx/scripts/deck_output.py +75 -90
  159. package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
  160. package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
  161. package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  162. package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  163. package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
  164. package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  165. package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
  166. package/skills/lit-pptx/scripts/qa_deck.py +10 -11
  167. package/skills/lit-pptx/scripts/render_pages.py +118 -0
  168. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
  169. package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  170. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
  171. package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  172. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
  173. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
  174. package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  175. package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  176. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
  177. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
  178. package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  179. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
  180. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
  181. package/skills/lit-recap/SKILL.md +4 -0
  182. package/skills/lit-schematic/SKILL.md +118 -0
  183. package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  184. package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  185. package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  186. package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  187. package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  188. package/skills/lit-schematic/references/isometric-craft.md +101 -0
  189. package/skills/lit-schematic/references/journal-figures.md +72 -0
  190. package/skills/lit-schematic/references/kit-api.md +134 -0
  191. package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  192. package/skills/lit-schematic/references/pid-drafting.md +103 -0
  193. package/skills/lit-schematic/references/pid-symbols.md +163 -0
  194. package/skills/lit-schematic/references/verification.md +100 -0
  195. package/skills/lit-schematic/scripts/doctor.mjs +57 -0
  196. package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  197. package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
  198. package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  199. package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  200. package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  201. package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  202. package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  203. package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  204. package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  205. package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  206. package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  207. package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  208. package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  209. package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  210. package/skills/lit-schematic/scripts/motion.mjs +129 -0
  211. package/skills/lit-schematic/scripts/qa.mjs +92 -0
  212. package/skills/lit-schematic/scripts/render.mjs +233 -0
  213. package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  214. package/skills/lit-scientific-visualization/SKILL.md +27 -4
  215. package/skills/lit-typographic-motion/SKILL.md +19 -17
  216. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
  217. package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
  218. package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
  219. package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
  220. package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
  221. package/skills/lit-typographic-motion/engine/board.mjs +138 -0
  222. package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
  223. package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
  224. package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
  225. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
  226. package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
  227. package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
  228. package/skills/lit-typographic-motion/engine/house.mjs +42 -0
  229. package/skills/lit-typographic-motion/engine/look.mjs +65 -8
  230. package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
  231. package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
  232. package/skills/lit-typographic-motion/engine/score.mjs +594 -0
  233. package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
  234. package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
  235. package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
  236. package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
  237. package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
  238. package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
  239. package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
  240. package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
  241. package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
  242. package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
  243. package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
  244. package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
  245. package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
  246. package/skills/lit-typographic-motion/engine/text.mjs +1 -1
  247. package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
  248. package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
  249. package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  250. package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  251. package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
  252. package/skills/lit-typographic-motion/gate.mjs +18 -2
  253. package/skills/lit-typographic-motion/probe.mjs +13 -1
  254. package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
  255. package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
  256. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
  257. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
  258. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
  259. package/skills/lit-typographic-motion/references/cover.md +56 -13
  260. package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
  261. package/skills/lit-typographic-motion/references/depth.md +112 -0
  262. package/skills/lit-typographic-motion/references/runtime.md +21 -6
  263. package/skills/lit-typographic-motion/references/sound.md +76 -0
  264. package/skills/lit-typographic-motion/references/stage.md +50 -7
  265. package/skills/lit-typographic-motion/references/studio.md +162 -0
  266. package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
  267. package/skills/lit-typographic-motion/references/treatment.md +108 -9
  268. package/skills/lit-typographic-motion/render.mjs +60 -3
  269. package/skills/lit-typographic-motion/styles/paper.json +185 -0
  270. package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
  271. package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
  272. package/skills/lit-typographic-motion/styles/tidal.json +187 -0
  273. package/skills/litresearch/SKILL.md +2 -0
  274. package/skills/managed-skill-manifest.json +517 -88
  275. package/skills/readme-studio/SKILL.md +1 -1
  276. package/skills/review-work/SKILL.md +2 -0
  277. package/skills/start-work/SKILL.md +8 -0
  278. package/skills/workflow-loop/SKILL.md +4 -0
  279. package/tools/check-pack-payload.mjs +5 -2
  280. package/tools/payload-substance-parity.json +4 -3
  281. package/tools/qa-cover-recipes.mjs +4 -3
  282. package/tools/version-manifests.json +2 -2
package/dist/tui.js CHANGED
@@ -4,15 +4,21 @@ import { jsx } from "@opentui/solid/jsx-runtime";
4
4
  import { buildMissionView, lastMission, leadingNotice, missionNotices } from "./tui/mission-model.js";
5
5
  import { idleMotion, motionTiming, planMotion, sampleMotion } from "./tui/mission-motion.js";
6
6
  import { chipBudget, chipLine, homeLine, missionPanel, paletteFromTheme, panelWidth } from "./tui/mission-text.js";
7
- import { createLedgerReader, createWorkReader, missionProjectRoot, readCardInput, readLatestResearchRun, readMissionInput } from "./tui/mission-source.js";
7
+ import { adhdSuggestDue, createLedgerReader, createWorkReader, earlierWorkWindowMs, hasEarlierWork, messageTimes, missionProjectRoot, wayBackDue, readCardInput, readLatestResearchRun, readMissionInput, readThreadSources } from "./tui/mission-source.js";
8
8
  import { chipShimmer, idleChipMotion, planChipMotion } from "./tui/mission-skills.js";
9
9
  import { bandCard, readBandOptions, readNotifyOptions, statusCard } from "./tui/mission-band.js";
10
+ import { activeSkillLook, turnGlow, waitingOnYou } from "./tui/mission-looks.js";
11
+ import { rainbowActive, rainbowAllowed, rainbowAt, rainbowShows, rainbowTickMs } from "./tui/mission-rainbow.js";
12
+ import { threadLineCount, threadsOf } from "./tui/mission-threads.js";
13
+ import { readPackageMetadata } from "./cli/json.js";
14
+ import { readInstalledVersions, restartVersion } from "./restart-notice.js";
10
15
  export const litOpenCodeTuiPluginId = "litopencode";
11
16
  const homeLineWidth = 69;
12
17
  const refreshDelayMs = 80;
13
18
  const ledgerPollMs = 2000;
14
19
  const toastMs = 4000;
15
20
  const liveTickMs = 1000;
21
+ const restartCheckMs = 10 * 60_000;
16
22
  const trackedSessionLimit = 4;
17
23
  const refreshEvents = [
18
24
  "message.updated",
@@ -50,6 +56,25 @@ const tui = async (api, options) => {
50
56
  let refreshTimer;
51
57
  let pollTimer;
52
58
  let liveTimer;
59
+ let bandTimer;
60
+ const suggested = new Set();
61
+ const suggestedDays = new Map();
62
+ const colorAllowed = rainbowAllowed(motionEnabled, process.env.NO_COLOR);
63
+ // The version this start loaded; an install newer than it shows `restart to <v>` and one toast.
64
+ const loaded = await readPackageMetadata().catch(() => undefined);
65
+ const [restartTo, setRestartTo] = createSignal(undefined);
66
+ let restartTimer;
67
+ async function checkRestart() {
68
+ if (loaded === undefined)
69
+ return;
70
+ const target = restartVersion(loaded.version, await readInstalledVersions(loaded.packageRoot).catch(() => []));
71
+ if (target === undefined || target === restartTo())
72
+ return;
73
+ if (restartTo() === undefined) {
74
+ api.ui.toast({ variant: "info", title: "🔥 LIT", message: `LitOpenCode ${target} is installed. Restart OpenCode to load it.`, duration: toastMs });
75
+ }
76
+ setRestartTo(target);
77
+ }
53
78
  const state = () => api.state;
54
79
  const palette = () => paletteFromTheme(api.theme.current, api.theme.mode());
55
80
  // OpenCode keeps the sidebar choice in its KV store under "sidebar" ("auto" or "hide"); a child
@@ -64,6 +89,30 @@ const tui = async (api, options) => {
64
89
  const session = api.state.session.get(sessionID);
65
90
  return typeof session?.parentID === "string" && session.parentID !== "";
66
91
  }
92
+ // lit-adhd plays the rainbow and a working lane without a real total plays a pulse; both step at 4 Hz
93
+ // and only while they are on screen, and neither runs under reduced motion or NO_COLOR.
94
+ function rainbowOn(entry, now) {
95
+ return colorAllowed && rainbowShows(rainbowActive(entry.view, entry.card.chips, entry.card.busy), entry.rainbowSeen, entry.lastPromptAt, now);
96
+ }
97
+ function bandStepping(entry) {
98
+ if (!colorAllowed)
99
+ return false;
100
+ if (entry.waiting || rainbowOn(entry, Date.now()))
101
+ return true;
102
+ return entry.card.threads?.some((row) => row.status === "working" && row.done === undefined) === true;
103
+ }
104
+ function syncBandTimer() {
105
+ const needed = [...sessions.values()].some(bandStepping);
106
+ if (needed && bandTimer === undefined)
107
+ bandTimer = setInterval(() => {
108
+ setClock(Date.now());
109
+ syncBandTimer();
110
+ }, rainbowTickMs);
111
+ if (!needed && bandTimer !== undefined) {
112
+ clearInterval(bandTimer);
113
+ bandTimer = undefined;
114
+ }
115
+ }
67
116
  function anyMoving(now) {
68
117
  for (const entry of sessions.values())
69
118
  if (sampleMotion(entry.motion, now).moving || chipShimmer(entry.chipMotion, now) !== undefined)
@@ -103,16 +152,48 @@ const tui = async (api, options) => {
103
152
  .notify({ title: "🔥 LIT complete", message: notice.message, notification: notify.desktop, sound: notify.sound ? { name: "done" } : false })
104
153
  .catch(() => undefined);
105
154
  }
155
+ // The card keeps the tallest lane count it reached while lanes are live or a turn runs, and drops back to
156
+ // four rows only once everything has settled, so the prompt does not jump between repaints.
157
+ function holdRows(held, rows, busy) {
158
+ if (rows > 0)
159
+ return Math.max(held, rows);
160
+ return busy ? held : 0;
161
+ }
162
+ // One toast per session, for a fresh root session in a workspace that has earlier work to pick up.
163
+ function suggestAdhd(sessionID) {
164
+ if (suggested.has(sessionID) || isChildSession(sessionID))
165
+ return;
166
+ suggested.add(sessionID);
167
+ const source = state();
168
+ const created = api.state.session.get(sessionID)?.time?.created;
169
+ const now = Date.now();
170
+ if (source.session.messages(sessionID).length > 0 || typeof created !== "number" || now - created > 2 * 60_000)
171
+ return;
172
+ const root = missionProjectRoot(source.path);
173
+ if (root === undefined || !hasEarlierWork(root, sessionID, now, earlierWorkWindowMs) || !adhdSuggestDue(suggestedDays, root, now))
174
+ return;
175
+ api.ui.toast({ variant: "info", title: "🔥 LIT", message: "Earlier work here. Say lit adhd to pick up where you left off.", duration: toastMs });
176
+ }
106
177
  function refresh(sessionID) {
107
178
  const now = Date.now();
108
179
  const source = state();
109
180
  const root = missionProjectRoot(source.path);
110
181
  const ledger = readLedger(root);
111
- const next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work: readWork(root, sessionID), research: readLatestResearchRun(root) }));
112
- const card = readCardInput(source, sessionID);
182
+ const work = readWork(root, sessionID);
183
+ const next = buildMissionView(readMissionInput(source, sessionID, ledger, now, { work, research: readLatestResearchRun(root) }));
184
+ const threads = threadsOf(readThreadSources(source, sessionID, root, now), now);
185
+ const times = messageTimes(source, sessionID);
186
+ const card = {
187
+ ...readCardInput(source, sessionID),
188
+ ...(threads === undefined ? {} : { threads }),
189
+ ...(wayBackDue(times.lastAt, now, hasEarlierWork(root, sessionID, now, earlierWorkWindowMs)) ? { earlier: true } : {})
190
+ };
113
191
  const entry = sessions.get(sessionID);
114
192
  const viewChanged = entry === undefined || JSON.stringify(entry.view) !== JSON.stringify(next);
115
- if (!viewChanged && JSON.stringify(entry.card) === JSON.stringify(card))
193
+ const rainbowSeen = rainbowActive(next, card.chips, card.busy) ? now : entry?.rainbowSeen;
194
+ const waiting = waitingOnYou(next, card.busy, { asked: card.asked === true, userSpokeLast: card.userSpokeLast === true, blockedReply: card.blockedReply === true, boundary: work?.boundary === true });
195
+ const promptMoved = entry?.lastPromptAt !== times.lastPromptAt;
196
+ if (!viewChanged && !promptMoved && JSON.stringify(entry.card) === JSON.stringify(card) && entry.rainbowSeen === rainbowSeen && entry.waiting === waiting)
116
197
  return;
117
198
  if (entry !== undefined && viewChanged) {
118
199
  const notices = missionNotices(entry.view, next);
@@ -127,12 +208,17 @@ const tui = async (api, options) => {
127
208
  view: next,
128
209
  card,
129
210
  motion: viewChanged ? planMotion(entry?.view, next, now, entry?.motion ?? idleMotion, motionEnabled) : entry.motion,
130
- chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled)
211
+ chipMotion: planChipMotion(entry?.card.chips, card.chips, now, entry?.chipMotion ?? idleChipMotion, motionEnabled),
212
+ reserve: holdRows(entry?.reserve ?? 0, threadLineCount(threads), card.busy),
213
+ ...(rainbowSeen === undefined ? {} : { rainbowSeen }),
214
+ ...(times.lastPromptAt === undefined ? {} : { lastPromptAt: times.lastPromptAt }),
215
+ waiting
131
216
  });
132
217
  setClock(now);
133
218
  setRevision(revision() + 1);
134
219
  ensureFrames();
135
220
  syncLiveTick();
221
+ syncBandTimer();
136
222
  }
137
223
  function refreshTracked() {
138
224
  refreshTimer = undefined;
@@ -143,7 +229,7 @@ const tui = async (api, options) => {
143
229
  // Ledger writes from a terminal or another session fire no TUI event, so an active mission
144
230
  // re-reads goals.json (one lstat while unchanged) every two seconds. Idle sessions do not poll.
145
231
  function syncPoll() {
146
- const active = [...sessions.values()].some((entry) => entry.view.active);
232
+ const active = [...sessions.values()].some((entry) => entry.view.active || entry.card.threads !== undefined);
147
233
  if (active && pollTimer === undefined)
148
234
  pollTimer = setInterval(refreshTracked, ledgerPollMs);
149
235
  if (!active && pollTimer !== undefined) {
@@ -164,6 +250,7 @@ const tui = async (api, options) => {
164
250
  queueMicrotask(() => {
165
251
  refresh(sessionID);
166
252
  syncPoll();
253
+ suggestAdhd(sessionID);
167
254
  });
168
255
  }
169
256
  // Opening a session loads its messages and parts over HTTP, which fires no bus event. Reading the
@@ -200,7 +287,11 @@ const tui = async (api, options) => {
200
287
  for (const type of refreshEvents) {
201
288
  api.event.on(type, scheduleRefresh);
202
289
  }
290
+ void checkRestart();
291
+ restartTimer = setInterval(() => void checkRestart(), restartCheckMs);
203
292
  api.lifecycle.onDispose(() => {
293
+ if (restartTimer !== undefined)
294
+ clearInterval(restartTimer);
204
295
  if (frameTimer !== undefined)
205
296
  clearInterval(frameTimer);
206
297
  if (refreshTimer !== undefined)
@@ -209,6 +300,8 @@ const tui = async (api, options) => {
209
300
  clearInterval(pollTimer);
210
301
  if (liveTimer !== undefined)
211
302
  clearInterval(liveTimer);
303
+ if (bandTimer !== undefined)
304
+ clearInterval(bandTimer);
212
305
  sessions.clear();
213
306
  });
214
307
  api.slots.register({
@@ -322,8 +415,20 @@ const tui = async (api, options) => {
322
415
  frame: snapshot.frame,
323
416
  chips: snapshot.card.chips,
324
417
  busy: snapshot.card.busy,
418
+ motion: motionEnabled,
419
+ reserve: snapshot.reserve,
420
+ ...(snapshot.card.threads === undefined ? {} : { threads: snapshot.card.threads }),
421
+ ...(snapshot.card.earlier === true ? { earlier: true } : {}),
422
+ ...(snapshot.waiting ? { turn: { glow: turnGlow(snapshot.now, colorAllowed) } } : {}),
423
+ ...(rainbowOn(snapshot, snapshot.now) ? { rainbow: rainbowAt(snapshot.now) } : {}),
424
+ ...(snapshot.card.lanes !== undefined && activeSkillLook(snapshot.view, snapshot.card.chips, snapshot.card.busy) === "review" ? { lanes: snapshot.card.lanes } : {}),
325
425
  ...(shimmer === undefined ? {} : { shimmer }),
326
- figures: { contextPercent: snapshot.card.contextPercent, ...(snapshot.card.branch === undefined ? {} : { branch: snapshot.card.branch }), inLane: snapshot.inLane },
426
+ figures: {
427
+ contextPercent: snapshot.card.contextPercent,
428
+ ...(snapshot.card.branch === undefined ? {} : { branch: snapshot.card.branch }),
429
+ inLane: snapshot.inLane,
430
+ ...(restartTo() === undefined ? {} : { restartTo: restartTo() })
431
+ },
327
432
  ...(last === undefined ? {} : { last })
328
433
  }, colors, width);
329
434
  const rows = lines.map(lineElement);
@@ -1,7 +1,7 @@
1
1
  export const frontendUiUxFeature = {
2
2
  id: "frontend-ui-ux",
3
3
  title: "Frontend UI/UX",
4
- summary: "Builds and inspects authorized interfaces using an evolving beta2 contract and offline design intelligence.",
4
+ summary: "Builds and inspects authorized interfaces using an evolving beta2 contract, offline design intelligence and a locked house style with a rich default.",
5
5
  bindings: [
6
6
  {
7
7
  kind: "config",
@@ -15,6 +15,12 @@ export const frontendUiUxFeature = {
15
15
  surface: "OpenCode native skill frontend-ui-ux",
16
16
  description: "Ships focused contract references plus an exact verified 167-file canonical design library. The canonical route is inert and separate from the normalized retrieval dataset."
17
17
  },
18
+ {
19
+ kind: "config",
20
+ id: "skills/frontend-ui-ux/kit",
21
+ surface: "OpenCode native skill frontend-ui-ux",
22
+ description: "Ships five house styles, the token-only component sheet and three layout starters; scripts/design-lock.mjs writes the design lock, tokens and fonts into a new project, and the probe checks the rendered page against it (DS-001 to DS-008)."
23
+ },
18
24
  {
19
25
  kind: "cli",
20
26
  id: "litopencode install",
@@ -29,6 +35,7 @@ export const frontendUiUxFeature = {
29
35
  }
30
36
  ],
31
37
  verification: [
38
+ "node --test test/frontend-design-lock.test.mjs",
32
39
  "node --test --test-name-pattern='uiux\\.' test/runtime-skills.test.mjs test/packed-artifact.test.mjs",
33
40
  "node --test --test-name-pattern='integration\\.installed-nested-assets' test/cli-install-surface.test.mjs",
34
41
  "npm run check:managed-skill-manifest",
package/dist/v2/events.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createAutoHandoff } from "../auto-handoff-hooks.js";
2
2
  import { LifecycleConflictError, LifecycleSafetyError, createBoundedAuthorityLifecycle, parseFencedProgress } from "../bounded-authority.js";
3
+ import { markLiveness } from "../session-liveness.js";
3
4
  function isRecord(value) {
4
5
  return typeof value === "object" && value !== null && !Array.isArray(value);
5
6
  }
@@ -100,6 +101,7 @@ export function createV2Events(options) {
100
101
  if (step.sessionID === sessionID)
101
102
  steps.delete(messageID);
102
103
  options.onSessionDeleted(sessionID);
104
+ await markLiveness(options.projectRoot, sessionID, "ended");
103
105
  await toHandoff("session.deleted", { info: { id: sessionID } });
104
106
  return;
105
107
  case "session.step.started": {
@@ -138,6 +140,7 @@ export function createV2Events(options) {
138
140
  await toHandoff("session.status", { sessionID, status: data.status });
139
141
  return;
140
142
  case "session.idle":
143
+ await markLiveness(options.projectRoot, sessionID, "idle");
141
144
  await toHandoff("session.idle", { sessionID });
142
145
  return;
143
146
  case "session.compaction.ended":
@@ -6,12 +6,14 @@
6
6
  // than from LitOpenCode's own instructions (local knowledge, a start-work receipt, an auto-handoff setting
7
7
  // reply) rides the latest user message instead, the channel it had on OpenCode 1.
8
8
  import { fileURLToPath } from "node:url";
9
- import { createActivationMemory, decideReentry } from "../activation-reentry.js";
9
+ import { typedPromptReply } from "../adhd-next.js";
10
+ import { createActivationMemory, decideReentry, recordActivation } from "../activation-reentry.js";
10
11
  import { detectChatActivationMode, promptForChatActivationMode } from "../activation-routing.js";
11
- import { startWorkPromptInjection } from "../activation-workflow-prompts.js";
12
+ import { adhdSessionNote, litAdhdPromptInjection, startWorkPromptInjection } from "../activation-workflow-prompts.js";
12
13
  import { autoHandoffSettingText } from "../auto-handoff.js";
13
14
  import { applyTrustedStartWorkDirective, parseStartWorkLifecycleDirective } from "../bounded-authority-hooks.js";
14
15
  import { queryKnowledge } from "../knowledge.js";
16
+ import { startTypedWork, typedStartArgument } from "../start-work-typed.js";
15
17
  import { createLitGoalOperations } from "../ledger.js";
16
18
  import { applyCompactionRuleReset, applyStaticRuleInjection } from "../rules/hooks.js";
17
19
  import { isReadOnlyWorkflowFamilyPlan } from "../workflow-families.js";
@@ -125,13 +127,42 @@ export function createV2SessionHooks(options) {
125
127
  catch { // no-excuse-ok: catch -- local relevance must never block the user message.
126
128
  // A malformed or unreadable knowledge store stays silent instead of inventing context.
127
129
  }
130
+ const typed = await typedPromptReply(options.projectRoot, event.sessionID, input, activationMemory, options.config.boundedAuthority);
131
+ if (typed !== undefined) {
132
+ state.turn.push(typed.text);
133
+ event.metadata = { ...(event.metadata ?? {}), litopencode: { mode: typed.mode, trigger: "lit", source: "prompt", typed: typed.kind } };
134
+ if (typed.continued)
135
+ options.onActivation?.(event.sessionID, typed.mode);
136
+ return;
137
+ }
138
+ // An installed alias file makes the host expand /lit-adhd (or /lit-rewind) itself, so the prompt
139
+ // arrives as the command body with no plugin mark; it still runs the brief and takes its look.
140
+ if (input.trimStart().startsWith(litAdhdPromptInjection.trim())) {
141
+ state.turn.push(adhdSessionNote(event.sessionID));
142
+ event.metadata = { ...(event.metadata ?? {}), litopencode: { mode: "lit-adhd", trigger: "lit", source: "alias" } };
143
+ options.onActivation?.(event.sessionID, "lit-adhd");
144
+ return;
145
+ }
128
146
  const mode = detectChatActivationMode(input, session.agent);
129
147
  if (mode === undefined)
130
148
  return;
131
149
  const trigger = mode === "lit-handoff" ? "handoff" : mode === "lit-scientific-visualization" ? mode : "lit";
132
150
  let activation = promptForChatActivationMode(mode, input);
151
+ if (mode === "lit-adhd")
152
+ activation = `${activation}\n${adhdSessionNote(event.sessionID)}`;
133
153
  if (mode === "start-work" && session.agent !== "lit-plan") {
134
154
  const directive = parseStartWorkLifecycleDirective(input, { chat: true });
155
+ const planArgument = directive === undefined ? typedStartArgument(input) : undefined;
156
+ if (planArgument !== undefined) {
157
+ const started = await startTypedWork({
158
+ projectRoot: options.projectRoot,
159
+ sessionID: event.sessionID,
160
+ ...(planArgument === "" ? {} : { plan: planArgument }),
161
+ options: options.config.boundedAuthority
162
+ });
163
+ if (started.quiet !== true)
164
+ state.user.push(started.text);
165
+ }
135
166
  if (directive?.action === "resume") {
136
167
  try {
137
168
  const receipt = await applyTrustedStartWorkDirective(options.projectRoot, event.sessionID, directive, options.config.boundedAuthority);
@@ -160,15 +191,17 @@ export function createV2SessionHooks(options) {
160
191
  activation = `${reentry.line}\n${activation}`;
161
192
  else if (reentry.kind === "confirm")
162
193
  activation = `${activation}\n${reentry.line}`;
194
+ if (reentry.kind !== "confirm" && mode !== "lit-handoff" && mode !== "lit-scientific-visualization") {
195
+ const recorded = await recordActivation({ memory: activationMemory, projectRoot: options.projectRoot, sessionID: event.sessionID, mode });
196
+ if (recorded.line !== undefined)
197
+ activation = `${activation}\n${recorded.line}`;
198
+ }
163
199
  state.activation = activation;
164
200
  event.metadata = {
165
201
  ...(event.metadata ?? {}),
166
202
  litopencode: { mode, trigger, source: "prompt", ...(reentry.kind === "full" ? {} : { reentry: reentry.kind }) }
167
203
  };
168
204
  options.onActivation?.(event.sessionID, mode);
169
- if (reentry.kind !== "confirm" && mode !== "lit-handoff" && mode !== "lit-scientific-visualization") {
170
- activationMemory.remember(event.sessionID, mode);
171
- }
172
205
  if (mode === "lit-task" || mode === "lit-recap" || mode === "lit-brainstorm" || isReadOnlyWorkflowFamilyPlan(mode))
173
206
  return;
174
207
  try {
@@ -0,0 +1,53 @@
1
+ export declare const workflowStackSchema = "litopencode.workflow-stack/v1";
2
+ export type FrameStatus = "active" | "paused";
3
+ export type StackFrame = {
4
+ readonly skill: string;
5
+ readonly status: FrameStatus;
6
+ readonly at: number;
7
+ readonly phase?: string;
8
+ /** Both counts or neither: an unknown total is never stored as zero. */
9
+ readonly done?: number;
10
+ readonly total?: number;
11
+ };
12
+ export type WorkflowStack = {
13
+ readonly schema: typeof workflowStackSchema;
14
+ readonly sessionID: string;
15
+ readonly frames: readonly StackFrame[];
16
+ readonly updatedAt: number;
17
+ };
18
+ export type FrameProgress = {
19
+ readonly phase?: string;
20
+ readonly done?: number;
21
+ readonly total?: number;
22
+ };
23
+ export declare function isSpineSkill(skill: string): boolean;
24
+ export declare function emptyStack(sessionID: string): WorkflowStack;
25
+ /** A new spine skill ends whatever the stack held: the user moved on to a different discipline. */
26
+ export declare function startSpine(stack: WorkflowStack, skill: string, now: number): WorkflowStack;
27
+ /** Records the spine's progress without pausing it, so a later pause or a card read has real numbers. */
28
+ export declare function pauseSpine(stack: WorkflowStack, now: number, progress?: FrameProgress): WorkflowStack;
29
+ /**
30
+ * Starts a side skill. An active spine is paused under it, keeping its stage and counts; a second side
31
+ * skill replaces the first. With no spine the side skill stands alone.
32
+ */
33
+ export declare function pushSide(stack: WorkflowStack, skill: string, now: number, progress: FrameProgress | undefined): WorkflowStack;
34
+ /** Closes the side frame and makes the paused spine active again at the stage it was paused in. */
35
+ export declare function resumeSpine(stack: WorkflowStack, now: number): {
36
+ readonly stack: WorkflowStack;
37
+ readonly resumed?: StackFrame;
38
+ };
39
+ export declare function stackFile(projectRoot: string, sessionID: string): string;
40
+ /** The session's stack, or undefined when the id is unsafe, the file is missing, a link, oversized or malformed. */
41
+ export declare function readStack(projectRoot: string, sessionID: string): Promise<WorkflowStack | undefined>;
42
+ export declare function writeStack(projectRoot: string, stack: WorkflowStack): Promise<void>;
43
+ export type SiblingOptions = {
44
+ readonly now: number;
45
+ readonly freshMs: number;
46
+ readonly max: number;
47
+ };
48
+ /** Other sessions' stacks that moved recently, for the thread rows. Links and the session's own file are skipped. */
49
+ export declare function readSiblingStacks(projectRoot: string, selfID: string, options: SiblingOptions): Promise<readonly WorkflowStack[]>;
50
+ /** The TUI reads on its own refresh and cannot await: the same checks as `readStack`, synchronously. */
51
+ export declare function readStackSync(projectRoot: string, sessionID: string): WorkflowStack | undefined;
52
+ /** Synchronous twin of `readSiblingStacks`. */
53
+ export declare function readSiblingStacksSync(projectRoot: string, selfID: string, options: SiblingOptions): readonly WorkflowStack[];
@@ -0,0 +1,196 @@
1
+ import { lstatSync, readFileSync, readdirSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createRuntimePaths } from "./state.js";
5
+ // A session's workflow stack: the discipline it is running (the spine) and, while another lit command
6
+ // is interjected, the paused spine underneath it. An interjected command pauses the spine and never
7
+ // erases it, so `lit continue` returns to the same stage and the same k/n. Transitions are pure; the
8
+ // store keeps one small JSON file per session so the TUI band and a restarted plugin read the same
9
+ // record.
10
+ export const workflowStackSchema = "litopencode.workflow-stack/v1";
11
+ const spineSkills = new Set(["lit-loop", "lit-plan", "start-work", "lit-task"]);
12
+ const safeSessionID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
13
+ const stackByteLimit = 64 * 1024;
14
+ const frameLimit = 4;
15
+ export function isSpineSkill(skill) {
16
+ return spineSkills.has(skill);
17
+ }
18
+ export function emptyStack(sessionID) {
19
+ return { schema: workflowStackSchema, sessionID, frames: [], updatedAt: 0 };
20
+ }
21
+ function counts(progress) {
22
+ const done = progress?.done;
23
+ const total = progress?.total;
24
+ return Number.isInteger(done) && Number.isInteger(total) && total > 0 && done >= 0 && done <= total
25
+ ? { done: done, total: total }
26
+ : {};
27
+ }
28
+ function frame(skill, status, at, progress) {
29
+ return { skill, status, at, ...(progress?.phase === undefined ? {} : { phase: progress.phase }), ...counts(progress) };
30
+ }
31
+ function withFrames(stack, frames, now) {
32
+ return { ...stack, frames: frames.slice(-frameLimit), updatedAt: now };
33
+ }
34
+ /** A new spine skill ends whatever the stack held: the user moved on to a different discipline. */
35
+ export function startSpine(stack, skill, now) {
36
+ return withFrames(stack, [frame(skill, "active", now)], now);
37
+ }
38
+ /** Records the spine's progress without pausing it, so a later pause or a card read has real numbers. */
39
+ export function pauseSpine(stack, now, progress) {
40
+ const [spine, ...rest] = stack.frames;
41
+ if (spine === undefined || !isSpineSkill(spine.skill))
42
+ return stack;
43
+ return withFrames(stack, [frame(spine.skill, "paused", spine.at, { phase: progress?.phase ?? spine.phase, done: progress?.done ?? spine.done, total: progress?.total ?? spine.total }), ...rest], now);
44
+ }
45
+ /**
46
+ * Starts a side skill. An active spine is paused under it, keeping its stage and counts; a second side
47
+ * skill replaces the first. With no spine the side skill stands alone.
48
+ */
49
+ export function pushSide(stack, skill, now, progress) {
50
+ const spine = stack.frames.find((candidate) => isSpineSkill(candidate.skill));
51
+ if (spine === undefined)
52
+ return withFrames(stack, [frame(skill, "active", now)], now);
53
+ const paused = pauseSpine({ ...stack, frames: [spine] }, now, progress).frames[0];
54
+ return withFrames(stack, [paused, frame(skill, "active", now)], now);
55
+ }
56
+ /** Closes the side frame and makes the paused spine active again at the stage it was paused in. */
57
+ export function resumeSpine(stack, now) {
58
+ const spine = stack.frames.find((candidate) => isSpineSkill(candidate.skill) && candidate.status === "paused");
59
+ if (spine === undefined)
60
+ return { stack };
61
+ const resumed = { ...spine, status: "active" };
62
+ return { stack: withFrames(stack, [resumed], now), resumed };
63
+ }
64
+ export function stackFile(projectRoot, sessionID) {
65
+ if (!safeSessionID.test(sessionID))
66
+ throw new Error("workflow stack session id must match [A-Za-z0-9_-]");
67
+ return path.join(createRuntimePaths(projectRoot).workflowStackDir, `${sessionID}.json`);
68
+ }
69
+ function isFrame(value) {
70
+ if (typeof value !== "object" || value === null)
71
+ return false;
72
+ const record = value;
73
+ const optionalInt = (field) => field === undefined || (Number.isInteger(field) && field >= 0);
74
+ return typeof record.skill === "string" && (record.status === "active" || record.status === "paused") && Number.isFinite(record.at) &&
75
+ (record.phase === undefined || typeof record.phase === "string") && optionalInt(record.done) && optionalInt(record.total);
76
+ }
77
+ async function readRegular(file) {
78
+ try {
79
+ const stat = await fs.lstat(file);
80
+ if (!stat.isFile() || stat.size > stackByteLimit)
81
+ return undefined;
82
+ return await fs.readFile(file, "utf8");
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ function parseStack(text, sessionID) {
89
+ if (text === undefined)
90
+ return undefined;
91
+ let value;
92
+ try {
93
+ value = JSON.parse(text);
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ if (typeof value !== "object" || value === null)
99
+ return undefined;
100
+ const record = value;
101
+ if (record.schema !== workflowStackSchema || record.sessionID !== sessionID || !Array.isArray(record.frames) || !record.frames.every(isFrame) || !Number.isFinite(record.updatedAt))
102
+ return undefined;
103
+ return { schema: workflowStackSchema, sessionID, frames: record.frames.slice(-frameLimit), updatedAt: record.updatedAt };
104
+ }
105
+ /** The session's stack, or undefined when the id is unsafe, the file is missing, a link, oversized or malformed. */
106
+ export async function readStack(projectRoot, sessionID) {
107
+ if (!safeSessionID.test(sessionID))
108
+ return undefined;
109
+ return parseStack(await readRegular(stackFile(projectRoot, sessionID)), sessionID);
110
+ }
111
+ export async function writeStack(projectRoot, stack) {
112
+ const file = stackFile(projectRoot, stack.sessionID);
113
+ const dir = path.dirname(file);
114
+ await fs.mkdir(dir, { recursive: true });
115
+ const dirStat = await fs.lstat(dir);
116
+ if (!dirStat.isDirectory())
117
+ throw new Error("workflow stack directory is not a directory");
118
+ try {
119
+ if ((await fs.lstat(file)).isSymbolicLink())
120
+ throw new Error("workflow stack file is a link");
121
+ }
122
+ catch (error) {
123
+ if (error.code !== "ENOENT")
124
+ throw error;
125
+ }
126
+ const temp = `${file}.${process.pid}.tmp`;
127
+ await fs.writeFile(temp, `${JSON.stringify(stack)}\n`, { mode: 0o600 });
128
+ await fs.rename(temp, file);
129
+ }
130
+ /** Other sessions' stacks that moved recently, for the thread rows. Links and the session's own file are skipped. */
131
+ export async function readSiblingStacks(projectRoot, selfID, options) {
132
+ const dir = createRuntimePaths(projectRoot).workflowStackDir;
133
+ let names;
134
+ try {
135
+ names = await fs.readdir(dir);
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ const found = [];
141
+ for (const name of names.sort()) {
142
+ if (!name.endsWith(".json"))
143
+ continue;
144
+ const sessionID = name.slice(0, -".json".length);
145
+ if (sessionID === selfID || !safeSessionID.test(sessionID))
146
+ continue;
147
+ const stack = parseStack(await readRegular(path.join(dir, name)), sessionID);
148
+ if (stack === undefined || stack.frames.length === 0 || options.now - stack.updatedAt > options.freshMs)
149
+ continue;
150
+ found.push(stack);
151
+ if (found.length >= options.max)
152
+ break;
153
+ }
154
+ return found;
155
+ }
156
+ function readRegularSync(file) {
157
+ try {
158
+ const stat = lstatSync(file);
159
+ return stat.isFile() && stat.size <= stackByteLimit ? readFileSync(file, "utf8") : undefined;
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
165
+ /** The TUI reads on its own refresh and cannot await: the same checks as `readStack`, synchronously. */
166
+ export function readStackSync(projectRoot, sessionID) {
167
+ if (!safeSessionID.test(sessionID))
168
+ return undefined;
169
+ return parseStack(readRegularSync(stackFile(projectRoot, sessionID)), sessionID);
170
+ }
171
+ /** Synchronous twin of `readSiblingStacks`. */
172
+ export function readSiblingStacksSync(projectRoot, selfID, options) {
173
+ const dir = createRuntimePaths(projectRoot).workflowStackDir;
174
+ let names;
175
+ try {
176
+ names = readdirSync(dir);
177
+ }
178
+ catch {
179
+ return [];
180
+ }
181
+ const found = [];
182
+ for (const name of names.sort()) {
183
+ if (!name.endsWith(".json"))
184
+ continue;
185
+ const sessionID = name.slice(0, -".json".length);
186
+ if (sessionID === selfID || !safeSessionID.test(sessionID))
187
+ continue;
188
+ const stack = parseStack(readRegularSync(path.join(dir, name)), sessionID);
189
+ if (stack === undefined || stack.frames.length === 0 || options.now - stack.updatedAt > options.freshMs)
190
+ continue;
191
+ found.push(stack);
192
+ if (found.length >= options.max)
193
+ break;
194
+ }
195
+ return found;
196
+ }
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.31"><title>release: 1.0.31</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.31</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.33"><title>release: 1.0.33</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.33</text></g></svg>