@litfamily/litgrok 1.0.26 → 1.0.28

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 (235) hide show
  1. package/.grok/hooks/lit-mark.mjs +42 -0
  2. package/.grok/hooks/lit-status-line.mjs +174 -21
  3. package/.grok/hooks/plan-gate.mjs +13 -3
  4. package/.grok/hooks/record-passive-event.mjs +40 -6
  5. package/.grok/hooks/session-end.json +15 -0
  6. package/.grok/hooks/session-end.mjs +6 -0
  7. package/.grok/hooks/session-liveness.mjs +102 -0
  8. package/.grok/hooks/session-rows.mjs +57 -0
  9. package/.grok/hooks/session-start.mjs +17 -0
  10. package/.grok/hooks/workflow-stage.mjs +5 -2
  11. package/.grok/rules/00-litgrok.md +14 -0
  12. package/.grok/skills/frontend-ui-ux/ATTRIBUTION.md +23 -0
  13. package/.grok/skills/frontend-ui-ux/SKILL.md +4 -1
  14. package/.grok/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
  15. package/.grok/skills/frontend-ui-ux/kit/components.css +414 -0
  16. package/.grok/skills/frontend-ui-ux/kit/fonts.json +22 -0
  17. package/.grok/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
  18. package/.grok/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
  19. package/.grok/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
  20. package/.grok/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
  21. package/.grok/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
  22. package/.grok/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
  23. package/.grok/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
  24. package/.grok/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
  25. package/.grok/skills/frontend-ui-ux/kit/ui.js +226 -0
  26. package/.grok/skills/frontend-ui-ux/references/adaptive-layout.md +10 -4
  27. package/.grok/skills/frontend-ui-ux/references/brand-and-imagery.md +11 -7
  28. package/.grok/skills/frontend-ui-ux/references/complete-contract.md +21 -0
  29. package/.grok/skills/frontend-ui-ux/references/composition.md +10 -6
  30. package/.grok/skills/frontend-ui-ux/references/craft-floor.md +256 -93
  31. package/.grok/skills/frontend-ui-ux/references/creative-directions.md +9 -6
  32. package/.grok/skills/frontend-ui-ux/references/default-editorial-pixel.json +28 -0
  33. package/.grok/skills/frontend-ui-ux/references/house-styles.md +234 -0
  34. package/.grok/skills/frontend-ui-ux/references/implementation-platforms.md +10 -6
  35. package/.grok/skills/frontend-ui-ux/references/inclusive-interface.md +11 -5
  36. package/.grok/skills/frontend-ui-ux/references/interaction-motion.md +34 -78
  37. package/.grok/skills/frontend-ui-ux/references/motion-guide.md +10 -7
  38. package/.grok/skills/frontend-ui-ux/references/performance-delivery.md +11 -8
  39. package/.grok/skills/frontend-ui-ux/references/probe-loop.md +1 -1
  40. package/.grok/skills/frontend-ui-ux/references/product-direction.md +9 -4
  41. package/.grok/skills/frontend-ui-ux/references/production-interview.md +17 -0
  42. package/.grok/skills/frontend-ui-ux/references/redesign-playbook.md +10 -6
  43. package/.grok/skills/frontend-ui-ux/references/slop-register.md +148 -73
  44. package/.grok/skills/frontend-ui-ux/references/system-foundations.md +10 -4
  45. package/.grok/skills/frontend-ui-ux/references/visual-language.md +10 -4
  46. package/.grok/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -72
  47. package/.grok/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
  48. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-driver.mjs +33 -0
  49. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
  50. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1555 -0
  51. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +153 -0
  52. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
  53. package/.grok/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +139 -0
  54. package/.grok/skills/frontend-ui-ux/scripts/interface-probe.mjs +363 -0
  55. package/.grok/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
  56. package/.grok/skills/lit-adhd/SKILL.md +155 -0
  57. package/.grok/skills/lit-adhd/references/card-examples.md +131 -0
  58. package/.grok/skills/lit-adhd/references/status-taxonomy.md +50 -0
  59. package/.grok/skills/lit-adhd/scripts/lib/brief.mjs +421 -0
  60. package/.grok/skills/lit-adhd/scripts/lib/collect.mjs +395 -0
  61. package/.grok/skills/lit-adhd/scripts/lib/gauge.mjs +87 -0
  62. package/.grok/skills/lit-adhd/scripts/lib/next-step.mjs +22 -0
  63. package/.grok/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
  64. package/.grok/skills/lit-adhd/scripts/lib/project-state-root.mjs +55 -0
  65. package/.grok/skills/lit-adhd/scripts/lib/redact.mjs +64 -0
  66. package/.grok/skills/lit-adhd/scripts/lib/suggest.mjs +140 -0
  67. package/.grok/skills/lit-adhd/scripts/lib/transcripts.mjs +232 -0
  68. package/.grok/skills/lit-adhd/scripts/rewind.mjs +6 -0
  69. package/.grok/skills/lit-diagram-drawer/SKILL.md +17 -5
  70. package/.grok/skills/lit-diagram-drawer/references/output-spec.md +2 -2
  71. package/.grok/skills/lit-diagram-drawer/references/style-guide.md +20 -11
  72. package/.grok/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
  73. package/.grok/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
  74. package/.grok/skills/lit-diagram-drawer/references/type-tree.md +3 -3
  75. package/.grok/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
  76. package/.grok/skills/lit-diagram-drawer/references/verifier-guide.md +4 -0
  77. package/.grok/skills/lit-diagram-drawer/scripts/visual-quality.mjs +2 -2
  78. package/.grok/skills/lit-handoff/SKILL.md +2 -0
  79. package/.grok/skills/lit-hijack/SKILL.md +459 -0
  80. package/.grok/skills/lit-hijack/references/capture-and-provenance.md +124 -0
  81. package/.grok/skills/lit-hijack/references/clean-room-build.md +130 -0
  82. package/.grok/skills/lit-hijack/references/distil-and-licence.md +128 -0
  83. package/.grok/skills/lit-hijack/references/gap-and-port-design.md +150 -0
  84. package/.grok/skills/lit-hijack/references/parity-and-report.md +130 -0
  85. package/.grok/skills/lit-hijack/scripts/hijack.mjs +890 -0
  86. package/.grok/skills/lit-hijack/scripts/lib/safe-fetch.mjs +213 -0
  87. package/.grok/skills/lit-plan/SKILL.md +3 -1
  88. package/.grok/skills/lit-plan/references/plan-schema.md +4 -0
  89. package/.grok/skills/lit-plan/scripts/scaffold-plan.mjs +9 -2
  90. package/.grok/skills/lit-plan/scripts/scope-roots.mjs +83 -0
  91. package/.grok/skills/lit-pptx/SKILL.md +10 -5
  92. package/.grok/skills/lit-pptx/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
  93. package/.grok/skills/lit-pptx/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
  94. package/.grok/skills/lit-pptx/examples/15-design-lecture-bauhaus-ko.md +139 -0
  95. package/.grok/skills/lit-pptx/examples/16-community-programme-riso-ko.md +158 -0
  96. package/.grok/skills/lit-pptx/examples/17-public-data-briefing-newsroom-ko.md +179 -0
  97. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
  98. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
  99. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
  100. package/.grok/skills/lit-pptx/fonts/jetbrains-mono/provenance.json +1 -0
  101. package/.grok/skills/lit-pptx/references/direction-step.md +34 -8
  102. package/.grok/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
  103. package/.grok/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
  104. package/.grok/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
  105. package/.grok/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
  106. package/.grok/skills/lit-pptx/references/tonalities/riso.md +137 -0
  107. package/.grok/skills/lit-pptx/scripts/compile-deck.js +1 -1
  108. package/.grok/skills/lit-pptx/scripts/craft_extras.py +61 -3
  109. package/.grok/skills/lit-pptx/scripts/deck_output.py +76 -62
  110. package/.grok/skills/lit-pptx/scripts/embed_fonts.py +14 -1
  111. package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
  112. package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
  113. package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +389 -582
  114. package/.grok/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
  115. package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +60 -8
  116. package/.grok/skills/lit-pptx/scripts/qa_deck.py +4 -4
  117. package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +2 -0
  118. package/.grok/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
  119. package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +2 -0
  120. package/.grok/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
  121. package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +2 -0
  122. package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -0
  123. package/.grok/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
  124. package/.grok/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
  125. package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +2 -0
  126. package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +2 -0
  127. package/.grok/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
  128. package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +3 -0
  129. package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +2 -0
  130. package/.grok/skills/lit-recap/SKILL.md +1 -1
  131. package/.grok/skills/lit-schematic/SKILL.md +140 -0
  132. package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
  133. package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
  134. package/.grok/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
  135. package/.grok/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
  136. package/.grok/skills/lit-schematic/references/cutaway-outputs.md +70 -0
  137. package/.grok/skills/lit-schematic/references/isometric-craft.md +101 -0
  138. package/.grok/skills/lit-schematic/references/journal-figures.md +72 -0
  139. package/.grok/skills/lit-schematic/references/kit-api.md +134 -0
  140. package/.grok/skills/lit-schematic/references/pfd-and-layout.md +53 -0
  141. package/.grok/skills/lit-schematic/references/pid-drafting.md +103 -0
  142. package/.grok/skills/lit-schematic/references/pid-symbols.md +163 -0
  143. package/.grok/skills/lit-schematic/references/verification.md +100 -0
  144. package/.grok/skills/lit-schematic/scripts/doctor.mjs +57 -0
  145. package/.grok/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
  146. package/.grok/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
  147. package/.grok/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
  148. package/.grok/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
  149. package/.grok/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
  150. package/.grok/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
  151. package/.grok/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
  152. package/.grok/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
  153. package/.grok/skills/lit-schematic/scripts/lib/png.mjs +132 -0
  154. package/.grok/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
  155. package/.grok/skills/lit-schematic/scripts/lib/route.mjs +218 -0
  156. package/.grok/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
  157. package/.grok/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
  158. package/.grok/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
  159. package/.grok/skills/lit-schematic/scripts/motion.mjs +129 -0
  160. package/.grok/skills/lit-schematic/scripts/qa.mjs +92 -0
  161. package/.grok/skills/lit-schematic/scripts/render.mjs +233 -0
  162. package/.grok/skills/lit-schematic/scripts/scaffold.mjs +60 -0
  163. package/.grok/skills/lit-scientific-visualization/SKILL.md +36 -12
  164. package/.grok/skills/lit-typographic-motion/SKILL.md +7 -4
  165. package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +11 -0
  166. package/.grok/skills/lit-typographic-motion/recipes/hero-3d/expected.json +62 -0
  167. package/.grok/skills/lit-typographic-motion/recipes/hero-3d/index.html +82 -0
  168. package/.grok/skills/lit-typographic-motion/references/choice-first.md +30 -0
  169. package/.grok/skills/lit-typographic-motion/references/cover.md +10 -1
  170. package/.grok/skills/lit-typographic-motion/references/depth.md +119 -0
  171. package/.grok/skills/lit-typographic-motion/references/treatment.md +9 -1
  172. package/.grok/skills/lit-typographic-motion/scripts/blender/runner.py +218 -0
  173. package/.grok/skills/lit-typographic-motion/scripts/lib/assets.mjs +230 -0
  174. package/.grok/skills/lit-typographic-motion/scripts/lib/blender.mjs +136 -0
  175. package/.grok/skills/lit-typographic-motion/scripts/lib/board.mjs +153 -0
  176. package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +61 -32
  177. package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +4 -2
  178. package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +20 -11
  179. package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +26 -2
  180. package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +12 -5
  181. package/.grok/skills/lit-typographic-motion/scripts/lib/frame-pool.mjs +78 -1
  182. package/.grok/skills/lit-typographic-motion/scripts/lib/gallery.mjs +160 -0
  183. package/.grok/skills/lit-typographic-motion/scripts/lib/house.mjs +43 -0
  184. package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +39 -1
  185. package/.grok/skills/lit-typographic-motion/scripts/lib/scaffold.mjs +174 -0
  186. package/.grok/skills/lit-typographic-motion/scripts/lib/score.mjs +640 -0
  187. package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +143 -0
  188. package/.grok/skills/lit-typographic-motion/scripts/lib/soundtrack.mjs +12 -3
  189. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-gate.mjs +44 -0
  190. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-qa.mjs +132 -3
  191. package/.grok/skills/lit-typographic-motion/scripts/lib/stage-session.mjs +51 -3
  192. package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +74 -9
  193. package/.grok/skills/lit-typographic-motion/scripts/lib/styles.mjs +224 -0
  194. package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +198 -4
  195. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/LICENSE +21 -0
  196. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/environments/RoomEnvironment.js +144 -0
  197. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
  198. package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/three.module.js +54571 -0
  199. package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +56 -6
  200. package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +20 -8
  201. package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +2 -2
  202. package/.grok/skills/lit-typographic-motion/scripts/stage/stage-kit.js +25 -0
  203. package/.grok/skills/lit-typographic-motion/styles/paper.json +1 -0
  204. package/.grok/skills/lit-typographic-motion/styles/swiss-signal.json +1 -0
  205. package/.grok/skills/lit-typographic-motion/styles/terminalcore.json +1 -0
  206. package/.grok/skills/lit-typographic-motion/styles/tidal.json +1 -0
  207. package/.grok/skills/litgrok/SKILL.md +1 -1
  208. package/.grok/skills/litresearch/SKILL.md +2 -0
  209. package/.grok/skills/readme-studio/SKILL.md +14 -12
  210. package/.grok/skills/readme-studio/references/choice-first.md +24 -0
  211. package/.grok/skills/start-work/SKILL.md +8 -1
  212. package/.grok-plugin/plugin.json +4 -1
  213. package/CHANGELOG.md +18 -0
  214. package/README.md +11 -11
  215. package/README_ko-KR.md +11 -11
  216. package/TRADEMARK.md +12 -0
  217. package/bin/litgrok.mjs +2 -0
  218. package/docs/assets/readme/badge-version.svg +1 -1
  219. package/docs/assets/showcase/brief.webp +0 -0
  220. package/docs/assets/showcase/deck.webp +0 -0
  221. package/docs/assets/showcase/page.webp +0 -0
  222. package/docs/assets/showcase/pid.webp +0 -0
  223. package/docs/assets/skills/lit-adhd.webp +0 -0
  224. package/docs/assets/skills/lit-hijack.webp +0 -0
  225. package/docs/assets/skills/lit-schematic.webp +0 -0
  226. package/docs/reference.md +11 -7
  227. package/docs/reference_ko-KR.md +11 -7
  228. package/hooks/hooks.json +11 -0
  229. package/package.json +6 -1
  230. package/plugin.json +1 -1
  231. package/.grok/skills/frontend-ui-ux/scripts/page-probe.mjs +0 -105
  232. package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +0 -91
  233. package/.grok/skills/frontend-ui-ux/scripts/rule-data.mjs +0 -56
  234. package/.grok/skills/frontend-ui-ux/scripts/static-probe.mjs +0 -27
  235. package/.grok/skills/frontend-ui-ux/scripts/static-server.mjs +0 -19
