@outerlayer/cli 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +238 -77
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-WQP2CZDW.js +94 -0
  7. package/dist/chunk-2E54Z3P5.js +232 -0
  8. package/dist/chunk-3TT7RQYB.js +55 -0
  9. package/dist/chunk-3YPKXE5L.js +1683 -0
  10. package/dist/{chunk-HNCCD2IX.js → chunk-5DLBAXY5.js} +209 -61
  11. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  12. package/dist/chunk-5Y3QQRUH.js +300 -0
  13. package/dist/chunk-6Y63SXFO.js +383 -0
  14. package/dist/chunk-774HEQL6.js +178 -0
  15. package/dist/chunk-7A6RVXPZ.js +2233 -0
  16. package/dist/chunk-7YIOOFVJ.js +71 -0
  17. package/dist/chunk-ABIWDUMS.js +104 -0
  18. package/dist/chunk-AUFG23AA.js +534 -0
  19. package/dist/chunk-B2G7JHHB.js +42 -0
  20. package/dist/chunk-BCJNHMZT.js +34 -0
  21. package/dist/chunk-BCLJSQCV.js +56 -0
  22. package/dist/chunk-BFESLKPP.js +61 -0
  23. package/dist/chunk-BJ3KTMDH.js +63 -0
  24. package/dist/chunk-BKO6JEZI.js +47 -0
  25. package/dist/chunk-BOLTI6LR.js +37 -0
  26. package/dist/chunk-CBPPJSAR.js +89 -0
  27. package/dist/chunk-CBZB6XY6.js +102 -0
  28. package/dist/{chunk-RCQXYLMO.js → chunk-DVDEBNQJ.js} +18 -18
  29. package/dist/{work-pr-cmd-GOYSPEN2.js → chunk-DVYHCMB2.js} +10 -25
  30. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  31. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  32. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  33. package/dist/chunk-F6GFZXZ3.js +60 -0
  34. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  35. package/dist/chunk-FAJSS2WN.js +1357 -0
  36. package/dist/chunk-FS2VYGZT.js +1764 -0
  37. package/dist/chunk-FV2CCGXK.js +9 -0
  38. package/dist/{chunk-BQB3U2VD.js → chunk-G7FSV7JX.js} +5113 -3729
  39. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  40. package/dist/chunk-HE2D4EY5.js +83 -0
  41. package/dist/chunk-L2DBRKRA.js +94 -0
  42. package/dist/chunk-LF6MLJCY.js +38 -0
  43. package/dist/chunk-M5POMKKW.js +1051 -0
  44. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  45. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  46. package/dist/chunk-N6LURUEF.js +64 -0
  47. package/dist/chunk-NYC54VBD.js +1084 -0
  48. package/dist/chunk-OGFGZQA3.js +23 -0
  49. package/dist/chunk-QOZKPLVJ.js +226 -0
  50. package/dist/chunk-RA3O54FC.js +30 -0
  51. package/dist/chunk-TBT347UY.js +41 -0
  52. package/dist/{chunk-3VQ4XXQA.js → chunk-UFFXNXLQ.js} +115 -71
  53. package/dist/chunk-USO2DKBD.js +421 -0
  54. package/dist/chunk-W4SQCOZU.js +206 -0
  55. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  56. package/dist/chunk-WO2BXCTQ.js +83 -0
  57. package/dist/chunk-WZBHBH3J.js +20 -0
  58. package/dist/{chunk-IWIUYLDR.js → chunk-XCCLFVXM.js} +118 -189
  59. package/dist/{context-materialize-J6CRQ7O7.js → chunk-XDDW4FRS.js} +134 -26
  60. package/dist/chunk-YYYXRJUW.js +101 -0
  61. package/dist/chunk-ZM2IMMYN.js +76 -0
  62. package/dist/chunk-ZMYPLFG3.js +71 -0
  63. package/dist/chunk-ZNA27WEV.js +470 -0
  64. package/dist/{cli-EFJIG2VT.js → cli-NHUXABYH.js} +597 -965
  65. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  66. package/dist/config-XVJYZ4IQ.js +9 -0
  67. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  68. package/dist/context-adopt-UMA4O5NY.js +105 -0
  69. package/dist/context-materialize-G2FUFC72.js +19 -0
  70. package/dist/dist-RW4UPPC2.js +6 -0
  71. package/dist/docker-NEGDLU6D.js +7 -0
  72. package/dist/doctor-53F2PYY7.js +43 -0
  73. package/dist/doctor-UFVL4PZY.js +426 -0
  74. package/dist/{emit-artifact-cmd-KPPD3YLO.js → emit-artifact-cmd-KTYW2TN2.js} +25 -19
  75. package/dist/emit-cmd-DCERAU27.js +9 -0
  76. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  77. package/dist/{emit-finding-cmd-VR5VEGXJ.js → emit-finding-cmd-UR4ID45Z.js} +35 -30
  78. package/dist/{emit-result-cmd-YOA5BJKL.js → emit-result-cmd-FDTUKGKE.js} +35 -61
  79. package/dist/exec-client-4XGXV3XN.js +8 -0
  80. package/dist/guest-init-GENRHNP7.js +8 -0
  81. package/dist/hook-fast-EYENPK6F.js +9 -0
  82. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  83. package/dist/host-key-KDYZ5ECC.js +7 -0
  84. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  85. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  86. package/dist/index.js +4 -4
  87. package/dist/init-DXBCOAJK.js +36 -0
  88. package/dist/init-YEXK35CF.js +221 -0
  89. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  90. package/dist/install-cmd-7WXOIFDO.js +55 -0
  91. package/dist/lima-XN65D7GN.js +55 -0
  92. package/dist/login-browser-7XGSV7MA.js +10 -0
  93. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  94. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  95. package/dist/loop-VZOLL4WQ.js +43 -0
  96. package/dist/machine-WSG52J75.js +35 -0
  97. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  98. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  99. package/dist/paths-OYKMVYJP.js +6 -0
  100. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  101. package/dist/real-deps-SU24ZA2K.js +21 -0
  102. package/dist/relay-L76HDX72.js +46 -0
  103. package/dist/settings-J2652U5N.js +7 -0
  104. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  105. package/dist/{status-JHVSZYC5.js → status-2UZKKNB7.js} +31 -12
  106. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  107. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  108. package/dist/version-BWM6VLDI.js +6 -0
  109. package/dist/{watch-NRLXJUST.js → watch-5C4ZBBGO.js} +20 -7
  110. package/dist/{work-claim-cmd-HXB7K27H.js → work-claim-cmd-LSO2B2EX.js} +34 -19
  111. package/dist/work-cmd-UOQO2AD4.js +17 -0
  112. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  113. package/dist/{work-launch-YNNCKIH3.js → work-launch-YI4CJDAP.js} +2 -1
  114. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  115. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  116. package/package.json +13 -3
  117. package/skill-pack/maintained/amend/SKILL.md +104 -0
  118. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  119. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  120. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  121. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  122. package/skill-pack/template/build/SKILL.md +193 -0
  123. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  124. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  125. package/skill-pack/template/build/references/evidence.md +42 -0
  126. package/skill-pack/template/build/references/release.md +74 -0
  127. package/skill-pack/template/build/references/review-briefs.md +275 -0
  128. package/skill-pack/template/build/references/review-loop.md +158 -0
  129. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  130. package/skill-pack/template/spec/SKILL.md +84 -0
  131. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-KKEU6FFR.js +0 -589
  134. package/dist/chunk-OZ7C3XUE.js +0 -34
  135. package/dist/chunk-WQ6VGRGZ.js +0 -150
  136. package/dist/emit-commit-credit-cmd-656BRAQ4.js +0 -121
  137. package/dist/hook-fast-I2XTHDE6.js +0 -8
  138. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  139. package/dist/init-PTBITAUO.js +0 -103
  140. package/dist/login-cmd-IRX6LZT7.js +0 -62
  141. package/dist/loop-BWE2BS6J.js +0 -646
  142. package/dist/sync-cmd-S7Q6DWU3.js +0 -15
  143. package/dist/work-cmd-ABP6XDBP.js +0 -16
