@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
@@ -4,7 +4,11 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { planChecklist, readLitMark, readWorkState } from "./mission-model.js";
7
+ import { isLaneResult, isReviewLane } from "./mission-looks.js";
7
8
  import { readSkillUse, skillChips } from "./mission-skills.js";
9
+ import { isRealCount } from "./mission-threads.js";
10
+ import { readSiblingStacksSync, readStackSync } from "../workflow-stack.js";
11
+ import { readLivenessSync } from "../session-liveness.js";
8
12
  const ledgerByteLimit = 1024 * 1024;
9
13
  /** The same root the server plugin uses: the worktree unless it is the filesystem root, else the directory. */
10
14
  export function missionProjectRoot(paths) {
@@ -67,7 +71,7 @@ export function createWorkReader() {
67
71
  return undefined;
68
72
  const inside = work.planPath !== "" && path.isAbsolute(work.planPath) && !path.relative(path.resolve(root), work.planPath).startsWith("..");
69
73
  const plan = inside ? readPlan(work.planPath) : undefined;
70
- return { status: work.status, iterations: work.iterations, ...(plan === undefined ? {} : { plan }) };
74
+ return { status: work.status, iterations: work.iterations, ...(plan === undefined ? {} : { plan }), ...(work.boundary === true ? { boundary: true } : {}) };
71
75
  };
72
76
  }
73
77
  function lstatOrUndefined(file) {
@@ -235,6 +239,35 @@ export function readMissionInput(state, sessionID, ledger, now, records = {}) {
235
239
  now
236
240
  };
237
241
  }
