@litfamily/lithermes 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 (96) hide show
  1. package/README.md +10 -9
  2. package/README_Ko-KR.md +10 -9
  3. package/assets/lithermes-plugin/README.md +28 -9
  4. package/assets/lithermes-plugin/__init__.py +8 -3
  5. package/assets/lithermes-plugin/auto_update.py +17 -3
  6. package/assets/lithermes-plugin/core.py +41 -2
  7. package/assets/lithermes-plugin/core_contexts.py +27 -9
  8. package/assets/lithermes-plugin/core_contract.py +7 -2
  9. package/assets/lithermes-plugin/core_review.py +144 -3
  10. package/assets/lithermes-plugin/core_routing.py +10 -0
  11. package/assets/lithermes-plugin/core_workflow_commands.py +3 -2
  12. package/assets/lithermes-plugin/deliverable_hedge_guard.py +171 -48
  13. package/assets/lithermes-plugin/diagnostics.py +5 -3
  14. package/assets/lithermes-plugin/humanizer_detector.py +117 -5
  15. package/assets/lithermes-plugin/lit_reentry.py +233 -0
  16. package/assets/lithermes-plugin/payload-version.json +184 -52
  17. package/assets/lithermes-plugin/plugin.yaml +1 -1
  18. package/assets/lithermes-plugin/rules/__init__.py +2 -0
  19. package/assets/lithermes-plugin/rules/bundled-rules/lit-humanizer.md +2 -0
  20. package/assets/lithermes-plugin/rules/engine.py +26 -11
  21. package/assets/lithermes-plugin/skills/deep-interview/SKILL.md +2 -1
  22. package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +3 -3
  23. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/hostexec.mjs +118 -0
  24. package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +6 -5
  25. package/assets/lithermes-plugin/skills/lit-brainstorm/SKILL.md +408 -0
  26. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/doctor.mjs +3 -3
  27. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/export.mjs +5 -5
  28. package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/hostexec.mjs +118 -0
  29. package/assets/lithermes-plugin/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -4
  30. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/acs/template.tex +3 -0
  31. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/elsevier/template.tex +3 -0
  32. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/ieee/template.tex +3 -0
  33. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/korean-generic/template.tex +3 -0
  34. package/assets/lithermes-plugin/skills/lit-docx/templates/latex/nature/template.tex +3 -0
  35. package/assets/lithermes-plugin/skills/lit-humanizer/SKILL.md +2 -0
  36. package/assets/lithermes-plugin/skills/lit-pptx/bin/hostexec.mjs +118 -0
  37. package/assets/lithermes-plugin/skills/lit-pptx/bin/office.mjs +16 -9
  38. package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +13 -12
  39. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +19 -6
  40. package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +25 -19
  41. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +2 -0
  42. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/browser.mjs +11 -4
  43. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/constants.mjs +29 -0
  44. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-assets.mjs +87 -0
  45. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-capture.mjs +189 -0
  46. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-gate.mjs +263 -0
  47. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-image.mjs +103 -0
  48. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-init.js +36 -0
  49. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-png.mjs +77 -0
  50. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-sheet.mjs +70 -0
  51. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-treatment.mjs +104 -0
  52. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-webp.mjs +112 -0
  53. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover-worker.mjs +19 -0
  54. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/cover.mjs +338 -0
  55. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  56. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/hostexec.mjs +118 -0
  57. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/look.mjs +103 -7
  58. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/loop-kit.js +80 -0
  59. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  60. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +8 -2
  61. package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  62. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +49 -0
  63. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +37 -0
  64. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +49 -0
  65. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +27 -0
  66. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +51 -0
  67. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  68. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +51 -0
  69. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +37 -0
  70. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +68 -0
  71. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/orbit-loop/index.html +112 -0
  72. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/painted-light/expected.json +96 -0
  73. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/painted-light/index.html +208 -0
  74. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/weight-tide/expected.json +68 -0
  75. package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  76. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/cover.md +458 -0
  77. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/craft-loop.md +4 -1
  78. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/runtime.md +13 -5
  79. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/stage.md +4 -2
  80. package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +6 -0
  81. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/fonts.json +11 -0
  82. package/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.lock +72 -0
  83. package/assets/lithermes-plugin/skills/litgoal/SKILL.md +19 -1
  84. package/assets/lithermes-plugin/skills/lsp-setup/scripts/verify-lsp.ts +16 -3
  85. package/assets/lithermes-plugin/skills/review-work/SKILL.md +38 -16
  86. package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
  87. package/package.json +2 -1
  88. package/readme-assets/badge-version.svg +1 -1
  89. package/readme-assets/cover-motion-still.webp +0 -0
  90. package/readme-assets/cover-motion.webp +0 -0
  91. package/src/cli.js +2 -2
  92. package/src/lib/check.js +9 -6
  93. package/src/lib/hermesDiscovery.js +2 -2
  94. package/src/lib/hostExec.js +81 -0
  95. package/src/lib/skillPayload.js +1 -1
  96. package/src/lib/updateNotifier.js +14 -4
