lastlight 0.4.0 → 0.5.1

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 (65) hide show
  1. package/README.md +17 -6
  2. package/deploy/entrypoint.sh +21 -6
  3. package/deploy/sandbox-entrypoint.sh +1 -1
  4. package/dist/config.d.ts +12 -1
  5. package/dist/config.js +26 -6
  6. package/dist/config.js.map +1 -1
  7. package/dist/connectors/messaging/base.js +22 -9
  8. package/dist/connectors/messaging/base.js.map +1 -1
  9. package/dist/connectors/slack/connector.d.ts +62 -9
  10. package/dist/connectors/slack/connector.js +216 -63
  11. package/dist/connectors/slack/connector.js.map +1 -1
  12. package/dist/connectors/slack/connector.test.d.ts +1 -0
  13. package/dist/connectors/slack/connector.test.js +135 -0
  14. package/dist/connectors/slack/connector.test.js.map +1 -0
  15. package/dist/connectors/types.d.ts +7 -0
  16. package/dist/engine/agent-executor.d.ts +32 -10
  17. package/dist/engine/agent-executor.js +239 -1
  18. package/dist/engine/agent-executor.js.map +1 -1
  19. package/dist/engine/agent-executor.seam.test.d.ts +1 -0
  20. package/dist/engine/agent-executor.seam.test.js +57 -0
  21. package/dist/engine/agent-executor.seam.test.js.map +1 -0
  22. package/dist/engine/dispatcher.d.ts +13 -0
  23. package/dist/engine/dispatcher.js +26 -7
  24. package/dist/engine/dispatcher.js.map +1 -1
  25. package/dist/engine/dispatcher.test.js.map +1 -1
  26. package/dist/engine/message-batcher.d.ts +32 -0
  27. package/dist/engine/message-batcher.js +109 -0
  28. package/dist/engine/message-batcher.js.map +1 -0
  29. package/dist/engine/message-batcher.test.d.ts +1 -0
  30. package/dist/engine/message-batcher.test.js +109 -0
  31. package/dist/engine/message-batcher.test.js.map +1 -0
  32. package/dist/engine/profiles.d.ts +9 -0
  33. package/dist/engine/profiles.js.map +1 -1
  34. package/dist/evals-api.d.ts +24 -0
  35. package/dist/evals-api.js +22 -0
  36. package/dist/evals-api.js.map +1 -0
  37. package/dist/index.js +41 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/sandbox/command-exec.integration.test.d.ts +1 -0
  40. package/dist/sandbox/command-exec.integration.test.js +183 -0
  41. package/dist/sandbox/command-exec.integration.test.js.map +1 -0
  42. package/dist/sandbox/docker.d.ts +32 -0
  43. package/dist/sandbox/docker.js +109 -4
  44. package/dist/sandbox/docker.js.map +1 -1
  45. package/dist/workflows/loader.test.js +83 -0
  46. package/dist/workflows/loader.test.js.map +1 -1
  47. package/dist/workflows/phase-executor.d.ts +28 -3
  48. package/dist/workflows/phase-executor.js +170 -45
  49. package/dist/workflows/phase-executor.js.map +1 -1
  50. package/dist/workflows/phase-executor.test.js +64 -1
  51. package/dist/workflows/phase-executor.test.js.map +1 -1
  52. package/dist/workflows/runner.test.js +32 -27
  53. package/dist/workflows/runner.test.js.map +1 -1
  54. package/dist/workflows/schema.d.ts +20 -0
  55. package/dist/workflows/schema.js +72 -2
  56. package/dist/workflows/schema.js.map +1 -1
  57. package/package.json +12 -2
  58. package/sandbox.Dockerfile +7 -0
  59. package/skills/browser-qa/scripts/agent-browser.mjs +15 -0
  60. package/skills/demo/SKILL.md +45 -19
  61. package/skills/demo/scripts/compose-demo.sh +77 -39
  62. package/workflows/examples/bash-smoke.yaml +41 -0
  63. package/workflows/prompts/demo.md +27 -5
  64. package/workflows/qa-test.yaml +1 -1
  65. package/workflows/verify.yaml +1 -1