242
+ /** A completed `review-work` call with action `lane`: one lane's reported result. */
243
+ export function readLaneReport(part) {
244
+ if (!isRecord(part) || part.type !== "tool" || part.tool !== "review-work" || !isRecord(part.state) || part.state.status !== "completed")
245
+ return undefined;
246
+ const input = isRecord(part.state.input) ? part.state.input : {};
247
+ if (input.action !== "lane" || !isReviewLane(input.lane) || !isLaneResult(input.result))
248
+ return undefined;
249
+ return { lane: input.lane, result: input.result };
250
+ }
251
+ /**
252
+ * True when the reply's first line of prose opens with `BLOCKED:`. The ignition mark comes first on the
253
+ * host (a fenced LIT block from experimental.text.complete, and the model's `LIT IGNITED` line), so those
254
+ * lines and blank ones are passed over; anything else ends the search.
255
+ */
256
+ export function opensBlocked(parts) {
257
+ const text = parts.map((part) => (isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "")).join("\n");
258
+ let fenced = false;
259
+ for (const line of text.split("\n")) {
260
+ const trimmed = line.trim();
261
+ if (trimmed.startsWith("```")) {
262
+ fenced = !fenced;
263
+ continue;
264
+ }
265
+ if (fenced || trimmed === "" || trimmed.includes("LIT IGNITED"))
266
+ continue;
267
+ return /^BLOCKED:/u.test(trimmed);
268
+ }
269
+ return false;
270
+ }
238
271
  /**
239
272
  * What the band card shows beside the mission: the session's skill chips, whether a turn is running,
240
273
  * context use and git branch. OpenCode keeps each session's last `session.status` ({ type: "busy" },
@@ -244,23 +277,205 @@ export function readCardInput(state, sessionID) {
244
277
  const messages = state.session.messages(sessionID);
245
278
  const uses = [];
246
279
  let since;
280
+ // A new review start clears the lanes, so the border shows only this review's reports.
281
+ let lanes;
247
282
  for (const message of messages) {
248
283
  const at = num(message.time?.created);
249
284
  if (message.role === "user")
250
285
  since = at;
251
286
  for (const part of state.part(message.id)) {
287
+ const report = readLaneReport(part);
288
+ if (report !== undefined) {
289
+ lanes = { ...lanes, [report.lane]: report.result };
290
+ continue;
291
+ }
252
292
  const use = readSkillUse(part, at);
253
293
  if (use !== undefined)
254
294
  uses.push(use);
295
+ const action = isRecord(part) && isRecord(part.state) && isRecord(part.state.input) ? part.state.input.action : undefined;
296
+ if (use?.name === "review-work" && action !== "status" && action !== "lane")
297
+ lanes = {};
255
298
  }
256
299
  }
257
300
  const status = state.session.status(sessionID)?.type;
258
301
  const busy = status === "busy" || status === "retry";
259
302
  const branch = state.vcs?.branch?.trim();
303
+ const last = messages.at(-1);
304
+ const asked = state.session.permission(sessionID).length > 0 || state.session.question(sessionID).length > 0;
260
305
  return {
261
306
  chips: skillChips(uses, { since, busy }),
262
307
  busy,
263
308
  contextPercent: contextPercent(state, messages),
264
- ...(branch === undefined || branch === "" ? {} : { branch })
309
+ ...(branch === undefined || branch === "" ? {} : { branch }),
310
+ ...(lanes === undefined ? {} : { lanes }),
311
+ ...(asked ? { asked } : {}),
312
+ ...(last?.role === "user" ? { userSpokeLast: true } : {}),
313
+ ...(last?.role === "assistant" && opensBlocked(state.part(last.id)) ? { blockedReply: true } : {})
314
+ };
315
+ }
316
+ export const earlierWorkWindowMs = 7 * 24 * 60 * 60_000;
317
+ const siblingFreshMs = 15 * 60_000;
318
+ const siblingMax = 6;
319
+ const taskLingerMs = 30_000;
320
+ const recentAssistantMessages = 3;
321
+ function isRecord(value) {
322
+ return typeof value === "object" && value !== null && !Array.isArray(value);
323
+ }
324
+ function countsOf(done, total) {
325
+ return isRealCount(done, total) ? { done, total } : {};
326
+ }
327
+ function taskSources(state, sessionID, now) {
328
+ const sources = [];
329
+ const children = new Set();
330
+ const assistants = state.session.messages(sessionID).filter((message) => message.role === "assistant").slice(-recentAssistantMessages);
331
+ for (const message of assistants) {
332
+ for (const part of state.part(message.id)) {
333
+ if (part.type !== "tool" || part.tool !== "task" || !isRecord(part.state))
334
+ continue;
335
+ const toolState = part.state;
336
+ const status = toolState.status;
337
+ const time = isRecord(toolState.time) ? toolState.time : {};
338
+ const start = typeof time.start === "number" ? time.start : undefined;
339
+ const end = typeof time.end === "number" ? time.end : undefined;
340
+ if (status !== "running" && status !== "completed" && status !== "error")
341
+ continue;
342
+ if (status !== "running" && (end === undefined || now - end > taskLingerMs))
343
+ continue;
344
+ const input = isRecord(toolState.input) ? toolState.input : {};
345
+ const metadata = isRecord(toolState.metadata) ? toolState.metadata : {};
346
+ const child = typeof metadata.sessionId === "string" ? metadata.sessionId : undefined;
347
+ if (child !== undefined)
348
+ children.add(child);
349
+ const todos = child === undefined ? [] : state.session.todo(child).filter((todo) => todo.status !== "cancelled");
350
+ const done = todos.filter((todo) => todo.status === "completed").length;
351
+ const name = typeof input.description === "string" && input.description.trim() !== "" ? input.description.trim() : typeof input.subagent_type === "string" ? input.subagent_type : "subagent";
352
+ // A running task last moved when its own session did; its start time alone would read a long, busy run as idle.
353
+ const moved = child === undefined ? undefined : lastActivity(state, child);
354
+ sources.push({
355
+ kind: "task",
356
+ name,
357
+ status: status === "running" ? "working" : status === "completed" ? "done" : "failed",
358
+ ...countsOf(done, todos.length),
359
+ ...(start === undefined ? {} : { startedAt: start }),
360
+ touchedAt: end ?? (start === undefined && moved === undefined ? now : Math.max(start ?? 0, moved ?? 0))
361
+ });
362
+ }
363
+ }
364
+ return { sources, children };
365
+ }
366
+ /** The latest time any message of a session was created or completed, if it has one. */
367
+ function lastActivity(state, sessionID) {
368
+ let latest;
369
+ for (const message of state.session.messages(sessionID)) {
370
+ for (const at of [message.time?.created, message.time?.completed]) {
371
+ if (typeof at === "number" && Number.isFinite(at) && (latest === undefined || at > latest))
372
+ latest = at;
373
+ }
374
+ }
375
+ return latest;
376
+ }
377
+ function stackSources(stack) {
378
+ return (stack?.frames ?? []).map((frame) => ({
379
+ kind: "stack",
380
+ name: frame.skill,
381
+ status: frame.status === "active" ? "working" : "paused",
382
+ ...countsOf(frame.done, frame.total),
383
+ ...(frame.phase === undefined ? {} : { phase: frame.phase }),
384
+ touchedAt: frame.at
385
+ }));
386
+ }
387
+ /** `<skill> · <herdr pane>` when the session runs in a herdr pane, else the last four characters of its id. */
388
+ function siblingName(skill, sessionID, liveness) {
389
+ return `${skill} · ${liveness?.herdrPane ?? sessionID.slice(-4)}`;
390
+ }
391
+ /** A sibling's own liveness record settles its status: ended reads done, idle reads idle (a held frame stays paused). */
392
+ function siblingSource(stack, liveness, finishedAt) {
393
+ const top = stack.frames.find((frame) => frame.status === "active") ?? stack.frames[stack.frames.length - 1];
394
+ const endedAt = liveness?.state === "ended" ? Date.parse(liveness.at) : finishedAt;
395
+ const status = endedAt !== undefined ? "done" : top.status !== "active" ? "paused" : liveness?.state === "idle" ? "idle" : "working";
396
+ return {
397
+ kind: "session",
398
+ name: siblingName(top.skill, stack.sessionID, liveness),
399
+ status,
400
+ ...countsOf(top.done, top.total),
401
+ ...(top.phase === undefined ? {} : { phase: top.phase }),
402
+ touchedAt: Math.max(stack.updatedAt, liveness === undefined ? 0 : Date.parse(liveness.at)),
403
+ ...(endedAt === undefined ? {} : { doneAt: endedAt }),
404
+ ...(liveness === undefined && finishedAt === undefined ? {} : { settled: true })
265
405
  };
266
406
  }