@@ -26,7 +26,7 @@ Hermes session runs npm, pip or a download. Run the pre-warm in a normal shell,
26
26
  outside any sandboxed session, with the same `HOME`, `HERMES_HOME` and
27
27
  `XDG_CACHE_HOME` the session will use.
28
28
 
29
- ## Status (five probes)
29
+ ## Status (six probes)
30
30
 
31
31
  `lithermes motion-runtime status` and `lithermes doctor` report, every time:
32
32
 
@@ -41,6 +41,9 @@ outside any sandboxed session, with the same `HOME`, `HERMES_HOME` and
41
41
  5. **Pre-warm**: READY with the cache path, or exactly which dependencies and
42
42
  fonts are missing or hash-mismatched and the command that fixes them; plus
43
43
  the state of the optional audio venv.
44
+ 6. **Cover encoder**: img2webp and dwebp (libwebp) with their version, the
45
+ gifski fallback, or not found (cover renders then stop with exit 21). These
46
+ are host tools, never bundled or installed by this product.
44
47
 
45
48
  ## Chrome launch
46
49
 
@@ -71,6 +74,8 @@ the render with exit 10 and that line. The engine never adds `--no-sandbox`.
71
74
  | 13 | GATE_FAIL_QA | the gate or the pre-flight failed; the report names the rules; fix and rerun the next round |
72
75
  | 14 | BLOCKED_DEPS_NOT_PREWARMED | the cache is not warm (or word-timing models are absent): run `lithermes motion-runtime install` outside this session |
73
76
  | 15 | BLOCKED_FONT_FETCH | a pinned font or licence is missing or its sha256 differs: rerun `lithermes motion-runtime install`; never accept or re-fetch it inside a session |
77
+ | 21 | BLOCKED_NO_COVER_ENCODER | cover mode found neither img2webp with dwebp nor gifski; install libwebp (or gifski) on the host |
78
+ | 22 | BLOCKED_ASSET_MISSING | cover mode: an asset listed in `assets.json` is missing; supply or generate it, never a stand-in |
74
79
  | 2 | usage | a missing or invalid brief or flag |
75
80
 
76
81
  Pre-flight order: brief, word-timing request, dependencies (14), fonts (15),
@@ -82,10 +87,13 @@ glyph coverage and timeline floors (13, nothing rendered), ffmpeg for `video`
82
87
  `lithermes motion-runtime install --audio` creates a Python venv in the cache
83
88
  from `runtime/requirements.lock` with
84
89
  `pip install --require-hashes --only-binary=:all:` (librosa and its pinned
85
- dependencies; Python 3.11 to 3.13; set `LITHERMES_MOTION_PYTHON` to choose the
86
- interpreter). When a brief names an `audio` file and the venv is ready and still
87
- matches its pins, `bin/audio.py` analyses that file once into
88
- `<out>/audio-grid.json` and the beats replace the fixed grid. If the venv is
90
+ dependencies; the newest Python from 3.14 down to 3.11 on PATH, picked the way the
91
+ Office runtime picks, so a python3 that is 3.15 is passed over; the lock pins
92
+ numpy 2.4, which starts at 3.11, and carries audioread's extra dependencies
93
+ for 3.13 and newer; set `LITHERMES_MOTION_PYTHON` to choose
94
+ the interpreter). When a brief names an
95
+ `audio` file and the venv is ready and still matches its pins, `bin/audio.py`
96
+ analyses that file once into `<out>/audio-grid.json` and the beats replace the fixed grid. If the venv is
89
97
  missing, drifted, or the analysis fails, the cuts use text timing and the
90
98
  manifest and report carry the warning. Either way the audio itself is always
91
99
  muxed into the MP4 (AAC 256k), padded or trimmed to the film with a 50 ms
@@ -69,8 +69,10 @@ or prefetch link, anywhere in the stage files.
69
69
  `font-display: block`, served from the pre-warmed cache and the reused lit-pptx
70
70
  Pretendard pair: `Pretendard` (400, 700), `Archivo` (500, 700, 900 at
71
71
  `font-stretch` 75 %, 100 %, 125 %), `Galmuri9`, `VT323`, `Silkscreen` (400,
72
- 700) and `MesloLGS NF`. Use Pretendard for Hangul. Any other family falls back
73
- to a system face; the QA reports it and fails it on copy.
72
+ 700) and `MesloLGS NF`, plus `JetBrains Mono` (a variable `wght` axis from 100
73
+ to 800 with a fixed advance, pinned in the same pre-warmed cache, so weight
74
+ motion never reflows a line). Use Pretendard for Hangul. Any other family falls
75
+ back to a system face; the QA reports it and fails it on copy.
74
76
 
75
77
  ### Text the QA can read
76
78
 
@@ -10,6 +10,12 @@ where, how it is shaped over time, what is drawn on screen, how it sounds, and
10
10
  what would make it excellent. Write it from the request, not from the example
11
11
  below.
12
12
 