package/README.md CHANGED
@@ -13,8 +13,9 @@ network, and when, is below.
13
13
  Launch a session naming the work it's for, then sync:
14
14
 
15
15
  ```
16
- npx @outerlayer/cli init # install the capture hooks
17
- npx @outerlayer/cli work add --issue 42 # create the work item; prints its number
16
+ npx @outerlayer/cli login # sign in, once per machine
17
+ npx @outerlayer/cli init # connect this repository to a factory, install the capture hooks
18
+ npx @outerlayer/cli work build --issue 42 --local # create the work item; prints its number
18
19
  OUTERLAYER_WORK=7 claude # launch a session naming that number
19
20
  npx @outerlayer/cli sync --dry-run # see exactly what would leave your machine
20
21
  npx @outerlayer/cli sync # upload sessions launched that way
@@ -23,7 +24,7 @@ npx @outerlayer/cli sync # upload sessions launched tha
23
24
  ## Privacy, stated plainly
24
25
 
25
26
  **A session uploads only when you launch it with `OUTERLAYER_WORK` naming
26
- the work item it's for.** Create the item first with `outerlayer work add`,
27
+ the work item it's for.** Create the item first with `outerlayer work build --local`,
27
28
  which prints its factory-scoped number, then set that number before
28
29
  starting your agent — `OUTERLAYER_WORK=7 claude`. The session-start hook
29
30
  then records the launch and the session uploads whole, from its first turn,
@@ -39,7 +40,7 @@ and never upload.
39
40
  sessions.** It runs when you run it. The hooks also fire `outerlayer sync