@@ -9,16 +9,19 @@
9
9
  # compose-demo.sh --output demo.mp4 --title "PR #42 — Add dark mode" \
10
10
  # [--subtitle "..."] [--layout single|side-by-side] \
11
11
  # [--labels "BEFORE (main)" "AFTER (PR)"] [--speed 1.5] \
12
- # [--trim START:END] [--target-size-mb 5] [--width 1280] [--height 720] \
13
- # [--title-secs 2.5] <clip1.webm> [clip2.webm]
12
+ # [--trim START:END] [--target-size-mb 10] [--crf 18] \
13
+ # [--width 1920] [--height 1080] [--title-secs 2.5] <clip1.webm> [clip2.webm]
14
14
  #
15
15
  # What it does:
16
16
  # 1. Normalizes each input clip (optional trim, speed-up, scale+pad to the
17
- # panel size, fps/pixfmt/SAR normalize, optional per-panel label).
17
+ # panel size with lanczos, fps/pixfmt/SAR normalize, optional per-panel
18
+ # label).
18
19
  # 2. Lays out the body: a single panel, or two panels side-by-side (hstack).
19
20
  # 3. Prepends a title/subtitle card (solid bg + drawtext).
20
- # 4. Two-pass H.264 encode targeting --target-size-mb so the mp4 lands under
21
- # GitHub's inline-embed limit; yuv420p + +faststart for web streaming.
21
+ # 4. Quality-first H.264 encode: a single-pass CRF encode (sharp for UI
22
+ # screencasts) that usually lands well under --target-size-mb; only if it
23
+ # overshoots does it fall back to a size-targeted two-pass ABR encode.
24
+ # yuv420p + +faststart for web streaming.
22
25
  # 5. Prints an ffprobe summary (resolution / duration / size).
23
26
  #
24
27
  # Prerequisites: ffmpeg, ffprobe (baked into lastlight-sandbox-qa:latest).
@@ -40,9 +43,10 @@ LABEL1=""
40
43
  LABEL2=""
41
44
  SPEED="1"
42
45
  TRIM=""
43
- TARGET_MB="5"
44
- WIDTH="1280"
45
- HEIGHT="720"
46
+ TARGET_MB="10"
47
+ CRF="18"
48
+ WIDTH="1920"
49
+ HEIGHT="1080"
46
50
  TITLE_SECS="2.5"
47
51
  CLIPS=()
48
52
 
@@ -57,6 +61,7 @@ while [[ $# -gt 0 ]]; do
57
61
  --speed) SPEED="$2"; shift 2;;
58
62
  --trim) TRIM="$2"; shift 2;;
59
63
  --target-size-mb) TARGET_MB="$2"; shift 2;;
64
+ --crf) CRF="$2"; shift 2;;
60
65
  --width) WIDTH="$2"; shift 2;;
61
66
  --height) HEIGHT="$2"; shift 2;;
62
67
  --title-secs) TITLE_SECS="$2"; shift 2;;
@@ -94,11 +99,19 @@ if [[ -n "$FONT" ]]; then FONTARG="fontfile=${FONT}"; else FONTARG="font=Sans";
94
99
  # gracefully on a build without it (omit text overlays) rather than failing the
95
100
  # whole compose — the demo video is still worth shipping.
96
101
  HAS_DRAWTEXT=0