407
+ /** Another session's start-work run that completed, with when, read from the bounded work record. */
408
+ function finishedWork(root, sessionID, now) {
409
+ const file = path.join(litLoopDir(root), "work-schema-3.json");
410
+ const stat = lstatOrUndefined(file);
411
+ if (stat === undefined || !stat.isFile() || stat.size > ledgerByteLimit)
412
+ return undefined;
413
+ try {
414
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
415
+ if (!isRecord(raw) || raw.schemaVersion !== 3 || raw.status !== "completed" || typeof raw.sessionID !== "string" || raw.sessionID === sessionID)
416
+ return undefined;
417
+ const at = typeof raw.updatedAt === "string" ? Date.parse(raw.updatedAt) : Number.NaN;
418
+ return Number.isFinite(at) && at <= now && now - at <= siblingFreshMs ? { sessionID: raw.sessionID, at } : undefined;
419
+ }
420
+ catch {
421
+ return undefined;
422
+ }
423
+ }
424
+ /**
425
+ * The lanes beside this session's own mission: its workflow stack frames, its running `task` calls and
426
+ * the stacks of other sessions in the workspace that moved in the last fifteen minutes, read with their
427
+ * liveness records. Another session's completed start-work run shows as done. Only counts that are real
428
+ * totals are carried; the input of a tool call is read for its description alone.
429
+ */
430
+ export function readThreadSources(state, sessionID, root, now) {
431
+ const tasks = taskSources(state, sessionID, now);
432
+ if (root === undefined)
433
+ return tasks.sources;
434
+ const siblings = readSiblingStacksSync(root, sessionID, { now, freshMs: siblingFreshMs, max: siblingMax }).filter((stack) => !tasks.children.has(stack.sessionID));
435
+ const finished = finishedWork(root, sessionID, now);
436
+ const others = siblings.map((stack) => siblingSource(stack, readLivenessSync(root, stack.sessionID), stack.sessionID === finished?.sessionID ? finished.at : undefined));
437
+ if (finished !== undefined && !siblings.some((stack) => stack.sessionID === finished.sessionID) && !tasks.children.has(finished.sessionID)) {
438
+ others.push({ kind: "session", name: siblingName("start-work", finished.sessionID, readLivenessSync(root, finished.sessionID)), status: "done", touchedAt: finished.at, doneAt: finished.at, settled: true });
439
+ }
440
+ return [...stackSources(readStackSync(root, sessionID)), ...tasks.sources, ...others];
441
+ }
442
+ /**
443
+ * Whether earlier work exists for the band's idle hint and the one suggestion: another session here
444
+ * touched its workflow stack within `withinMs`.
445
+ */
446
+ /** When the session's last message and its last user prompt were created, from the reactive store. */
447
+ export function messageTimes(state, sessionID) {
448
+ let lastAt;
449
+ let lastPromptAt;
450
+ for (const message of state.session.messages(sessionID)) {
451
+ const created = message.time?.created;
452
+ if (typeof created !== "number")
453
+ continue;
454
+ if (lastAt === undefined || created > lastAt)
455
+ lastAt = created;
456
+ if (message.role === "user" && (lastPromptAt === undefined || created > lastPromptAt))
457
+ lastPromptAt = created;
458
+ }
459
+ return { ...(lastAt === undefined ? {} : { lastAt }), ...(lastPromptAt === undefined ? {} : { lastPromptAt }) };
460
+ }
461
+ /** The way back in is offered after this long without a message in the session. */
462
+ export const adhdBreakMs = 30 * 60_000;
463
+ /** The idle card names lit adhd in a fresh session or after a break, only when earlier work exists. */
464
+ export function wayBackDue(lastActivityAt, now, earlier) {
465
+ return earlier && (lastActivityAt === undefined || now - lastActivityAt >= adhdBreakMs);
466
+ }
467
+ function localDay(at) {
468
+ const date = new Date(at);
469
+ return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
470
+ }
471
+ /** At most one start suggestion per workspace per local day; records the day when it answers true. */
472
+ export function adhdSuggestDue(days, root, now) {
473
+ const day = localDay(now);
474
+ if (days.get(root) === day)
475
+ return false;
476
+ days.set(root, day);
477
+ return true;
478
+ }
479
+ export function hasEarlierWork(root, sessionID, now, withinMs) {
480
+ return root !== undefined && readSiblingStacksSync(root, sessionID, { now, freshMs: withinMs, max: 1 }).length > 0;
481
+ }
@@ -53,6 +53,8 @@ export declare function chipBudget(input: {
53
53
  readonly child: boolean;
54
54
  }): number;
