wicked-garden 12.40.0 → 12.42.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -1
- package/package.json +1 -1
- package/scripts/demo/postprocess.mjs +76 -42
- package/scripts/demo/record.mjs +312 -267
- package/scripts/demo/stage.mjs +149 -27
- package/scripts/demo/walkthrough.mjs +1150 -0
- package/skills/demo/SKILL.md +14 -5
- package/skills/demo/refs/gotchas.md +12 -5
- package/skills/demo/refs/review-checklist.md +2 -2
- package/skills/demo/refs/storyline-api.md +5 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.
|
|
1
|
+
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, real multi-model second opinions. Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.42.0"}], "version": "12.42.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.42.0",
|
|
4
4
|
"description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), real multi-model second opinions (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
|
|
5
5
|
"wicked_vault_version": "^0.5.0",
|
|
6
6
|
"wicked_ledger_version": "^0.4.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.42.0] — 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Demo recorder: encodes while it records; no frame file touches disk.** Before, `scripts/demo/stage.mjs` wrote every changed screencast frame as a JPEG (q95, 1920x1080: ~8 MB/s, about 5 GB per 10-minute take) and `postprocess.mjs` encoded them afterwards from an ffconcat list. Now a 30 fps ticker hands the newest screencast picture to an ffmpeg child over stdin (`-f image2pipe`), which writes the take's master `segments/<key>/capture.mp4` live: H.264 at a near-lossless CRF 12 with one-second fragments (`+frag_keyframe+empty_moov`), so a take that dies leaves a file that plays up to its last complete second. Master frame k is the picture at t0 + k/30, so wall clock → video time stays one line and `videoClock()` keeps its contract (`outAt`, the trim, the time-lapse spans). `buildVideo()` is now a cut of the master: `trim` for the opening, a `select` that keeps one frame in `factor` across every `Stage.fast()` span and re-times the rest back to back, the chapter markers, the final CRF 17 pass; no frame files are read. Each segment keeps `capture.mp4` + `timeline.json` (what `--reencode` re-cuts from) in place of `frames/`; a failed take's `failed-<take>/` keeps the master only when no video could be cut from it, as it kept the frames. The timeline is rewritten at every mark, so a recorder killed mid-take leaves a cuttable master and clock. A dead encoder fails the take at its next step (`encoder_failed`) instead of filming into nothing. `DEMO_KEEP_FRAMES=1` also dumps every screencast frame to `frames/` (with `timeline.json` `frames` timestamps) for debugging. Measured on one take against the 12.41.0 recipe run on the same frames: see `tests/demo/test_live_encode.py` (SSIM against the frames recipe and against a lossless build of the frames; the pipe's video is as close to the frames as the single-encode recipe was). `contact_sheet.py` already read the video. Timelines recorded by earlier versions (frames, no master) are not re-cut: re-record the segment.
|
|
9
|
+
|
|
10
|
+
## [12.41.0] — 2026-10-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Demo: `walkthrough.mjs record` turns a storyline into a sealed, vault-checked proof (WT-G2, DES-walkthrough-proof §4.4–4.7).** `wicked-garden run scripts/demo/walkthrough.mjs record` reads `WICKED_EVIDENCE_ROOT`, `WICKED_RUN_ID`, `WICKED_RUN_UNIT` and `WICKED_TREE` and copies the storyline into the root. It `git archive`s the tree into `<root>/app/`, starts the fixture by argv on a free loopback port with `DATA_DIR=<root>/data`, and records each segment through the existing recorder with the fixture origin as the one writable origin. At each `ctx.check(id)` it captures by collector: `locator`, `probe` (parses json, jsonl, sqlite-json or text), `artifact` (under `DATA_DIR` only; absolute paths and `../` are refused), `guard` (foreign writes, console errors) or `join`. Captures are recorded into a vault anchored at `<root>/vault/`, never an ancestor vault, under a contract derived from the storyline, and each chapter is cross-checked. A chapter is PASS only from the vault's cross-check. A failing verifier or a check that is never reached is FAIL. A vault, `jq` or ledger that is missing or failing makes the chapter INCONCLUSIVE, never PASS. `unjailed_host`, `fixture_unavailable`, `storyline_refused` and the disk cap list every chapter as INCONCLUSIVE. Ledger rows (`crew_run_id`, `step_id`, `tree`, `take`) and a 2.1 manifest per chapter with `scenario_evidence.walkthrough` go under `<root>/.wicked-qe/`. Last, the tool writes `result.json` and `progress.json` and prints `WALKTHROUGH-SEAL` (`tree`, `storyline_sha`, `contract_shas`, `bundle_sha` over every file except `app/` and `data/`, `overall`, `chapters`). `seal --root` recomputes it. `record.mjs` exports `makeSegmentRecorder` and `stage.mjs` takes a console handler that is attached before navigation. Tests: `tests/demo/test_walkthrough.py` against `tests/demo/fixtures/`.
|
|
14
|
+
|
|
5
15
|
## [12.40.0] — 2026-10-02
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -961,7 +971,9 @@ through `v9.2.18`. Major themes across that span:
|
|
|
961
971
|
|
|
962
972
|
For each of those releases, see the corresponding tag in git history.
|
|
963
973
|
|
|
964
|
-
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.
|
|
974
|
+
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.42.0...HEAD
|
|
975
|
+
[12.42.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.41.0...v12.42.0
|
|
976
|
+
[12.41.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.40.0...v12.41.0
|
|
965
977
|
[12.40.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.39.0...v12.40.0
|
|
966
978
|
[12.39.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.38.1...v12.39.0
|
|
967
979
|
[12.38.1]: https://github.com/mikeparcewski/wicked-garden/compare/v12.38.0...v12.38.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.42.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Curated toolkit for what AI coding agents can't do alone: evidence-gated work, codegraph relationships, deterministic multi-file refactor, multi-model second opinions, and cross-session memory. Installs as a Claude Code plugin.",
|
|
6
6
|
"keywords": [
|
|
@@ -1,60 +1,91 @@
|
|
|
1
|
-
// Part of the wicked-garden-demo skill.
|
|
1
|
+
// Part of the wicked-garden-demo skill. Cuts a take's master (capture.mp4 + timeline.json) into a 1080p H.264 MP4
|
|
2
|
+
// with chapter markers.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
4
|
+
// The master is the take at a constant FPS, encoded live while it recorded (stage.mjs): frame k is the picture at
|
|
5
|
+
// t0 + k/FPS, so wall clock and master time are the same line. The cut drops the opening trim, keeps one frame in
|
|
6
|
+
// `factor` across every span marked by Stage.fast() and re-times what is left back to back, so long model waits play
|
|
7
|
+
// as a visible time-lapse while the on-screen badge states the real elapsed time; the chapter marks land where
|
|
8
|
+
// videoClock().outAt() says. No frame file is read or written.
|
|
5
9
|
// Usage: wicked-garden run scripts/demo/postprocess.mjs <outDir> [output.mp4]
|
|
6
10
|
import fs from "node:fs";
|
|
7
11
|
import path from "node:path";
|
|
8
12
|
import { spawnSync } from "node:child_process";
|
|
9
13
|
import { pathToFileURL } from "node:url";
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/** The master's constant frame rate. */
|
|
16
|
+
export const FPS = 30;
|
|
17
|
+
/** The master's file name inside a take's directory. */
|
|
18
|
+
export const CAPTURE = "capture.mp4";
|
|
19
|
+
/** ffprobe beside a given ffmpeg binary (`FFMPEG=/path/to/ffmpeg` is honoured for both). */
|
|
20
|
+
export const ffprobeOf = (ffmpeg) => ffmpeg.replace(/ffmpeg(\.exe)?$/i, "ffprobe$1");
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
|
-
* The video clock of a timeline
|
|
15
|
-
*
|
|
23
|
+
* The video clock of a timeline, in master frames so it agrees with the cut to the frame: `inFrame`, the first
|
|
24
|
+
* master frame kept; `spans`, the time-lapse spans as [a, b) frame ranges with their factor; `keptBefore(k)`, how many
|
|
25
|
+
* cut frames precede master frame k; and `outAt(t)`, the position in the cut video (seconds) of a wall-clock instant.
|
|
26
|
+
* Throws when a closed take says nothing was recorded past the trim; a timeline a killed take left (no frame count)
|
|
27
|
+
* is clocked from its t0 and the master is counted by buildVideo.
|
|
16
28
|
*/
|
|
17
29
|
export function videoClock(tl, trimStart = 0) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const fps = tl.capture?.fps ?? FPS;
|
|
31
|
+
const inFrame = Math.max(0, Math.ceil(trimStart * fps - 1e-9));
|
|
32
|
+
if (typeof tl.capture?.frames === "number" && tl.capture.frames <= inFrame) throw new Error("no frames recorded");
|
|
33
|
+
const toFrame = (t) => Math.round((t - tl.t0) * fps);
|
|
34
|
+
// Inside a span only frame a and every frame where floor((k - a) / factor) advances are kept: one in `factor`.
|
|
35
|
+
// Spans never overlap here: a later span starts where the one before it ends (a nested Stage.fast() counts once).
|
|
36
|
+
const spans = [];
|
|
37
|
+
for (const s of [...tl.marks.speed].sort((x, y) => x.start - y.start)) {
|
|
38
|
+
const a = Math.max(inFrame, toFrame(s.start), spans.length ? spans[spans.length - 1].b : 0), b = toFrame(s.end);
|
|
39
|
+
if (b > a && s.factor > 1) spans.push({ a, b, f: s.factor });
|
|
40
|
+
}
|
|
41
|
+
const keptIn = (a, b, f) => (b > a ? Math.floor((b - a - 1) / f) + 1 : 0);
|
|
42
|
+
const keptBefore = (k) => {
|
|
43
|
+
let kept = Math.max(0, k - inFrame);
|
|
44
|
+
for (const s of spans) {
|
|
45
|
+
const b = Math.min(s.b, k);
|
|
46
|
+
if (b > s.a) kept -= (b - s.a) - keptIn(s.a, b, s.f);
|
|
29
47
|
}
|
|
30
|
-
return
|
|
48
|
+
return kept;
|
|
31
49
|
};
|
|
32
|
-
|
|
50
|
+
const outAt = (t) => keptBefore(toFrame(t)) / fps;
|
|
51
|
+
return { inFrame, fps, spans, keptIn, keptBefore, outAt, speed: tl.marks.speed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Frames in the master, by demuxing it (exact for a master whose writer died: the complete packets count). */
|
|
55
|
+
function countFrames(ffprobe, file) {
|
|
56
|
+
const r = spawnSync(ffprobe, ["-v", "error", "-select_streams", "v:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file],
|
|
57
|
+
{ encoding: "utf8" });
|
|
58
|
+
if (r.status !== 0) throw new Error(`ffprobe failed on ${file}: ${(r.error?.message ?? r.stderr ?? "").slice(-500)}`);
|
|
59
|
+
const n = Number(r.stdout.trim());
|
|
60
|
+
if (!Number.isInteger(n)) throw new Error(`ffprobe gave no frame count for ${file}: ${r.stdout.slice(0, 200)}`);
|
|
61
|
+
return n;
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
export function buildVideo(outDir, outFile, { ffmpeg = process.env.FFMPEG || "ffmpeg", tail = 0.2, trimStart = 0 } = {}) {
|
|
36
65
|
const tl = JSON.parse(fs.readFileSync(path.join(outDir, "timeline.json"), "utf8"));
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const outTimes = frames.map((f) => outAt(f.t));
|
|
42
|
-
// The screencast only emits a frame when the picture changes, so a static hold at the end (a closing card, a final
|
|
43
|
-
// pause) has no frame of its own: end at the moment recording stopped, not at the last captured frame.
|
|
44
|
-
const total = Math.max(outTimes[outTimes.length - 1] + 1 / FPS, outAt(tl.tEnd ?? frames[frames.length - 1].t)) + tail;
|
|
45
|
-
const ticks = Math.ceil(total * FPS);
|
|
46
|
-
const runs = [];
|
|
47
|
-
for (let k = 0, i = 0; k < ticks; k++) {
|
|
48
|
-
while (i + 1 < frames.length && outTimes[i + 1] <= k / FPS) i++;
|
|
49
|
-
if (runs.length && runs[runs.length - 1].i === i) runs[runs.length - 1].n++;
|
|
50
|
-
else runs.push({ i, n: 1 });
|
|
66
|
+
const capture = path.join(outDir, tl.capture?.file ?? CAPTURE);
|
|
67
|
+
if (!fs.existsSync(capture)) {
|
|
68
|
+
throw new Error(tl.capture?.error ?? `${path.basename(capture)} not found in ${outDir}` +
|
|
69
|
+
(Array.isArray(tl.frames) && !tl.capture ? " (recorded from frames by an earlier version; re-record the segment)" : ""));
|
|
51
70
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
const { inFrame, fps, spans, keptBefore, outAt, speed } = videoClock(tl, trimStart);
|
|
72
|
+
// What the master holds is what counts: an encoder that died late still left playable fragments to cut, and its
|
|
73
|
+
// failure is the explanation only when nothing can be.
|
|
74
|
+
let nFrames;
|
|
75
|
+
try { nFrames = countFrames(ffprobeOf(ffmpeg), capture); } catch (e) { throw new Error(tl.capture?.error ?? e.message); }
|
|
76
|
+
if (nFrames <= inFrame) throw new Error(tl.capture?.error ?? "no frames recorded");
|
|
77
|
+
|
|
78
|
+
// Master frame k is the picture at t0 + k/fps. Inside a marked span one frame in `factor` is kept; the kept frames
|
|
79
|
+
// are re-timed back to back at fps. In the select expression n counts frames after the trim (n = k - inFrame).
|
|
80
|
+
const keep = spans.filter((s) => s.a < nFrames).reduce((rest, s) => {
|
|
81
|
+
const a = s.a - inFrame, b = Math.min(s.b, nFrames) - inFrame - 1;
|
|
82
|
+
return `if(between(n\\,${a}\\,${b})\\,gt(floor((n-${a})/${s.f})\\,floor((n-${a}-1)/${s.f}))\\,${rest})`;
|
|
83
|
+
}, "1");
|
|
84
|
+
const kept = keptBefore(nFrames);
|
|
85
|
+
const total = kept / fps + tail;
|
|
56
86
|
|
|
57
|
-
|
|
87
|
+
// A chapter marked after the master's last frame (a take killed within the second) sits at the end, not past it.
|
|
88
|
+
const chapters = tl.marks.chapters.map((c) => ({ ...c, out: Math.min(Math.max(0, outAt(c.t)), kept / fps) }));
|
|
58
89
|
const meta = [";FFMETADATA1", `title=${(tl.title || "Demo").replace(/[=;#\\\n]/g, " ")}`];
|
|
59
90
|
chapters.forEach((c, i) => {
|
|
60
91
|
const start = Math.round(c.out * 1000), end = Math.round((i + 1 < chapters.length ? chapters[i + 1].out : total) * 1000);
|
|
@@ -62,15 +93,18 @@ export function buildVideo(outDir, outFile, { ffmpeg = process.env.FFMPEG || "ff
|
|
|
62
93
|
});
|
|
63
94
|
fs.writeFileSync(path.join(outDir, "chapters.ffmeta"), meta.join("\n") + "\n");
|
|
64
95
|
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
// tpad holds the last picture for `tail` (a static close has frames of its own in the master, but the cut
|
|
97
|
+
// should not end on the very tick recording stopped).
|
|
98
|
+
const vf = `trim=start_frame=${inFrame},select='${keep}',setpts=N/(${fps}*TB),tpad=stop_mode=clone:stop_duration=${tail.toFixed(3)},format=yuv420p`;
|
|
99
|
+
const args = ["-y", "-i", capture, "-i", "chapters.ffmeta", "-map_metadata", "1", "-map_chapters", "1", "-vf", vf, "-r", String(fps), "-fps_mode", "cfr",
|
|
100
|
+
"-c:v", "libx264", "-preset", "slow", "-crf", "17", "-tune", "stillimage", "-movflags", "+faststart", path.resolve(outFile)];
|
|
67
101
|
const r = spawnSync(ffmpeg, args, { cwd: outDir, stdio: ["ignore", "ignore", "pipe"] });
|
|
68
|
-
if (r.status !== 0) throw new Error(`ffmpeg failed: ${r.stderr?.toString().slice(-2000)}`);
|
|
102
|
+
if (r.status !== 0) throw new Error(`ffmpeg failed: ${(r.error?.message ?? r.stderr?.toString() ?? "").slice(-2000)}`);
|
|
69
103
|
|
|
70
104
|
const fmt = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
|
71
105
|
const md = ["| Time | Chapter |", "|---|---|", ...chapters.map((c) => `| ${fmt(c.out)} | ${c.title} |`)];
|
|
72
106
|
fs.writeFileSync(path.join(outDir, "chapters.md"), md.join("\n") + "\n");
|
|
73
|
-
return { seconds: total, chapters: chapters.map((c) => ({ at: fmt(c.out), title: c.title })), speedups: speed.length, frames:
|
|
107
|
+
return { seconds: total, chapters: chapters.map((c) => ({ at: fmt(c.out), title: c.title })), speedups: speed.length, frames: nFrames - inFrame };
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|