diff --git a/README.md b/README.md
index 23f80ae..abd89aa 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,9 @@ node bin/trace.mjs validate examples/strands-forcing.json --json
node bin/trace.mjs deliver examples/strands-forcing.json /tmp/loop-trace/demo.html --json
# 用浏览器打开 /tmp/loop-trace/demo.html:T 切主题,E 导出 PNG
+# 一次渲染全部示例到画廊目录
+node bin/trace.mjs demo --all /tmp/loop-trace/gallery
+
# 管道友好:输入传 "-" 即可从 stdin 读 IR
cat examples/strands-forcing.json | node bin/trace.mjs validate - --json
```
diff --git a/bin/trace.mjs b/bin/trace.mjs
index fc056f0..9325d7b 100644
--- a/bin/trace.mjs
+++ b/bin/trace.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// agent-loop-trace v0.1 — zero-dep validate + deliver, mirrors archify chain.
-import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
+import { readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, resolve } from "node:path";
@@ -195,12 +195,37 @@ if (cmd === "doctor") {
const receipt = { schemaVersion: 1, ok: true, command: "deliver", input: inAbs, output: outAbs, specification: { sha256: sha256Bytes(raw), bytes: raw.length }, artifact: { sha256: sha256Bytes(Buffer.from(html)), bytes: Buffer.byteLength(html) }, validation: { checksPassed: checks.length, checkCount: checks.length, compositionStatus: "pass", errors: 0, warnings: 0 } };
console.log(JSON.stringify(receipt, null, 2));
} else if (cmd === "demo") {
- const [dir] = args;
+ const all = args.includes("--all");
+ const [dir] = args.filter((a) => a !== "--all");
+ if (!dir) {
+ console.error("usage: trace.mjs demo
[--all]");
+ process.exit(2);
+ }
mkdirSync(resolve(dir), { recursive: true });
- const spec = loadJson(resolve(ROOT, "examples/strands-forcing.json"));
- const { checks } = validateSpec(spec);
- writeFileSync(resolve(dir, "loop-trace-demo.html"), renderHtml(spec, { checks }));
- console.log(`Demo ready: ${resolve(dir, "loop-trace-demo.html")}`);
+ let rendered = 0;
+ if (all) {
+ const names = readdirSync(resolve(ROOT, "examples"))
+ .filter((f) => f.endsWith(".json"))
+ .sort();
+ for (const name of names) {
+ const spec = loadJson(resolve(ROOT, "examples", name));
+ const { checks } = validateSpec(spec);
+ const out = resolve(dir, `loop-trace-${name.replace(/\.json$/, "")}.html`);
+ writeFileSync(out, renderHtml(spec, { checks }));
+ console.log(`Demo ready: ${out}`);
+ rendered += 1;
+ }
+ } else {
+ const spec = loadJson(resolve(ROOT, "examples/strands-forcing.json"));
+ const { checks } = validateSpec(spec);
+ writeFileSync(resolve(dir, "loop-trace-demo.html"), renderHtml(spec, { checks }));
+ console.log(`Demo ready: ${resolve(dir, "loop-trace-demo.html")}`);
+ rendered = 1;
+ }
+ if (rendered === 0) {
+ console.error("no example specs found under examples/");
+ process.exit(1);
+ }
} else {
console.error('usage: trace.mjs [doctor|validate|deliver|demo] ... (input "-" reads the spec from stdin)');
process.exit(2);
diff --git a/test/trace.test.mjs b/test/trace.test.mjs
index 49b8c1e..a0fc341 100644
--- a/test/trace.test.mjs
+++ b/test/trace.test.mjs
@@ -91,3 +91,21 @@ test("deliver refuses to render an invalid spec", () => {
const receipt = JSON.parse(r.stdout);
assert.equal(receipt.ok, false);
});
+
+test("demo --all renders every example into a gallery dir", () => {
+ const dir = mkdtempSync(join(tmpdir(), "loop-trace-gallery-"));
+ const r = run(["demo", "--all", dir]);
+ assert.equal(r.code, 0);
+ for (const name of ["strands-forcing", "dspy-reward"]) {
+ const html = readFileSync(join(dir, `loop-trace-${name}.html`), "utf8");
+ assert.match(html, /