97
- if ffmpeg -hide_banner -filters 2>/dev/null | grep -q 'drawtext'; then
98
- HAS_DRAWTEXT=1
99
- else
100
- echo "compose-demo.sh: warning: this ffmpeg lacks the drawtext filter title card and labels omitted." >&2
101
- fi
102
+ # Detect the drawtext filter WITHOUT a pipe. Under `set -o pipefail`, any
103
+ # `ffmpeg -filters | grep -q drawtext` (or even `printf "$var" | grep -q`) is a
104
+ # false-negative trap: `grep -q` exits on the first match and closes the pipe,
105
+ # the writer (still emitting `drawtext` sits ~16 KB into ~38 KB of output)
106
+ # dies with SIGPIPE (141), and `pipefail` promotes that to the pipeline's exit
107
+ # status, so the `if` wrongly concludes drawtext is absent (~19 of 20 runs).
108
+ # Capture into a variable and match with a pure bash `case` — no subprocess,
109
+ # no pipe, no SIGPIPE.
110
+ FFMPEG_FILTERS="$(ffmpeg -hide_banner -filters 2>/dev/null || true)"
111
+ case "$FFMPEG_FILTERS" in
112
+ *drawtext*) HAS_DRAWTEXT=1 ;;
113
+ *) echo "compose-demo.sh: warning: this ffmpeg lacks the drawtext filter — title card and labels omitted." >&2 ;;
114
+ esac
102
115
 
103
116
  # Panel size: side-by-side splits the width into two equal panels.
104
117
  PH="$HEIGHT"
@@ -115,17 +128,19 @@ build_panel() {
115
128
  [[ -n "$end" ]] && trim_args+=(-to "$end")
116
129
  fi
117
130
  local vf="setpts=PTS/${SPEED}"
118
- vf+=",scale=${PW}:${PH}:force_original_aspect_ratio=decrease"
131
+ vf+=",scale=${PW}:${PH}:force_original_aspect_ratio=decrease:flags=lanczos"
119
132
  vf+=",pad=${PW}:${PH}:(ow-iw)/2:(oh-ih)/2:color=black"
120
133
  vf+=",fps=30,format=yuv420p,setsar=1"
121
134
  if [[ -n "$labelfile" && "$HAS_DRAWTEXT" == "1" ]]; then
122
- vf+=",drawtext=${FONTARG}:textfile=${labelfile}:x=(w-text_w)/2:y=h-th-24"
123
- vf+=":fontsize=26:fontcolor=white:box=1:boxcolor=black@0.55:boxborderw=12"
135
+ # Anchor labels to the TOP of the panel. At the bottom they're hidden behind
136
+ # the browser/GitHub video player's control bar (scrubber + play button).
137
+ vf+=",drawtext=${FONTARG}:textfile=${labelfile}:x=(w-text_w)/2:y=28"
138
+ vf+=":fontsize=34:fontcolor=white:box=1:boxcolor=black@0.55:boxborderw=14"
124
139
  fi
125
140
  # `${arr[@]+"${arr[@]}"}` expands to nothing when the array is empty without
126
141
  # tripping `set -u` (an empty `"${arr[@]}"` errors on bash < 4.4, e.g. macOS).
127
142
  ffmpeg -y -loglevel error ${trim_args[@]+"${trim_args[@]}"} -i "$input" \
128
- -vf "$vf" -an -c:v libx264 -crf 20 -preset veryfast "$outfile"
143
+ -vf "$vf" -an -c:v libx264 -crf "$CRF" -preset medium "$outfile"
129
144
  }
130
145
 
131
146
  # Per-panel labels (side-by-side) written to files to avoid drawtext escaping.
@@ -140,9 +155,16 @@ build_panel "${CLIPS[0]}" "$WORK/panel0.mp4" "$LABELFILE1" 1
140
155
  BODY="$WORK/body.mp4"
141
156
  if [[ "$LAYOUT" == "side-by-side" ]]; then
142
157
  build_panel "${CLIPS[1]}" "$WORK/panel1.mp4" "$LABELFILE2" 1
158
+ # Pad the shorter panel by freezing its last frame so both stay aligned for the
159
+ # full body. Without this, hstack lets the shorter side end early and go
160
+ # blank/black — which reads as "the before didn't render".
161
+ D0="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$WORK/panel0.mp4" || echo 0)"
162
+ D1="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$WORK/panel1.mp4" || echo 0)"
163
+ PAD0="$(awk -v a="$D0" -v b="$D1" 'BEGIN{d=b-a; if(d<0)d=0; printf "%.3f", d}')"
164
+ PAD1="$(awk -v a="$D0" -v b="$D1" 'BEGIN{d=a-b; if(d<0)d=0; printf "%.3f", d}')"
143
165
  ffmpeg -y -loglevel error -i "$WORK/panel0.mp4" -i "$WORK/panel1.mp4" \