40
41
  --quiet` in the background after each agent turn and when a session ends, at
41
42
  most once every five minutes, as soon as `outerlayer login` has saved
42
- credentials. So a launched session uploads while it is still running, not
43
+ credentials and `outerlayer connect` has chosen the repository's factory. So a launched session uploads while it is still running, not
43
44
  only once it is over. Set `"autoSync": false` in `~/.outerlayer/config.json`
44
45
  to leave every automatic upload — the background sync and the daemon's
45
46
  streaming below — to your own command.
@@ -58,41 +59,202 @@ along before that sync ever runs.
58
59
  These commands also talk to your workspace. None of them sends session
59
60
  content:
60
61
 
61
- - **`outerlayer work add`, `remove`, `status`, `list`, `link-session`, `pr`,
62
- `claim`, `renew`, `release`** read and write your Floor. The session-start
62
+ - **`outerlayer work build`, `remove`, `status`, `list`, `link-session`, `pr`,
63
+ `claim`, `renew`, `release`, `comment`, `threads`** read and write your factory's work items. The session-start
63
64
  hook spawns `outerlayer work link-session` when you launch a session with
64
65
  `OUTERLAYER_WORK`, naming the item and the session id — the item itself
65
- must already exist, created ahead of time with `outerlayer work add`.
66
+ must already exist, created ahead of time with `outerlayer work build --local`.
66
67
  `outerlayer work pr` sends the session id and the pull request number and
67
- repository — never the session's own content. `outerlayer work claim`
68
+ repository — never the session's own content. `outerlayer work open-pr`
69
+ sends the pull request's title and the text of the body file you name. In a
70
+ host build, which holds an item key, it goes to the gateway, which opens or
71
+ edits the pull request as the factory's GitHub App on the branch the claim
72
+ named, and no `gh` command runs. In your own session it runs `gh pr create`
73
+ or `gh pr edit` under your login, then sends the pull request number and
74
+ repository the way `work pr` does. `outerlayer work claim`
68
75
  records a lease for this host before it starts working on an item, so two
69
76
  hosts never build the same piece of work at once; `renew` extends it and
70
- `release` marks it done. A runner key needs `git_connection.read` to list
71
- and read items, `work_item_claim.insert` to claim, and
72
- `work_item_claim.update` to renew or release — the two claim permissions
73
- are not granted to a dashboard role by default, since a claim is a host's
77
+ `release` marks it done. `outerlayer work comment` posts one comment on a
78
+ work item's general thread or a criterion thread — the detected session id
79
+ rides along the same way it does for `emit`, only ever narrowing what the
80
+ comment may do; `outerlayer work threads` reads them back. A runner key
81
+ needs `git.read` and `work.read` to
82
+ list and read items, and `work.claim` to claim, renew or release — `work.claim`
83
+ is not granted to a dashboard role by default, since a claim is a host's
74
84
  lease, not a person's.
75
85
  - **`outerlayer runner start`** lists, claims, renews and releases work
76
86
  items the same way `work claim`/`renew`/`release` do, on the schedule its
77
87
  config sets — it sends nothing about the job it runs beyond that; the
78
88
  agent it starts is a separate process with its own credentials, from the
79
- `runner` block, never the copy-out daemon's. `runner check` makes one
80
- list call to confirm the key works and sends nothing else; `runner init`,
81
- `runner status` and `runner logs` make no network call at all.
89
+ `runner` block, never the copy-out daemon's. It also sends a heartbeat at
90
+ the end of every poll: the host, the CLI version, the poll interval, the
91
+ slots in use and total, and the result of that poll's own request for
92
+ work — success, or the error code and message the platform returned.
93
+ Before a build's command starts, the runner also runs `git ls-remote` and
94
+ `git fetch` in the job's workdir, with the host's own git credentials, to
95
+ put it on the branch the claim named. Nothing about the job leaves the host
96
+ that way.
97
+ When the gateway names a higher CLI version than the one running,
98
+ `outerlayer runner start` also calls the public npm registry
99
+ (`registry.npmjs.org`) for `@outerlayer/cli` only: that version's metadata
100
+ at once, and once no build is in flight, its tarball and its provenance
101
+ attestation, plus an `npm install` and an `npm audit signatures` run
102
+ against that same registry. The calls carry no credentials and nothing
103
+ about the host or the factory. Set `runner.autoUpdate` to `false` to stop
104
+ them.
105
+ `runner check` makes a list call and a host-key lookup to confirm the key
106
+ works and sends nothing else; `runner status` and `runner logs` make no network call at all.
107
+ On a runner host, any command that uses the saved runner key signs each
108
+ request it makes with the host key, the way the runner does. The signature
109
+ covers the request's method, path, query and body, and adds nothing else
110
+ to it.
111
+ - **`outerlayer runner init`** makes no network call, except `runner init --vm`
112
+ on a Mac. That runs `limactl`, and Lima downloads a Linux VM image. Inside
113
+ the VM, `apt` downloads Node and `npm` downloads this CLI. Your runner key goes to the VM on the standard
114
+ input of a `limactl shell` command and is never a file or an argument on the
115
+ Mac.
116
+ - **`outerlayer login`**, with no key on stdin, signs in to your account by
117
+ browser approval. It sends your dashboard a one-time public key and this
118
+ machine's host name, then polls for the answer. The answer is a token for
119
+ your account, encrypted to that public key, so only this process can read
120
+ it. It comes with the gateway address and your email, and no factory
121
+ or organization. The token is saved to `~/.outerlayer/config.json` and
122
+ never printed. If this machine held a token from an earlier login, login
123
+ asks the dashboard to revoke it. A key piped on stdin makes no network
124
+ call.
125
+ - **`outerlayer connect`**, and the first step of `outerlayer init`, send a
126
+ request with your login's token: `GET /v1/me/factories?repository=<host/owner/name>`,
127
+ naming this checkout's remote. The gateway answers with the factories you can
128
+ use, and whether this repository is linked to each. Once a factory is chosen,
129
+ a second request, `GET /v1/apps/<factory id>/git/links`, asks which
130
+ repositories that factory has linked, to say whether this one is among them.
131
+ Nothing else leaves the machine. The choice is saved in the repository's git directory, at
132
+ `outerlayer/connection.json`, which git never tracks. With a factory key it
133
+ makes no network call. `outerlayer init --local` makes none either.
134
+ - **`outerlayer logout`** sends the saved token to your dashboard
135
+ (`POST /api/cli/logout`) so it is revoked, then deletes it from the config
136
+ file. It sends nothing else.
137
+ - **`outerlayer runner image`** runs `git fetch` against the repository's
138
+ remote, `https://github.com/<owner>/<name>.git` unless `--remote` names
139
+ another, with the host's own git credentials. It sends nothing about the
140
+ host. Docker pulls base images, features and Claude Code's npm package
141
+ from registries while the image builds, with the host's own `docker
142
+ login`. The tools layer also downloads the pinned `gh` and `tini` releases
143
+ from GitHub, checking each against a digest. The image build runs outside
144
+ any work item and sends nothing to your factory.
145
+ - **`outerlayer runner start` with `builtin:vm` hooks** runs each build in its
146
+ own Firecracker microVM, which has no network device. The guest kernel and
147
+ helper are not published yet, so no host is offered `builtin:vm` and
148
+ `outerlayer doctor` fails it. Once they are, the first microVM
149
+ build downloads a pinned Firecracker release, a guest kernel and a guest
150
+ helper from github.com, and checks each against a sha256 pinned in the CLI;
151
+ the requests send nothing about the host. The build reaches the network only
152
+ through the same tunnel a container build uses, which the runner serves on
153
+ Unix sockets and the guest reaches over vsock. The guest's exec agent accepts
154
+ connections from the host only, and the runner runs every step of the build
155
+ through it, including the build's command (`outerlayer vm-exec` on the host).
156
+ - **`outerlayer runner start` with `builtin:container` hooks** runs each
157
+ build in its own Docker container, which has no network but loopback. The
158
+ build reaches the network only through a tunnel the runner serves on a
159
+ Unix socket, which the container's own `outerlayer broker-relay` process
160
+ reaches from loopback. The tunnel refuses loopback, private, link-local,
161
+ shared-address, multicast and unique-local addresses and the host's own,
162
+ and it terminates no TLS. The tunnel also serves the build a local git
163
+ remote. Git in the container still shows
164
+ `https://github.com/<owner>/<name>.git` and reaches the runner instead,
165
+ which forwards only git's fetch and push requests and Git LFS batch calls
166
+ to github.com. For the item's repository the runner adds a GitHub App
167
+ token it asked your gateway for (`POST /v1/work-items/{id}/claim/tokens`):
168
+ a read token for a fetch, and a push token for a push, which it forwards
169
+ only when every ref the push updates is the branch the claim names. Any
170
+ other repository is forwarded with no credential. Before it forwards a
171
+ push to a branch outside `outerlayer/`, the runner runs `git fetch` and
172
+ `git receive-pack` in a bare mirror it keeps under its own directory, with
173
+ the read token, to check that the push moves the branch forward. The
174
+ build's `gh` holds the read token in a directory the runner owns and
175
+ nothing else. When the attempt ends, the runner revokes every token it was
176
+ issued with `DELETE https://api.github.com/installation/token`, sent with
177
+ the token it revokes. The release for the attempt then lists the hosts the
178
+ build reached, each address the tunnel refused with its range, the names,
179
+ never the values, of the host variables the runner's config passed in, the
180
+ names of the destinations it called, which kinds of token the build used,
181
+ and whether its commits were authored by the GitHub App's bot account and
182
+ credited the member who asked for the build. The runner sets that author in
183
+ the build's environment and adds the `Co-authored-by` line with git hooks it
184
+ writes into the build's own directory.
185
+ - **`outerlayer runner start` with `builtin:container` hooks** also asks your
186
+ gateway which repository governs the item's repository (`GET
187
+ /v1/context/source`, signed with the host key), after the recipe's
188
+ lifecycle commands. When one does, the runner runs `git fetch` against that
189
+ control plane on the host, with the host's own git credentials, and copies
190
+ its context into the checkout through the build's directory. The build
191
+ receives files and no credential for the control plane. The runner deletes
192
+ the staged copy before the command starts. A host that cannot read the
193
+ control plane fails the build at provision.
194
+ - **`outerlayer runner start` with `builtin:process` hooks** runs each build
195
+ as a process under the runner's own user, with no isolation, and the
196
+ attempt records `shared-user`. The runner clones the item's repository
197
+ from `https://github.com/<owner>/<name>.git` with the host's own git
198
+ credentials. It serves the same tunnel on a loopback port, which the
199
+ build reaches through its proxy settings, and the release lists the same
200
+ hosts and refusals.
201
+ - **`outerlayer runner start` with your own executable hooks** runs
202
+ `git ls-remote` against `https://github.com/<owner>/<name>.git`, with the
203
+ host's own git credentials, before each provision hook, to tell the hook
204
+ the commit the default branch points at. It sends nothing about the host,
205
+ and a host whose git cannot reach the remote loses only that hint.
206
+ - **`outerlayer runner start` with `build.destinations`** serves each
207
+ destination the config lists on a local address, and forwards each request
208
+ a build sends there to the destination's upstream, over HTTPS, with the
209
+ header the config names in place of any credential the build sent. The
210
+ header's secret is read from the runner's own environment.
211
+ It is never in a build's environment or files, and the release names a
212
+ destination and nothing more.
213
+ - **`outerlayer runner start` with `builtin:vm`, `builtin:container` or
214
+ `builtin:process` hooks** also serves two built-in destinations, `gateway` and `claude`, to
215
+ each build. The gateway destination forwards a build's requests to your
216
+ gateway with the attempt's item key, and the `claude` destination forwards
217
+ them to `https://api.anthropic.com` with the host's `CLAUDE_CODE_OAUTH_TOKEN`
218
+ or `ANTHROPIC_API_KEY`, read from the runner's own environment. A build holds a placeholder for each and never the value, and the
219
+ release names the two destinations and nothing more. A build under
220
+ `builtin:process` runs as the runner's user and can still read the runner's
221
+ environment and the attempt's job file.
222
+ - **`outerlayer doctor`**, once a factory key is saved (`echo "$KEY" | outerlayer login`),
223
+ asks your gateway (`GET /v1/repositories/access`, with that key) which
224
+ GitHub App permissions each connected repository's installation has not
225
+ accepted, and whether each default branch requires a pull request. The
226
+ request carries nothing about the host, and the gateway stores nothing.
227
+ - **`outerlayer runner check`** also asks your gateway (`GET /v1/runner/host-key`,
228
+ with the runner key, signed with the host key) whether the key holds
229
+ `work.claim`, so a key that cannot claim is named before the host takes work.
230
+ The request carries nothing about the host beyond the signature the key's
231
+ own requests already carry, and the gateway stores nothing.
232
+ - **`outerlayer doctor`** on a runner host, one with a `runner` block in its
233
+ config, also asks the npm registry for the latest Claude Code version, to
234
+ say whether the version the runner's images pin is behind. The request
235
+ names the package and nothing about the host. On a host with
236
+ `builtin:container` hooks it also asks your gateway which repository governs
237
+ each repository in `repos.include` (`GET /v1/context/source`), then runs
238
+ `git ls-remote` against that control plane with the host's own git
239
+ credentials, to say whether the host can read it.
82
240
  - **`outerlayer emit artifact`** uploads the file you name — a screenshot, a
