@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.
- package/.grok/hooks/lit-mark.mjs +42 -0
- package/.grok/hooks/lit-status-line.mjs +174 -21
- package/.grok/hooks/plan-gate.mjs +13 -3
- package/.grok/hooks/record-passive-event.mjs +40 -6
- package/.grok/hooks/session-end.json +15 -0
- package/.grok/hooks/session-end.mjs +6 -0
- package/.grok/hooks/session-liveness.mjs +102 -0
- package/.grok/hooks/session-rows.mjs +57 -0
- package/.grok/hooks/session-start.mjs +17 -0
- package/.grok/hooks/workflow-stage.mjs +5 -2
- package/.grok/rules/00-litgrok.md +14 -0
- package/.grok/skills/frontend-ui-ux/ATTRIBUTION.md +23 -0
- package/.grok/skills/frontend-ui-ux/SKILL.md +4 -1
- package/.grok/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
- package/.grok/skills/frontend-ui-ux/kit/components.css +414 -0
- package/.grok/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/.grok/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/.grok/skills/frontend-ui-ux/references/adaptive-layout.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/brand-and-imagery.md +11 -7
- package/.grok/skills/frontend-ui-ux/references/complete-contract.md +21 -0
- package/.grok/skills/frontend-ui-ux/references/composition.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/craft-floor.md +256 -93
- package/.grok/skills/frontend-ui-ux/references/creative-directions.md +9 -6
- package/.grok/skills/frontend-ui-ux/references/default-editorial-pixel.json +28 -0
- package/.grok/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/.grok/skills/frontend-ui-ux/references/implementation-platforms.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/inclusive-interface.md +11 -5
- package/.grok/skills/frontend-ui-ux/references/interaction-motion.md +34 -78
- package/.grok/skills/frontend-ui-ux/references/motion-guide.md +10 -7
- package/.grok/skills/frontend-ui-ux/references/performance-delivery.md +11 -8
- package/.grok/skills/frontend-ui-ux/references/probe-loop.md +1 -1
- package/.grok/skills/frontend-ui-ux/references/product-direction.md +9 -4
- package/.grok/skills/frontend-ui-ux/references/production-interview.md +17 -0
- package/.grok/skills/frontend-ui-ux/references/redesign-playbook.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/slop-register.md +148 -73
- package/.grok/skills/frontend-ui-ux/references/system-foundations.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-language.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -72
- package/.grok/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-driver.mjs +33 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1555 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +153 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +139 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe.mjs +363 -0
- package/.grok/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/.grok/skills/lit-adhd/SKILL.md +155 -0
- package/.grok/skills/lit-adhd/references/card-examples.md +131 -0
- package/.grok/skills/lit-adhd/references/status-taxonomy.md +50 -0
- package/.grok/skills/lit-adhd/scripts/lib/brief.mjs +421 -0
- package/.grok/skills/lit-adhd/scripts/lib/collect.mjs +395 -0
- package/.grok/skills/lit-adhd/scripts/lib/gauge.mjs +87 -0
- package/.grok/skills/lit-adhd/scripts/lib/next-step.mjs +22 -0
- package/.grok/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
- package/.grok/skills/lit-adhd/scripts/lib/project-state-root.mjs +55 -0
- package/.grok/skills/lit-adhd/scripts/lib/redact.mjs +64 -0
- package/.grok/skills/lit-adhd/scripts/lib/suggest.mjs +140 -0
- package/.grok/skills/lit-adhd/scripts/lib/transcripts.mjs +232 -0
- package/.grok/skills/lit-adhd/scripts/rewind.mjs +6 -0
- package/.grok/skills/lit-diagram-drawer/SKILL.md +17 -5
- package/.grok/skills/lit-diagram-drawer/references/output-spec.md +2 -2
- package/.grok/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/.grok/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/verifier-guide.md +4 -0
- package/.grok/skills/lit-diagram-drawer/scripts/visual-quality.mjs +2 -2
- package/.grok/skills/lit-handoff/SKILL.md +2 -0
- package/.grok/skills/lit-hijack/SKILL.md +459 -0
- package/.grok/skills/lit-hijack/references/capture-and-provenance.md +124 -0
- package/.grok/skills/lit-hijack/references/clean-room-build.md +130 -0
- package/.grok/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/.grok/skills/lit-hijack/references/gap-and-port-design.md +150 -0
- package/.grok/skills/lit-hijack/references/parity-and-report.md +130 -0
- package/.grok/skills/lit-hijack/scripts/hijack.mjs +890 -0
- package/.grok/skills/lit-hijack/scripts/lib/safe-fetch.mjs +213 -0
- package/.grok/skills/lit-plan/SKILL.md +3 -1
- package/.grok/skills/lit-plan/references/plan-schema.md +4 -0
- package/.grok/skills/lit-plan/scripts/scaffold-plan.mjs +9 -2
- package/.grok/skills/lit-plan/scripts/scope-roots.mjs +83 -0
- package/.grok/skills/lit-pptx/SKILL.md +10 -5
- package/.grok/skills/lit-pptx/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/.grok/skills/lit-pptx/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/.grok/skills/lit-pptx/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/.grok/skills/lit-pptx/examples/16-community-programme-riso-ko.md +158 -0
- package/.grok/skills/lit-pptx/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/provenance.json +1 -0
- package/.grok/skills/lit-pptx/references/direction-step.md +34 -8
- package/.grok/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/.grok/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/.grok/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/.grok/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/.grok/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/.grok/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/.grok/skills/lit-pptx/scripts/craft_extras.py +61 -3
- package/.grok/skills/lit-pptx/scripts/deck_output.py +76 -62
- package/.grok/skills/lit-pptx/scripts/embed_fonts.py +14 -1
- package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +389 -582
- package/.grok/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +60 -8
- package/.grok/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/.grok/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +3 -0
- package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +2 -0
- package/.grok/skills/lit-recap/SKILL.md +1 -1
- package/.grok/skills/lit-schematic/SKILL.md +140 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/.grok/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/.grok/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/.grok/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/.grok/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/.grok/skills/lit-schematic/references/journal-figures.md +72 -0
- package/.grok/skills/lit-schematic/references/kit-api.md +134 -0
- package/.grok/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/.grok/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/.grok/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/.grok/skills/lit-schematic/references/verification.md +100 -0
- package/.grok/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/.grok/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/.grok/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
- package/.grok/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/.grok/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/.grok/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/.grok/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/.grok/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/.grok/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/.grok/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/.grok/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/.grok/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/.grok/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/.grok/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/.grok/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/.grok/skills/lit-schematic/scripts/render.mjs +233 -0
- package/.grok/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/.grok/skills/lit-scientific-visualization/SKILL.md +36 -12
- package/.grok/skills/lit-typographic-motion/SKILL.md +7 -4
- package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +11 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/expected.json +62 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/index.html +82 -0
- package/.grok/skills/lit-typographic-motion/references/choice-first.md +30 -0
- package/.grok/skills/lit-typographic-motion/references/cover.md +10 -1
- package/.grok/skills/lit-typographic-motion/references/depth.md +119 -0
- package/.grok/skills/lit-typographic-motion/references/treatment.md +9 -1
- package/.grok/skills/lit-typographic-motion/scripts/blender/runner.py +218 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/assets.mjs +230 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/blender.mjs +136 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/board.mjs +153 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +61 -32
- package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +4 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +20 -11
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +26 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +12 -5
- package/.grok/skills/lit-typographic-motion/scripts/lib/frame-pool.mjs +78 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/gallery.mjs +160 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/house.mjs +43 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +39 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/scaffold.mjs +174 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/score.mjs +640 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +143 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/soundtrack.mjs +12 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-gate.mjs +44 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-qa.mjs +132 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-session.mjs +51 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +74 -9
- package/.grok/skills/lit-typographic-motion/scripts/lib/styles.mjs +224 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +198 -4
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/LICENSE +21 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/three.module.js +54571 -0
- package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +56 -6
- package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +20 -8
- package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +2 -2
- package/.grok/skills/lit-typographic-motion/scripts/stage/stage-kit.js +25 -0
- package/.grok/skills/lit-typographic-motion/styles/paper.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/swiss-signal.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/terminalcore.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/tidal.json +1 -0
- package/.grok/skills/litgrok/SKILL.md +1 -1
- package/.grok/skills/litresearch/SKILL.md +2 -0
- package/.grok/skills/readme-studio/SKILL.md +14 -12
- package/.grok/skills/readme-studio/references/choice-first.md +24 -0
- package/.grok/skills/start-work/SKILL.md +8 -1
- package/.grok-plugin/plugin.json +4 -1
- package/CHANGELOG.md +18 -0
- package/README.md +11 -11
- package/README_ko-KR.md +11 -11
- package/TRADEMARK.md +12 -0
- package/bin/litgrok.mjs +2 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/showcase/brief.webp +0 -0
- package/docs/assets/showcase/deck.webp +0 -0
- package/docs/assets/showcase/page.webp +0 -0
- package/docs/assets/showcase/pid.webp +0 -0
- package/docs/assets/skills/lit-adhd.webp +0 -0
- package/docs/assets/skills/lit-hijack.webp +0 -0
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference.md +11 -7
- package/docs/reference_ko-KR.md +11 -7
- package/hooks/hooks.json +11 -0
- package/package.json +6 -1
- package/plugin.json +1 -1
- package/.grok/skills/frontend-ui-ux/scripts/page-probe.mjs +0 -105
- package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +0 -91
- package/.grok/skills/frontend-ui-ux/scripts/rule-data.mjs +0 -56
- package/.grok/skills/frontend-ui-ux/scripts/static-probe.mjs +0 -27
- package/.grok/skills/frontend-ui-ux/scripts/static-server.mjs +0 -19
package/.grok/hooks/lit-mark.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
594
|
-
clip: 0.5, minCells:
|
|
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 (
|
|
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')
|
|
606
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
443
|
-
&& (/(?:^|[\s/])lit-plan(?:$|[^\w-])/iu.test(
|
|
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,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
|
+
}
|