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

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 +9 -3
  2. package/dist/{agent-setup-55ZDEXCS.js → agent-setup-APFM4MZ4.js} +10 -10
  3. package/dist/{artifact-link-cmd-6FF2Y6QI.js → artifact-link-cmd-IHHMU4EA.js} +2 -2
  4. package/dist/{build-disk-JHTMPRAA.js → build-disk-55M4ZMDX.js} +4 -4
  5. package/dist/build-info.json +1 -1
  6. package/dist/{builder-Y4P7QARW.js → builder-DQZCPB7J.js} +3 -3
  7. package/dist/{check-MRYFTLOO.js → check-AEMFDR55.js} +28 -28
  8. package/dist/{chunk-EUVE2EAE.js → chunk-2WZ66FXA.js} +3 -3
  9. package/dist/{chunk-BJQ25STI.js → chunk-35DSTT37.js} +4 -4
  10. package/dist/{chunk-Q2CMEQV7.js → chunk-4NKHOCTF.js} +3 -3
  11. package/dist/{chunk-VT4TTJPG.js → chunk-5FSVA3CO.js} +5 -5
  12. package/dist/{chunk-SDU2IE22.js → chunk-67KCGRKA.js} +12 -12
  13. package/dist/{chunk-HXA5GIHK.js → chunk-6ANIBEQ6.js} +6 -6
  14. package/dist/{chunk-AVTI3FL5.js → chunk-6ZLI652Y.js} +1 -1
  15. package/dist/{chunk-HE4FBF4T.js → chunk-7HZH5D7H.js} +2 -2
  16. package/dist/{chunk-6Z4SZHV2.js → chunk-A5FFANL7.js} +5 -5
  17. package/dist/{chunk-PWH3BEZF.js → chunk-AXIK6F52.js} +13 -13
  18. package/dist/{chunk-7HHNGZS5.js → chunk-B6WW6AKO.js} +4 -4
  19. package/dist/{chunk-ULBNTSAD.js → chunk-C66Q2CLW.js} +2 -2
  20. package/dist/{chunk-UIUGTW5C.js → chunk-C7WGCI77.js} +2 -2
  21. package/dist/{chunk-B2JURP6A.js → chunk-CCOU4NXE.js} +2 -2
  22. package/dist/{chunk-SVDOA4W2.js → chunk-DHUHQPRQ.js} +7 -7
  23. package/dist/{chunk-EWVYQLD3.js → chunk-E4F7YE7P.js} +1 -1
  24. package/dist/chunk-ER6KQCVJ.js +18 -0
  25. package/dist/{chunk-2EGCI2VW.js → chunk-GFTWZSOW.js} +10 -10
  26. package/dist/{chunk-AU346ENF.js → chunk-HUBRTT2C.js} +4 -4
  27. package/dist/{chunk-QPWFVE3X.js → chunk-JB4R5IFJ.js} +2 -2
  28. package/dist/{chunk-W2AEL4BJ.js → chunk-LHKWWEQZ.js} +1 -1
  29. package/dist/{chunk-G63KT5I5.js → chunk-M24H6JQW.js} +1 -1
  30. package/dist/{chunk-XL2XWWU7.js → chunk-M3RYZRMH.js} +3 -3
  31. package/dist/{chunk-RQJMN5TJ.js → chunk-ONUACB5C.js} +2 -2
  32. package/dist/{chunk-JHTJPQSL.js → chunk-PILHDNHX.js} +4 -4
  33. package/dist/{chunk-HJYBWJAS.js → chunk-PJI5D2XT.js} +2 -2
  34. package/dist/{chunk-LJG6EMLJ.js → chunk-PNIFQX73.js} +1 -1
  35. package/dist/{chunk-FR7HWWCF.js → chunk-QKEOAQ2W.js} +1 -1
  36. package/dist/{chunk-5DNCFOXA.js → chunk-QQXNS4G2.js} +1 -1
  37. package/dist/{chunk-XHHTMFCX.js → chunk-RLAKWXDK.js} +1 -1
  38. package/dist/{chunk-2N67C6IV.js → chunk-T4S2DVOA.js} +5 -5
  39. package/dist/{chunk-GFYEMCPP.js → chunk-T4TT76GG.js} +1 -1
  40. package/dist/{chunk-BKADJZ7F.js → chunk-UAJYWZ2F.js} +3 -3
  41. package/dist/{chunk-DISTLL2Q.js → chunk-UHFU3CBN.js} +1 -1
  42. package/dist/{chunk-E6CO3MJE.js → chunk-V7Q5OKN7.js} +12 -1
  43. package/dist/{chunk-2WMZPJVE.js → chunk-VJGQEASF.js} +4 -4
  44. package/dist/{chunk-NSPRS7WK.js → chunk-X37GC7DM.js} +1 -1
  45. package/dist/{chunk-3RIFVLPO.js → chunk-X62C7V4B.js} +12 -12
  46. package/dist/{chunk-DY7Y7UT4.js → chunk-XMPE5TT4.js} +2 -2
  47. package/dist/{chunk-T5REPMNL.js → chunk-Z4GLZTDF.js} +2 -2
  48. package/dist/{claude-launcher-NTYHJUJM.js → claude-launcher-B55NWRH5.js} +5 -5
  49. package/dist/{cli-K6XGL4EZ.js → cli-5YT5EL2R.js} +146 -103
  50. package/dist/{command-7FAIIQGF.js → command-DYLRDCGC.js} +19 -19
  51. package/dist/{config-QXMUWJAQ.js → config-RRY7DIJG.js} +5 -5
  52. package/dist/{connect-cmd-P5GASVAF.js → connect-cmd-7C6W2SEP.js} +8 -8
  53. package/dist/{container-start-CVP3NTDG.js → container-start-F7PBMTAL.js} +20 -20
  54. package/dist/{context-cleanup-BX5Y3AFL.js → context-cleanup-EKTPKJJT.js} +3 -3
  55. package/dist/{context-show-cmd-3EPJ7CJC.js → context-show-cmd-I5JC7EFH.js} +9 -9
  56. package/dist/{control-plane-launch-A3YCHQNQ.js → control-plane-launch-NOKM6CBH.js} +6 -6
  57. package/dist/{dist-ACTRIBVW.js → dist-LLYL5UCV.js} +2 -2
  58. package/dist/{docker-ANW76BNW.js → docker-W7WWFZ3Q.js} +3 -3
  59. package/dist/{doctor-GJAHI3KU.js → doctor-LYOFFCQS.js} +20 -20
  60. package/dist/{emit-artifact-cmd-FDPJKGOO.js → emit-artifact-cmd-RO34UG7A.js} +13 -13
  61. package/dist/{emit-cmd-OX6LYS6I.js → emit-cmd-GY7Z26DY.js} +6 -6
  62. package/dist/{emit-criteria-cmd-3SVGCMJN.js → emit-criteria-cmd-Y36QCPAM.js} +10 -10
  63. package/dist/{emit-demo-cmd-6DF2QIAS.js → emit-demo-cmd-YMWVF5U4.js} +104 -34
  64. package/dist/{emit-finding-cmd-TLJWBGQE.js → emit-finding-cmd-MH3ZWUNV.js} +10 -10
  65. package/dist/{emit-result-cmd-CJNVDV5V.js → emit-result-cmd-ZARZZZ3Y.js} +12 -12
  66. package/dist/{emit-step-cmd-347CZO7Y.js → emit-step-cmd-A6HQJDBY.js} +10 -10
  67. package/dist/{findings-cmd-LHTYEHYB.js → findings-cmd-OLLBWJXJ.js} +10 -10
  68. package/dist/{fleet-readiness-KGR5AC2J.js → fleet-readiness-VB5QSFSY.js} +8 -8
  69. package/dist/{governing-source-JZQZUTMA.js → governing-source-ZX7VXTOA.js} +13 -13
  70. package/dist/{hook-fast-6WDMNZG5.js → hook-fast-4RYMBFPO.js} +1 -1
  71. package/dist/index.js +4 -4
  72. package/dist/{init-ZQ5B3TJZ.js → init-BQCGL7UD.js} +19 -19
  73. package/dist/{init-XDOKWBL2.js → init-NCCMB2UE.js} +19 -19
  74. package/dist/{init-cmd-YMGW2WXJ.js → init-cmd-OW2L5HTI.js} +18 -18
  75. package/dist/{init-update-X7YLZL6H.js → init-update-7LUMOECE.js} +17 -17
  76. package/dist/{install-QPA7DQHO.js → install-5BQ3TFBT.js} +6 -6
  77. package/dist/{layout-JGF7MJHG.js → layout-RGVPCX7Y.js} +4 -4
  78. package/dist/{login-browser-3RIZQJY2.js → login-browser-BC2Z5QKG.js} +2 -2
  79. package/dist/{loop-UJL2UCUM.js → loop-BU634Y6A.js} +28 -28
  80. package/dist/{machine-2YPAFFCH.js → machine-RI4KHRYP.js} +19 -19
  81. package/dist/{machine-skills-AGERT2HN.js → machine-skills-CS5HAQHF.js} +4 -4
  82. package/dist/{runner-env-HOUXKWIJ.js → runner-env-QSLLOFNF.js} +7 -7
  83. package/dist/{runner-identity-KPUVTRLS.js → runner-identity-DQAXW4DL.js} +3 -3
  84. package/dist/{runner-key-FUJD66R6.js → runner-key-UWL475RU.js} +19 -19
  85. package/dist/{session-sender-7B3ZDWMC.js → session-sender-OGDUJ2KK.js} +13 -13
  86. package/dist/{spec-cmd-Q26NE22H.js → spec-cmd-WB4UOFPG.js} +11 -11
  87. package/dist/{starter-pack-UBEC5LTE.js → starter-pack-XHATFTO3.js} +5 -5
  88. package/dist/{status-2KNTILGQ.js → status-GFZFND6L.js} +24 -24
  89. package/dist/{sync-cmd-SSUYTMLJ.js → sync-cmd-GPZD2C45.js} +12 -12
  90. package/dist/{watch-L5NCO46E.js → watch-5I3GURXY.js} +12 -12
  91. package/dist/{work-claim-cmd-NJNOQFQE.js → work-claim-cmd-J52UVCAF.js} +15 -15
  92. package/dist/{work-cmd-W7WEHMQJ.js → work-cmd-PQZ5RYDB.js} +10 -10
  93. package/dist/{work-comment-cmd-SSUOGVDD.js → work-comment-cmd-WI6B3IOS.js} +10 -10
  94. package/dist/work-demo-cmd-3NDGSQKJ.js +62 -0
  95. package/dist/{work-logs-cmd-WPMWMDX6.js → work-logs-cmd-QCYZ5X5A.js} +10 -10
  96. package/dist/{work-open-pr-cmd-NO5YLOXD.js → work-open-pr-cmd-BRDF3ZKL.js} +11 -11
  97. package/dist/{work-pr-cmd-37B5KECR.js → work-pr-cmd-WY2TBAKW.js} +10 -10
  98. package/dist/work-proofs-cmd-R25HT6WP.js +111 -0
  99. package/dist/{work-stop-cmd-3BJLXUJ3.js → work-stop-cmd-C52JPZ4E.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 +11 -2
  105. package/skill-pack/maintained/outerlayer-build/references/evidence.md +8 -0
  106. package/skill-pack/maintained/outerlayer-build/references/release.md +3 -2
  107. package/skill-pack/maintained/outerlayer-evidence/SKILL.md +6 -4
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { WorkCommandError, resolveCredentials, getJson } from './chunk-5FSVA3CO.js';
4
+ import './chunk-IP7BIBXJ.js';
5
+ import './chunk-CCOU4NXE.js';
6
+ import './chunk-VJGQEASF.js';
7
+ import { sanitizeTerminal } from './chunk-E4F7YE7P.js';
8
+ import { resolveRepoIdentity } from './chunk-QQXNS4G2.js';
9
+ import './chunk-PJI5D2XT.js';
10
+ import './chunk-X37GC7DM.js';
11
+ import './chunk-G2JWZDFA.js';
12
+ import { C } from './chunk-ANPNT6PM.js';
13
+ import { defaultFetch } from './chunk-6RKD7YZ6.js';
14
+ import './chunk-PJDQPF5N.js';
15
+ import './chunk-SOI6K5DY.js';
16
+ import './chunk-PA6CK6WQ.js';
17
+ import './chunk-BALIE3BA.js';
18
+ import './chunk-BJ3KTMDH.js';
19
+ import './chunk-M24H6JQW.js';
20
+ import './chunk-5BHIBJDA.js';
21
+ import './chunk-CDDED64E.js';
22
+ import './chunk-RA3O54FC.js';
23
+ import { parseWorkItemEnv } from './chunk-T2Q7ISPE.js';
24
+ import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
25
+ import './chunk-CRB3POP4.js';
26
+ import './chunk-PNIFQX73.js';
27
+ import './chunk-V7Q5OKN7.js';
28
+ import './chunk-VNVZDWO3.js';
29
+ import './chunk-HACGUL52.js';
30
+ import './chunk-MESOOLF4.js';
31
+ import './chunk-Z5GEFMRW.js';
32
+
33
+ createRequire(import.meta.url);
34
+
35
+ // src/work-proofs-cmd.ts
36
+ function classifyProofs(data, checkoutHead) {
37
+ const heads = /* @__PURE__ */ new Set();
38
+ if (checkoutHead !== void 0 && checkoutHead !== "") heads.add(checkoutHead.toLowerCase());
39
+ data.pullRequests?.forEach((pr) => {
40
+ if ((pr.state === "open" || pr.state === "merged") && typeof pr.headSha === "string" && pr.headSha !== "") {
41
+ heads.add(pr.headSha.toLowerCase());
42
+ }
43
+ });
44
+ const entries = [];
45
+ for (const criterion of data.criteria?.items ?? []) {
46
+ if (criterion.proof === null || criterion.proof === "test") continue;
47
+ const newest = criterion.newestProof;
48
+ if (newest === null || newest === void 0) {
49
+ entries.push({ id: criterion.id, proof: criterion.proof, state: "missing", commit: null });
50
+ } else if (newest.commitSha !== null && heads.has(newest.commitSha.toLowerCase())) {
51
+ entries.push({ id: criterion.id, proof: criterion.proof, state: "current", commit: newest.commitSha });
52
+ } else {
53
+ entries.push({ id: criterion.id, proof: criterion.proof, state: "out-of-date", commit: newest.commitSha });
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+ var STATE_LABEL = { current: "current", "out-of-date": "out of date", missing: "missing" };
59
+ function render(entries, json) {
60
+ if (json) return JSON.stringify(entries);
61
+ if (entries.length === 0) return "No criterion declares a proof other than test.";
62
+ return entries.map((e) => `${sanitizeTerminal(e.id)} ${sanitizeTerminal(e.proof)} ${STATE_LABEL[e.state]} ${sanitizeTerminal(e.commit ?? "no commit")}`).join("\n");
63
+ }
64
+ async function runWorkProofs(opts) {
65
+ const env = opts.env ?? process.env;
66
+ let item;
67
+ if (opts.item !== void 0) {
68
+ item = parseTrackerNumber(opts.item);
69
+ if (item === void 0) throw new WorkCommandError(trackerNumberError("--item", opts.item));
70
+ } else if (env.OUTERLAYER_WORK !== void 0) {
71
+ const parsed = parseWorkItemEnv(env.OUTERLAYER_WORK);
72
+ if (!("number" in parsed)) throw new WorkCommandError(parsed.reason);
73
+ item = parsed.number;
74
+ }
75
+ if (item === void 0) {
76
+ throw new WorkCommandError("no work item. Pass --item <number>, or set OUTERLAYER_WORK to the item's number.");
77
+ }
78
+ const creds = resolveCredentials(opts);
79
+ const { data } = await getJson(creds.url, `/v1/work-items/${item}`, creds.apiKey, creds.appId, opts.fetchImpl ?? defaultFetch);
80
+ const body = data;
81
+ const items = body.criteria?.items ?? [];
82
+ if (items.some((c) => c.proof !== null && c.proof !== "test" && !("newestProof" in c))) {
83
+ throw new WorkCommandError(
84
+ "the gateway does not report each criterion's newest proof. Update the gateway, then run this again."
85
+ );
86
+ }
87
+ const checkoutHead = opts.checkoutHead ?? resolveRepoIdentity(opts.cwd ?? process.cwd()).commitSha;
88
+ if (!checkoutHead) {
89
+ throw new WorkCommandError("cannot read the checkout's HEAD. Run this inside the git checkout, or pass the head explicitly.");
90
+ }
91
+ const entries = classifyProofs(body, checkoutHead);
92
+ const output = render(entries, opts.json === true);
93
+ if (!opts.quiet) process.stdout.write(output + "\n");
94
+ return { entries, output, exitCode: entries.every((e) => e.state === "current") ? 0 : 1 };
95
+ }
96
+ async function runWorkProofsCommand(opts) {
97
+ try {
98
+ const result = await runWorkProofs(opts);
99
+ process.exitCode = result.exitCode;
100
+ } catch (err) {
101
+ if (err instanceof WorkCommandError) {
102
+ process.stderr.write(`${C.red}\u2717${C.reset} ${err.message}
103
+ `);
104
+ process.exitCode = 2;
105
+ return;
106
+ }
107
+ throw err;
108
+ }
109
+ }
110
+
111
+ export { classifyProofs, runWorkProofs, runWorkProofsCommand };
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { resolveCredentials, postJson, WorkCommandError } from './chunk-VT4TTJPG.js';
3
+ import { resolveCredentials, postJson, WorkCommandError } from './chunk-5FSVA3CO.js';
4
4
  import './chunk-IP7BIBXJ.js';
5
- import './chunk-B2JURP6A.js';
6
- import './chunk-2WMZPJVE.js';
7
- import './chunk-EWVYQLD3.js';
8
- import './chunk-5DNCFOXA.js';
9
- import './chunk-HJYBWJAS.js';
10
- import './chunk-NSPRS7WK.js';
5
+ import './chunk-CCOU4NXE.js';
6
+ import './chunk-VJGQEASF.js';
7
+ import './chunk-E4F7YE7P.js';
8
+ import './chunk-QQXNS4G2.js';
9
+ import './chunk-PJI5D2XT.js';
10
+ import './chunk-X37GC7DM.js';
11
11
  import './chunk-G2JWZDFA.js';
12
12
  import './chunk-ANPNT6PM.js';
13
13
  import { defaultFetch } from './chunk-6RKD7YZ6.js';
@@ -16,14 +16,14 @@ import './chunk-SOI6K5DY.js';
16
16
  import './chunk-PA6CK6WQ.js';
17
17
  import './chunk-BALIE3BA.js';
18
18
  import './chunk-BJ3KTMDH.js';
19
- import './chunk-G63KT5I5.js';
19
+ import './chunk-M24H6JQW.js';
20
20
  import './chunk-5BHIBJDA.js';
21
21
  import './chunk-CDDED64E.js';
22
22
  import './chunk-RA3O54FC.js';
23
23
  import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
24
24
  import './chunk-CRB3POP4.js';
25
- import './chunk-LJG6EMLJ.js';
26
- import './chunk-E6CO3MJE.js';
25
+ import './chunk-PNIFQX73.js';
26
+ import './chunk-V7Q5OKN7.js';
27
27
  import './chunk-VNVZDWO3.js';
28
28
  import './chunk-HACGUL52.js';
29
29
  import './chunk-MESOOLF4.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outerlayer/cli",
3
- "version": "0.33.0-next.20261023003700",
3
+ "version": "0.33.0-next.20261023030517",
4
4
  "description": "OuterLayer CLI — commands: init, claude, connect, doctor, login, logout, sync, daemon (alias: watch), emit, findings, work, spec, runner, hooks, mcp, context. Capture and sync coding-agent sessions, and record proof of factory work.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Magu Studios, Inc.",
@@ -6,7 +6,7 @@ description: >
6
6
  reply on the thread with a replacement artifact or a sentence, and push. Also resolve a
7
7
  merge conflict with the base branch. Use when a work item has threads waiting on an
8
8
  agent or a conflicting pull request, or when asked to "answer the review" or "fix what
9
- the reviewer failed". Invoke with /outerlayer-amend.
9
+ the reviewer failed".
10
10
  ---
11
11
 
12
12
  Run the CLI as `outerlayer`. If that command is not on PATH, run `npx -y @outerlayer/cli` with the same arguments. Every `outerlayer ...` command below follows this rule. Write the full command each time. Do not put it in a shell variable: zsh runs `$VAR args` as one command name, not as a command with arguments.
@@ -22,7 +22,7 @@ or fails a thread. A session can only reply.
22
22
  A session never records a pass or fail on an artifact.
23
23
 
24
24
  `OUTERLAYER_WORK` is the work item's number. Check out the pull request's
25
- branch first if the working tree is not already on it.
25
+ branch first, unless the working tree is on it.
26
26
 
27
27
  ## Arguments
28
28
 
@@ -31,17 +31,16 @@ branch first if the working tree is not already on it.
31
31
 
32
32
  ## Builds and local sessions
33
33
 
34
- A session a runner started is a build: `OUTERLAYER_JOB_DIR` is set there and
35
- nowhere else. A build's GitHub token cannot comment on the issue or the pull
36
- request, and its pushes go through the runner. A local session uses the
37
- person's own git remote and `gh` login.
34
+ A runner build sets `OUTERLAYER_JOB_DIR`; no other session does. A build's
35
+ GitHub token cannot comment on the issue or the pull request, and its pushes
36
+ go through the runner. A local session uses the person's own git remote and
37
+ `gh` login.
38
38
 
39
39
  ## Several repositories
40
40
 
41
- `OUTERLAYER_REPOSITORIES` lists a build's repositories, each with its `path`,
42
- `branch` and whether it is `primary`. Fix each thread in the checkout of the
43
- repository its pull request is in. Commit and push in each checkout you
44
- change, to its own branch only.
41
+ `OUTERLAYER_REPOSITORIES` lists a build's repositories, each with `path`,
42
+ `branch` and `primary`. Fix each thread in the checkout of its pull request's
43
+ repository. Commit and push in each checkout you change, to its own branch only.
45
44
 
46
45
  ## Steps
47
46
 
@@ -59,7 +58,7 @@ change, to its own branch only.
59
58
  **Hand off a conflict you cannot resolve.** If the resolution needs a
60
59
  product decision, or the gate stays red after one fix, abort the merge.
61
60
  Push nothing. Post on the general thread with `--ready`. Name the
62
- conflicting files, the base commits behind them and the decision needed.
61
+ conflicting files, their base commits and the decision needed.
63
62
  Tell the person to fail the general thread with their answer.
64
63
  2. **Read the threads.**
65
64
  `outerlayer work threads --item "$OUTERLAYER_WORK" --json` prints
@@ -69,26 +68,24 @@ change, to its own branch only.
69
68
  `body`, `action` and `artifact`. Keep the threads whose `waitingOn` is
70
69
  `agent`.
71
70
  If none is waiting and step 1 resolved no conflict, say so and stop. Take
72
- the pull request (`repository`, `prNumber`) from the step 1 status JSON's
73
- `pullRequests`.
71
+ the pull request (`repository`, `prNumber`) from step 1's `pullRequests`.
74
72
  3. **Re-orient.** The status JSON's `issue.body` is OuterLayer's copy of the
75
73
  issue. It is the spec: find each thread's criterion there. Read the pull
76
- request, with its evidence comment, through the git host. Read every person's comment since the last agent
77
- comment on the thread. Read them literally.
74
+ request and its evidence comment through the git host. Read every person's
75
+ comment since the last agent comment, literally.
78
76
  Then read `checks` in the status JSON. A failing check lists its check
79
- items under `items`, one per thing it judged, each with a `result` and a
80
- `reason`. When a person asks you to fix failing checks or criteria, fix
81
- each failing check item by its reason.
77
+ items under `items`, each with a `result` and a `reason`. When a person asks you to fix failing
78
+ checks or criteria, fix each failing check item by its reason.
82
79
  4. **Decide each thread.** Reply on every criterion thread waiting on an
83
80
  agent, with `--criterion`. Read each note and pick one:
84
- - **Answer it.** A question gets an answer. Reply with the answer and
85
- change nothing. Say which file or test you looked at.
81
+ - **Answer it.** A question gets an answer. Change nothing. Say which
82
+ file or test you looked at.
86
83
  - **Fix it.** A requested change gets a fix. Change the code, tests, docs or
87
- evidence so the criterion is met as written, and attach new proof when the
88
- criterion declares one.
84
+ evidence so the criterion is met as written, and attach new proof when it
85
+ declares one.
89
86
  - **The spec is wrong.** The criterion cannot be met as written, for
90
- example because it names a path a repository rule forbids. Never rewrite
91
- a criterion: hand it off (step 7) with the wording change to make.
87
+ example one that names a path a repository rule forbids. Never rewrite it:
88
+ hand it off (step 7) with the wording change.
92
89
  - **Cannot tell.** Hand an ambiguous comment off with `--ready`: quote it
93
90
  and ask one question.
94
91
  5. **Fix, prove, reply.** An answer needs no commit: reply on its criterion
@@ -107,13 +104,12 @@ change, to its own branch only.
107
104
  Note the id it prints. Run `outerlayer sync`, so the attach can find the
108
105
  artifact. Then reply, which supersedes the old proof:
109
106
  `outerlayer work comment --item "$OUTERLAYER_WORK" --criterion <criterion> --body "<what changed>" --artifact <uuid> --attach`.
110
- A replacement whose bytes match the current proof is flagged `unchanged`,
111
- so the thread still waits on you.
107
+ A replacement with the current proof's bytes is flagged `unchanged`, and
108
+ the thread still waits on you.
112
109
  - **A check the factory host records**, such as `acceptance-criteria`, is
113
110
  judged again after this session ends. Name each failing check item and
114
111
  your fix. Never claim the check passes.
115
- - **Otherwise** (the general thread, or a fix that needs no new proof),
116
- reply with a sentence naming what changed and the commit:
112
+ - **Otherwise**, reply with a sentence naming what changed and the commit:
117
113
  `outerlayer work comment --item "$OUTERLAYER_WORK" [--criterion <criterion>] --body "<what changed>" --ready`.
118
114
  Omit `--criterion` on the general thread.
119
115
  6. **Watch CI.** Skip this step when you pushed nothing. Run
@@ -128,14 +124,20 @@ change, to its own branch only.
128
124
  sentence of fact, then the decision a person must make. For a spec problem,
129
125
  quote the criterion and give the replacement wording. `--ready` hands the
130
126
  thread back to a person.
131
- 8. **Check nothing is left waiting.** Read the threads again. A thread you
132
- started with that still waits on an agent has a reply that did not land or
133
- was flagged `unchanged`. Fix that, or report it.
134
- 9. **Report.** Write one line per thread: the criterion (or the general
127
+ 8. **Check nothing is left waiting.** Reread threads. A thread still waiting
128
+ on an agent has a reply that did not land or was flagged `unchanged`. Fix
129
+ or report it. Then run `outerlayer sync` and `outerlayer work proofs`.
130
+ 9. **Record the demo again.** Skip this step when you pushed nothing. With
131
+ the app running, run in each pushed checkout
132
+ `node .outerlayer/skills/outerlayer-amend/scripts/rerecord-demo.mjs --item "$OUTERLAYER_WORK" --pr <prNumber> --repository <owner/name>`.
133
+ It prints the demo line for the report.
134
+ 10. **Report.** Write one line per thread: the criterion (or the general
135
135
  thread), and fixed (with the replacement) or handed off (with the reason).
136
136
  Add a line for a merge conflict (merged with the base sha, or handed off
137
- with the reason), and a CI line when you pushed: green, or each red check
138
- and why. Post the lines once on the general thread:
137
+ with the reason), a CI line when you pushed (green, or each red check and
138
+ why), the demo line from step 9, and a line naming each proof missing or
139
+ out of date.
140
+ Post the lines once on the general thread:
139
141
  `outerlayer work comment --item "$OUTERLAYER_WORK" --body "<the lines>"`.
140
142
  A plain comment from an agent leaves the general thread waiting on a person.
141
143
  A local session also posts them on the pull request. End with the same lines.
@@ -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,7 +253,14 @@ 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
256
+ Write the demo, when a criterion needs one, as a Playwright test that is part
257
+ 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
+ For each other capture, put the capture in a script under
257
264
  `build-reports/evidence/` and run the script. A later fix can invalidate what
258
265
  you captured, and regenerating it must be one command. The scripts go when the
259
266
  run's report directory goes. The emitted artifacts outlive the run.
@@ -266,7 +273,9 @@ criteria's order. Run it, then run `outerlayer emit demo <manifest> --item
266
273
  emit artifact ... --for <criterion id> --pr <number>`. Always pass `--pr`.
267
274
  When the tree invalidates a video, record and emit the whole demo again: the
268
275
  newer demo replaces the older one. Regenerate any other invalidated evidence
269
- with `--replaces`. Then run `outerlayer sync`.
276
+ with `--replaces`. Then run `outerlayer sync`, and run `outerlayer work
277
+ proofs --item "$OUTERLAYER_WORK"`. For each proof it lists as out of date or
278
+ missing, capture and emit that criterion again, and run the command once more.
270
279
 
271
280
  Write `build-reports/evidence.md` naming every criterion with its artifact, or
272
281
  with the reason none could be produced. A criterion you could not evidence is
@@ -19,6 +19,14 @@ id. One that declares `proof: video` needs a video: its chapter of the
19
19
  item's one demo. A screenshot never satisfies `proof: video`. A criterion with no proof form is proven by its
20
20
  test, and needs no artifact.
21
21
 
22
+ After the evidence agent returns, and after every later commit, run
23
+ `outerlayer sync`, then `outerlayer work proofs --item "$OUTERLAYER_WORK"`. It
24
+ lists each declared proof as current, out of date or missing at the checkout's
25
+ head. For each criterion it lists as out of date or missing, run the evidence
26
+ step again for that criterion only, then run the command once more before the
27
+ run ends. A criterion still listed goes in `build-reports/evidence.md` with the
28
+ reason. The command leaves out criteria that declare a test proof.
29
+
22
30
  When a commit lands on the branch after the evidence agent captured, decide for
23
31
  each capture from that commit's diff. A capture is stale when the diff touches
24
32
  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,7 +16,7 @@ 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
19
+ Capture AFTER the state exists: run the app, the test, or the
20
20
  flow first, and shoot the working result. A screenshot of code is not
21
21
  evidence; a screenshot of the rendered page is. If the state takes setup
22
22
  (seed data, a logged-in user), finish the setup, verify by eye, then capture.
@@ -24,14 +24,14 @@ evidence; a screenshot of the rendered page is. If the state takes setup
24
24
  ## Per-kind mechanics
25
25
 
26
26
  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:
27
+ artifact shares one upload cap of 8 MiB:
28
28
 
29
29
  - **screenshot** (`.png`, `.jpg`) — one focused window or region showing the
30
30
  proven state. Crop noise; keep enough chrome (URL bar, test summary line)
31
31
  to show it is real.
32
32
  - **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.
33
+ Record it as a demo chapter (see "Record a demo"). Video most likely
34
+ hits the 8 MiB cap: trim dead time.
35
35
  - **report** (`.html`, `.pdf`) — generated reports: coverage, benchmark,
36
36
  audit output. Emit the file the tool produced, unedited.
37
37
  - **log** (`.txt`, `.log`) — command output proving a run happened: test
@@ -99,6 +99,8 @@ for each criterion:
99
99
  });
100
100
  });
101
101
 
102
+ Commit the test, so an amend can run it again.
103
+
102
104
  Install it with `npm install --save-dev @outerlayer/playwright`. Use the
103
105
  recorded criterion id as the first argument, or `null` for an opening scene.
104
106
  Keep each narration line short enough to say in the chapter's time. Put the