83
241
  recording, a report, a log — along with its caption. With no recorded
84
242
  session to attach it to, it uploads immediately, anchored to a pull request
85
243
  or to the git checkout.
86
- - **`outerlayer emit <name>`** and **`outerlayer emit commit-credit`** send
87
- one check's outcome, and one commit's attribution, for a work item. Run
88
- from inside a session, they send only when that session carries a launch
89
- record — the session's own content may not leave by a second route. Run
90
- from CI or a plain shell, where there is no session, they send as they
91
- always have.
244
+ - **`outerlayer emit <name>`** sends one check's outcome for a work item.
245
+ Run from inside a session, it sends only when that session carries a
246
+ launch record — the session's own content may not leave by a second
247
+ route. Run from CI or a plain shell, where there is no session, it sends
248
+ as it always has.
92
249
  - **`outerlayer emit finding`** and **`outerlayer emit findings <file>`** send
93
- one finding, or a whole batch of them, for a work item — the same
94
- anchoring as `emit <name>` (`--item`, or the recorded session's own item),
95
- except a session may record findings on the item it was launched for.
250
+ one finding, or a whole batch of them, for a work item — a problem an agent
251
+ hit in the factory. The anchoring is the same as `emit <name>` (`--item`, or
252
+ the recorded session's own item).
253
+ - **`outerlayer emit criteria <file>`** sends the acceptance criteria of a work
254
+ item as one list (`POST /v1/criteria`): each criterion's id, text and
255
+ declared proof kind, from the JSON file you name. The anchoring is the same
256
+ as `emit finding`. Nothing but the file's content and the item number is
257
+ sent.
96
258
  - **`outerlayer mcp serve`** is the stdio MCP server your editor spawns. It
97
259
  forwards every JSON-RPC message the editor sends to the gateway and returns
98
260
  the reply.
@@ -124,9 +286,9 @@ space, past both stubs, so the same test records every program each run
124
286
  starts and asserts none of them fetches over the network.
125
287
 
126
288
  Failures the hook cannot show you — a refused `OUTERLAYER_WORK` value, a
127
- `work add` that could not reach the Floor — are appended to
289
+ `work link-session` that could not reach the factory — are appended to
128
290
  `~/.outerlayer/spool/hook-errors.log`, and the next session start says so.
129
- The addition retries a gateway it cannot reach a few times, with backoff,
291
+ The link retries a gateway it cannot reach a few times, with backoff,
130
292
  before it gives up.
131
293
 
132
294
  - **The tier is applied before anything leaves.** The default tier is
@@ -144,45 +306,47 @@ before it gives up.
144
306
  | Command | What it does |
145
307
  |---|---|
146
308
  | `outerlayer sync` | Upload sessions launched with `OUTERLAYER_WORK` to your OuterLayer cloud workspace (incremental — only what's new since the last sync). Tier-gated client-side (`--tier metrics\|redacted\|full`, default `full`); `--dry-run` prints exactly what would leave the machine; `--all` re-sends everything (idempotent server-side). Credentials come from `outerlayer login`, `OUTERLAYER_*` env vars, or `--url/--app-id`. |
147
- | `outerlayer login [--url] [--app-id]` | Save the cloud URL, app id, and API key to `~/.outerlayer/config.json` once. The key is read from stdin (`echo "$KEY" \| outerlayer login …`) or a prompt with echo off; it is never a flag. `--no-input` refuses to prompt. |
148
- | `outerlayer init` | Install the capture hooks and the status-line segment. `--json` for scripts. Run through `npx`, it copies the CLI to `~/.outerlayer/cli` and points the hooks at that copy, so they survive npm clearing its cache; `npx @outerlayer/cli@latest init` upgrades it. From any other install, the hooks run that install. Claude Code deletes transcripts after ~30 days; run `outerlayer daemon` separately to mirror them first — `init` does not start it for you. |
309
+ | `outerlayer login [--dashboard] [--no-browser] [--check <id>]` | Sign in to your account by browser approval: prints an approve link and a code, a signed-in person approves it, and a token for their account is saved to `~/.outerlayer/config.json` without ever being shown. It chooses no factory; `outerlayer connect` does. Without a terminal it prints one JSON document and exits 75; `--check <id>` finishes it (0 signed in, 75 still waiting, 1 denied or expired). A factory key piped on stdin (`echo "$KEY" \| outerlayer login --url … --app-id …`) is saved as before; it is never a flag, and it always wins over the login. `--no-input` refuses to wait on a person. |
310
+ | `outerlayer logout` | Revoke this machine's login on the dashboard and delete it from the config file. A saved factory key and factory are left alone. If the dashboard cannot be reached it still deletes the local copy, exits 1, and says the token stays valid until revoked on `/profile/security`. |
311
+ | `outerlayer connect [--factory <org>/<factory>] [--json]` | Choose the factory this repository's work goes to, and say what it chose and why. With a login it lists the factories you can use and takes the first rule that leaves one answer: `--factory`, the factory this repository is already connected to, the one factory it is linked to, the one factory you can use; on a terminal it then asks, and with no terminal it exits 1 listing each `--factory` value. The choice is saved in the repository's git directory, shared by every worktree and never committed. A factory key already belongs to one factory, so `connect` changes nothing for it. It never links a repository. Once a factory is chosen it asks that factory whether this repository is linked. If not, it still saves the choice, prints the factory's Work page link to stderr and exits 2. `--json` writes one JSON document to stdout. |
312
+ | `outerlayer init` | Connect this repository (`outerlayer connect`), install the capture hooks and the status-line segment, and run `doctor`. It never signs in: without a login it skips the connect step and says to run `outerlayer login`. `--local` installs the hooks only, with no network call. `--json` prints one document naming each step's outcome. Run through `npx`, it copies the CLI to `~/.outerlayer/cli` and points the hooks at that copy, so they survive npm clearing its cache; `npx @outerlayer/cli@latest init --local` upgrades it. From any other install, the hooks run that install. Claude Code deletes transcripts after ~30 days; run `outerlayer daemon` separately to mirror them first — `init` does not start it for you. |
149
313
  | `outerlayer daemon` | Run the copy-out daemon in the foreground (`--once` for a single sweep, which uploads nothing). Once cloud credentials exist, it also streams a launched session's new turns as they land — see Privacy, above. `outerlayer watch` is the former name and still works, with a warning. |
150
- | `outerlayer doctor` | Check the installation: hooks, status-line freshness, and sync health. `--json` prints the checks and a summary for scripts. |
314
+ | `outerlayer doctor` | Check the installation: hooks, status-line freshness, sync health and, with a factory key, the GitHub App access builds need. `--json` prints the checks and a summary for scripts. |
151
315
  | `outerlayer context emit [--check]` | Compile `.outerlayer/` into each configured target tool's native files (targets come from `.outerlayer/config.json`). `--check` computes outputs and diffs against disk without writing (CI mode). Bare `outerlayer emit` with no name still compiles, with a deprecation warning. |
152
316
  | `outerlayer import ruler` | Port a `.ruler/` tree ([Ruler](https://github.com/intellectronica/ruler)) into the equivalent `.outerlayer/` tree — mostly a rename; never overwrites an existing `.outerlayer/`. |
153
317
  | `outerlayer hooks wrap` / `outerlayer hooks unwrap` | Auto-wrap (or undo wrapping) `PreToolUse`/`PostToolUse` hooks for execution evidence — one spawn per firing. |
154
318
  | `outerlayer emit artifact <file> --caption <text> [--for <criterion-id>] [--pr <n>]` | Upload a proof artifact — screenshot, recording, report, or log — with its caption. Inside a recorded session it spools locally and ships on the next `sync`; otherwise it uploads immediately, anchored to a pull request or the current checkout. `--replaces` retires artifacts an earlier run uploaded. |
155
- | `outerlayer emit <name> --result <pass\|fail> [--link <url>] [--body <text>\|--body-file <path>] --item <number>` | Record one named check's outcome on a work item. A check that ran carries the run URL as its proof (`--link`); a judgment you are making yourself carries one sentence (`--body`, or `--body-file` to read it from a file — `-` reads standard input). A `fail` needs at least one of the two; a `pass` needs neither. `--item` names the work item by the number printed when the item was created — always required, in or out of a recorded session; from inside a session it can only name an item that session was NOT launched for (a session cannot record a check on its own item). Prints the recorded check's id. Who recorded it comes from your API key, never from what you send. |
156
- | `outerlayer emit artifact-review --result <pass\|fail> --artifact <id> [--body <text>\|--body-file <path>]` | Record a person's own pass or fail on one artifact — evidence already emitted, bound to a criterion. `--artifact` names it and replaces `--item`; the gateway resolves the work item from the artifact's own pull request. `--link` is refused. A `fail` reads its sentence from `--body`/`--body-file`, or from standard input when neither is given. Refused from inside a recorded session — an artifact verdict is a person's act, the same rule the gateway enforces. Prints the recorded verdict's id. |
157
- | `outerlayer emit commit-credit --pr <n> …` | Send one commit's attribution for a pull request. |
158
- | `outerlayer emit finding --id <id> --subject <change\|context> --title <text> --file <path> --kind <behavior\|hygiene\|proof> --verdict <confirmed\|refuted\|unverified> --source <implementer\|reviewer\|refuter\|gate> --where <label> [--item <number>] …` | Record one finding — what was found wrong about the change (`--subject change`) or about a rule it ran on (`--subject context`, which then needs `--rule-path`, `--rule-quote` and `--rule-relation` together — all three are required, not just `--rule-path`). Validated against the same contract the gateway checks before anything is sent. `--item` names the work item; without it, inside a recorded session, the item that session was launched for is used automatically — unlike `emit <name>`, a session may record findings on its own item. Re-emitting the same `--id` on the item replaces that finding. |
159
- | `outerlayer emit findings <file> [--item <number>]` | Record a whole batch at once, read from a `FindingBatch` JSON file (`-` for standard input) — the same shape and validation as `emit finding`, one record per subject/title/file/kind/verdict/source/where. Anchored the same way: `--item`, else a recorded session's own item. |
319
+ | `outerlayer emit <name> --result <pass\|fail> [--link <url>] [--body <text>\|--body-file <path>] --item <number>` | Record one named check's outcome on a work item. A check that ran carries the run URL as its proof (`--link`); a judgment you are making yourself carries one sentence (`--body`, or `--body-file` to read it from a file — `-` reads standard input). A `fail` needs at least one of the two; a `pass` needs neither. `--item` names the work item by the number printed when the item was created — always required, in or out of a recorded session; a session records checks on its own item like any other, stored as the session's, but cannot clear a person's fail. Prints the recorded check's id. Who recorded it comes from your API key, never from what you send. |
320
+ | `outerlayer work comment --item <n> [--criterion <id>] --body <text> [--artifact <id>] [--pass\|--fail\|--attach\|--ready]` | Post one comment on a work item's general thread, or on one acceptance criterion's own thread with `--criterion`. `--pass`/`--fail` need a person's own credential and apply to the whole item, so they work on the general thread only and the gateway refuses them with `--criterion`; `--attach` makes the named artifact that criterion's current proof; `--ready` hands a thread back to a person. The detected session id rides along automatically, only ever narrowing what the comment may do. |
321
+ | `outerlayer work threads --item <n>` | Read a work item's threads back — the general thread first, then one per criterion with proof or a comment, each with its `waitingOn`. |
322
+ | `outerlayer emit finding --id <id> --category <context\|flaky-test\|tooling\|environment\|defect\|other> --title <text> --file <path> --where <label> [--line <n>] [--item <number>] …` | Record one problem you hit in the factory. `--category` is one of `context` (an instruction that is wrong, broken or missing), `flaky-test`, `tooling`, `environment`, `defect` (a bug outside your own change) or `other`. A `context` finding also needs `--rule-path` and `--rule-relation` (`wrong`, `broken` or `missing`), and `--rule-quote` unless the relation is `missing`. Findings are informational; nothing reads one to pass or fail the work. `--item` names the work item; without it, inside a recorded session, the item that session was launched for is used automatically. Re-emitting the same `--id` on the item replaces that finding. |
323
+ | `outerlayer emit findings <file> [--item <number>]` | Record a whole batch at once, read from a JSON file (`-` for standard input) with `"schemaVersion": 2` — the same shape and validation as `emit finding`, one record per id, category, title, file, line, rule and where. Anchored the same way: `--item`, else a recorded session's own item. |
324
+ | `outerlayer emit criteria <file> [--item <number>]` | Record a work item's acceptance criteria as one list, read from a JSON file (`-` for standard input): `{"criteria": [{"id": "AC-1", "text": "Given …", "proof": null}]}`. An `id` is 1 to 64 characters of `A-Z a-z 0-9 . _ : -`, the same id `emit artifact --for` binds evidence to. `proof` is `video`, `screenshot`, `report`, `log`, `file` or `null` (any bound artifact proves it). The list holds 1 to 200 criteria, each `text` at most 2,000 characters, with no id twice. It is validated before anything is sent. This list is the only source for the item's Criteria tab. Once an item has a list, only a person can replace it: run the command outside an agent session, with your own login. Anchored the same way as `emit findings`: `--item`, else a recorded session's own item. |
160
325
  | `outerlayer mcp install [--transport stdio\|http] [--url] [--name] [--command]` | Write (or update) an `mcpServers` entry in `.mcp.json` for the OuterLayer gateway. Default `stdio`: the client spawns `outerlayer mcp serve`, which reads the API key from `~/.outerlayer/config.json` (or `OUTERLAYER_API_KEY`) each time it connects, so a reconnect picks up a newly saved or rotated key. `--transport http` writes a direct `POST /v1/mcp` entry referencing `${OUTERLAYER_API_KEY}`, resolved by the client from the environment it was launched with. Never writes an API key. Pass `--url`/`--app-id` for self-host. |
161
326
  | `outerlayer mcp serve [--url] [--app-id]` | Stdio MCP server bridging stdin/stdout JSON-RPC to the gateway's `POST /v1/mcp`. What the stdio `.mcp.json` entry runs; exits 1 with a clear message when no API key is configured. |
162
- | `outerlayer work add --issue <n>\|--pr <n> [--repo] [--note]` | Records that a source — a recorded session, a CI run, or the API key's bound member — is working on an issue or pull request. The only way an item becomes visible on the Floor. |
163
- | `outerlayer work remove --issue <n>\|--pr <n> --reason <text> [--repo]` | Withdraws the caller's own addition, recording the reason. Never deletes anything; the item leaves the Floor only once no live addition remains on it. Removing again is a no-op that still succeeds. |
327
+ | `outerlayer work build --issue <n\|KEY>\|--item <n> [--tracker linear\|jira] [--local] [--repo] [--note] [--retry <count>]` | Starts work on an item and puts it on the Work page. `--issue` takes a GitHub issue number or a Linear or Jira key such as `ENG-123`; the gateway verifies a key against the factory's tracker connection before adding it. `--tracker` is needed only when both Linear and Jira are connected. A key gets a repository only from `--repo`, never from the checkout. Without `--local`, records a build request that a host takes when it has a free slot. With `--local` (issue only), no host takes it: it prints the item's number and the line that starts your own session on it, such as `OUTERLAYER_WORK=7 claude`. `--retry` resends after a 5xx or 429 answer, up to that many more times. |
328
+ | `outerlayer work remove --issue <n>\|--pr <n> --reason <text> [--repo]` | Withdraws the caller's own addition, recording the reason. Never deletes anything; the item leaves the Work page only once no live addition remains on it. Removing again is a no-op that still succeeds. |
164
329
  | `outerlayer work status --issue <n>\|--pr <n> [--repo]` | Shows one item's stage, section, gate ledger, linked pull requests and sessions, and its additions. |
165
- | `outerlayer work list [--stage] [--section] [--repo] [--unclaimed] [--startable] [--needs amend]` | Lists live work items for the current factory, filterable by stage, section, claim state and whether an open fail is waiting for an answer. |
330
+ | `outerlayer work list [--stage] [--section] [--repo] [--claimed] [--unclaimed] [--startable] [--needs amend]` | Lists live work items for the current factory, filterable by stage, section, claim state, startability and whether a comment thread is waiting on an agent. |
166
331
  | `outerlayer work pr <n> [--repo] [--session-id]` | Run inside a session on a work item: declares that pull request `<n>` in the checkout's repository belongs to that item. Idempotent — declaring the same pull request twice is a no-op. Refuses when the session is on no item, or the repository is not connected. |
167
- | `outerlayer work claim --item <n>\|--issue <n> --kind implement\|amend [--repo] [--host] [--seconds]` | Records a lease for this host (default: its own hostname), so two hosts never build the same item at once. A live lease already held by a different host is refused; claiming again under the same host extends it. Lease length defaults to 900 seconds, the server's own cap. |
168
- | `outerlayer work renew --item <n>\|--issue <n> [--repo] [--host] [--seconds]` | Extends this host's own live lease. Refused if the lease has expired, or if a different host holds it. |
332
+ | `outerlayer work open-pr --title <text> --body-file <path> [--repo] [--session-id] [--json]` | Opens the pull request for the work item this session is on, or edits the one already open on its branch. A host build, which holds an item key, sends it to the gateway, which opens it as the factory's GitHub App from the branch the claim named; no `gh` command runs. Your own session runs `gh pr create` or `gh pr edit` under your login and then declares the pull request on the item. Run it again after changing the title or description. |
333
+ | `outerlayer work claim --item <n>\|--issue <n> --kind implement\|amend [--repo] [--host] [--seconds]` | Records a lease for this host (default: its own hostname), so two hosts never build the same item at once. The lease belongs to the runner key that claimed it: a live lease held by a different key is refused, even under the same host name, and claiming again with the same key extends it. The first claim creates the host key (`host-key.pem` beside the config file) and binds the runner key to it; from then on every request made with that key is signed. Lease length defaults to 900 seconds, the server's own cap. |
334
+ | `outerlayer work renew --item <n>\|--issue <n> [--repo] [--host] [--seconds]` | Extends this host's own live lease. Refused if the lease has expired, or if a different runner key holds it (403 `claim_not_held`). |
169
335
  | `outerlayer work release --item <n>\|--issue <n> [--repo] [--host] [--outcome]` | Marks this host's lease released, optionally recording how the attempt ended. Releasing an already-released lease is a no-op that returns the recorded release time and outcome. |
170
- | `outerlayer runner init [--config <path>]` | Writes a `runner` block with defaults and three example hooks, keeping every key the config already had. Refuses rather than overwriting an existing block. |
336
+ | `outerlayer runner init [--vm\|--no-vm] [--config <path>]` | Writes a `runner` block with defaults, keeping every key the config already had. It names `builtin:vm` on a Linux machine where the user can use `/dev/kvm` and Docker Engine can build images once the guest kernel and helper are published (not yet), `builtin:container` on that machine until then, `builtin:container` on any other machine with Docker Engine, and `builtin:process` on one without, and writes no hook script. Refuses rather than overwriting an existing block. On macOS it asks whether to create a Linux VM with Lima, installs Docker Engine and the runner in it, and starts both at login; `--vm` answers yes and `--no-vm` answers no, and a run with no terminal answers no. The runner key and the host key then live only in the VM: the Mac's config loses `apiKey` and `runner` and gains a `runnerVm` note, which `outerlayer doctor` reads. |
171
337
  | `outerlayer runner check [--config <path>]` | Validates the config, the hooks and the key, prints the settings the runner would use, and exits non-zero on a problem. Takes no work and writes no pid file. |
172
338
  | `outerlayer runner start [--config <path>]` | Runs the loop: claim work, run it, sync, clean up, report, release with an outcome. Repeat. Reads the `runner` block from the config file (default `~/.outerlayer/config.json`). Refuses to start on a bad config, an unrunnable hook, or a refused key. |
339
+ | `outerlayer runner image --repository <owner/name> [--remote <url>] [--config <path>] [--json]` | Builds the image a repository's builds run in. Fetches the head of the default branch into the runner's cache, builds its `.devcontainer/devcontainer.json` with the devcontainer CLI, ignores every setting that reaches the host or widens the container's privileges, and adds this CLI and a pinned Claude Code. Reuses the image until the recipe or the tools change, keeps the newest three, and writes `recipe.json` beside the image record. Refuses a Compose recipe (`recipe_needs_docker`) and a repository with only named configurations (`recipe_ambiguous`). |
173
340
  | `outerlayer runner stop [--config <path>] [--now]` | Takes no more work and waits for running jobs to finish, naming each every five seconds. `--now` ends them immediately with outcome `stopped`. Ctrl-C on a foreground runner drains; a second one stops now. |
174
341
  | `outerlayer runner status [--config <path>] [--recent] [--json]` | A header naming the runner's own state, then a row per running job — item, queue, stage, elapsed, started, lease, job directory. `--recent` adds finished jobs, newest first, capped at 20, with their outcomes. |
175
342
  | `outerlayer runner logs <item> [--config <path>] [-f]` | Prints the newest attempt's log for one item, following it with `-f`. |
176
343
 
177
- The `work` commands need an API key that carries `Ingest traces`
178
- (`trace.write`) to add, remove, and declare a pull request (`pr`), and `Read
179
- the Work page` (`git_connection.read`) to look an item up — which `remove` and
180
- `status` both do before they act. Tick both when you mint the key, in
181
- Settings → API keys. Claiming a lease needs `Claim work items`
182
- (`work_item_claim.insert`); renewing or releasing one needs `Renew or
183
- release work item claims` (`work_item_claim.update`). Withdrawing an
184
- addition somebody else made additionally needs `Withdraw others' work from
185
- the Work page` (`git_connection.update`).
344
+ The `work` commands need an API key that carries `work.insert` to add,
345
+ remove, and declare a pull request (`pr`, `open-pr`), and `work.read` to look an item up
346
+ — which `remove` and `status` both do before they act. Tick both when you
347
+ mint the key, in Settings → API keys. Claiming, renewing or releasing a
348
+ lease needs `work.claim`. Withdrawing an addition somebody else made
349
+ additionally needs `work.update`.
186
350
 
187
351
  ### Recording your own pass or fail on a check
188
352
 
@@ -209,10 +373,9 @@ Record a pass once the work is right:
209
373
  outerlayer emit code-review --result pass --item 412
210
374
  ```
211
375
 
212
- `--item` is always required, in or out of a recorded session. A session
213
- records a check only on an item it was NOT launched for, by naming it with
214
- `--item`; a check on the item the session itself was launched for needs a
215
- machine key (an API key run outside the recorded session) or CI.
376
+ `--item` is always required, in or out of a recorded session. Inside a
377
+ recorded session, the check is stored as the session's, on the item it was
378
+ launched for or any other. A session cannot record over a person's fail.
216
379
 
217
380
  Three things to know. Who recorded a check is decided by the API key you
218
381
  used, never by anything in the request. A key bound to a membership records
@@ -222,30 +385,28 @@ because a fail with neither is something nobody can act on. And a check only
222
385
  holds a gate once a validator in `.outerlayer/validators/` declares its emit
223
386
  name; without one the check is recorded and read back, and blocks nothing.
224
387
 
225
- A review of the work itself is different: it is recorded from the Review
226
- tab on the work item page, not the CLI, and holds every pull request of the
227
- item until a person records a pass — no validator declaration needed.
228
- `outerlayer emit` refuses `work-review` outright; recording it anywhere but
229
- the work item page is not supported. The gateway refuses a review, and an
230
- artifact verdict, on an item that is closed or shipped, has no evaluation
231
- yet, or whose evaluation is still waiting on a session link, with the code
232
- `item_not_reviewable`. A recorded pass carries the head commit of every
233
- pull request the item had at the time; once one of them moves, that pass
234
- stops counting and a fresh review is needed. A repository can require a
235
- current pass before the evidence check completes by setting the base
236
- branch's `review` policy key to `required` (default `optional`, alongside
237
- `merge_gate`) — the check then stays in progress, not failed, until a
238
- current pass exists.
239
-
240
- `outerlayer emit artifact-review` sits between the two: a person's own pass
241
- or fail, like a review, but on one piece of evidence rather than the whole
242
- item. A fail here holds the item the same way a review's fail does, and
243
- counts the work as bad on Quality once, however many artifacts carry one —
244
- it also lists the item on a queue the API exposes, naming the artifact and
245
- why it failed, so a host with a session linked to the item can answer it
246
- with a replacement, or the fail's own recorder can pass over it directly.
247
- The item's own pass stays available only once every required artifact has
248
- a pass and no fail is still open.
388
+ A review of the work itself is different: a person records it as a `pass`
389
+ or `fail` on a work item's general thread (the item page, or `outerlayer
390
+ work comment` on the general thread), never as a named check — no validator declaration needed. The gateway refuses a
391
+ pass or fail on an item that is closed or shipped, has no evaluation yet,
392
+ or whose evaluation is still waiting on a session link, with the code
393
+ `not_reviewable`. A recorded pass on the general thread carries the head
394
+ commit of every pull request the item had at the time; once one of them
395
+ moves, that pass stops counting and a fresh review is needed. A repository
396
+ can require a current pass before the evidence check completes by setting
397
+ the base branch's `review` policy key to `required` (default `optional`,
398
+ alongside `merge_gate`) — the check then stays in progress, not failed,
399
+ until a current pass exists.
400
+
401
+ A criterion thread is scoped to one acceptance criterion rather than the
402
+ whole item: an agent attaches proof with `outerlayer work comment
403
+ --criterion <id> --artifact <id> --attach`, and a person leaves a note on
404
+ it with a plain comment. A pass or fail is a verdict on the whole item, so
405
+ the gateway refuses one on a criterion thread. A fail on any thread counts
406
+ the work as bad on Quality once, however many threads carry one — a fail on
407
+ the general thread also lists the item on the amend queue `outerlayer
408
+ runner start` reads, naming the thread waiting on an agent, until a
409
+ person's next pass or fail clears it.
249
410
 
250
411
  Every command accepts `--no-color`, and color is off on its own when stdout is not a terminal, when `NO_COLOR` is set, or when `TERM` is `dumb`. `FORCE_COLOR=1` turns it on for a pipe. Commands that print anything accept `--json`.
251
412