@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.
- package/README.md +11 -4
- package/dist/{agent-setup-55ZDEXCS.js → agent-setup-YHEW3WXB.js} +10 -10
- package/dist/{artifact-link-cmd-6FF2Y6QI.js → artifact-link-cmd-7FYKWA7J.js} +2 -2
- package/dist/{build-disk-JHTMPRAA.js → build-disk-ECTLWTPC.js} +4 -4
- package/dist/build-info.json +1 -1
- package/dist/{builder-Y4P7QARW.js → builder-LUPHKUFM.js} +3 -3
- package/dist/{check-MRYFTLOO.js → check-4QNLMDP7.js} +28 -28
- package/dist/{chunk-3RIFVLPO.js → chunk-2JLM4GUF.js} +12 -12
- package/dist/{chunk-G63KT5I5.js → chunk-44V5SD4G.js} +1 -1
- package/dist/{chunk-ULBNTSAD.js → chunk-5M2J7B5N.js} +2 -2
- package/dist/{chunk-LJG6EMLJ.js → chunk-6CXNWKWD.js} +1 -1
- package/dist/{chunk-BKADJZ7F.js → chunk-6SPD266O.js} +3 -3
- package/dist/{chunk-Q2CMEQV7.js → chunk-7IFYI7XF.js} +3 -3
- package/dist/{chunk-AU346ENF.js → chunk-7UPMYRPS.js} +4 -4
- package/dist/{chunk-NSPRS7WK.js → chunk-7V5GUEDU.js} +1 -1
- package/dist/{chunk-EUVE2EAE.js → chunk-AKZRGFGT.js} +3 -3
- package/dist/{chunk-2N67C6IV.js → chunk-D3EDZPJR.js} +5 -5
- package/dist/{chunk-BJQ25STI.js → chunk-DKFUXYGD.js} +4 -4
- package/dist/{chunk-HE4FBF4T.js → chunk-EHOFFTVX.js} +2 -2
- package/dist/{chunk-HXA5GIHK.js → chunk-EJL6CWSP.js} +6 -6
- package/dist/chunk-ER6KQCVJ.js +18 -0
- package/dist/{chunk-DISTLL2Q.js → chunk-EXZACZ2P.js} +1 -1
- package/dist/{chunk-E6CO3MJE.js → chunk-F2D5BSXS.js} +38 -2
- package/dist/{chunk-UIUGTW5C.js → chunk-FT7MUQGO.js} +2 -2
- package/dist/{chunk-PWH3BEZF.js → chunk-FZUFUVLK.js} +13 -13
- package/dist/{chunk-XL2XWWU7.js → chunk-GO2DUPQS.js} +3 -3
- package/dist/{chunk-B2JURP6A.js → chunk-HLZMAJJD.js} +2 -2
- package/dist/{chunk-2EGCI2VW.js → chunk-IUPY54BZ.js} +10 -10
- package/dist/{chunk-GFYEMCPP.js → chunk-JEVKOZBB.js} +1 -1
- package/dist/{chunk-DY7Y7UT4.js → chunk-KUE4V2SK.js} +2 -2
- package/dist/{chunk-SVDOA4W2.js → chunk-L6MDOEHN.js} +7 -7
- package/dist/{chunk-HJYBWJAS.js → chunk-LYGI3T66.js} +2 -2
- package/dist/{chunk-EWVYQLD3.js → chunk-MPC2UAW2.js} +1 -1
- package/dist/{chunk-5DNCFOXA.js → chunk-NIY2UBT6.js} +1 -1
- package/dist/{chunk-FR7HWWCF.js → chunk-OGMXVPCS.js} +1 -1
- package/dist/{chunk-6Z4SZHV2.js → chunk-QKNFU5GA.js} +5 -5
- package/dist/{chunk-2WMZPJVE.js → chunk-S5CNYXUA.js} +4 -4
- package/dist/{chunk-7HHNGZS5.js → chunk-SQQD5MST.js} +4 -4
- package/dist/{chunk-QPWFVE3X.js → chunk-UK4RGH46.js} +2 -2
- package/dist/{chunk-JHTJPQSL.js → chunk-V673DFOL.js} +4 -4
- package/dist/{chunk-RQJMN5TJ.js → chunk-VWQCYOBS.js} +2 -2
- package/dist/{chunk-W2AEL4BJ.js → chunk-X4JEBMYN.js} +1 -1
- package/dist/{chunk-T5REPMNL.js → chunk-XCRET2GZ.js} +2 -2
- package/dist/{chunk-XHHTMFCX.js → chunk-YGZLRX5U.js} +1 -1
- package/dist/{chunk-SDU2IE22.js → chunk-YSM4EEUK.js} +12 -12
- package/dist/{chunk-VT4TTJPG.js → chunk-YXMGHGHR.js} +5 -5
- package/dist/{chunk-AVTI3FL5.js → chunk-ZF24JN7R.js} +1 -1
- package/dist/{claude-launcher-NTYHJUJM.js → claude-launcher-YRPW4SWA.js} +5 -5
- package/dist/{cli-K6XGL4EZ.js → cli-BC7JWQUQ.js} +149 -103
- package/dist/{command-7FAIIQGF.js → command-SQ4466SL.js} +19 -19
- package/dist/{config-QXMUWJAQ.js → config-MVA7YTFW.js} +5 -5
- package/dist/{connect-cmd-P5GASVAF.js → connect-cmd-KWXV2CGC.js} +8 -8
- package/dist/{container-start-CVP3NTDG.js → container-start-AQJ64LCF.js} +20 -20
- package/dist/{context-cleanup-BX5Y3AFL.js → context-cleanup-A2FGVVPH.js} +3 -3
- package/dist/{context-show-cmd-3EPJ7CJC.js → context-show-cmd-6F4UF4ND.js} +9 -9
- package/dist/{control-plane-launch-A3YCHQNQ.js → control-plane-launch-7SOH6IMA.js} +6 -6
- package/dist/{dist-ACTRIBVW.js → dist-IG2DKUJJ.js} +2 -2
- package/dist/{docker-ANW76BNW.js → docker-YKYQJHL2.js} +3 -3
- package/dist/{doctor-GJAHI3KU.js → doctor-4YZOP343.js} +20 -20
- package/dist/{emit-artifact-cmd-FDPJKGOO.js → emit-artifact-cmd-6QTTWBVN.js} +13 -13
- package/dist/{emit-cmd-OX6LYS6I.js → emit-cmd-3FG7FEGO.js} +6 -6
- package/dist/{emit-criteria-cmd-3SVGCMJN.js → emit-criteria-cmd-22VVCL2W.js} +10 -10
- package/dist/{emit-demo-cmd-6DF2QIAS.js → emit-demo-cmd-IQRS7BP2.js} +243 -51
- package/dist/{emit-finding-cmd-TLJWBGQE.js → emit-finding-cmd-TXDPJFVT.js} +10 -10
- package/dist/{emit-result-cmd-CJNVDV5V.js → emit-result-cmd-QHJDUC7J.js} +12 -12
- package/dist/{emit-step-cmd-347CZO7Y.js → emit-step-cmd-TQCOLXAA.js} +10 -10
- package/dist/{findings-cmd-LHTYEHYB.js → findings-cmd-EURKADAY.js} +10 -10
- package/dist/{fleet-readiness-KGR5AC2J.js → fleet-readiness-GXDSFTLP.js} +8 -8
- package/dist/{governing-source-JZQZUTMA.js → governing-source-IRRP7G5G.js} +13 -13
- package/dist/{hook-fast-6WDMNZG5.js → hook-fast-3IYSWDL4.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{init-ZQ5B3TJZ.js → init-63X6ECKF.js} +19 -19
- package/dist/{init-XDOKWBL2.js → init-T64C4MID.js} +19 -19
- package/dist/{init-cmd-YMGW2WXJ.js → init-cmd-7CGCDLBZ.js} +18 -18
- package/dist/{init-update-X7YLZL6H.js → init-update-LFKSC6Y4.js} +17 -17
- package/dist/{install-QPA7DQHO.js → install-47Z7VNWP.js} +6 -6
- package/dist/{layout-JGF7MJHG.js → layout-44S4OYLK.js} +4 -4
- package/dist/{login-browser-3RIZQJY2.js → login-browser-BJZYKWHF.js} +2 -2
- package/dist/{loop-UJL2UCUM.js → loop-YJF27PUS.js} +28 -28
- package/dist/{machine-2YPAFFCH.js → machine-VBITNWTR.js} +19 -19
- package/dist/{machine-skills-AGERT2HN.js → machine-skills-GOKW3U5N.js} +4 -4
- package/dist/{runner-env-HOUXKWIJ.js → runner-env-ILP6BAC2.js} +7 -7
- package/dist/{runner-identity-KPUVTRLS.js → runner-identity-MF3AYBR6.js} +3 -3
- package/dist/{runner-key-FUJD66R6.js → runner-key-5SGBLTDJ.js} +19 -19
- package/dist/{session-sender-7B3ZDWMC.js → session-sender-2DYAJIMM.js} +13 -13
- package/dist/{spec-cmd-Q26NE22H.js → spec-cmd-E6SINBX6.js} +11 -11
- package/dist/{starter-pack-UBEC5LTE.js → starter-pack-FWTXE6XB.js} +5 -5
- package/dist/{status-2KNTILGQ.js → status-AYHBGIAP.js} +24 -24
- package/dist/{sync-cmd-SSUYTMLJ.js → sync-cmd-NGX2PGIW.js} +12 -12
- package/dist/{watch-L5NCO46E.js → watch-ZIWZVU3Z.js} +12 -12
- package/dist/{work-claim-cmd-NJNOQFQE.js → work-claim-cmd-CIVWB7EJ.js} +15 -15
- package/dist/{work-cmd-W7WEHMQJ.js → work-cmd-OO6K6ELP.js} +10 -10
- package/dist/{work-comment-cmd-SSUOGVDD.js → work-comment-cmd-SY7NY2CR.js} +10 -10
- package/dist/work-demo-cmd-Y7EU335L.js +62 -0
- package/dist/{work-logs-cmd-WPMWMDX6.js → work-logs-cmd-PWOFLTGD.js} +10 -10
- package/dist/{work-open-pr-cmd-NO5YLOXD.js → work-open-pr-cmd-AJUTKYWN.js} +11 -11
- package/dist/{work-pr-cmd-37B5KECR.js → work-pr-cmd-DH43AH3V.js} +10 -10
- package/dist/work-proofs-cmd-6W6DDAP6.js +111 -0
- package/dist/{work-stop-cmd-3BJLXUJ3.js → work-stop-cmd-WZHBEKE4.js} +10 -10
- package/package.json +1 -1
- package/skill-pack/maintained/outerlayer-amend/SKILL.md +36 -34
- package/skill-pack/maintained/outerlayer-amend/scripts/rerecord-demo.mjs +187 -0
- package/skill-pack/maintained/outerlayer-build/SKILL.md +7 -2
- package/skill-pack/maintained/outerlayer-build/references/agent-briefs.md +16 -4
- package/skill-pack/maintained/outerlayer-build/references/evidence.md +11 -1
- package/skill-pack/maintained/outerlayer-build/references/release.md +3 -2
- 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
|
-
|
|
257
|
-
|
|
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.
|
|
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
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
|
34
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
105
|
-
|
|
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`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|