@outerlayer/cli 0.33.0-next.20261023003700 → 0.33.0-next.20261023120437

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 (107) hide show
  1. package/README.md +11 -4
  2. package/dist/{agent-setup-55ZDEXCS.js → agent-setup-YHEW3WXB.js} +10 -10
  3. package/dist/{artifact-link-cmd-6FF2Y6QI.js → artifact-link-cmd-7FYKWA7J.js} +2 -2
  4. package/dist/{build-disk-JHTMPRAA.js → build-disk-ECTLWTPC.js} +4 -4
  5. package/dist/build-info.json +1 -1
  6. package/dist/{builder-Y4P7QARW.js → builder-LUPHKUFM.js} +3 -3
  7. package/dist/{check-MRYFTLOO.js → check-4QNLMDP7.js} +28 -28
  8. package/dist/{chunk-3RIFVLPO.js → chunk-2JLM4GUF.js} +12 -12
  9. package/dist/{chunk-G63KT5I5.js → chunk-44V5SD4G.js} +1 -1
  10. package/dist/{chunk-ULBNTSAD.js → chunk-5M2J7B5N.js} +2 -2
  11. package/dist/{chunk-LJG6EMLJ.js → chunk-6CXNWKWD.js} +1 -1
  12. package/dist/{chunk-BKADJZ7F.js → chunk-6SPD266O.js} +3 -3
  13. package/dist/{chunk-Q2CMEQV7.js → chunk-7IFYI7XF.js} +3 -3
  14. package/dist/{chunk-AU346ENF.js → chunk-7UPMYRPS.js} +4 -4
  15. package/dist/{chunk-NSPRS7WK.js → chunk-7V5GUEDU.js} +1 -1
  16. package/dist/{chunk-EUVE2EAE.js → chunk-AKZRGFGT.js} +3 -3
  17. package/dist/{chunk-2N67C6IV.js → chunk-D3EDZPJR.js} +5 -5
  18. package/dist/{chunk-BJQ25STI.js → chunk-DKFUXYGD.js} +4 -4
  19. package/dist/{chunk-HE4FBF4T.js → chunk-EHOFFTVX.js} +2 -2
  20. package/dist/{chunk-HXA5GIHK.js → chunk-EJL6CWSP.js} +6 -6
  21. package/dist/chunk-ER6KQCVJ.js +18 -0
  22. package/dist/{chunk-DISTLL2Q.js → chunk-EXZACZ2P.js} +1 -1
  23. package/dist/{chunk-E6CO3MJE.js → chunk-F2D5BSXS.js} +38 -2
  24. package/dist/{chunk-UIUGTW5C.js → chunk-FT7MUQGO.js} +2 -2
  25. package/dist/{chunk-PWH3BEZF.js → chunk-FZUFUVLK.js} +13 -13
  26. package/dist/{chunk-XL2XWWU7.js → chunk-GO2DUPQS.js} +3 -3
  27. package/dist/{chunk-B2JURP6A.js → chunk-HLZMAJJD.js} +2 -2
  28. package/dist/{chunk-2EGCI2VW.js → chunk-IUPY54BZ.js} +10 -10
  29. package/dist/{chunk-GFYEMCPP.js → chunk-JEVKOZBB.js} +1 -1
  30. package/dist/{chunk-DY7Y7UT4.js → chunk-KUE4V2SK.js} +2 -2
  31. package/dist/{chunk-SVDOA4W2.js → chunk-L6MDOEHN.js} +7 -7
  32. package/dist/{chunk-HJYBWJAS.js → chunk-LYGI3T66.js} +2 -2
  33. package/dist/{chunk-EWVYQLD3.js → chunk-MPC2UAW2.js} +1 -1
  34. package/dist/{chunk-5DNCFOXA.js → chunk-NIY2UBT6.js} +1 -1
  35. package/dist/{chunk-FR7HWWCF.js → chunk-OGMXVPCS.js} +1 -1
  36. package/dist/{chunk-6Z4SZHV2.js → chunk-QKNFU5GA.js} +5 -5
  37. package/dist/{chunk-2WMZPJVE.js → chunk-S5CNYXUA.js} +4 -4
  38. package/dist/{chunk-7HHNGZS5.js → chunk-SQQD5MST.js} +4 -4
  39. package/dist/{chunk-QPWFVE3X.js → chunk-UK4RGH46.js} +2 -2
  40. package/dist/{chunk-JHTJPQSL.js → chunk-V673DFOL.js} +4 -4
  41. package/dist/{chunk-RQJMN5TJ.js → chunk-VWQCYOBS.js} +2 -2
  42. package/dist/{chunk-W2AEL4BJ.js → chunk-X4JEBMYN.js} +1 -1
  43. package/dist/{chunk-T5REPMNL.js → chunk-XCRET2GZ.js} +2 -2
  44. package/dist/{chunk-XHHTMFCX.js → chunk-YGZLRX5U.js} +1 -1
  45. package/dist/{chunk-SDU2IE22.js → chunk-YSM4EEUK.js} +12 -12
  46. package/dist/{chunk-VT4TTJPG.js → chunk-YXMGHGHR.js} +5 -5
  47. package/dist/{chunk-AVTI3FL5.js → chunk-ZF24JN7R.js} +1 -1
  48. package/dist/{claude-launcher-NTYHJUJM.js → claude-launcher-YRPW4SWA.js} +5 -5
  49. package/dist/{cli-K6XGL4EZ.js → cli-BC7JWQUQ.js} +149 -103
  50. package/dist/{command-7FAIIQGF.js → command-SQ4466SL.js} +19 -19
  51. package/dist/{config-QXMUWJAQ.js → config-MVA7YTFW.js} +5 -5
  52. package/dist/{connect-cmd-P5GASVAF.js → connect-cmd-KWXV2CGC.js} +8 -8
  53. package/dist/{container-start-CVP3NTDG.js → container-start-AQJ64LCF.js} +20 -20
  54. package/dist/{context-cleanup-BX5Y3AFL.js → context-cleanup-A2FGVVPH.js} +3 -3
  55. package/dist/{context-show-cmd-3EPJ7CJC.js → context-show-cmd-6F4UF4ND.js} +9 -9
  56. package/dist/{control-plane-launch-A3YCHQNQ.js → control-plane-launch-7SOH6IMA.js} +6 -6
  57. package/dist/{dist-ACTRIBVW.js → dist-IG2DKUJJ.js} +2 -2
  58. package/dist/{docker-ANW76BNW.js → docker-YKYQJHL2.js} +3 -3
  59. package/dist/{doctor-GJAHI3KU.js → doctor-4YZOP343.js} +20 -20
  60. package/dist/{emit-artifact-cmd-FDPJKGOO.js → emit-artifact-cmd-6QTTWBVN.js} +13 -13
  61. package/dist/{emit-cmd-OX6LYS6I.js → emit-cmd-3FG7FEGO.js} +6 -6
  62. package/dist/{emit-criteria-cmd-3SVGCMJN.js → emit-criteria-cmd-22VVCL2W.js} +10 -10
  63. package/dist/{emit-demo-cmd-6DF2QIAS.js → emit-demo-cmd-IQRS7BP2.js} +243 -51
  64. package/dist/{emit-finding-cmd-TLJWBGQE.js → emit-finding-cmd-TXDPJFVT.js} +10 -10
  65. package/dist/{emit-result-cmd-CJNVDV5V.js → emit-result-cmd-QHJDUC7J.js} +12 -12
  66. package/dist/{emit-step-cmd-347CZO7Y.js → emit-step-cmd-TQCOLXAA.js} +10 -10
  67. package/dist/{findings-cmd-LHTYEHYB.js → findings-cmd-EURKADAY.js} +10 -10
  68. package/dist/{fleet-readiness-KGR5AC2J.js → fleet-readiness-GXDSFTLP.js} +8 -8
  69. package/dist/{governing-source-JZQZUTMA.js → governing-source-IRRP7G5G.js} +13 -13
  70. package/dist/{hook-fast-6WDMNZG5.js → hook-fast-3IYSWDL4.js} +1 -1
  71. package/dist/index.js +4 -4
  72. package/dist/{init-ZQ5B3TJZ.js → init-63X6ECKF.js} +19 -19
  73. package/dist/{init-XDOKWBL2.js → init-T64C4MID.js} +19 -19
  74. package/dist/{init-cmd-YMGW2WXJ.js → init-cmd-7CGCDLBZ.js} +18 -18
  75. package/dist/{init-update-X7YLZL6H.js → init-update-LFKSC6Y4.js} +17 -17
  76. package/dist/{install-QPA7DQHO.js → install-47Z7VNWP.js} +6 -6
  77. package/dist/{layout-JGF7MJHG.js → layout-44S4OYLK.js} +4 -4
  78. package/dist/{login-browser-3RIZQJY2.js → login-browser-BJZYKWHF.js} +2 -2
  79. package/dist/{loop-UJL2UCUM.js → loop-YJF27PUS.js} +28 -28
  80. package/dist/{machine-2YPAFFCH.js → machine-VBITNWTR.js} +19 -19
  81. package/dist/{machine-skills-AGERT2HN.js → machine-skills-GOKW3U5N.js} +4 -4
  82. package/dist/{runner-env-HOUXKWIJ.js → runner-env-ILP6BAC2.js} +7 -7
  83. package/dist/{runner-identity-KPUVTRLS.js → runner-identity-MF3AYBR6.js} +3 -3
  84. package/dist/{runner-key-FUJD66R6.js → runner-key-5SGBLTDJ.js} +19 -19
  85. package/dist/{session-sender-7B3ZDWMC.js → session-sender-2DYAJIMM.js} +13 -13
  86. package/dist/{spec-cmd-Q26NE22H.js → spec-cmd-E6SINBX6.js} +11 -11
  87. package/dist/{starter-pack-UBEC5LTE.js → starter-pack-FWTXE6XB.js} +5 -5
  88. package/dist/{status-2KNTILGQ.js → status-AYHBGIAP.js} +24 -24
  89. package/dist/{sync-cmd-SSUYTMLJ.js → sync-cmd-NGX2PGIW.js} +12 -12
  90. package/dist/{watch-L5NCO46E.js → watch-ZIWZVU3Z.js} +12 -12
  91. package/dist/{work-claim-cmd-NJNOQFQE.js → work-claim-cmd-CIVWB7EJ.js} +15 -15
  92. package/dist/{work-cmd-W7WEHMQJ.js → work-cmd-OO6K6ELP.js} +10 -10
  93. package/dist/{work-comment-cmd-SSUOGVDD.js → work-comment-cmd-SY7NY2CR.js} +10 -10
  94. package/dist/work-demo-cmd-Y7EU335L.js +62 -0
  95. package/dist/{work-logs-cmd-WPMWMDX6.js → work-logs-cmd-PWOFLTGD.js} +10 -10
  96. package/dist/{work-open-pr-cmd-NO5YLOXD.js → work-open-pr-cmd-AJUTKYWN.js} +11 -11
  97. package/dist/{work-pr-cmd-37B5KECR.js → work-pr-cmd-DH43AH3V.js} +10 -10
  98. package/dist/work-proofs-cmd-6W6DDAP6.js +111 -0
  99. package/dist/{work-stop-cmd-3BJLXUJ3.js → work-stop-cmd-WZHBEKE4.js} +10 -10
  100. package/package.json +1 -1
  101. package/skill-pack/maintained/outerlayer-amend/SKILL.md +36 -34
  102. package/skill-pack/maintained/outerlayer-amend/scripts/rerecord-demo.mjs +187 -0
  103. package/skill-pack/maintained/outerlayer-build/SKILL.md +7 -2
  104. package/skill-pack/maintained/outerlayer-build/references/agent-briefs.md +16 -4
  105. package/skill-pack/maintained/outerlayer-build/references/evidence.md +11 -1
  106. package/skill-pack/maintained/outerlayer-build/references/release.md +3 -2
  107. package/skill-pack/maintained/outerlayer-evidence/SKILL.md +21 -18
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // Records an item's demo again at the pull request's new head, after an
3
+ // amend pushed. It reads the item's newest demo record, and records again
4
+ // only a demo that was recorded at an earlier commit and names the test that
5
+ // recorded it. Needs only Node.
6
+ //
7
+ // node rerecord-demo.mjs --item <number> --pr <number> [--repository <owner/name>] [--project <playwright project>]
8
+ //
9
+ // Run it after the amend's last push, with the app running as the
10
+ // repository's instructions say. It prints one line for the amend's report:
11
+ // recorded again, current, or why not. It posts one comment on the item's
12
+ // general thread when the demo is out of date and cannot be recorded again,
13
+ // when a chapter failed its checks, or when the recording failed. It posts
14
+ // nothing when the item has no demo, or the demo is current.
15
+ import { spawnSync } from "node:child_process";
16
+ import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
19
+ import { pathToFileURL } from "node:url";
20
+
21
+ const USAGE = "usage: rerecord-demo.mjs --item <number> --pr <number> [--repository <owner/name>] [--project <playwright project>]";
22
+ const CONFIG_NAMES = ["ts", "js", "mjs", "cjs", "mts", "cts"].map((extension) => `playwright.config.${extension}`);
23
+
24
+ /** The `spawnSync` that runs a program in `cwd`; tests pass a stand-in. */
25
+ function runProgram(command, args, options) {
26
+ return spawnSync(command, args, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024, ...options });
27
+ }
28
+
29
+ /** `owner/name` of a repository in any spelling the factory stores, lower-case. */
30
+ function repositoryName(repository) {
31
+ return repository
32
+ .replace(/\.git$/, "")
33
+ .split("/")
34
+ .filter((part) => part !== "")
35
+ .slice(-2)
36
+ .join("/")
37
+ .toLowerCase();
38
+ }
39
+
40
+ /** A test path inside the repository, or `null` when the path is absolute or leaves it. */
41
+ function insidePath(test) {
42
+ if (typeof test !== "string" || test === "" || isAbsolute(test) || test.split("/").includes("..")) return null;
43
+ return test;
44
+ }
45
+
46
+ /** The nearest folder from the test's own up to the root that holds a Playwright config, else the root. */
47
+ function configFolder(root, testFile) {
48
+ let folder = dirname(testFile);
49
+ while (folder.startsWith(root)) {
50
+ if (CONFIG_NAMES.some((name) => existsSync(join(folder, name)))) return folder;
51
+ if (folder === root) break;
52
+ folder = dirname(folder);
53
+ }
54
+ return root;
55
+ }
56
+
57
+ /** Every `demo/manifest.json` under `folder`. */
58
+ function findManifests(folder) {
59
+ const found = [];
60
+ for (const entry of readdirSync(folder, { withFileTypes: true })) {
61
+ const path = join(folder, entry.name);
62
+ if (entry.isDirectory()) found.push(...findManifests(path));
63
+ else if (entry.name === "manifest.json" && basename(folder) === "demo") found.push(path);
64
+ }
65
+ return found;
66
+ }
67
+
68
+ /** The last line of a command's output that parses as a JSON object, or `null`. */
69
+ function lastJson(text) {
70
+ for (const line of String(text).split("\n").reverse()) {
71
+ try {
72
+ const value = JSON.parse(line);
73
+ if (value !== null && typeof value === "object") return value;
74
+ } catch {
75
+ // not the JSON line
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Records the demo again when it is out of date and names its test.
83
+ * `spawn(command, args, { cwd })` returns `{ status, stdout, stderr }`.
84
+ * Returns the outcome and the line for the amend's report.
85
+ */
86
+ export function rerecordDemo({ item, pr, repository, project, cwd, spawn = runProgram }) {
87
+ const cli = (args) => {
88
+ const result = spawn("outerlayer", args, { cwd });
89
+ // Without `outerlayer` on PATH, the CLI runs through npx.
90
+ return result.error?.code === "ENOENT" ? spawn("npx", ["-y", "@outerlayer/cli", ...args], { cwd }) : result;
91
+ };
92
+ // The line for the amend's report says so when a comment did not post, so nobody waits for it.
93
+ const comment = (body) => (cli(["work", "comment", "--item", String(item), "--body", body]).status === 0 ? "" : " The comment on the item's thread was not posted.");
94
+
95
+ const read = cli(["work", "demo", "--item", String(item), "--json"]);
96
+ const answer = read.status === 0 ? lastJson(read.stdout) : null;
97
+ if (answer === null) {
98
+ const detail = String(read.stderr ?? "").trim();
99
+ return { outcome: "error", line: `Demo: could not read the item's demo${detail === "" ? "" : ` (${detail})`}.` };
100
+ }
101
+ const demo = answer.demo ?? null;
102
+ if (demo === null) return { outcome: "none", line: "Demo: the item has no demo to record again." };
103
+ if (repository !== undefined && repositoryName(String(demo.repository ?? "")) !== repositoryName(repository)) {
104
+ return { outcome: "other-repository", line: `Demo: the item's demo is in ${demo.repository}, not ${repository}.` };
105
+ }
106
+
107
+ const head = String(spawn("git", ["rev-parse", "HEAD"], { cwd }).stdout ?? "").trim();
108
+ const short = (sha) => (sha === "" ? "an unknown commit" : sha.slice(0, 7));
109
+ if (demo.commitSha === head) return { outcome: "current", line: `Demo: current, recorded at ${short(head)}.` };
110
+
111
+ const byHand = `outerlayer emit demo <manifest> --item ${item} --pr ${pr} --replaces ${demo.artifactId}`;
112
+ const stale = `The demo is out of date: it was recorded at ${short(String(demo.commitSha ?? ""))}, and the branch is now at ${short(head)}.`;
113
+ const root = String(spawn("git", ["rev-parse", "--show-toplevel"], { cwd }).stdout ?? "").trim() || cwd;
114
+ if (demo.record === null) {
115
+ const why = "Its record cannot be read, so the amend cannot tell which test to run.";
116
+ const unposted = comment(`${stale} ${why} To record it again, run the demo test, then run \`${byHand}\`.`);
117
+ return { outcome: "not-recorded", line: `Demo: out of date at ${short(String(demo.commitSha ?? ""))}; ${why.charAt(0).toLowerCase()}${why.slice(1)}${unposted}` };
118
+ }
119
+ const test = insidePath(demo.record?.test);
120
+ const testFile = test === null ? null : join(root, test);
121
+
122
+ if (test === null || !existsSync(testFile)) {
123
+ const why =
124
+ demo.record?.test === undefined || demo.record?.test === null
125
+ ? "Its record names no test, so the amend cannot record it again."
126
+ : `The test ${demo.record.test} is not in the checkout, so the amend cannot record it again.`;
127
+ const unposted = comment(`${stale} ${why} To record it again, run the demo test, then run \`${byHand}\`.`);
128
+ return { outcome: "not-recorded", line: `Demo: out of date at ${short(String(demo.commitSha ?? ""))}; ${why.charAt(0).toLowerCase()}${why.slice(1)}${unposted}` };
129
+ }
130
+
131
+ const output = mkdtempSync(join(tmpdir(), "outerlayer-rerecord-"));
132
+ try {
133
+ const folder = configFolder(root, testFile);
134
+ // Playwright reads the filter as a regular expression, so a path such as app/[id]/x.spec.ts needs its characters escaped.
135
+ const filter = relative(folder, testFile).split("\\").join("/").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
136
+ const run = spawn("npx", ["playwright", "test", filter, "--output", output, ...(project === undefined ? [] : ["--project", project])], { cwd: folder });
137
+ // A failed check still writes the manifest, and emit demo reports the failed chapter. Only a missing manifest stops here.
138
+ const manifests = existsSync(output) ? findManifests(output) : [];
139
+ if (manifests.length !== 1) {
140
+ const why =
141
+ manifests.length === 0
142
+ ? `Running ${test} left no demo manifest (exit ${run.status}).`
143
+ : `Running ${test} left ${manifests.length} demo manifests, so the amend cannot tell which to store. Pass --project <name> so the test runs once.`;
144
+ const unposted = comment(`${stale} ${why} To record it again, run the demo test, then run \`${byHand}\`.`);
145
+ return { outcome: "error", line: `Demo: out of date at ${short(String(demo.commitSha ?? ""))}; ${why}${unposted}` };
146
+ }
147
+
148
+ const emit = cli(["emit", "demo", manifests[0], "--item", String(item), "--pr", String(pr), "--replaces", demo.artifactId, "--json"]);
149
+ const result = emit.status === 0 || emit.status === 1 ? lastJson(emit.stdout) : null;
150
+ if (result === null) {
151
+ const why = `Storing the new demo failed: ${String(emit.stderr ?? "").trim().split("\n")[0] || `exit ${emit.status}`}.`;
152
+ const unposted = comment(`${stale} ${why} To record it again, run \`${byHand}\` on the manifest of a new run.`);
153
+ return { outcome: "error", line: `Demo: out of date at ${short(String(demo.commitSha ?? ""))}; ${why}${unposted}` };
154
+ }
155
+ const synced = cli(["sync"]);
156
+ const unsynced = synced.status === 0 ? "" : ` The sync failed (exit ${synced.status}), so the new demo is stored but the session record is not uploaded. Run \`outerlayer sync\`.`;
157
+ const failures = Array.isArray(result.failures) ? result.failures : [];
158
+ if (failures.length > 0) {
159
+ const unposted = comment(`The demo was recorded again at ${short(head)}, but a chapter failed its checks. A failed chapter is stored but not bound to its criterion:\n${failures.map((line) => `- ${line}`).join("\n")}`);
160
+ return { outcome: "failed-chapters", line: `Demo: recorded again at ${short(head)}, but ${failures.join("; ")}.${unsynced}${unposted}` };
161
+ }
162
+ if (unsynced !== "") return { outcome: "sync-failed", line: `Demo: recorded again at ${short(head)}.${unsynced}` };
163
+ return { outcome: "recorded", line: `Demo: recorded again at ${short(head)}.` };
164
+ } finally {
165
+ rmSync(output, { recursive: true, force: true });
166
+ }
167
+ }
168
+
169
+ /** The value after `flag`, or `undefined`. */
170
+ function flagValue(argv, flag) {
171
+ const at = argv.indexOf(flag);
172
+ return at === -1 ? undefined : argv[at + 1];
173
+ }
174
+
175
+ function main(argv) {
176
+ const item = Number(flagValue(argv, "--item"));
177
+ const pr = Number(flagValue(argv, "--pr"));
178
+ if (!Number.isInteger(item) || item <= 0 || !Number.isInteger(pr) || pr <= 0) {
179
+ process.stderr.write(`${USAGE}\n`);
180
+ return 2;
181
+ }
182
+ const { line } = rerecordDemo({ item, pr, repository: flagValue(argv, "--repository"), project: flagValue(argv, "--project"), cwd: process.cwd() });
183
+ process.stdout.write(`${line}\n`);
184
+ return 0;
185
+ }
186
+
187
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = main(process.argv.slice(2));
@@ -334,7 +334,8 @@ Run them in order.
334
334
  6. **Evidence.** Run `outerlayer emit step "Evidence" --item "$OUTERLAYER_WORK"`,
335
335
  then apply the team's changes for this phase. Read [references/evidence.md](references/evidence.md)
336
336
  when the pull request opens, and run it. Capture the proof each criterion
337
- declares, emit it.
337
+ declares, emit it. Then check the proofs with `outerlayer work proofs`, as
338
+ that file says.
338
339
 
339
340
  Optional steps a team may turn off: the manual test and screenshots in the
340
341
  Evidence phase, for a repository with no user interface.
@@ -342,7 +343,11 @@ Evidence phase, for a repository with no user interface.
342
343
  ## Ending the run
343
344
 
344
345
  Run this last, after the pull request is open and the evidence is emitted, and
345
- run no other tool call after it:
346
+ run no other tool call after it. Before it, run `outerlayer sync` and then
347
+ `outerlayer work proofs --item "$OUTERLAYER_WORK"`. End the run only when it
348
+ exits 0, or when `build-reports/evidence.md` names each proof it still lists
349
+ and the reason. Exit 2 means it could not decide: fix the reason it prints and
350
+ run it again:
346
351
 
347
352
  outerlayer work release --item "$OUTERLAYER_WORK"
348
353
 
@@ -253,12 +253,22 @@ application's own paths. When a state cannot be reached that way, the criterion
253
253
  is not evidenced, and `evidence.md` says why. Before emitting, look at each
254
254
  capture: it shows the application's navigation and real rows.
255
255
 
256
- For each criterion, put the capture in a script under
257
- `build-reports/evidence/` and run the script. A later fix can invalidate what
256
+ Write the demo, when a criterion needs a video or a screenshot, as a Playwright
257
+ test that is part of the change. Put it where the repository keeps its Playwright tests, in the
258
+ folder the instructions name, else in `e2e/demo/`. Name it `*.demo.spec.ts`.
259
+ Commit it in the pull request. The demo record names this test, so an amend
260
+ can run it again at a new head. If the repository's CI cannot start the app,
261
+ mark the file so that CI skips it, as `apps/e2e` does.
262
+
263
+ Record each criterion that declares `proof: screenshot` as a chapter of the same
264
+ demo, with a `demo.screenshot()` call where the result shows. The frame then
265
+ comes from the committed test, so an amend records it again at the new head. Put
266
+ a capture in a script under `build-reports/evidence/` and run the script only
267
+ when no demo test can reach its state. A later fix can invalidate what
258
268
  you captured, and regenerating it must be one command. The scripts go when the
259
269
  run's report directory goes. The emitted artifacts outlive the run.
260
270
 
261
- Record the criteria that declare `proof: video` as one demo, as the
271
+ Record the criteria that declare `proof: video` or `proof: screenshot` as one demo, as the
262
272
  outerlayer-evidence skill's "Record a demo" says. Write one Playwright test with
263
273
  `@outerlayer/playwright` and one chapter for each of those criteria, in the
264
274
  criteria's order. Run it, then run `outerlayer emit demo <manifest> --item
@@ -266,7 +276,9 @@ criteria's order. Run it, then run `outerlayer emit demo <manifest> --item
266
276
  emit artifact ... --for <criterion id> --pr <number>`. Always pass `--pr`.
267
277
  When the tree invalidates a video, record and emit the whole demo again: the
268
278
  newer demo replaces the older one. Regenerate any other invalidated evidence
269
- with `--replaces`. Then run `outerlayer sync`.
279
+ with `--replaces`. Then run `outerlayer sync`, and run `outerlayer work
280
+ proofs --item "$OUTERLAYER_WORK"`. For each proof it lists as out of date or
281
+ missing, capture and emit that criterion again, and run the command once more.
270
282
 
271
283
  Write `build-reports/evidence.md` naming every criterion with its artifact, or
272
284
  with the reason none could be produced. A criterion you could not evidence is
@@ -15,10 +15,20 @@ the issue, the pull request number, the criteria that declare a proof form, the
15
15
  instructions-file pointer and the outerlayer-evidence skill.
16
16
 
17
17
  A criterion that declares `proof: screenshot` needs a screenshot bound to its
18
- id. One that declares `proof: video` needs a video: its chapter of the
18
+ id. The evidence agent records it as a chapter of the item's demo, with a
19
+ `demo.screenshot()` call. It emits a screenshot with `emit artifact` only for a
20
+ state no demo test can reach. One that declares `proof: video` needs a video: its chapter of the
19
21
  item's one demo. A screenshot never satisfies `proof: video`. A criterion with no proof form is proven by its
20
22
  test, and needs no artifact.
21
23
 
24
+ After the evidence agent returns, and after every later commit, run
25
+ `outerlayer sync`, then `outerlayer work proofs --item "$OUTERLAYER_WORK"`. It
26
+ lists each declared proof as current, out of date or missing at the checkout's
27
+ head. For each criterion it lists as out of date or missing, run the evidence
28
+ step again for that criterion only, then run the command once more before the
29
+ run ends. A criterion still listed goes in `build-reports/evidence.md` with the
30
+ reason. The command leaves out criteria that declare a test proof.
31
+
22
32
  When a commit lands on the branch after the evidence agent captured, decide for
23
33
  each capture from that commit's diff. A capture is stale when the diff touches
24
34
  a file the capture's subject runs through: the feature's own files, the shared
@@ -90,8 +90,9 @@ report. With several judges, run it on the merged report:
90
90
  Then run `outerlayer sync`. List every unsatisfied criterion in the final
91
91
  report. A criterion that declares a proof form such as a screenshot or video is
92
92
  proven only by an artifact bound to it on the pull request. The judge cannot
93
- see that, so confirm each has an artifact of the declared kind, and emit any
94
- that is missing in the evidence phase.
93
+ see that. Run `outerlayer work proofs --item "$OUTERLAYER_WORK"`, as the
94
+ evidence phase says, and emit again any proof it lists as out of date or
95
+ missing.
95
96
 
96
97
  ## The criteria verdict
97
98
 
@@ -16,22 +16,20 @@ spec asks for — a reviewer should see the change working, not a gallery.
16
16
 
17
17
  ## When to capture
18
18
 
19
- Capture AFTER the state exists, never before: run the app, the test, or the
20
- flow first, and shoot the working result. A screenshot of code is not
21
- evidence; a screenshot of the rendered page is. If the state takes setup
22
- (seed data, a logged-in user), finish the setup, verify by eye, then capture.
19
+ Capture AFTER the state exists: run the app, test, or
20
+ flow first, and shoot the working result. A screenshot of code is not evidence. Finish setup (seed data, a login) and check by eye first.
23
21
 
24
22
  ## Per-kind mechanics
25
23
 
26
24
  Kind is inferred from the file's media type — name files honestly. Every
27
- artifact shares one upload cap of 8 MiB, whatever its kind:
25
+ artifact shares one upload cap of 8 MiB:
28
26
 
29
27
  - **screenshot** (`.png`, `.jpg`) — one focused window or region showing the
30
28
  proven state. Crop noise; keep enough chrome (URL bar, test summary line)
31
29
  to show it is real.
32
30
  - **video** (`.webm`, `.mp4`) — a recording of the flow, start to outcome.
33
- Record it as a demo chapter (see "Record a demo"). Video is the kind most
34
- likely to hit the 8 MiB cap: trim dead time.
31
+ Record it as a demo chapter (see "Record a demo"). Video most likely
32
+ hits the 8 MiB cap: trim dead time.
35
33
  - **report** (`.html`, `.pdf`) — generated reports: coverage, benchmark,
36
34
  audit output. Emit the file the tool produced, unedited.
37
35
  - **log** (`.txt`, `.log`) — command output proving a run happened: test
@@ -82,9 +80,9 @@ the recorded list does not hold gets no row.
82
80
 
83
81
  Record every video proof as a chapter of the item's one demo. A demo has
84
82
  one chapter for each criterion that declares a video, in the criteria's
85
- order. Each chapter is that criterion's video proof, with a spoken line that
86
- says what happens and a list of the checks that ran. Emit a video with
87
- `emit artifact` only when Playwright cannot drive the flow.
83
+ order. Each chapter is that criterion's video proof, with a spoken line and the
84
+ checks that ran. Emit a video with `emit artifact` only when Playwright cannot
85
+ drive the flow.
88
86
 
89
87
  Write a Playwright test with `@outerlayer/playwright`, one `demo.chapter`
90
88
  for each criterion:
@@ -99,11 +97,17 @@ for each criterion:
99
97
  });
100
98
  });
101
99
 
100
+ Commit the test: an amend runs it again.
101
+
102
+ Record each `proof: screenshot` criterion as a chapter too. Call `await
103
+ demo.screenshot()` inside it, right after the `expect` that shows the result.
104
+ Options: `{ fullPage: true }`, `{ masks: true }`. `emit demo` stores the frame
105
+ when every check passed, or says why not.
106
+
102
107
  Install it with `npm install --save-dev @outerlayer/playwright`. Use the
103
108
  recorded criterion id as the first argument, or `null` for an opening scene.
104
- Keep each narration line short enough to say in the chapter's time. Put the
105
- `expect` calls that prove the criterion inside the chapter, because each one
106
- is recorded as a check.
109
+ Keep each narration line short. Put the `expect` calls that prove the
110
+ criterion inside the chapter: each is recorded as a check.
107
111
 
108
112
  Run the test. It writes `demo/manifest.json` and `demo/video.webm` in the
109
113
  test's output folder. Then emit the demo:
@@ -115,11 +119,10 @@ Each chapter is its own video proof with a time range in that video. A
115
119
  chapter whose checks all passed is bound to its criterion. A chapter with
116
120
  a failed check is stored unbound, and the command exits 1: fix the app and
117
121
  record again. The command needs `ffmpeg` and `espeak-ng` on PATH. Pass
118
- `--no-narration` to store a demo with no audio when you cannot install
119
- `espeak-ng`. It refuses before it uploads anything when a chapter is out of
120
- order, a criterion id is not recorded, a line is too long for its chapter,
121
- or a chapter's clip is over 8 MiB. A video over 8 MiB falls back to one clip
122
- for each chapter. In a runner build everything it needs is installed.
122
+ `--no-narration` when you cannot install `espeak-ng`. It refuses before it
123
+ uploads anything when a chapter is out of order, a criterion id is not
124
+ recorded, a line is too long, or a chapter's clip is over 8 MiB. A video over 8 MiB falls back to one clip
125
+ for each chapter. A runner build has everything installed.
123
126
 
124
127
  ## The noise rule
125
128