55
55
  export declare function cellWidth(value: string): number;
56
+ /** Record text without terminal escape sequences or control characters; tabs and newlines become spaces later. */
57
+ export declare function stripControls(value: string): string;
56
58
  /** Cuts `value` to at most `max` cells, ending in an ellipsis when anything was dropped. */
57
59
  export declare function truncateCells(value: string, max: number): string;
58
60
  export declare function padCells(value: string, width: number): string;
@@ -120,9 +120,17 @@ export function cellWidth(value) {
120
120
  width += charWidth(char);
121
121
  return width;
122
122
  }
123
+ // Escape and control sequences (CSI, OSC, C1) from plan rows, todos, branches or commit text would act on
124
+ // the terminal instead of being drawn, so they are removed before any record text reaches a cell.
125
+ const terminalSequence = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b[@-Z\\-_]|\u009b[0-?]*[ -/]*[@-~]|\u009d[^\u0007\u009c]*[\u0007\u009c]?/gu;
126
+ const controlCharacter = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
127
+ /** Record text without terminal escape sequences or control characters; tabs and newlines become spaces later. */
128
+ export function stripControls(value) {
129
+ return value.replace(terminalSequence, "").replace(controlCharacter, "");
130
+ }
123
131
  /** Cuts `value` to at most `max` cells, ending in an ellipsis when anything was dropped. */