144
- -filter_complex "[0:v][1:v]hstack=inputs=2,format=yuv420p,setsar=1[v]" \
145
- -map "[v]" -an -c:v libx264 -crf 20 -preset veryfast "$BODY"
166
+ -filter_complex "[0:v]tpad=stop_mode=clone:stop_duration=${PAD0}[p0];[1:v]tpad=stop_mode=clone:stop_duration=${PAD1}[p1];[p0][p1]hstack=inputs=2,format=yuv420p,setsar=1[v]" \
167
+ -map "[v]" -an -c:v libx264 -crf "$CRF" -preset medium "$BODY"
146
168
  else
147
169
  BODY="$WORK/panel0.mp4"
148
170
  fi
@@ -159,25 +181,14 @@ if [[ "$HAS_DRAWTEXT" == "1" ]]; then
159
181
  fi
160
182
  TITLE_VF+=",format=yuv420p"
161
183
  ffmpeg -y -loglevel error -f lavfi -i "color=c=0x101418:s=${WIDTH}x${HEIGHT}:d=${TITLE_SECS}:r=30" \
162
- -vf "$TITLE_VF" -c:v libx264 -crf 20 -preset veryfast "$WORK/title.mp4"
184
+ -vf "$TITLE_VF" -c:v libx264 -crf "$CRF" -preset medium "$WORK/title.mp4"
163
185
  TITLE_CLIP="$WORK/title.mp4"
164
186
  TITLE_DUR="$TITLE_SECS"
165
187
  fi
166
188
 
167
- # ── Stage C: (concat title +) body, two-pass size cap, faststart ─────────────
168
- BODY_DUR="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$BODY" || echo 0)"
169
- TOTAL_DUR="$(awk -v a="$TITLE_DUR" -v b="$BODY_DUR" 'BEGIN{printf "%.3f", a + b}')"
170
- # Target video bitrate (kbit/s) = target_bytes * 8 / duration / 1000, with a
171
- # small headroom factor for the muxer; floored so very short clips still encode.
172
- BR_K="$(awk -v mb="$TARGET_MB" -v d="$TOTAL_DUR" 'BEGIN{
173
- if (d <= 0) d = 1;
174
- br = (mb * 1024 * 1024 * 8) / d / 1000 * 0.92;
175
- if (br < 200) br = 200;
176
- printf "%d", br;
177
- }')"
178
-
189
+ # ── Stage C: (concat title +) body final mp4 ───────────────────────────────
179
190
  # With a title card: concat it ahead of the body. Without: pass the body
180
- # through (`null`) so the same two-pass/faststart encode applies either way.
191
+ # through (`null`) so the same encode applies either way.
181
192
  if [[ -n "$TITLE_CLIP" ]]; then
182
193
  IN_ARGS=(-i "$TITLE_CLIP" -i "$BODY")
183
194
  FILTER='[0:v][1:v]concat=n=2:v=1:a=0[v]'
@@ -185,16 +196,43 @@ else
185
196
  IN_ARGS=(-i "$BODY")
186
197
  FILTER='[0:v]null[v]'
187
198
  fi
188
- PASSLOG="$WORK/ff2pass"
189
- echo "compose-demo.sh: encoding (${TOTAL_DUR}s, target ${TARGET_MB}MB ${BR_K}kbit/s, two-pass)…" >&2
190
- ffmpeg -y -loglevel error "${IN_ARGS[@]}" \
191
- -filter_complex "$FILTER" -map "[v]" -an \
192
- -c:v libx264 -b:v "${BR_K}k" -pass 1 -passlogfile "$PASSLOG" -preset medium -f mp4 /dev/null
199
+
200
+ # Quality-first: a single-pass CRF encode (visually crisp for UI screencasts
201
+ # constant quality, not a starved average bitrate) usually lands well under the
202
+ # size cap. The old always-two-pass-ABR path starved short, near-static clips
203
+ # down to a few tens of KB; CRF spends the bits the content needs.
204
+ echo "compose-demo.sh: encoding (crf ${CRF}, preset slow)…" >&2
193
205
  ffmpeg -y -loglevel error "${IN_ARGS[@]}" \