13
+ A README, hero, banner or cover loop is not a film. Its treatment sets
14
+ `"mode": "cover"` and follows `cover.md` instead of this file: no beats, no arc,
15
+ no sound, no copy lines, and none of the film rules below (the 1.2 s beat, the
16
+ 10 s floor, the 16:9 and 9:16 formats, the genre arcs). `stage` and `run` refuse
17
+ a cover treatment, and `cover` refuses a film treatment, each with exit 16.
18
+
13
19
  ## Load order
14
20
 
15
21
  1. This file, always first.
@@ -115,6 +115,12 @@
115
115
  "sha256": "7b5a420d4e16285213e7e2b1d187d086835c2dd7a7f64c5b8057434f082fc46f",
116
116
  "license": "licenses/EMS-OFL.txt"
117
117
  },
118
+ {
119
+ "path": "fonts/JetBrainsMono-wght-latin.woff2",
120
+ "url": "https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5.3.0/files/jetbrains-mono-latin-wght-normal.woff2",
121
+ "sha256": "18be452724bfdc236c074ca94a249a7f41a86752c7d04ab258ce9ed5651f6a7e",
122
+ "license": "licenses/JetBrainsMono-OFL.txt"
123
+ },
118
124
  {
119
125
  "path": "licenses/Galmuri-OFL.md",
120
126
  "url": "https://raw.githubusercontent.com/quiple/galmuri/v2.40.4/ofl.md",
@@ -149,6 +155,11 @@
149
155
  "path": "licenses/Archivo-OFL.txt",
150
156
  "url": "https://raw.githubusercontent.com/mexicat/pdoom-video/ca251e3dddda422b364385eb484b5a3593a0990d/app/public/fonts/src/OFL.txt",
151
157
  "sha256": "108b4e57c9c796d3d38d0428ca7ee39de47ad93187302718d9b2d8864b9b716b"
158
+ },
159
+ {
160
+ "path": "licenses/JetBrainsMono-OFL.txt",
161
+ "url": "https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5.3.0/LICENSE",
162
+ "sha256": "403581b69dac5cff4079205e01c6b467e56af449ecbd7247693ddb1baafa005b"
152
163
  }
153
164
  ]
154
165
  }
@@ -1,5 +1,61 @@
1
1
  # This file was autogenerated by uv via the following command:
2
2
  # uv pip compile --python-version 3.11 --generate-hashes --only-binary :all: packages/lithermes-installer/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.in --output-file packages/lithermes-installer/assets/lithermes-plugin/skills/lit-typographic-motion/runtime/requirements.lock
3
+ # Hand-merged after the uv header above: audioop-lts, standard-aifc, standard-chunk and standard-sunau, with their hashes and
4
+ # the python_full_version >= '3.13' marker, taken from `uv pip compile --universal --python-version 3.11` of requirements.in.
5
+ # audioread needs them from Python 3.13; every other pin is the original 3.11 resolution.
6
+ audioop-lts==0.2.2 ; python_full_version >= '3.13' \
7
+ --hash=sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a \
8
+ --hash=sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449 \
9
+ --hash=sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3 \
10
+ --hash=sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0 \
11
+ --hash=sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7 \
12
+ --hash=sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58 \
13
+ --hash=sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911 \
14
+ --hash=sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7 \
15
+ --hash=sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e \
16
+ --hash=sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd \
17
+ --hash=sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636 \
18
+ --hash=sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623 \
19
+ --hash=sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b \
20
+ --hash=sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5 \
21
+ --hash=sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a \
22
+ --hash=sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a \
23
+ --hash=sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6 \
24
+ --hash=sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d \
25
+ --hash=sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303 \
26
+ --hash=sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602 \
27
+ --hash=sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b \
28
+ --hash=sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f \
29
+ --hash=sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0 \
30
+ --hash=sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09 \
31
+ --hash=sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132 \
32
+ --hash=sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753 \
33
+ --hash=sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9 \
34
+ --hash=sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547 \
35
+ --hash=sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c \
36
+ --hash=sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75 \
37
+ --hash=sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e \
38
+ --hash=sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093 \
39
+ --hash=sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b \
40
+ --hash=sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6 \
41
+ --hash=sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd \
42
+ --hash=sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8 \
43
+ --hash=sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb \
44
+ --hash=sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969 \
45
+ --hash=sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7 \
46
+ --hash=sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe \
47
+ --hash=sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917 \
48
+ --hash=sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc \
49
+ --hash=sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9 \
50
+ --hash=sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3 \
51
+ --hash=sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5 \
52
+ --hash=sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf \
53
+ --hash=sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6 \
54
+ --hash=sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19 \
55
+ --hash=sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800
56
+ # via
57
+ # standard-aifc
58
+ # standard-sunau
3
59
  audioread==3.1.0 \
4
60
  --hash=sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190 \
5
61
  --hash=sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4
@@ -711,6 +767,22 @@ soxr==1.1.0 \
711
767
  --hash=sha256:f4977323ef9c3aa3c2a26ff5fe0191c84b8fd759daf7afb1f25a91a55ad8b730 \