124
132
  export function truncateCells(value, max) {
125
- const clean = value.replace(/\s+/gu, " ").trim();
133
+ const clean = stripControls(value).replace(/\s+/gu, " ").trim();
126
134
  if (cellWidth(clean) <= max)
127
135
  return clean;
128
136
  if (max <= 0)
@@ -0,0 +1,58 @@
1
+ import { type Line, type MissionPalette } from "./mission-text.ts";
2
+ export type ThreadStatus = "working" | "paused" | "done" | "failed" | "idle";
3
+ export type ThreadSource = {
4
+ readonly kind: "stack" | "task" | "session";
5
+ readonly name: string;
6
+ readonly status: ThreadStatus;
7
+ /** Both counts or neither. */
8
+ readonly done?: number;
9
+ readonly total?: number;
10
+ /** A word for what is known, such as the stage a frame is on. */
11
+ readonly phase?: string;
12
+ /** When the lane started, for the elapsed time of a task. */
13
+ readonly startedAt?: number;
14
+ /** When the lane last moved; an old one reads as idle. */
15
+ readonly touchedAt?: number;
16
+ /** When a done lane finished; it leaves the card a minute later. */
17
+ readonly doneAt?: number;
18
+ /** The status came from the session's liveness record, so the five-minute idle guess does not apply. */
19
+ readonly settled?: boolean;
20
+ };
21
+ export type ThreadRow = {
22
+ readonly key: string;
23
+ readonly name: string;
24
+ readonly status: ThreadStatus;
25
+ readonly done?: number;
26
+ readonly total?: number;
27
+ readonly facts: string;
28
+ };
29
+ export declare const threadRowCap = 4;
30
+ /** A lane untouched for this long is shown as idle rather than working. */
31
+ export declare const idleAfterMs: number;
32
+ /** A finished lane stays this long, so a glance still catches it, then leaves. */
33
+ export declare const doneLingerMs = 60000;
34
+ export declare function isRealCount(done: unknown, total: unknown): done is number;
35
+ /**
36
+ * The rows for the sources, or undefined when only one lane of the session's own work exists, which the
37
+ * four-row card already shows; one other session is still a row. A source untouched for five minutes reads
38
+ * as idle unless a liveness record settled it, a done lane leaves a minute after it finished, and other
39
+ * sessions follow the session's own lanes, working first, then paused, finished and idle.
40
+ */
41
+ export declare function threadsOf(sources: readonly ThreadSource[], now: number): ThreadRow[] | undefined;
42
+ /** How many card rows the threads take: the fork line, the shown lanes and the `+N more` line. */
43
+ export declare function threadLineCount(rows: readonly ThreadRow[] | undefined): number;
44
+ /** `[▉▉▉░░░░░] 3/10` for a real total, an empty string otherwise. */
45
+ export declare function threadGauge(done: number | undefined, total: number | undefined, cells: number): string;
46
+ /** An indeterminate pulse: five cells that scan while motion is on, one steady frame otherwise. */
47
+ export declare function pulseText(now: number, motion: boolean): string;
48
+ export type ThreadDrawOptions = {
49
+ readonly now: number;
50
+ readonly motion: boolean;
51
+ /** A colour per gauge index while the rainbow plays; the gauge keeps the palette colour otherwise. */
52
+ readonly gaugeColor?: (index: number) => string;
53
+ };
54
+ /**
55
+ * The lines under the skill row: `┬ 3 lanes in parallel`, then one `├─`/`└─` line per lane (four at most;
56
+ * `+N more` names the rest). Each line is fitted to `width` by the caller.
57
+ */
58
+ export declare function threadLines(rows: readonly ThreadRow[], palette: MissionPalette, width: number, options: ThreadDrawOptions): Line[];
@@ -0,0 +1,128 @@
1
+ // Thread rows for the band: the lanes of work that run beside the session's own mission. A lane is a
2
+ // frame on this session's workflow stack (a paused spine under an interjected command), a `task` tool
3
+ // call that runs a subagent, or another session in the same workspace whose stack moved lately. The
4
+ // module is pure: sources go in, rows and drawn lines come out.
5
+ //
6
+ // A gauge needs a real total. Without one a row says what is known in words (how long it has run, which
7
+ // step it is on) and, while it is working, plays a small pulse; it never shows a question mark.
8
+ import { cellWidth, lineWidth, truncateCells } from "./mission-text.js";
9
+ export const threadRowCap = 4;
10
+ /** A lane untouched for this long is shown as idle rather than working. */
11
+ export const idleAfterMs = 5 * 60_000;
12
+ /** A finished lane stays this long, so a glance still catches it, then leaves. */
13
+ export const doneLingerMs = 60_000;
14
+ const sessionRank = { working: 0, paused: 1, done: 2, failed: 2, idle: 3 };
15
+ const pulseFrames = ["▓▒░░░", "░▓▒░░", "░░▓▒░", "░░░▓▒", "░░▒▓░", "░▒▓░░"];
16
+ const steadyPulse = "░▒▓▒░";
17
+ const pulseMs = 250;
18
+ const statusWordCells = 7;
19
+ export function isRealCount(done, total) {
20
+ return Number.isInteger(done) && Number.isInteger(total) && total > 0 && done >= 0 && done <= total;
21
+ }
22
+ function minutes(from, now) {
23
+ if (from === undefined || !Number.isFinite(from) || from > now)
24
+ return undefined;
25
+ const total = Math.floor((now - from) / 60_000);
26
+ return total < 1 ? "<1m" : total < 60 ? `${total}m` : `${Math.floor(total / 60)}h${String(total % 60).padStart(2, "0")}`;
27
+ }
28
+ function factsFor(source, status, now) {
29
+ const parts = [];
30
+ if (source.phase !== undefined && source.phase !== "")
31
+ parts.push(source.phase);
32
+ const elapsed = minutes(source.startedAt ?? (status === "working" ? source.touchedAt : undefined), now);
33
+ if (elapsed !== undefined && status === "working")
34
+ parts.push(elapsed);
35
+ if (parts.length === 0)
36
+ parts.push(status === "done" ? "finished" : status === "failed" ? "stopped" : status === "paused" ? "held" : status === "idle" ? "quiet" : "started");
37
+ return parts.join(" · ");
38
+ }
39
+ /**
40
+ * The rows for the sources, or undefined when only one lane of the session's own work exists, which the
41
+ * four-row card already shows; one other session is still a row. A source untouched for five minutes reads
42
+ * as idle unless a liveness record settled it, a done lane leaves a minute after it finished, and other
43
+ * sessions follow the session's own lanes, working first, then paused, finished and idle.
44
+ */
45
+ export function threadsOf(sources, now) {
46
+ const live = sources.filter((source) => !(source.status === "done" && source.doneAt !== undefined && now - source.doneAt > doneLingerMs));
47
+ const statusOf = (source) => source.status === "working" && source.settled !== true && source.touchedAt !== undefined && now - source.touchedAt > idleAfterMs ? "idle" : source.status;
48
+ const own = live.filter((source) => source.kind !== "session");
49
+ const others = live.filter((source) => source.kind === "session").sort((left, right) => sessionRank[statusOf(left)] - sessionRank[statusOf(right)]);
50
+ if (others.length === 0 && own.length < 2)
51
+ return undefined;
52
+ return [...own, ...others].map((source, index) => {
53
+ const status = statusOf(source);
54
+ return {
55
+ key: `${source.kind}:${index}:${source.name}`,
56
+ name: source.name,
57
+ status,
58
+ ...(isRealCount(source.done, source.total) ? { done: source.done, total: source.total } : {}),
59
+ facts: factsFor(source, status, now)
60
+ };
61
+ });
62
+ }
63
+ /** How many card rows the threads take: the fork line, the shown lanes and the `+N more` line. */
64
+ export function threadLineCount(rows) {
65
+ if (rows === undefined || rows.length === 0)
66
+ return 0;
67
+ return 1 + Math.min(rows.length, threadRowCap) + (rows.length > threadRowCap ? 1 : 0);
68
+ }
69
+ /** `[▉▉▉░░░░░] 3/10` for a real total, an empty string otherwise. */
70
+ export function threadGauge(done, total, cells) {
71
+ if (!isRealCount(done, total) || cells < 1)
72
+ return "";
73
+ const full = Math.round((done / total) * cells);
74
+ return `[${"▉".repeat(full)}${"░".repeat(cells - full)}] ${done}/${total}`;
75
+ }
76
+ /** An indeterminate pulse: five cells that scan while motion is on, one steady frame otherwise. */
77
+ export function pulseText(now, motion) {
78
+ return motion ? pulseFrames[Math.floor(now / pulseMs) % pulseFrames.length] : steadyPulse;
79
+ }
80
+ const statusWords = { working: "working", paused: "paused", done: "done", failed: "failed", idle: "idle" };
81
+ function statusColor(status, palette) {
82
+ if (status === "working")
83
+ return palette.info;
84
+ if (status === "done")
85
+ return palette.success;
86
+ if (status === "failed")
87
+ return palette.error;
88
+ if (status === "paused")
89
+ return palette.warning;
90
+ return palette.muted;
91
+ }
92
+ function rowLine(row, index, last, palette, width, options) {
93
+ const branch = { text: last ? "└─ " : "├─ ", fg: palette.borderSubtle };
94
+ const word = { text: statusWords[row.status].padEnd(statusWordCells), fg: statusColor(row.status, palette), bold: row.status === "working" };
95
+ const gap = { text: " ", fg: palette.muted };
96
+ const gaugeColor = options.gaugeColor?.(index) ?? palette.accent;
97
+ const real = threadGauge(row.done, row.total, 8);
98
+ const trailing = (room) => {
99
+ if (real !== "" && cellWidth(real) <= room)
100
+ return [{ text: real, fg: gaugeColor }];
101
+ const facts = truncateCells(row.facts, Math.max(0, room - (row.status === "working" ? 6 : 0)));
102
+ const pulse = row.status === "working" ? [{ text: ` ${pulseText(options.now, options.motion)}`, fg: gaugeColor }] : [];
103
+ return [{ text: facts, fg: palette.muted }, ...pulse];
104
+ };
105
+ const fixed = lineWidth([branch, gap, word, gap]);
106
+ const room = Math.max(0, width - fixed);
107
+ const nameRoom = Math.max(6, Math.min(24, Math.floor(room / 2)));
108
+ const name = { text: truncateCells(row.name, nameRoom), fg: row.status === "idle" || row.status === "done" ? palette.muted : palette.text };
109
+ const pad = { text: " ".repeat(Math.max(0, Math.min(24, nameRoom) - cellWidth(name.text))), fg: palette.muted };
110
+ const left = [branch, name, pad, gap, word, gap];
111
+ return [...left, ...trailing(Math.max(0, width - lineWidth(left)))];
112
+ }
113
+ /**
114
+ * The lines under the skill row: `┬ 3 lanes in parallel`, then one `├─`/`└─` line per lane (four at most;
115
+ * `+N more` names the rest). Each line is fitted to `width` by the caller.
116
+ */
117
+ export function threadLines(rows, palette, width, options) {
118
+ if (rows.length === 0)
119
+ return [];
120
+ const shown = rows.slice(0, threadRowCap);
121
+ const more = rows.length - shown.length;
122
+ const fork = [{ text: "┬ ", fg: palette.borderSubtle }, { text: `${rows.length} ${rows.length === 1 ? "lane" : "lanes"} in parallel`, fg: palette.muted }];
123
+ const lines = [fork];
124
+ shown.forEach((row, index) => lines.push(rowLine(row, index, index === shown.length - 1 && more === 0, palette, width, options)));
125
+ if (more > 0)
126
+ lines.push([{ text: "└─ ", fg: palette.borderSubtle }, { text: `+${more} more`, fg: palette.muted }]);
127
+ return lines;
128
+ }