194
206
  -filter_complex "$FILTER" -map "[v]" -an \
195
- -c:v libx264 -b:v "${BR_K}k" -pass 2 -passlogfile "$PASSLOG" -preset medium \
207
+ -c:v libx264 -crf "$CRF" -preset slow \
196
208
  -pix_fmt yuv420p -movflags +faststart "$OUTPUT"
197
209
 
210
+ # Size cap: only if the quality encode overshoots --target-size-mb do we redo it
211
+ # as a size-targeted two-pass ABR encode (keeps it under GitHub's inline limit).
212
+ OUT_BYTES="$(wc -c < "$OUTPUT" | tr -d ' ')"
213
+ CAP_BYTES="$(awk -v mb="$TARGET_MB" 'BEGIN{printf "%d", mb*1024*1024}')"
214
+ if [[ "$OUT_BYTES" -gt "$CAP_BYTES" ]]; then
215
+ BODY_DUR="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$BODY" || echo 0)"
216
+ TOTAL_DUR="$(awk -v a="$TITLE_DUR" -v b="$BODY_DUR" 'BEGIN{printf "%.3f", a + b}')"
217
+ # Target video bitrate (kbit/s) = target_bytes * 8 / duration / 1000, with a
218
+ # small headroom factor for the muxer; floored so very short clips still encode.
219
+ BR_K="$(awk -v mb="$TARGET_MB" -v d="$TOTAL_DUR" 'BEGIN{
220
+ if (d <= 0) d = 1;
221
+ br = (mb * 1024 * 1024 * 8) / d / 1000 * 0.92;
222
+ if (br < 200) br = 200;
223
+ printf "%d", br;
224
+ }')"
225
+ PASSLOG="$WORK/ff2pass"
226
+ echo "compose-demo.sh: crf encode was $(awk -v b="$OUT_BYTES" 'BEGIN{printf "%.1f", b/1024/1024}')MB > ${TARGET_MB}MB cap — re-encoding size-capped (${TOTAL_DUR}s → ${BR_K}kbit/s, two-pass)…" >&2
227
+ ffmpeg -y -loglevel error "${IN_ARGS[@]}" \
228
+ -filter_complex "$FILTER" -map "[v]" -an \
229
+ -c:v libx264 -b:v "${BR_K}k" -pass 1 -passlogfile "$PASSLOG" -preset medium -f mp4 /dev/null
230
+ ffmpeg -y -loglevel error "${IN_ARGS[@]}" \
231
+ -filter_complex "$FILTER" -map "[v]" -an \
232
+ -c:v libx264 -b:v "${BR_K}k" -pass 2 -passlogfile "$PASSLOG" -preset medium \
233
+ -pix_fmt yuv420p -movflags +faststart "$OUTPUT"
234
+ fi
235
+
198
236
  # ── Stage D: report ───────────────────────────────────────────────────────────
199
237
  RES="$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x "$OUTPUT" || echo '?')"
200
238
  DUR="$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT" || echo '?')"