712
768
  --hash=sha256:feebcba99ac99adb8009d46c8f4c1956b8c167576b0ae8a6fb47502e9a6f78e7
713
769
  # via librosa
770
+ standard-aifc==3.13.0 ; python_full_version >= '3.13' \
771
+ --hash=sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43 \
772
+ --hash=sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66
773
+ # via
774
+ # audioread
775
+ # librosa
776
+ standard-chunk==3.13.0 ; python_full_version >= '3.13' \
777
+ --hash=sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c \
778
+ --hash=sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654
779
+ # via standard-aifc
780
+ standard-sunau==3.13.0 ; python_full_version >= '3.13' \
781
+ --hash=sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622 \
782
+ --hash=sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908
783
+ # via
784
+ # audioread
785
+ # librosa
714
786
  threadpoolctl==3.7.0 \
715
787
  --hash=sha256:61348cfb77d53b9242e0017029244b559b810c142ced65b4e21eeca1843959a7 \
716
788
  --hash=sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: litgoal
3
- description: Durable repo-native goal runtime with embedded success criteria, evidence ledger, checkpoints, steering, and an evidence-gated completion contract.
3
+ description: Sets and keeps the finish line: a durable repo-native goal runtime with embedded success criteria, evidence ledger, checkpoints, steering, and an evidence-gated completion contract that lit-loop and start-work run toward.
4
4
  ---
5
5
 
6
6
  ## #contract.activation
@@ -115,6 +115,24 @@ limitations_channel: inline
115
115
  | broaden scope while editing | keep changes tied to the user request and preserve unrelated worktree state |
116
116
  | skip payload sync | run `sync-plugin -- --in-place` and report the hash-manifest change |
117
117
 
118
+ ## What litgoal is for
119
+
120
+ litgoal sets and keeps the finish line; lit-loop and start-work run toward it.
121
+
122
+ | Question | Owner |
123
+ |---|---|
124
+ | What counts as done, and what proves it? | `litgoal`: objective, success criteria, evidence, checkpoints, the completion gate |
125
+ | Do the work toward that finish line in one go | `lit-loop` (bare `lit <task>` or `/lit-loop`) |
126
+ | Execute an approved plan row by row toward it | `start-work` (`/start-work <approved-plan>`) |
127
+ | Is the plan, or the finished work, any good? | `review-work` |
128
+
129
+ litgoal never does the work itself. It records the finish line, keeps it across
130
+ turns and sessions, injects the unmet criteria into every turn, and refuses
131
+ `goal_complete` until the evidence is there. The route name is the skill name:
132
+ `litgoal <objective>`, `lit goal <objective>`, `/litgoal`, and
133
+ `lithermes:litgoal` all open this skill. Hermes' own native `/goal` stays
134
+ user-managed and unobserved; litgoal never mutates it.
135
+
118
136
  ## LitHermes durable runtime (authoritative — overrides any legacy examples below)
119
137
 
120
138
  LitHermes ships litgoal as a **real durable runtime**, not prose. State persists under
@@ -118,6 +118,19 @@ function makeReader(onMessage: (message: JsonRpcMessage) => void): (chunk: Buffe
118
118
  }
119
119
  }
120
120
 