@@ -104,6 +104,48 @@ export function lockup(productName) {
104
104
  return standard.map((row, index) => row.padEnd(28) + (index === 5 ? ` ${label}` : ''));
105
105
  }
106
106
 
107
+ // The psychedelic rewind hue for the lit-adhd card and ignition mark: one hue at a time at a
108
+ // steady lightness, so the card never flashes. `motion` is 'full' (a 24-second hue cycle),
109
+ // 'subtle' (a slow 96-second drift) or 'off' (one still rainbow frame). Returns [r, g, b].
110
+ export function rewindHue(nowMs, motion = 'full') {
111
+ return hueToRgb(rewindBaseAngle(nowMs, motion));
112
+ }
113
+
114
+ // One full wheel across a run of `total` cells, so each cell of a pill or mark takes its own
115
+ // step round the wheel at the same steady lightness. Colour moves only on fixed glyphs.
116
+ export function rewindCellRgb(nowMs, motion, index, total) {
117
+ const base = rewindBaseAngle(nowMs, motion);
118
+ const step = total > 1 ? 360 / total : 0;
119
+ return hueToRgb((((base + step * index) % 360) + 360) % 360);
120
+ }
121
+
122
+ function rewindBaseAngle(nowMs, motion = 'full') {
123
+ if (motion !== 'subtle' && motion !== 'off' && motion !== 'full') motion = 'full';
124
+ const time = Number.isFinite(nowMs) ? nowMs : 0;
125
+ const hue = motion === 'off' ? 280 : (((time % 96_000) + 96_000) % 96_000) / 96_000 * 360;
126
+ const period = motion === 'subtle' ? 1 : 4;
127
+ return (((hue * period) % 360) + 360) % 360;
128
+ }
129
+
130
+ function hueToRgb(angle) {
131
+ const saturation = 0.85;
132
+ const lightness = 0.62;
133
+ const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
134
+ const part = chroma * (1 - Math.abs(((angle / 60) % 2) - 1));
135
+ const match = lightness - chroma / 2;
136
+ let red;
137
+ let green;
138
+ let blue;
139
+ if (angle < 60) [red, green, blue] = [chroma, part, 0];
140
+ else if (angle < 120) [red, green, blue] = [part, chroma, 0];
141
+ else if (angle < 180) [red, green, blue] = [0, chroma, part];
142
+ else if (angle < 240) [red, green, blue] = [0, part, chroma];
143
+ else if (angle < 300) [red, green, blue] = [part, 0, chroma];
144
+ else [red, green, blue] = [chroma, 0, part];
145
+ const channel = (value) => Math.max(0, Math.min(255, Math.round((value + match) * 255)));
146
+ return [channel(red), channel(green), channel(blue)];
147
+ }
148
+
107
149
  export function colorize(rows, { mode = 'none' } = {}) {
108
150
  if (!['truecolor', '256', 'none'].includes(mode)) throw new TypeError('Invalid color mode');
109
151
  if (mode === 'none') return [...rows];
@@ -5,7 +5,9 @@ import { closeSync, constants, fstatSync, openSync, readFileSync, readSync, real
5
5
  import { dirname, isAbsolute, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { resolveAutoHandoff } from './auto-handoff.mjs';
8
+ import { rewindCellRgb } from './lit-mark.mjs';
8
9
  import { readHudRecordForCwd, writeContextRecord } from './litgrok-hud-state.mjs';
10
+ import { readOtherSessions } from './session-rows.mjs';
9
11
  import { STAGES, readWorkflowState } from './workflow-stage.mjs';
10
12
 
11
13
  const MAX_INPUT_BYTES = 64 * 1024;
@@ -38,17 +40,17 @@ const GLYPHS = {
38
40
  basic: {
39
41
  fire: '🔥', tick: '✓', cross: '✗', dot: '·', lit: '━', unlit: '─', run: '●', warn: '⚠', elbow: '└', next: '▸', sep: '│',
40
42
  full: '▮', empty: '▯', rail: '▎', ellipsis: '…', spinner: BRAILLE, bar: ['█', '░', ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']],
41
- timer: '', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
43
+ timer: '', burst: '★', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
42
44
  },
43
45
  nerd: {
44
46
  fire: '🔥', tick: '', cross: '', dot: '·', lit: '━', unlit: '─', run: '', warn: '', elbow: '└', next: '▸', sep: '│',
45
47
  full: '▮', empty: '▯', rail: '▎', ellipsis: '…', spinner: BRAILLE, bar: ['█', '░', ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']],
46
- timer: ' ', tools: (n) => ` ${n}`, agents: (n) => ` ${n}`, git: ' ',
48
+ timer: ' ', burst: '★', tools: (n) => ` ${n}`, agents: (n) => ` ${n}`, git: ' ',
47
49
  },
48
50
  ascii: {
49
51
  fire: '', tick: '+', cross: 'x', dot: '-', lit: '=', unlit: '-', run: '*', warn: '!', elbow: '`', next: '>', sep: '|',
50
52
  full: '#', empty: '-', rail: '|', ellipsis: '~', spinner: ['|', '/', '-', '\\'], bar: ['#', '-', ['']],
51
- timer: '', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
53
+ timer: '', burst: '*', tools: (n) => plural(n, 'tool'), agents: (n) => plural(n, 'agent'), git: 'git ',
52
54
  },
53
55
  };
54
56
  // Grok's own tool names, shortened to the words people know them by. Any other name shows as sent.
@@ -158,9 +160,17 @@ function painter(depth) {
158
160
  return {
159
161
  depth,
160
162
  fg: (text, role, bold = false) => (depth && text ? `${bold ? '\u001b[1m' : ''}${sgr(ROLES[role], depth)}${text}\u001b[0m` : text),
163
+ hue: (text, rgb, bold = false) => (depth && text ? `${bold ? '\u001b[1m' : ''}${sgr(rgb, depth)}${text}\u001b[0m` : text),
161
164
  };
162
165
  }
163
166
 
167
+ // The rewind look's motion: 'full' cycles the hue, 'subtle' drifts it slowly, 'off' holds one still
168
+ // rainbow frame. Anything else is 'full'.
169
+ function hudMotion(env = process.env) {
170
+ const motion = (env.LITGROK_HUD_MOTION ?? 'full').toLowerCase();
171
+ return motion === 'off' || motion === 'subtle' ? motion : 'full';
172
+ }
173
+
164
174
  // A filled chip on the pink (the brand) or the accent (the current stage) with dark text and a space of
165
175
  // padding each side; a Nerd Font gives it rounded caps, and without colour it falls back to brackets.
166
176
  function chip(text, background, paint, nerd, open = '(', close = ')') {
@@ -474,13 +484,68 @@ function disciplineName(name, glyphs, cells = Infinity) {
474
484
  return clipCells(name, Math.min(cells, name === 'lit-scientific-visualization' ? 32 : 24), glyphs.ellipsis);
475
485
  }
476
486
 
487
+ // The look registry: a discipline maps to one look, and every route that runs a skill goes through
488
+ // the collector's discipline, so bare words, slash commands, skill loads and aliases all select it.
489
+ // Grok's one line can only tint and label, so a look is a tint plus a label (the card adds the pill).
490
+ const LOOKS = { 'lit-adhd': 'rewind', 'review-work': 'lanes' };
491
+ function lookOf(discipline, stage = null) {
492
+ if (typeof discipline === 'string' && Object.hasOwn(LOOKS, discipline)) return LOOKS[discipline];
493
+ if (stage === 'review') return 'lanes';
494
+ return 'plain';
495
+ }
496
+
497
+ // review-work's six lanes. Progress is real only: the test lane follows the workflow test verdict
498
+ // and a lane with no report stays dim, never a fake fill.
499
+ const REVIEW_LANES = ['behavior', 'test', 'safety', 'integration', 'documentation', 'regression'];
500
+ function laneStates(workflow) {
501
+ const verdict = workflow?.test?.verdict;
502
+ return REVIEW_LANES.map((name) => ({
503
+ name,
504
+ state: name === 'test' && (verdict === 'pass' || verdict === 'fail') ? verdict : 'unreported',
505
+ }));
506
+ }
507
+
508
+ // Waiting on the user: a held plan gate (the typed start-work boundary) or a live Ask tool call.
509
+ // The next UserPromptSubmit starts a new turn, so the label stops the moment the user types.
510
+ function waitingOf(activity) {
511
+ if (!activity) return null;
512
+ if (activity.planGate) return { reason: 'plan gate' };
513
+ if (activity.live?.name === 'ask_user_question') return { reason: 'question' };
514
+ return null;
515
+ }
516
+
477
517
  // The ignition mark of the one-line row: the pink 🔥 LIT chip, then IGNITED · <discipline> 🔥 in the accent.
478
518
  // Without colour it reads 🔥 LIT IGNITED · <discipline> 🔥. A narrow row clips the discipline, never the frame.
519
+ // While lit-adhd is the discipline the mark keeps its shape but the discipline rides a hue that turns
520
+ // (one still rainbow frame when the HUD motion is off): the tint, never a flash.
479
521
  function ignitionSegment(discipline, glyphs, paint, nerd, options = {}) {
480
522
  const label = glyphs.fire ? `${glyphs.fire} LIT` : 'LIT';
481
523
  const brand = chip(label, 'pink', paint, nerd, '', '');
482
524
  const build = (name) => `${brand.plain} IGNITED ${glyphs.dot} ${name}${glyphs.fire ? ` ${glyphs.fire}` : ''}`;
483
525
  const frame = width(build(''));
526
+ const ignitionLook = options.look ?? lookOf(discipline);
527
+ if (ignitionLook === 'rewind') {
528
+ // Each cell of the mark past the brand rides its own step round the wheel (one still rainbow
529
+ // frame when the HUD motion is off): the tint, never a flash.
530
+ const motion = hudMotion();
531
+ const when = options.now ?? Date.now();
532
+ const rainbow = (text) => {
533
+ const cells = [...text];
534
+ return cells.map((cell, index) => paint.hue(cell, rewindCellRgb(when, motion, index, cells.length), true)).join('');
535
+ };
536
+ return segment(build(disciplineName(discipline, glyphs)), (text) => `${brand.painted}${rainbow(text.slice(brand.plain.length))}`, {
537
+ ...options, minCells: frame + 1, shrink: (cells) => build(disciplineName(discipline, glyphs, Math.max(1, cells - frame))),
538
+ });
539
+ }
540
+ if (ignitionLook === 'lanes') {
541
+ const lanes = options.lanes ?? laneStates(null);
542
+ const reported = lanes.filter((lane) => lane.state !== 'unreported').length;
543
+ const full = build(`REVIEW ${reported}/${lanes.length}`);
544
+ return segment(full, (text) => `${brand.painted}${paint.fg(text.slice(brand.plain.length), 'accent', true)}`, {
545
+ ...options, minCells: frame + 1,
546
+ shrink: (cells) => (cells < width(full) ? clipCells(full, Math.max(1, cells), glyphs.ellipsis) : full),
547
+ });
548
+ }
484
549
  return segment(build(disciplineName(discipline, glyphs)), (text) => `${brand.painted}${paint.fg(text.slice(brand.plain.length), 'accent', true)}`, {
485
550
  ...options, minCells: frame + 1, shrink: (cells) => build(disciplineName(discipline, glyphs, Math.max(1, cells - frame))),
486
551
  });
@@ -498,11 +563,23 @@ function figureLeads(figures, glyphs, paint, afterMark) {
498
563
  // handoff percent, cost and branch. It is the whole HUD between workflows and in LITGROK_HUD_LINES=1
499
564
  // mode. A narrow row gives up the branch, the handoff percent, the cost and the version, then clips the
500
565
  // model name and last the discipline.
501
- function compactRow({ status, discipline, version, glyphs, paint, nerd, handoff, maxCells }) {
566
+ function compactRow({ status, discipline, version, glyphs, paint, nerd, handoff, maxCells, looks = null, phase = null }) {
502
567
  const [model, context, ...extras] = figureSegments(status, { glyphs, paint, handoff });
503
- const figures = [model, context, ...(discipline ? [ignitionSegment(discipline, glyphs, paint, nerd, { clip: 1 })] : []), ...extras];
568
+ const figures = [model, context];
569
+ if (discipline) {
570
+ figures.push(looks?.look === 'lanes'
571
+ ? ignitionSegment(discipline, glyphs, paint, nerd, { clip: 1, lanes: looks.lanes, look: looks.look })
572
+ : ignitionSegment(discipline, glyphs, paint, nerd, { clip: 1 }));
573
+ }
574
+ figures.push(...extras);
504
575
  const left = [brandSegment(version, glyphs, paint, { clip: 3 }), ...figureLeads(figures, glyphs, paint, true)];
505
- return layoutLine({ left, separator: { plain: ' ', painted: ' ' }, rail: { plain: '', painted: '' }, maxCells, ellipsis: glyphs.ellipsis });
576
+ const right = [];
577
+ if (looks?.waiting) right.push(segment(`YOUR TURN · ${looks.waiting.reason}`, (text) => paint.fg(text, 'yellow', true), {
578
+ drop: 0.6, clip: 3, minCells: width('YOUR TURN'),
579
+ shrink: (cells) => (cells < width(`YOUR TURN · ${looks.waiting.reason}`) ? clipCells('YOUR TURN', Math.max(1, cells), glyphs.ellipsis) : `YOUR TURN · ${looks.waiting.reason}`),
580
+ }));
581
+ if (looks?.burst) right.push(segment(`${glyphs.burst} complete${phase?.timer ? ` ${phase.timer}` : ''}`, (text) => paint.fg(text, 'pink', true), { drop: 1 }));
582
+ return layoutLine({ left, right, separator: { plain: ' ', painted: ' ' }, rail: { plain: '', painted: '' }, maxCells, ellipsis: glyphs.ellipsis });
506
583
  }
507
584
 
508
585
  // The current stage's label: its name, the plan's checked tasks while working, and a tick once the
@@ -584,14 +661,50 @@ function railFor(paint, glyphs) {
584
661
  // The card's title: the mark, the pink 🔥 LIT chip and the discipline, then at the right edge the turn's
585
662
  // tool calls and subagents beside its state, as the family card's corner reads (● 1m42s running,
586
663
  // ✓ complete, ⚠ a warning, idle).
587
- function titleLine({ discipline, version, glyphs, paint, nerd, phase, warning, activity, maxCells, rail }) {
664
+ function titleLine({ discipline, version, glyphs, paint, nerd, phase, warning, activity, maxCells, rail, now, looks = null }) {
588
665
  const brand = chip(glyphs.fire ? `${glyphs.fire} LIT` : 'LIT', 'pink', paint, nerd);
589
666
  const name = discipline ?? 'workflow';
667
+ const look = looks?.look ?? lookOf(discipline);
668
+ const lanes = look === 'lanes' ? (looks?.lanes ?? laneStates(null)) : null;
669
+ const reported = lanes ? lanes.filter((lane) => lane.state !== 'unreported').length : 0;
670
+ // The rewind pill rides the wheel cell by cell, like the ignition mark below. The lanes pill
671
+ // names review-work's six lanes; a lane with a verdict takes its colour, the rest stay dim.
672
+ const nameText = look === 'rewind' ? `✺ REWIND ${disciplineName(name, glyphs)}`
673
+ : lanes ? `REVIEW ${lanes.map((lane) => lane.name).join(' · ')}` : `${glyphs.dot} ${disciplineName(name, glyphs)}`;
674
+ const paintLane = (laneName) => {
675
+ const state = lanes.find((lane) => lane.name === laneName)?.state;
676
+ if (state === 'pass') return paint.fg(laneName, 'green', true);
677
+ if (state === 'fail') return paint.fg(laneName, 'red', true);
678
+ return paint.fg(laneName, 'dim');
679
+ };
680
+ const namePaint = look === 'rewind'
681
+ ? (text) => {
682
+ const motion = hudMotion();
683
+ const when = now ?? Date.now();
684
+ const cells = [...text];
685
+ return cells.map((cell, index) => paint.hue(cell, rewindCellRgb(when, motion, index, cells.length), true)).join('');
686
+ }
687
+ : lanes
688
+ ? (text) => {
689
+ const prefix = 'REVIEW ';
690
+ if (!text.startsWith(prefix)) return paint.fg(text, 'accent', true);
691
+ const names = text.slice(prefix.length).split(' · ');
692
+ const painted = names.map((laneName) => (lanes.some((lane) => lane.name === laneName)
693
+ ? paintLane(laneName) : paint.fg(laneName, 'accent', true)));
694
+ return `${paint.fg(prefix, 'accent', true)}${painted.join(paint.fg(' · ', 'dim'))}`;
695
+ }
696
+ : (text) => `${paint.fg(glyphs.dot, 'dim')} ${paint.fg(text.slice(glyphs.dot.length + 1), discipline ? 'accent' : 'dim', Boolean(discipline))}`;
697
+ const shrinkName = lanes
698
+ ? (cells) => {
699
+ const short = `REVIEW ${reported}/${lanes.length}`;
700
+ return cells < width(nameText) ? clipCells(short, Math.max(1, cells), glyphs.ellipsis) : nameText;
701
+ }
702
+ : (cells) => `${glyphs.dot} ${disciplineName(name, glyphs, Math.max(1, cells - width(glyphs.dot) - 1))}`;
590
703
  const left = [
591
704
  brandSegment(version, glyphs, paint, { clip: 3 }),
592
705
  { ...segment(brand.plain, () => brand.painted), lead: ' | ', leadPainted: paint.fg(' | ', 'accent') },
593
- segment(`${glyphs.dot} ${disciplineName(name, glyphs)}`, (text) => `${paint.fg(glyphs.dot, 'dim')} ${paint.fg(text.slice(glyphs.dot.length + 1), discipline ? 'accent' : 'dim', Boolean(discipline))}`, {
594
- clip: 0.5, minCells: width(glyphs.dot) + 2, shrink: (cells) => `${glyphs.dot} ${disciplineName(name, glyphs, Math.max(1, cells - width(glyphs.dot) - 1))}`,
706
+ segment(nameText, namePaint, {
707
+ clip: 0.5, minCells: lanes ? width(`REVIEW ${reported}/${lanes.length}`) : width(glyphs.dot) + 2, shrink: shrinkName,
595
708
  }),
596
709
  ];
597
710
  const right = [];
@@ -600,10 +713,16 @@ function titleLine({ discipline, version, glyphs, paint, nerd, phase, warning, a
600
713
  if (phase.phase === 'running' && activity.agents > 0) parts.push(glyphs.agents(activity.agents));
601
714
  if (parts.length > 0) right.push(segment(parts.join(` ${glyphs.dot} `), (text) => paint.fg(text, 'dim'), { drop: 4 }));
602
715
  }
603
- if (warning) right.push(segment(`${glyphs.warn} ${warning[1]}`, (text) => paint.fg(text, 'yellow', true), { drop: 1 }));
716
+ if (looks?.waiting) right.push(segment(`YOUR TURN · ${looks.waiting.reason}`, (text) => paint.fg(text, 'yellow', true), {
717
+ drop: 0.6, clip: 3, minCells: width('YOUR TURN'),
718
+ shrink: (cells) => (cells < width(`YOUR TURN · ${looks.waiting.reason}`) ? clipCells('YOUR TURN', Math.max(1, cells), glyphs.ellipsis) : `YOUR TURN · ${looks.waiting.reason}`),
719
+ }));
720
+ else if (warning) right.push(segment(`${glyphs.warn} ${warning[1]}`, (text) => paint.fg(text, 'yellow', true), { drop: 1 }));
604
721
  if (phase.phase === 'running') right.push(segment(`${glyphs.run} ${phase.timer ? `${glyphs.timer}${phase.timer}` : 'running'}`, (text) => paint.fg(text, 'accent', true), { drop: 2 }));
605
- else if (phase.phase === 'complete') right.push(segment(`${glyphs.tick} complete ${phase.timer}`, (text) => paint.fg(text, 'green', true), { drop: 2 }));
606
- else if (!warning) right.push(segment('idle', (text) => paint.fg(text, 'dim'), { drop: 2 }));
722
+ else if (phase.phase === 'complete') {
723
+ if (looks?.burst) right.push(segment(`${glyphs.burst} complete ${phase.timer}`, (text) => paint.fg(text, 'pink', true), { drop: 2 }));
724
+ else right.push(segment(`${glyphs.tick} complete ${phase.timer}`, (text) => paint.fg(text, 'green', true), { drop: 2 }));
725
+ } else if (!warning && !looks?.waiting) right.push(segment('idle', (text) => paint.fg(text, 'dim'), { drop: 2 }));
607
726
  return layoutLine({ left, right, separator: { plain: ' ', painted: ' ' }, rail, maxCells, ellipsis: glyphs.ellipsis });
608
727
  }
609
728
 
@@ -641,6 +760,28 @@ function detailLine({ workflow, live, warning, glyphs, paint, now, maxCells, rai
641
760
  return layoutLine({ left, right, separator: { plain: ` ${glyphs.dot} `, painted: paint.fg(` ${glyphs.dot} `, 'dim') }, rail: elbow, maxCells, ellipsis: glyphs.ellipsis });
642
761
  }
643
762
 
763
+ // One row per other session of this workspace: `● start-work · w2B:p2 running`. The name clips first;
764
+ // the state word stays.
765
+ function otherSessionLine(row, { glyphs, paint, maxCells, rail }) {
766
+ const mark = row.state === 'running' ? glyphs.run : row.state === 'done' ? glyphs.tick : glyphs.dot;
767
+ const role = row.state === 'running' ? 'accent' : row.state === 'done' ? 'green' : 'dim';
768
+ const name = row.pane ? `${plainText(row.label, 'session')} ${glyphs.dot} ${row.pane}` : plainText(row.label, 'session');
769
+ const text = mark ? `${mark} ${name}` : name;
770
+ const lead = mark ? `${mark} ` : '';
771
+ return layoutLine({
772
+ left: [
773
+ segment(text, (value) => paint.fg(value, role, row.state !== 'idle'), {
774
+ clip: 1, minCells: width(lead) + 1, shrink: (cells) => `${lead}${clipCells(name, Math.max(1, cells - width(lead)), glyphs.ellipsis)}`,
775
+ }),
776
+ segment(row.state, (value) => paint.fg(value, role)),
777
+ ],
778
+ separator: { plain: ' ', painted: ' ' },
779
+ rail,
780
+ maxCells,
781
+ ellipsis: glyphs.ellipsis,
782
+ });
783
+ }
784
+
644
785
  function figuresLine({ status, glyphs, paint, handoff, maxCells, rail }) {
645
786
  return layoutLine({
646
787
  left: figureLeads(figureSegments(status, { glyphs, paint, handoff }), glyphs, paint, false),
@@ -681,10 +822,8 @@ function render(input) {
681
822
  handoff = { active: false, percent: null };
682
823
  }
683
824
  const version = installedVersion();
684
- const shared = { status: input, discipline, version, glyphs, paint, nerd, handoff, maxCells };
685
- if (process.env.LITGROK_HUD_LINES === '1') return [compactRow(shared)];
686
-
687
825
  const now = Date.now();
826
+ const shared = { status: input, discipline, version, glyphs, paint, nerd, handoff, maxCells, now };
688
827
  let activity = null;
689
828
  try {
690
829
  activity = turnActivity(readLedgerTail(input), input?.prompt_id);
@@ -697,20 +836,34 @@ function render(input) {
697
836
  } catch {
698
837
  workflow = null;
699
838
  }
700
- // The card shows while a discipline is active or the session has a workflow stage; otherwise the
701
- // HUD is the single row.
702
- if (!discipline && !workflow?.stage) return [compactRow(shared)];
703
839
  const turn = turnState(input, activity, now);
704
840
  const phase = phaseOf(now, activity, turn);
841
+ const looks = {
842
+ look: lookOf(discipline, workflow?.stage ?? null),
843
+ lanes: laneStates(workflow),
844
+ waiting: waitingOf(activity),
845
+ burst: phase.phase === 'complete' && workflow?.stage === 'done',
846
+ };
847
+ if (process.env.LITGROK_HUD_LINES === '1') return [compactRow({ ...shared, looks, phase })];
848
+ let others = [];
849
+ try {
850
+ others = readOtherSessions(statusDirs(input), input?.session_id, now);
851
+ } catch {
852
+ others = [];
853
+ }
854
+ const rail = railFor(paint, glyphs);
855
+ const otherLines = others.map((row) => otherSessionLine(row, { glyphs, paint, maxCells, rail }));
856
+ // The card shows while a discipline is active or the session has a workflow stage; otherwise the
857
+ // HUD is the single row, with the other sessions' rows under it.
858
+ if (!discipline && !workflow?.stage) return [compactRow({ ...shared, looks, phase }), ...otherLines];
705
859
  const warning = warningText(input, activity, handoff);
706
860
  const live = turn.running ? activity?.live ?? null : null;
707
- const rail = railFor(paint, glyphs);
708
- const lines = [titleLine({ ...shared, phase, warning, activity: phase.phase === 'idle' ? null : activity, rail })];
861
+ const lines = [titleLine({ ...shared, phase, warning, activity: phase.phase === 'idle' ? null : activity, rail, looks })];
709
862
  if (workflow?.stage) lines.push(mapLine(workflow, { paint, glyphs, nerd, running: turn.running, now }, rail, maxCells));
710
863
  const detail = detailLine({ workflow, live, warning, glyphs, paint, now, maxCells, rail });
711
864
  if (detail) lines.push(detail);
712
865
  lines.push(figuresLine({ ...shared, rail }));
713
- return lines;
866
+ return [...lines, ...otherLines];
714
867
  }
715
868
 
716
869
 
@@ -3,14 +3,16 @@
3
3
  // Host contract (documented Stop decision control): stdout `{"decision":"block","reason"}` keeps
4
4
  // the agent working; empty stdout with exit 0 allows the stop; failures fail open. The prompt text
5
5
  // is never persisted, so the gate arms on `permissionMode === "plan"`, on the ledger's lit-plan
6
- // discipline marker for the turn, and on a plan file written this turn that carries zero rows. At
6
+ // discipline marker for the turn, on a plan file written this turn that carries zero rows, and on
7
+ // one whose todos name a folder outside the project with no `Scope roots:` line covering it. At
7
8
  // most two blocks per session; after that it passes with a stderr warning so a condition the model
8
9
  // cannot resolve never loops.
9
10
 
10
11
  import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node:fs';
11
12
  import { join } from 'node:path';
12
13
 
13
- import { countImplementationRows } from '../skills/lit-plan/scripts/scaffold-plan.mjs';
14
+ import { countImplementationRows, planLinesOutsideFences } from '../skills/lit-plan/scripts/scaffold-plan.mjs';
15
+ import { describeScopePath, uncoveredScopePaths } from '../skills/lit-plan/scripts/scope-roots.mjs';
14
16
  import { readLedgerRecords } from './record-passive-event.mjs';
15
17
 
16
18
  export const PLAN_GATE_SIGNAL = 'plan-gate-block';
@@ -38,7 +40,13 @@ function plansWrittenSince(workspaceRoot, since) {
38
40
  const status = statSync(path);
39
41
  if (status.size > MAX_PLAN_BYTES) continue;
40
42
  if (since !== null && status.mtimeMs < since) continue;
41
- plans.push({ name: `plans/${entry.name}`, mtimeMs: status.mtimeMs, rows: countImplementationRows(readFileSync(path, 'utf8')) });
43
+ const text = readFileSync(path, 'utf8');
44
+ plans.push({
45
+ name: `plans/${entry.name}`,
46
+ mtimeMs: status.mtimeMs,
47
+ rows: countImplementationRows(text),
48
+ outside: uncoveredScopePaths(planLinesOutsideFences(text), workspaceRoot),
49
+ });
42
50
  }
43
51
  return plans.sort((left, right) => right.mtimeMs - left.mtimeMs);
44
52
  }
@@ -62,6 +70,8 @@ export function evaluatePlanGate({ event, workspaceRoot, sessionId, ledgerFile }
62
70
  reason = `lit-plan must persist plans/<slug>.md before finishing this planning turn: run \`node ${SCAFFOLD} <slug>\` from the project root, append column-zero \`- [ ] N. <title>\` task rows under "## Todos", then run \`node ${SCAFFOLD} --check plans/<slug>.md\`.`;
63
71
  } else if (newest && newest.rows === 0 && (planMode || since !== null)) {
64
72
  reason = `${newest.name} was written this turn but has no column-zero \`- [ ] 1. <title>\` task row; start-work cannot execute it. Append the task rows under "## Todos", then run \`node ${SCAFFOLD} --check ${newest.name}\`.`;
73
+ } else if (newest && newest.outside.length > 0 && (planMode || since !== null)) {
74
+ reason = `${newest.name} names folders outside the project that start-work would have to stop and ask for. ${newest.outside.map(describeScopePath).join(' ')} Then run \`node ${SCAFFOLD} --check ${newest.name}\`.`;
65
75
  }
66
76
  if (reason === null) return { block: false };
67
77
 
@@ -19,6 +19,7 @@ import {
19
19
  } from 'node:fs';
20
20
  import { basename, dirname, isAbsolute, join } from 'node:path';
21
21
  import { writeHudRecords } from './litgrok-hud-state.mjs';
22
+ import { writeLiveness } from './session-liveness.mjs';
22
23
  import { entryFromPrompt, readWorkflowState, testVerdict, updateWorkflowState, workflowStatePath } from './workflow-stage.mjs';
23
24
 
24
25
  const MAX_EVENT_BYTES = 1024 * 1024;
@@ -30,9 +31,15 @@ const KEPT_SEGMENTS = 2;
30
31
  const ROTATION_LOCK_STALE_MS = 30 * 1000;
31
32
  const TOOL_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
32
33
  const RECORD_SCHEMA = 'litgrok.hook-event/v1';
34
+ const LIVENESS_BY_EVENT = Object.freeze({ PostToolUse: 'alive', PostToolUseFailure: 'alive', Stop: 'idle', SessionEnd: 'ended' });
33
35
  // A finished call by one of these tools cannot have changed a plan file, so it skips the plan reread.
34
36
  const READ_ONLY_TOOLS = /^(?:read_file|grep|list_dir|glob|todo_write|web_search|web_fetch|read|ls)$/iu;
35
- const HUD_DISCIPLINE_PATTERN = /(?:^|[\s/])(lit-scientific-visualization|lit-handoff|lit-brainstorm|autoconference|autoresearch|lit-plan|litwork|lit)(?=$|[^\w-])/iu;
37
+ const HUD_DISCIPLINE_PATTERN = /(?:^|[\s/])(lit-scientific-visualization|lit-handoff|lit-brainstorm|lit-adhd|lit-rewind|autoconference|autoresearch|lit-plan|litwork|review-work|lit)(?=$|[^\w-])/iu;
38
+ // The skill also runs from bare words, the rule and the catalog, where no prompt names it. Its
39
+ // collector starting is the run, so that call switches the card to the rewind look. The command
40
+ // text is only inspected, never stored: the ledger keeps the tool name alone.
41
+ const ADHD_COLLECTOR_SCRIPT = /lit-adhd\/scripts\/rewind\.mjs/u;
42
+ const ADHD_COLLECTOR_RUN = /(?:^|[\s"'])(?:--brief|--json)(?:$|[\s"'=])/u;
36
43
 
37
44
  function maskCode(text) {
38
45
  return text.replace(/[^\r\n]/gu, '\u0000');
@@ -69,9 +76,13 @@ function maskInlineCode(text) {
69
76
  return output + text.slice(cursor);
70
77
  }
71
78
 
79
+ // A message relayed from another session is quoted data, never this user's route; an unclosed
80
+ // block runs to the end of the prompt.
81
+ const CROSS_SESSION_BLOCK = /<cross-session-message\b[^>]*>[\s\S]*?(?:<\/cross-session-message>|$)/gu;
82
+
72
83
  function visiblePromptText(prompt) {
73
84
  let fence = null;
74
- const lines = prompt.match(/[^\n]*\n|[^\n]+$/gu) ?? [];
85
+ const lines = prompt.replace(CROSS_SESSION_BLOCK, ' ').match(/[^\n]*\n|[^\n]+$/gu) ?? [];
75
86
  const withoutFences = lines.map((line) => {
76
87
  if (fence !== null) {
77
88
  const close = line.match(/^[\t ]{0,3}(`+|~+)[\t ]*(?:\r?\n)?$/u)?.[1];
@@ -93,7 +104,9 @@ function hudDisciplineFromPrompt(prompt) {
93
104
  const match = HUD_DISCIPLINE_PATTERN.exec(visiblePromptText(prompt));
94
105
  if (!match) return null;
95
106
  const token = match[1].toLowerCase();
96
- return token === 'lit' ? 'litwork' : token;
107
+ if (token === 'lit') return 'litwork';
108
+ if (token === 'lit-rewind') return 'lit-adhd';
109
+ return token;
97
110
  }
98
111
 
99
112
  class RecorderError extends Error {
@@ -431,7 +444,8 @@ export async function recordPassiveEvent(expectedEvent) {
431
444
  const tool = toolRecord(event, expectedEvent);
432
445
  if (tool) record.tool = tool;
433
446
  if (typeof event.promptId === 'string') record.promptId = event.promptId;
434
- const entry = expectedEvent === 'UserPromptSubmit' && typeof event.prompt === 'string' ? entryFromPrompt(visiblePromptText(event.prompt)) : null;
447
+ const visiblePrompt = expectedEvent === 'UserPromptSubmit' && typeof event.prompt === 'string' ? visiblePromptText(event.prompt) : null;
448
+ const entry = visiblePrompt === null ? null : entryFromPrompt(visiblePrompt);
435
449
  const ledgerFile = environment.sessionId === null ? null : ledgerPath(environment.realWorkspaceRoot, environment.sessionId);
436
450
  // A bare `lit` continues the discipline the last explicit switch selected, and with it that discipline's gate.
437
451
  const continued = entry?.entry === 'continue' && ledgerFile !== null
@@ -439,12 +453,24 @@ export async function recordPassiveEvent(expectedEvent) {
439
453
  : null;
440
454
  const selected = entry === null || entry.entry === 'continue' ? undefined
441
455
  : entry.discipline === 'lit-brainstorm' ? 'lit-brainstorm' : entry.stage === 'plan' ? 'lit-plan' : null;
442
- const discipline = expectedEvent === 'UserPromptSubmit' && typeof event.prompt === 'string'
443
- && (/(?:^|[\s/])lit-plan(?:$|[^\w-])/iu.test(event.prompt) || entry?.discipline === 'lit-plan' || continued === 'lit-plan') ? 'lit-plan' : null;
456
+ const discipline = visiblePrompt !== null
457
+ && (/(?:^|[\s/])lit-plan(?:$|[^\w-])/iu.test(visiblePrompt) || entry?.discipline === 'lit-plan' || continued === 'lit-plan') ? 'lit-plan' : null;
444
458
  let hudDiscipline = null;
445
459
  if (expectedEvent === 'UserPromptSubmit') {
446
460
  hudDiscipline = continued ?? (entry?.discipline !== undefined ? entry.discipline : hudDisciplineFromPrompt(event.prompt));
447
461
  }
462
+ if (expectedEvent === 'PreToolUse') {
463
+ const command = event?.toolInput?.command ?? event?.toolInput?.cmd;
464
+ if (typeof command === 'string' && ADHD_COLLECTOR_SCRIPT.test(command) && ADHD_COLLECTOR_RUN.test(command)) {
465
+ writeHudRecords({
466
+ env: process.env,
467
+ sessionId: environment.sessionId,
468
+ cwd: environment.eventCwd,
469
+ workspaceRoot: environment.realWorkspaceRoot,
470
+ discipline: 'lit-adhd',
471
+ });
472
+ }
473
+ }
448
474
  if (discipline !== null) record.discipline = discipline;
449
475
  if (entry?.stage && continued !== 'lit-brainstorm') record.stage = entry.stage;
450
476
  // Only the verdict and one count of a test run are kept, never the command or its output.
@@ -454,6 +480,14 @@ export async function recordPassiveEvent(expectedEvent) {
454
480
  if (ledgerFile !== null) {
455
481
  appendRecord(ledgerFile, record);
456
482
  await noteWorkflow(ledgerFile, environment, expectedEvent, record, entry?.entry, selected);
483
+ // Liveness is best effort: a refused or failed write never changes this hook's output.
484
+ if (Object.hasOwn(LIVENESS_BY_EVENT, expectedEvent)) {
485
+ try {
486
+ writeLiveness({ ledgerFile, sessionId: environment.sessionId, cwd: environment.eventCwd, state: LIVENESS_BY_EVENT[expectedEvent] });
487
+ } catch {
488
+ // The ledger record above is the durable trace.
489
+ }
490
+ }
457
491
  }
458
492
  if (expectedEvent === 'UserPromptSubmit') {
459
493
  writeHudRecords({
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionEnd": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "session-end.mjs",
9
+ "timeout": 5
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // No end-reason field is documented, so this records only the session end and marks its liveness `ended`.
4
+ import { recordPassiveEvent } from './record-passive-event.mjs';
5
+
6
+ await recordPassiveEvent('SessionEnd');
@@ -0,0 +1,102 @@
1
+ // Liveness: one small record per session under `.grok/litgrok/liveness/<key>.json`, so the status line
2
+ // and lit-adhd can tell a session that is working (`alive`, from PostToolUse and its failure hook)
3
+ // from one waiting at a turn end (`idle`, from Stop) and one that is gone (`ended`, from SessionEnd).
4
+ // A session that dies without SessionEnd leaves its last record behind; a reader treats a record older
5
+ // than fifteen minutes as ended. The herdr pane id is kept only when it is a plain short token.
6
+
7
+ import { randomUUID } from 'node:crypto';
8
+ import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
9
+ import { basename, dirname, join } from 'node:path';
10
+
11
+ export const LIVENESS_REPEAT_MS = 30 * 1000;
12
+ export const LIVENESS_STALE_MS = 15 * 60 * 1000;
13
+ const LIVENESS_STATES = new Set(['alive', 'idle', 'ended']);
14
+ const MAX_RECORD_BYTES = 4096;
15
+ const PANE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u;
16
+
17
+ function livenessError(code) {
18
+ const error = new Error(code);
19
+ error.code = code;
20
+ return error;
21
+ }
22
+
23
+ // `.grok/litgrok/session-ledger/<key>.jsonl` -> `.grok/litgrok/liveness/<key>.json`.
24
+ export function livenessPath(ledgerFile) {
25
+ return join(dirname(dirname(ledgerFile)), 'liveness', basename(ledgerFile).replace(/\.jsonl$/u, '.json'));
26
+ }
27
+
28
+ function ensureLivenessDirectory(path) {
29
+ try {
30
+ mkdirSync(path, { mode: 0o700 });
31
+ } catch (error) {
32
+ if (error?.code !== 'EEXIST') throw error;
33
+ }
34
+ const status = lstatSync(path);
35
+ if (!status.isDirectory() || status.isSymbolicLink()) throw livenessError('LIVENESS_DIRECTORY_UNSAFE');
36
+ }
37
+
38
+ export function paneIdFrom(env) {
39
+ const value = env?.HERDR_PANE_ID;
40
+ return typeof value === 'string' && PANE_ID.test(value) ? value : null;
41
+ }
42
+
43
+ function validRecord(record) {
44
+ return record !== null && typeof record === 'object' && !Array.isArray(record)
45
+ && typeof record.sessionId === 'string' && record.sessionId.length > 0 && record.sessionId.length <= 512
46
+ && typeof record.cwd === 'string'
47
+ && LIVENESS_STATES.has(record.state)
48
+ && (record.pane === null || (typeof record.pane === 'string' && PANE_ID.test(record.pane)))
49
+ && typeof record.at === 'string' && Number.isFinite(Date.parse(record.at));
50
+ }
51
+
52
+ function readRaw(path) {
53
+ try {
54
+ const status = lstatSync(path);
55
+ if (!status.isFile() || status.isSymbolicLink() || status.size > MAX_RECORD_BYTES) return null;
56
+ const record = JSON.parse(readFileSync(path, 'utf8'));
57
+ return validRecord(record) ? record : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /** The record at `path`, with `state: 'ended'` once it is older than fifteen minutes, or null. */
64
+ export function readLiveness(path, now = Date.now()) {
65
+ const record = readRaw(path);
66
+ if (record === null) return null;
67
+ return now - Date.parse(record.at) > LIVENESS_STALE_MS ? { ...record, state: 'ended' } : record;
68
+ }
69
+
70
+ /**
71
+ * Writes this session's state. Returns `'unchanged'` when the same state and pane were written less
72
+ * than thirty seconds ago, `'written'` otherwise. Refuses a target that is not a regular file.
73
+ */
74
+ export function writeLiveness({ ledgerFile, sessionId, cwd, state, env = process.env, now = Date.now() }) {
75
+ if (!LIVENESS_STATES.has(state)) throw livenessError('LIVENESS_STATE_INVALID');
76
+ const target = livenessPath(ledgerFile);
77
+ ensureLivenessDirectory(dirname(target));
78
+ try {
79
+ const existing = lstatSync(target);
80
+ if (!existing.isFile() || existing.isSymbolicLink()) throw livenessError('LIVENESS_RECORD_UNSAFE');
81
+ } catch (error) {
82
+ if (error?.code !== 'ENOENT') throw error;
83
+ }
84
+ const record = { sessionId, cwd, state, pane: paneIdFrom(env), at: new Date(now).toISOString() };
85
+ if (!validRecord(record)) throw livenessError('LIVENESS_RECORD_INVALID');
86
+ const previous = readRaw(target);
87
+ if (previous && previous.state === state && previous.pane === record.pane && now - Date.parse(previous.at) < LIVENESS_REPEAT_MS) {
88
+ return 'unchanged';
89
+ }
90
+ const temporary = join(dirname(target), `.liveness-${randomUUID()}.tmp`);
91
+ try {
92
+ writeFileSync(temporary, `${JSON.stringify(record)}\n`, { flag: 'wx', mode: 0o600 });
93
+ renameSync(temporary, target);
94
+ } finally {
95
+ try {
96
+ unlinkSync(temporary);
97
+ } catch (error) {
98
+ if (error?.code !== 'ENOENT') throw error;
99
+ }
100
+ }
101
+ return 'written';
102
+ }