@@ -0,0 +1,41 @@
1
+ kind: agent
2
+ name: bash-smoke
3
+ description: "Smoke test for deterministic bash + script phases (run in sandbox)"
4
+ phases:
5
+ - name: phase_0
6
+ type: context
7
+
8
+ # Deterministic shell command in the sandbox. stdout is captured and exposed
9
+ # downstream via output_var.
10
+ - name: emit
11
+ type: bash
12
+ command: "echo hello-from-bash"
13
+ output_var: greeting
14
+
15
+ # A second bash phase reads the first phase's output via template substitution
16
+ # AND via the forwarded LL_OUT_<PHASE> env var.
17
+ - name: consume
18
+ type: bash
19
+ command: "echo template={{phaseOutputs.emit}} ; echo env=$LL_OUT_EMIT"
20
+
21
+ # Inline JS via node.
22
+ - name: js
23
+ type: script
24
+ runtime: js
25
+ script: |
26
+ console.log("node version:", process.version);
27
+ console.log("answer:", 6 * 7);
28
+
29
+ # Inline Python via uv. The PEP 723 block declares a dependency that uv
30
+ # resolves from PyPI (pypi.org is on the strict egress allowlist) into a
31
+ # cached venv. unrestricted_egress is NOT needed for PyPI.
32
+ - name: py
33
+ type: script
34
+ runtime: python
35
+ timeout_seconds: 120
36
+ script: |
37
+ # /// script
38
+ # dependencies = ["cowsay"]
39
+ # ///
40
+ import cowsay
41
+ cowsay.cow("hello from uv")
@@ -44,11 +44,33 @@ so `doctor` should pass.
44
44
 
45
45
  ## Workspace + run the app
46
46
 
47
- You are already inside the **{{repo}}** repo at branch {{branch}} the harness
48
- pre-cloned it and your cwd is the repo root (no `cd`). Follow the `building`
49
- skill to install dependencies and start the dev-server in the background; poll
50
- with `curl` until it answers on `localhost:<port>`. For a before/after demo,
51
- run each branch in turn and record the same scripted interaction against each.
47
+ You are already inside the **{{repo}}** repo the harness pre-cloned it and your
48
+ cwd is the repo root (no `cd`){{#if baseBranch}}, **checked out at the PR head
49
+ branch `{{branch}}`**, so the workspace already holds the PR's code: this checkout
50
+ *is* your "after" state{{/if}}. Follow the `building` skill to install
51
+ dependencies and start the dev-server in the background; poll with `curl` until
52
+ it answers on `localhost:<port>`.
53
+
54
+ {{#if baseBranch}}**This is a PR → record a `side-by-side` before/after.** Record
55
+ the **after** first, straight from the current checkout (`{{branch}}`) — do not
56
+ re-checkout for it. Then record the **before** from the base branch
57
+ `{{baseBranch}}`. The pre-clone is shallow + single-branch, so the base isn't
58
+ present locally yet — fetch it explicitly by ref (never trust an ambiguously
59
+ named local branch):
60
+
61
+ ```
62
+ git fetch --depth 1 origin {{baseBranch}}
63
+ git checkout -B {{baseBranch}}-base FETCH_HEAD
64
+ ```
65
+
66
+ Re-run `building`'s install + restart the dev-server after switching. When you
67
+ composite, the **first** clip is the left/BEFORE panel and the **second** is the
68
+ right/AFTER panel.
69
+
70
+ **Verify before you ship:** the disambiguating change must actually appear in the
71
+ *after* recording and be absent from the *before* (use the driver's `text`/
72
+ `assertText` to read it on both). If both states look identical, your checkout is
73
+ wrong — STOP and fix it; do not ship a side-by-side that proves nothing.{{/if}}
52
74
 
53
75
  ## Capture and compose
54
76
 
@@ -76,7 +76,7 @@ phases:
76
76
  # workflow-level `final_message`. Pure composition — no skills, no repo work.
77
77
  - name: synthesize
78
78
  label: Summary
79
- depends_on: [qa_test]
79
+ depends_on: [qa_test, qa_browser]
80
80
  prompt: prompts/qa-synth.md
81
81
  model: "{{models.qa-test}}"
82
82
  variant: "{{variants.qa-test}}"
@@ -74,7 +74,7 @@ phases:
74
74
  # workflow-level `final_message`. Pure composition — no skills, no repo work.
75
75
  - name: synthesize
76
76
  label: Summary
77
- depends_on: [verify]
77
+ depends_on: [verify, verify_browser]
78
78
  prompt: prompts/verify-synth.md
79
79
  model: "{{models.verify}}"
80
80
  variant: "{{variants.verify}}"