121
+ // Windows installs npm language servers as .cmd shims, which Node refuses to spawn without a
122
+ // shell (EINVAL). A shell line is built only from the configured command, each part quoted;
123
+ // a quote, percent sign or line break in it is refused rather than escaped.
124
+ function launch(bin: string, args: string[]) {
125
+ const stdio: ["pipe", "pipe", "pipe"] = ["pipe", "pipe", "pipe"]
126
+ if (process.platform !== "win32") return spawn(bin, args, { stdio })
127
+ const quote = (part: string): string => {
128
+ if (/["%\r\n\0]/.test(part)) throw new Error(`refusing language server argument with a quote, percent sign or line break: ${part.slice(0, 40)}`)
129
+ return /^[\w@+=:,./\\-]+$/.test(part) ? part : `"${part.replace(/\\+$/, (run) => run + run)}"`
130
+ }
131
+ return spawn([bin, ...args].map(quote).join(" "), { stdio, shell: true })
132
+ }
133
+
121
134
  function languageId(ext: string): string {
122
135
  const server = LANGUAGES.find((entry) => entry.extensions.includes(ext))
123
136
  return server?.language ?? ext.replace(/^\./, "")
@@ -145,9 +158,9 @@ async function run(filePath: string, timeoutMs: number, configPath: string): Pro
145
158
  return await new Promise<number>((resolveResult) => {
146
159
  let child
147
160
  try {
148
- child = spawn(bin, binArgs, { stdio: ["pipe", "pipe", "pipe"] })
149
- } catch {
150
- process.stdout.write(`FAIL ${absolute}: language server not installed (${bin})\n`)
161
+ child = launch(bin, binArgs)
162
+ } catch (error) {
163
+ process.stdout.write(`FAIL ${absolute}: language server could not start (${bin}): ${error instanceof Error ? error.message : String(error)}\n`)
151
164
  resolveResult(1)
152
165
  return
153
166
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: review-work
3
- description: Hermes-native draft-plan and post-implementation review: objective-achievability audit for plans, or five delegate_task completion lanes with DoneClaim skepticism.
3
+ description: Hermes-native plan review (after lit-plan, before start-work) and work review (after lit-loop or start-work), auto-picked from what exists or forced with lit review plan|work: an objective-achievability audit for plans, or five delegate_task lanes with DoneClaim skepticism for work.
4
4
  ---
5
5
 
6
6
  ## #contract.activation
@@ -117,28 +117,48 @@ limitations_channel: methodology_paragraph
117
117
 
118
118
  # Review Work
119
119
 
120
- `review-work` has two read-only modes. **Plan-review mode** audits a draft before
121
- execution. Completion-review mode is the existing five-lane gate used after
122
- meaningful implementation work and before claiming done, preparing a release, or
123
- asking for a commit. Select from the evidence supplied; do not force a draft plan
124
- through implementation lanes or treat a completed diff as a plan.
120
+ `review-work` has two read-only modes, named for what is under review.
121
+
122
+ - **Plan review** runs after `lit-plan` and before `start-work`: is the plan
123
+ complete, ordered, and verifiable, so that execution never has to invent scope,
124
+ evidence, or a success path?
125
+ - **Work review** runs after `lit-loop` or `start-work`: do the diff, the tests, and
126
+ the evidence back the claim that the work is done?
125
127
 
126
128
  In both modes, reviewers **never implement** production work. A parent may revise
127
129
  only plan text after an `ITERATE` verdict; it must **revise only when needed**, keep
128
- the approved objective unchanged, and re-run the affected plan-review lanes.
130
+ the approved objective unchanged, and re-run the affected plan review lanes.
129
131
 
130
132
  ## Mode Selection
131
133
 
132
- | Mode | Select when | Output |
134
+ The mode is forced by the user or auto-picked from what exists. Say which mode was
135
+ picked on the first line of the report, before any finding:
136
+
137
+ ```text
138
+ Review mode: plan (forced by the user)
139
+ Review mode: work (auto-picked: changed files outside plans/)
140
+ ```
141
+
142
+ **Forcing a mode.** `lit review plan <target>` and `/review-work plan <target>` run
143
+ plan review; `lit review work <target>` and `/review-work work <target>` run work
144
+ review. A forced mode is never overridden by what exists.
145
+
146
+ **Auto-pick.** When no mode is forced, decide in this order and stop at the first
147
+ rule that matches:
148
+
149
+ | Evidence present | Mode | Output |
133
150
  |---|---|---|
134
- | `plan-review` | A draft plan or checklist is the object under review and implementation evidence is not claimed. | `PASS`, `ITERATE`, or `NEEDS-CONTEXT`; findings and, only for `ITERATE`, bounded replacement plan text. |
135
- | `completion-review` | A diff, tests, real-surface evidence, or DoneClaim is under review. | Existing five-lane `REVIEW PASSED|FAILED|BLOCKED` receipt. |
151
+ | a named PR, branch or diff; changed files outside `plans/`; or a plan with ticked rows (work signals) | work | five-lane `REVIEW PASSED|FAILED|BLOCKED` receipt |
152
+ | a plan with no ticked rows and no changed files outside `plans/` | plan | `PLAN REVIEW PASS|ITERATE|NEEDS-CONTEXT`; findings and, only for `ITERATE`, bounded replacement plan text |
153
+ | neither a plan nor a diff | ask | ask the user one short question: review a plan (which one) or work (which diff, branch or PR)? Do not guess a target and do not dispatch a lane |
136
154
 
137
- If both a plan and completed work are supplied, audit the implementation against
138
- the approved plan in completion-review mode. If the mode cannot be determined,
139
- return `NEEDS-CONTEXT` with the one missing input instead of guessing.
155
+ If both a plan and finished work are supplied, audit the implementation against
156
+ the approved plan in work review. The `/review-work` command applies this same
157
+ rule over the real worktree; the natural `lit review` route states it in its
158
+ wrapper and the model applies it. Never force a draft plan through the
159
+ implementation lanes, and never treat a finished diff as a plan.
140
160
 
141
- ## Plan-Review Mode
161
+ ## Plan Review Mode
142
162
 
143
163
  Plan review asks whether execution can begin and end without the implementer
144
164
  inventing scope, evidence, or a success path. It does not reward checklist length.
@@ -186,6 +206,7 @@ aggregate. Otherwise any `ITERATE` controls it; all lanes must pass for `PASS`.
186
206
  ### Plan Verdict Contract
187
207
 
188
208
  ```text
209
+ Review mode: plan (<forced by the user | auto-picked: reason>)
189
210
  PLAN REVIEW PASS|ITERATE|NEEDS-CONTEXT
190
211
  Objective achievable: yes|no|unknown
191
212
  Lane verdicts: scope-objective=<...>; dependencies-gates=<...>; atomic-checklist=<...>; acceptance-evidence=<...>; failure-decision-cleanup=<...>
@@ -200,9 +221,9 @@ the objective is sound but one or more bounded plan defects need correction.
200
221
  Never mutate production files, run execution tasks, publish, bump a version,
201
222
  commit, push, tag, or start a gateway while reviewing a plan.
202
223
 
203
- ## Completion-Review Mode
224
+ ## Work Review Mode
204
225
 
205
- This remains the LitHermes completion gate. It is intentionally skeptical: a
226
+ Work review is the LitHermes completion gate, used after `lit-loop` or `start-work`. It is intentionally skeptical: a
206
227
  green test suite is useful evidence, not a delivery claim.
207
228
 
208
229
  ## Contract
@@ -375,6 +396,7 @@ Before saying work is done, try to falsify the DoneClaim:
375
396
  Required final review wording:
376
397
 
377
398
  ```text
399
+ Review mode: work (<forced by the user | auto-picked: reason>)
378
400
  REVIEW PASSED|FAILED|BLOCKED
379
401
  DoneClaim: <one sentence>
380
402
  Lane verdicts: scope-diff=<...>; tests-evidence=<...>; package-payload=<...>; security-provenance=<...>; real-surface-docs=<...>
@@ -485,7 +485,7 @@ isolated Hermes home. Use `--hermes-home`; never `--home`.
485
485
 
486
486
  ```text
487
487
  $ node packages/lithermes-installer/bin/lithermes.js install --yes --offline --no-hud --hermes-home "$ISOLATED"
488
- Installed LitHermes 1.0.26
488
+ Installed LitHermes 1.0.28
489
489
  plugin: $ISOLATED/plugins/lithermes
490
490
  model config: updated
491
491
 
@@ -502,8 +502,8 @@ installed payload: PASS
502
502
  enabled config: PASS
503
503
 
504
504
  $ hermes lithermes doctor
505
- [OK] plugin.yaml readable (version 1.0.26)
506
- [OK] skills bundled: 36
505
+ [OK] plugin.yaml readable (version 1.0.28)
506
+ [OK] skills bundled: 37
507
507
  [OK] litgoal durable runtime importable
508
508
  ```
509
509
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litfamily/lithermes",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "Hermes Agent plugin that plans, builds, checks and hands off your work when you add one word, lit, to a request.",
5
5
  "keywords": [
6
6
  "hermes-agent",
@@ -56,6 +56,7 @@
56
56
  "qa:negative-control": "node qa/negative-gate-matrix.js --self-check",
57
57
  "qa:behaviors": "node qa/behavior-replacement-probe.js",
58
58
  "qa:real-surface": "npm run qa:negative-matrix && npm run qa:behaviors && npm run qa:negative-control",
59
+ "qa:cover-recipes": "node qa/cover-recipes.js",
59
60
  "sync-plugin": "node scripts/sync-plugin.js",
60
61
  "pack:dry": "npm run clean:payload && npm pack --dry-run --ignore-scripts --json",
61
62
  "clean:payload": "find assets -name __pycache__ -type d -prune -exec rm -rf {} + && find assets -name '*.py[co]' -delete",
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.26"><title>release: 1.0.26</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.26</text></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.28"><title>release: 1.0.28</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.28</text></g></svg>
Binary file
package/src/cli.js CHANGED
@@ -538,8 +538,8 @@ async function main(argv, runtime = {}) {
538
538
  else {
539
539
  const warm = motionRuntime('install', flags);
540
540
  receipt = warm.status === 0
541
- ? `Motion runtime: ${warm.output.split('\n')[0]}`
542
- : `Motion runtime: pre-warm incomplete (${warm.output.split('\n').filter(Boolean).at(-1)}); run \`lithermes motion-runtime install\` before rendering a film.`;
541
+ ? `Motion runtime: ${warm.output.split(/\r?\n/)[0]}`
542
+ : `Motion runtime: pre-warm incomplete (${warm.output.split(/\r?\n/).filter(Boolean).at(-1)}); run \`lithermes motion-runtime install\` before rendering a film.`;
543
543
  }
544
544
  (flags.json ? console.error : console.log)(receipt);
545
545
  }
package/src/lib/check.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const { spawnSync } = require("node:child_process");
4
+ const { spawnTool } = require("./hostExec");
4
5
  const { configHasLitHermes, formatConcurrencyCapability, inspectHermesCapabilities, readConfig, readExternalSkillDirsConfig, readStatusBarFields, suggestStatusBarFields } = require("./config");
5
6
  const { detectHermesRuntimeRepo, detectHermesVersion, ensureHermesHome, expandHome, inspectHermesHostCapabilities, LitHermesError } = require("./hermesDiscovery");
6
7
  const { formatManagedRoute } = require("./modelRoutePolicy");
@@ -205,7 +206,8 @@ function checkLitHermes(flags = {}) {
205
206
  }
206
207
 
207
208
  // MO-A-44: the motion runtime's five probes (Chrome, ffmpeg and preview rung,
208
- // WebGL2 renderer, software-GL warning, pre-warm state) on every doctor run.
209
+ // WebGL2 renderer, software-GL warning, pre-warm state) and the cover encoder,
210
+ // on every doctor run.
209
211
  // They report; they never change the doctor's pass/fail.
210
212
  function motionProbeLines(pluginPath, hermesHome) {
211
213
  const installed = path.join(pluginPath, "skills", "lit-typographic-motion", "bin", "runtime.mjs");
@@ -214,13 +216,14 @@ function motionProbeLines(pluginPath, hermesHome) {
214
216
  const result = spawnSync(process.execPath, [script, "status"], {
215
217
  encoding: "utf8", timeout: 60000, env: { ...process.env, HERMES_HOME: hermesHome },
216
218
  });
217
- const lines = String(result.stdout || "").split("\n").filter(Boolean);
218
- if (result.status === 0 && lines.length === 5) return lines.map((line) => `motion ${line}`);
219
- const reason = (result.stderr || result.error?.message || "no output").split("\n")[0];
219
+ const lines = String(result.stdout || "").split(/\r?\n/).filter(Boolean);
220
+ if (result.status === 0 && lines.length === 6) return lines.map((line) => `motion ${line}`);
221
+ const reason = (result.stderr || result.error?.message || "no output").split(/\r?\n/)[0];
220
222
  return [
221
223
  "motion Chrome: unknown (status failed)", "motion ffmpeg: unknown (status failed)",
222
224
  "motion WebGL2 renderer: unknown (status failed)", "motion software GL: unknown (status failed)",
223
225
  `motion pre-warm: status failed (${reason}); run \`lithermes motion-runtime status\``,
226
+ "motion cover encoder: unknown (status failed)",
224
227
  ];
225
228
  }
226
229
 
@@ -257,7 +260,7 @@ function doctorLitHermes(flags = {}) {
257
260
  let loaded = { status: "PARTIAL", reason: "offline mode did not query hermes plugins list" };
258
261
  if (!flags.offline) {
259
262
  const env = { ...process.env, HERMES_HOME: hermesHome };
260
- const result = spawnSync("hermes", ["plugins", "list", "--json", "--enabled", "--user"], {
263
+ const result = spawnTool("hermes", ["plugins", "list", "--json", "--enabled", "--user"], {
261
264
  encoding: "utf8",
262
265
  env,
263
266
  timeout: 15000,
@@ -266,7 +269,7 @@ function doctorLitHermes(flags = {}) {
266
269
  && !String(result.stderr || "").trim()
267
270
  && enabledUserJsonIncludesPlugin(result.stdout, "lithermes");
268
271
  const command = enabledUser
269
- ? spawnSync("hermes", ["lithermes", "status"], { encoding: "utf8", env, timeout: 15000 })
272
+ ? spawnTool("hermes", ["lithermes", "status"], { encoding: "utf8", env, timeout: 15000 })
270
273
  : null;
271
274
  const provenLoaded = enabledUser
272
275
  && command.status === 0
@@ -1,7 +1,7 @@
1
1
  const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
- const { spawnSync } = require("node:child_process");
4
+ const { spawnTool } = require("./hostExec");
5
5
 
6
6
  class LitHermesError extends Error {
7
7
  constructor(message, exitCode = 1) {
@@ -124,7 +124,7 @@ function ensureHermesHome(flags = {}, { forInstall = false } = {}) {
124
124
  }
125
125
 
126
126
  function detectHermesVersion() {
127
- const result = spawnSync("hermes", ["--version"], { encoding: "utf8", timeout: 15000 });
127
+ const result = spawnTool("hermes", ["--version"], { encoding: "utf8", timeout: 15000 });
128
128
  return result.status === 0 ? (result.stdout || result.stderr || "") : "";
129
129
  }
130
130
 
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ // Child processes that survive Windows. Since the CVE-2024-27980 fix, Node refuses to spawn a
4
+ // .cmd or .bat file without a shell (spawnSync npm.cmd EINVAL), and PATH lookups there depend on
5
+ // PATHEXT. Everything takes its platform, env and fs probes as arguments so tests can simulate
6
+ // win32 without touching globals. The payload skills carry the same logic as hostexec.mjs.
7
+
8
+ const childProcess = require("node:child_process");
9
+ const fs = require("node:fs");
10
+ const path = require("node:path");
11
+
12
+ const SHELL_SCRIPT = /\.(?:cmd|bat)$/i;
13
+ const SAFE_ARG = /^[\w@+=:,./\\-]+$/;
14
+
15
+ function whichSync(name, { platform = process.platform, env = process.env, exists = fs.existsSync } = {}) {
16
+ const win = platform === "win32";
17
+ const p = win ? path.win32 : path.posix;
18
+ const exts = win ? String(env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean) : [""];
19
+ const hasExt = win && exts.some((e) => name.toLowerCase().endsWith(e.toLowerCase()));
20
+ // A bare name never matches on Windows: npm leaves an extensionless sh script beside npm.cmd.
21
+ const names = win ? (hasExt ? [name] : exts.map((e) => name + e)) : [name];
22
+ const dirs = p.isAbsolute(name) || /[\\/]/.test(name) ? [""] : String(env.PATH ?? env.Path ?? "").split(win ? ";" : ":").map((d) => d.trim().replace(/^"|"$/g, "")).filter(Boolean);
23
+ for (const dir of dirs) {
24
+ for (const candidate of names) {
25
+ const full = dir ? p.join(dir, candidate) : candidate;
26
+ if (exists(full)) return full;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ // Quoted for cmd.exe, which still expands %VAR% inside quotes, so those are refused, never escaped.
33
+ function quoteCmdArg(arg) {
34
+ const text = String(arg);
35
+ if (/["%\r\n\0]/.test(text)) throw new Error(`refusing an argument with a quote, percent sign or line break on a Windows shell: ${JSON.stringify(text.slice(0, 40))}`);
36
+ if (text !== "" && SAFE_ARG.test(text)) return text;
37
+ return `"${text.replace(/\\+$/, (run) => run + run)}"`;
38
+ }
39
+
40
+ // Where an npm or npx shim's JavaScript entry lives, so Windows can run it with node directly.
41
+ function npmCli(name, resolved, { env, execPath, exists }) {
42
+ const cli = `${name}-cli.js`;
43
+ const dirs = [];
44
+ if (typeof env.npm_execpath === "string" && /\.js$/i.test(env.npm_execpath)) dirs.push(path.win32.dirname(env.npm_execpath));
45
+ dirs.push(path.win32.join(path.win32.dirname(resolved), "node_modules", "npm", "bin"));
46
+ dirs.push(path.win32.join(path.win32.dirname(execPath), "node_modules", "npm", "bin"));
47
+ return dirs.map((dir) => path.win32.join(dir, cli)).find((file) => exists(file)) || null;
48
+ }
49
+
50
+ function prepareSpawn(command, args = [], options = {}, ctx = {}) {
51
+ const { platform = process.platform, execPath = process.execPath, exists = fs.existsSync } = ctx;
52
+ if (platform !== "win32") return { command, args, options };
53
+ const env = ctx.env || options.env || process.env;
54
+ const resolved = whichSync(command, { platform, env, exists });
55
+ if (!resolved || !SHELL_SCRIPT.test(resolved)) return { command: resolved || command, args, options };
56
+ const base = path.win32.basename(resolved).replace(SHELL_SCRIPT, "").toLowerCase();
57
+ if (base === "npm" || base === "npx") {
58
+ const cli = npmCli(base, resolved, { env, execPath, exists });
59
+ if (cli) return { command: execPath, args: [cli, ...args], options };
60
+ }
61
+ const line = [resolved, ...args].map(quoteCmdArg).join(" ");
62
+ return { command: line, args: [], options: { ...options, shell: true } };
63
+ }
64
+
65
+ function spawnTool(command, args, options, ctx = {}) {
66
+ const run = ctx.spawnSync || childProcess.spawnSync;
67
+ const prepared = prepareSpawn(command, args, options, ctx);
68
+ return run(prepared.command, prepared.args, prepared.options);
69
+ }
70
+
71
+ // One line naming the real cause: the spawn error, the exit status and the last stderr line.
72
+ function describeSpawnFailure(label, result, { limit = 200 } = {}) {
73
+ if (!result) return `${label} returned no result`;
74
+ const tail = String(result.stderr || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1) || "";
75
+ const clean = tail.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, limit);
76
+ if (result.error) return `${label} could not run: ${result.error.code || "error"} ${result.error.message}${clean ? ` (${clean})` : ""}`;
77
+ if (result.signal) return `${label} stopped by ${result.signal}${clean ? ` (${clean})` : ""}`;
78
+ return `${label} exited ${result.status}${clean ? `: ${clean}` : ""}`;
79
+ }
80
+
81
+ module.exports = { describeSpawnFailure, prepareSpawn, quoteCmdArg, spawnTool, whichSync };
@@ -16,7 +16,7 @@ const MAX_PAYLOAD_TOTAL_BYTES = 24 * 1024 * 1024;
16
16
  const requiredSkills = [
17
17
  "lit-burnoff-file", "autoconference", "autoresearch", "browser-drive", "comment-checker", "debugging",
18
18
  "deep-interview",
19
- "frontend-ui-ux", "readme-studio", "lit-commit", "lit-crucible", "lit-init",
19
+ "frontend-ui-ux", "readme-studio", "lit-commit", "lit-brainstorm", "lit-crucible", "lit-init",
20
20
  "lit-humanizer", "lit-comprehend", "lit-handoff", "lit-plan", "lit-recap",
21
21
  "lit-scientific-visualization", "lit-diagram-drawer", "lit-pptx", "lit-docx", "lit-typographic-motion", "litgoal", "litresearch", "litwork", "lsp",
22
22
  "lsp-setup", "lit-code", "refactor", "lit-burnoff", "review-work",