@b00dea/cockpit 0.0.0-bootstrap.0 → 0.1.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 (363) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +21 -1
  4. package/bin/cockpit.mjs +33 -0
  5. package/cockpit.schema.json +2574 -0
  6. package/dist/autonomy.d.ts +44 -0
  7. package/dist/autonomy.js +156 -0
  8. package/dist/bounded-read.d.ts +14 -0
  9. package/dist/bounded-read.js +42 -0
  10. package/dist/browser/commands.d.ts +62 -0
  11. package/dist/browser/commands.js +349 -0
  12. package/dist/browser/package-paths.d.ts +1 -0
  13. package/dist/browser/package-paths.js +6 -0
  14. package/dist/browser/proxy.d.ts +22 -0
  15. package/dist/browser/proxy.js +37 -0
  16. package/dist/browser/runtime.d.ts +117 -0
  17. package/dist/browser/runtime.js +183 -0
  18. package/dist/browser/surfaces.d.ts +141 -0
  19. package/dist/browser/surfaces.js +556 -0
  20. package/dist/browser/ui-gate.d.ts +15 -0
  21. package/dist/browser/ui-gate.js +18 -0
  22. package/dist/cli-guard.d.ts +5 -0
  23. package/dist/cli-guard.js +71 -0
  24. package/dist/cli-main.d.ts +139 -0
  25. package/dist/cli-main.js +1405 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.js +16 -0
  28. package/dist/command-table.d.ts +113 -0
  29. package/dist/command-table.js +512 -0
  30. package/dist/config/load.d.ts +171 -0
  31. package/dist/config/load.js +345 -0
  32. package/dist/config/sibling-toml.d.ts +48 -0
  33. package/dist/config/sibling-toml.js +160 -0
  34. package/dist/config/surface-tools-toml.d.ts +9 -0
  35. package/dist/config/surface-tools-toml.js +37 -0
  36. package/dist/config/verified-write.d.ts +43 -0
  37. package/dist/config/verified-write.js +89 -0
  38. package/dist/diagnostics.d.ts +26 -0
  39. package/dist/diagnostics.js +335 -0
  40. package/dist/emit.d.ts +22 -0
  41. package/dist/emit.js +43 -0
  42. package/dist/entrypoint-kinds.d.ts +3 -0
  43. package/dist/entrypoint-kinds.js +11 -0
  44. package/dist/environment/application/destructive.d.ts +6 -0
  45. package/dist/environment/application/destructive.js +425 -0
  46. package/dist/environment/application/doctor.d.ts +89 -0
  47. package/dist/environment/application/doctor.js +235 -0
  48. package/dist/environment/application/guard.d.ts +43 -0
  49. package/dist/environment/application/guard.js +622 -0
  50. package/dist/environment/application/health.d.ts +134 -0
  51. package/dist/environment/application/health.js +379 -0
  52. package/dist/environment/application/lifecycle-context.d.ts +228 -0
  53. package/dist/environment/application/lifecycle-context.js +154 -0
  54. package/dist/environment/application/lifecycle-state.d.ts +25 -0
  55. package/dist/environment/application/lifecycle-state.js +201 -0
  56. package/dist/environment/application/lifecycle.d.ts +17 -0
  57. package/dist/environment/application/lifecycle.js +729 -0
  58. package/dist/environment/application/logs.d.ts +45 -0
  59. package/dist/environment/application/logs.js +127 -0
  60. package/dist/environment/application/setup.d.ts +129 -0
  61. package/dist/environment/application/setup.js +741 -0
  62. package/dist/environment/application/shared.d.ts +11 -0
  63. package/dist/environment/application/shared.js +61 -0
  64. package/dist/environment/claims.d.ts +389 -0
  65. package/dist/environment/claims.js +1709 -0
  66. package/dist/environment/codex-hook-trust.d.ts +11 -0
  67. package/dist/environment/codex-hook-trust.js +185 -0
  68. package/dist/environment/compose-environment.d.ts +1 -0
  69. package/dist/environment/compose-environment.js +6 -0
  70. package/dist/environment/compose-ownership.d.ts +21 -0
  71. package/dist/environment/compose-ownership.js +117 -0
  72. package/dist/environment/compose-topology.d.ts +117 -0
  73. package/dist/environment/compose-topology.js +413 -0
  74. package/dist/environment/contract.d.ts +489 -0
  75. package/dist/environment/contract.js +761 -0
  76. package/dist/environment/declaration.d.ts +14 -0
  77. package/dist/environment/declaration.js +39 -0
  78. package/dist/environment/destructive-composition.d.ts +29 -0
  79. package/dist/environment/destructive-composition.js +333 -0
  80. package/dist/environment/docker-daemon.d.ts +30 -0
  81. package/dist/environment/docker-daemon.js +78 -0
  82. package/dist/environment/docker-endpoint.d.ts +10 -0
  83. package/dist/environment/docker-endpoint.js +19 -0
  84. package/dist/environment/env-schema-observation.d.ts +46 -0
  85. package/dist/environment/env-schema-observation.js +241 -0
  86. package/dist/environment/evidence.d.ts +136 -0
  87. package/dist/environment/evidence.js +437 -0
  88. package/dist/environment/executable.d.ts +26 -0
  89. package/dist/environment/executable.js +167 -0
  90. package/dist/environment/generated-application/authorities.d.ts +5 -0
  91. package/dist/environment/generated-application/authorities.js +61 -0
  92. package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
  93. package/dist/environment/generated-application/bootstrap-skill.js +328 -0
  94. package/dist/environment/generated-application/check.d.ts +13 -0
  95. package/dist/environment/generated-application/check.js +189 -0
  96. package/dist/environment/generated-application/custom-surface.d.ts +26 -0
  97. package/dist/environment/generated-application/custom-surface.js +289 -0
  98. package/dist/environment/generated-application/prepare.d.ts +29 -0
  99. package/dist/environment/generated-application/prepare.js +276 -0
  100. package/dist/environment/generated-application/publication.d.ts +33 -0
  101. package/dist/environment/generated-application/publication.js +547 -0
  102. package/dist/environment/generated-application/shared.d.ts +484 -0
  103. package/dist/environment/generated-application/shared.js +349 -0
  104. package/dist/environment/generated-application/surface-observation.d.ts +4 -0
  105. package/dist/environment/generated-application/surface-observation.js +69 -0
  106. package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
  107. package/dist/environment/generated-application/surface-outputs.js +389 -0
  108. package/dist/environment/generated-application/surfaces.d.ts +3 -0
  109. package/dist/environment/generated-application/surfaces.js +699 -0
  110. package/dist/environment/generated-application/sync.d.ts +6 -0
  111. package/dist/environment/generated-application/sync.js +109 -0
  112. package/dist/environment/git-observation.d.ts +183 -0
  113. package/dist/environment/git-observation.js +544 -0
  114. package/dist/environment/health-composition.d.ts +11 -0
  115. package/dist/environment/health-composition.js +628 -0
  116. package/dist/environment/host-observation.d.ts +56 -0
  117. package/dist/environment/host-observation.js +52 -0
  118. package/dist/environment/identity.d.ts +48 -0
  119. package/dist/environment/identity.js +85 -0
  120. package/dist/environment/init-application.d.ts +308 -0
  121. package/dist/environment/init-application.js +697 -0
  122. package/dist/environment/install-policy-observation.d.ts +71 -0
  123. package/dist/environment/install-policy-observation.js +284 -0
  124. package/dist/environment/install-policy.d.ts +48 -0
  125. package/dist/environment/install-policy.js +96 -0
  126. package/dist/environment/instance.d.ts +33 -0
  127. package/dist/environment/instance.js +122 -0
  128. package/dist/environment/instruction-pointer.d.ts +19 -0
  129. package/dist/environment/instruction-pointer.js +144 -0
  130. package/dist/environment/lifecycle-composition.d.ts +29 -0
  131. package/dist/environment/lifecycle-composition.js +675 -0
  132. package/dist/environment/lifecycle-stage.d.ts +115 -0
  133. package/dist/environment/lifecycle-stage.js +398 -0
  134. package/dist/environment/lifecycle.d.ts +75 -0
  135. package/dist/environment/lifecycle.js +354 -0
  136. package/dist/environment/logs.d.ts +132 -0
  137. package/dist/environment/logs.js +519 -0
  138. package/dist/environment/observations.d.ts +46 -0
  139. package/dist/environment/observations.js +344 -0
  140. package/dist/environment/operator-application.d.ts +50 -0
  141. package/dist/environment/operator-application.js +155 -0
  142. package/dist/environment/ports.d.ts +182 -0
  143. package/dist/environment/ports.js +1205 -0
  144. package/dist/environment/posture-observation.d.ts +68 -0
  145. package/dist/environment/posture-observation.js +253 -0
  146. package/dist/environment/read-composition.d.ts +27 -0
  147. package/dist/environment/read-composition.js +352 -0
  148. package/dist/environment/readiness-composition.d.ts +38 -0
  149. package/dist/environment/readiness-composition.js +130 -0
  150. package/dist/environment/reclaim.d.ts +333 -0
  151. package/dist/environment/reclaim.js +1835 -0
  152. package/dist/environment/repository-identity.d.ts +13 -0
  153. package/dist/environment/repository-identity.js +108 -0
  154. package/dist/environment/runtime-measurement-record.d.ts +38 -0
  155. package/dist/environment/runtime-measurement-record.js +111 -0
  156. package/dist/environment/runtime-measurement.d.ts +27 -0
  157. package/dist/environment/runtime-measurement.js +224 -0
  158. package/dist/environment/runtime-probe-residue.d.ts +43 -0
  159. package/dist/environment/runtime-probe-residue.js +148 -0
  160. package/dist/environment/runtime-probe.d.ts +72 -0
  161. package/dist/environment/runtime-probe.js +725 -0
  162. package/dist/environment/runtime.d.ts +505 -0
  163. package/dist/environment/runtime.js +2335 -0
  164. package/dist/environment/seed.d.ts +16 -0
  165. package/dist/environment/seed.js +43 -0
  166. package/dist/environment/selftest-application.d.ts +86 -0
  167. package/dist/environment/selftest-application.js +713 -0
  168. package/dist/environment/sensitive.d.ts +3 -0
  169. package/dist/environment/sensitive.js +48 -0
  170. package/dist/environment/setup-observation.d.ts +62 -0
  171. package/dist/environment/setup-observation.js +1473 -0
  172. package/dist/environment/setup.d.ts +43 -0
  173. package/dist/environment/setup.js +176 -0
  174. package/dist/environment/sibling-instructions.d.ts +33 -0
  175. package/dist/environment/sibling-instructions.js +224 -0
  176. package/dist/environment/sibling-registration.d.ts +45 -0
  177. package/dist/environment/sibling-registration.js +538 -0
  178. package/dist/environment/state.d.ts +371 -0
  179. package/dist/environment/state.js +2013 -0
  180. package/dist/environment/templates.d.ts +140 -0
  181. package/dist/environment/templates.js +685 -0
  182. package/dist/environment/tool-registration.d.ts +9 -0
  183. package/dist/environment/tool-registration.js +96 -0
  184. package/dist/environment/upgrade-application.d.ts +19 -0
  185. package/dist/environment/upgrade-application.js +175 -0
  186. package/dist/environment-exec/execute.d.ts +102 -0
  187. package/dist/environment-exec/execute.js +1175 -0
  188. package/dist/environment-exec/libexec-program.d.ts +6 -0
  189. package/dist/environment-exec/libexec-program.js +22 -0
  190. package/dist/environment-exec/verified-digest.d.ts +4 -0
  191. package/dist/environment-exec/verified-digest.js +16 -0
  192. package/dist/exact-keys.d.ts +7 -0
  193. package/dist/exact-keys.js +15 -0
  194. package/dist/fix-catalogue.d.ts +32 -0
  195. package/dist/fix-catalogue.js +60 -0
  196. package/dist/freshness.d.ts +29 -0
  197. package/dist/freshness.js +75 -0
  198. package/dist/generated-environment/adopt.d.ts +16 -0
  199. package/dist/generated-environment/adopt.js +275 -0
  200. package/dist/generated-environment/command.d.ts +47 -0
  201. package/dist/generated-environment/command.js +75 -0
  202. package/dist/generated-environment/transaction.d.ts +186 -0
  203. package/dist/generated-environment/transaction.js +1770 -0
  204. package/dist/grading/evaluate.d.ts +71 -0
  205. package/dist/grading/evaluate.js +403 -0
  206. package/dist/grading/grade.d.ts +33 -0
  207. package/dist/grading/grade.js +57 -0
  208. package/dist/guards/evaluator.d.ts +113 -0
  209. package/dist/guards/evaluator.js +1226 -0
  210. package/dist/guards/harness-adapter.d.ts +17 -0
  211. package/dist/guards/harness-adapter.js +57 -0
  212. package/dist/guards/host.d.ts +8 -0
  213. package/dist/guards/host.js +10 -0
  214. package/dist/guards/invocation-parser.d.ts +66 -0
  215. package/dist/guards/invocation-parser.js +595 -0
  216. package/dist/guards/local-identity.d.ts +40 -0
  217. package/dist/guards/local-identity.js +123 -0
  218. package/dist/guards/policy-pin.d.ts +13 -0
  219. package/dist/guards/policy-pin.js +175 -0
  220. package/dist/guards/protected-locations.d.ts +62 -0
  221. package/dist/guards/protected-locations.js +461 -0
  222. package/dist/guards/read-only.d.ts +37 -0
  223. package/dist/guards/read-only.js +315 -0
  224. package/dist/init/command.d.ts +40 -0
  225. package/dist/init/command.js +310 -0
  226. package/dist/init/derive.d.ts +51 -0
  227. package/dist/init/derive.js +715 -0
  228. package/dist/init/draft.d.ts +138 -0
  229. package/dist/init/draft.js +1205 -0
  230. package/dist/init/render.d.ts +35 -0
  231. package/dist/init/render.js +82 -0
  232. package/dist/init/scaffold.d.ts +53 -0
  233. package/dist/init/scaffold.js +121 -0
  234. package/dist/invocation.d.ts +13 -0
  235. package/dist/invocation.js +20 -0
  236. package/dist/package-metadata.d.ts +9 -0
  237. package/dist/package-metadata.js +19 -0
  238. package/dist/readiness.d.ts +53 -0
  239. package/dist/readiness.js +88 -0
  240. package/dist/references/named.d.ts +24 -0
  241. package/dist/references/named.js +27 -0
  242. package/dist/refusal.d.ts +15 -0
  243. package/dist/refusal.js +7 -0
  244. package/dist/registry-derivation.d.ts +31 -0
  245. package/dist/registry-derivation.js +32 -0
  246. package/dist/registry-resolve/command.d.ts +47 -0
  247. package/dist/registry-resolve/command.js +226 -0
  248. package/dist/registry-resolve/registry.d.ts +68 -0
  249. package/dist/registry-resolve/registry.js +292 -0
  250. package/dist/registry-resolve/render-shell.d.ts +18 -0
  251. package/dist/registry-resolve/render-shell.js +36 -0
  252. package/dist/repair.d.ts +29 -0
  253. package/dist/repair.js +8 -0
  254. package/dist/schema-json.d.ts +1 -0
  255. package/dist/schema-json.js +177 -0
  256. package/dist/schema.d.ts +1322 -0
  257. package/dist/schema.js +934 -0
  258. package/dist/setup/command.d.ts +4 -0
  259. package/dist/setup/command.js +4 -0
  260. package/dist/sha256.d.ts +1 -0
  261. package/dist/sha256.js +4 -0
  262. package/dist/surfaces/adoption.d.ts +41 -0
  263. package/dist/surfaces/adoption.js +217 -0
  264. package/dist/surfaces/consumer-instructions.d.ts +20 -0
  265. package/dist/surfaces/consumer-instructions.js +43 -0
  266. package/dist/surfaces/contract.d.ts +42 -0
  267. package/dist/surfaces/contract.js +169 -0
  268. package/dist/surfaces/core-declarations.d.ts +24 -0
  269. package/dist/surfaces/core-declarations.js +79 -0
  270. package/dist/surfaces/development-markers.d.ts +1 -0
  271. package/dist/surfaces/development-markers.js +18 -0
  272. package/dist/surfaces/generator-adapter.d.ts +71 -0
  273. package/dist/surfaces/generator-adapter.js +809 -0
  274. package/dist/surfaces/harness-registry.d.ts +180 -0
  275. package/dist/surfaces/harness-registry.js +117 -0
  276. package/dist/surfaces/mcp.d.ts +95 -0
  277. package/dist/surfaces/mcp.js +186 -0
  278. package/dist/surfaces/merge.d.ts +30 -0
  279. package/dist/surfaces/merge.js +105 -0
  280. package/dist/surfaces/ownership.d.ts +190 -0
  281. package/dist/surfaces/ownership.js +795 -0
  282. package/dist/surfaces/projection.d.ts +136 -0
  283. package/dist/surfaces/projection.js +681 -0
  284. package/dist/surfaces/render.d.ts +118 -0
  285. package/dist/surfaces/render.js +633 -0
  286. package/dist/surfaces/restrictions.d.ts +130 -0
  287. package/dist/surfaces/restrictions.js +447 -0
  288. package/dist/surfaces/selftest.d.ts +162 -0
  289. package/dist/surfaces/selftest.js +393 -0
  290. package/dist/surfaces/skill-contract.d.ts +85 -0
  291. package/dist/surfaces/skill-contract.js +449 -0
  292. package/dist/surfaces/sync.d.ts +57 -0
  293. package/dist/surfaces/sync.js +164 -0
  294. package/dist/team-metadata.d.ts +41 -0
  295. package/dist/team-metadata.js +512 -0
  296. package/dist/testing/gates.d.ts +91 -0
  297. package/dist/testing/gates.js +182 -0
  298. package/dist/testing/hooks.d.ts +59 -0
  299. package/dist/testing/hooks.js +139 -0
  300. package/dist/testing/verdict.d.ts +29 -0
  301. package/dist/testing/verdict.js +62 -0
  302. package/dist/work-orders/application.d.ts +99 -0
  303. package/dist/work-orders/application.js +383 -0
  304. package/dist/work-orders/composition.d.ts +124 -0
  305. package/dist/work-orders/composition.js +1010 -0
  306. package/dist/work-orders/contract.d.ts +61 -0
  307. package/dist/work-orders/contract.js +122 -0
  308. package/dist/work-orders/store.d.ts +158 -0
  309. package/dist/work-orders/store.js +197 -0
  310. package/docs/README.md +65 -0
  311. package/docs/architecture.md +136 -0
  312. package/docs/capability-matrix.md +50 -0
  313. package/docs/cli-reference.md +599 -0
  314. package/docs/command-list.md +51 -0
  315. package/docs/compose-patterns.md +99 -0
  316. package/docs/concepts.md +171 -0
  317. package/docs/configuration-essentials.md +172 -0
  318. package/docs/configuration-reference.md +621 -0
  319. package/docs/environment.md +760 -0
  320. package/docs/extending.md +43 -0
  321. package/docs/faq.md +111 -0
  322. package/docs/getting-started.md +608 -0
  323. package/docs/harness-surfaces.md +433 -0
  324. package/docs/joining-a-team.md +76 -0
  325. package/docs/release-policy.md +49 -0
  326. package/docs/safety-and-autonomy.md +419 -0
  327. package/docs/troubleshooting.md +361 -0
  328. package/docs/tutorial.md +170 -0
  329. package/docs/work-and-browser.md +254 -0
  330. package/libexec/browser-operation.cjs +186 -0
  331. package/libexec/fence-marker.cjs +101 -0
  332. package/libexec/fence-marker.d.cts +24 -0
  333. package/libexec/fenced-group-runner.cjs +77 -0
  334. package/libexec/fenced-supervisor.cjs +137 -0
  335. package/libexec/generated-filesystem.cjs +731 -0
  336. package/libexec/install-filesystem.cjs +225 -0
  337. package/libexec/rulesync-generator.NOTICE.md +1112 -0
  338. package/libexec/rulesync-generator.cjs +743 -0
  339. package/libexec/surface-generator-worker.cjs +266 -0
  340. package/libexec/surface-output-reader.cjs +99 -0
  341. package/libexec/surface-scratch-cleanup.cjs +14 -0
  342. package/libexec/validate-harness-evidence.d.mts +183 -0
  343. package/libexec/validate-harness-evidence.mjs +1312 -0
  344. package/package.json +76 -1
  345. package/surfaces/README.md +33 -0
  346. package/surfaces/agents/evaluator.agent.md +76 -0
  347. package/surfaces/guards/bash-guard.sh +48 -0
  348. package/surfaces/guards/claude.adapter.json +25 -0
  349. package/surfaces/guards/codex.adapter.json +24 -0
  350. package/surfaces/guards/opencode.adapter.json +25 -0
  351. package/surfaces/references/browser.md +63 -0
  352. package/surfaces/references/test-data-and-personas.md +84 -0
  353. package/surfaces/references/testing-tiers.md +11 -0
  354. package/surfaces/skills/cockpit-browser.skill.md +54 -0
  355. package/surfaces/skills/cockpit-explain.skill.md +32 -0
  356. package/surfaces/skills/cockpit-fix.skill.md +37 -0
  357. package/surfaces/skills/cockpit-setup.skill.md +39 -0
  358. package/surfaces/skills/cockpit-work.skill.md +34 -0
  359. package/surfaces/skills/collection.json +5 -0
  360. package/surfaces/skills/procedures/explain.md +61 -0
  361. package/surfaces/skills/procedures/fix.md +81 -0
  362. package/surfaces/skills/procedures/setup.md +379 -0
  363. package/surfaces/skills/procedures/work.md +105 -0
@@ -0,0 +1,71 @@
1
+ import { type EvaluatorTool } from '../schema.js';
2
+ type Executable = Readonly<{
3
+ path: string;
4
+ sha256: string;
5
+ bytes: number;
6
+ }>;
7
+ export type EvaluationAttempt = Readonly<{
8
+ tool: EvaluatorTool;
9
+ code: string;
10
+ message: string;
11
+ }>;
12
+ /** The tools a caller may run, in order, with those that could not be found or probed passed over. */
13
+ export type EvaluatorChain = Readonly<{
14
+ located: readonly Readonly<{
15
+ tool: EvaluatorTool;
16
+ executable: Executable;
17
+ }>[];
18
+ skipped: readonly EvaluationAttempt[];
19
+ }>;
20
+ export type EvaluationRequest = Readonly<{
21
+ chain: EvaluatorChain;
22
+ role: string;
23
+ document: Readonly<Record<string, unknown>>;
24
+ schema: Readonly<Record<string, unknown>>;
25
+ repository: string;
26
+ timeoutMs: number;
27
+ timeoutField: string;
28
+ executableDirectories: readonly string[];
29
+ environment: Readonly<Record<string, string | undefined>>;
30
+ }>;
31
+ export type EvaluationResult = Readonly<{
32
+ kind: 'answer';
33
+ value: Readonly<Record<string, unknown>>;
34
+ sessionId: string;
35
+ tool: EvaluatorTool;
36
+ fallbacks: readonly EvaluationAttempt[];
37
+ }> | Readonly<{
38
+ kind: 'refusal';
39
+ code: string;
40
+ message: string;
41
+ fix: string;
42
+ attempts: readonly EvaluationAttempt[];
43
+ }>;
44
+ /** The listed tools found on the search path, without running them: what doctor reports. */
45
+ export declare function findEvaluators(tools: readonly EvaluatorTool[], directories: readonly string[]): Promise<Readonly<{
46
+ found: readonly EvaluatorTool[];
47
+ missing: readonly EvaluationAttempt[];
48
+ }>>;
49
+ /** No listed evaluator can run here; one literal fix so the diagnostic inventory captures it. */
50
+ export declare function evaluatorUnavailable(message: string): {
51
+ code: "EVALUATOR_UNAVAILABLE";
52
+ message: string;
53
+ fix: string;
54
+ };
55
+ /**
56
+ * The default order puts the tool running this command last, so work done in one tool is graded by
57
+ * another first and the same tool, in a fresh headless session, is only the fallback. Measured
58
+ * 2026-11-03 (Claude Code 2.1.317, Codex 0.166.0): a shell command run by Claude Code has CLAUDECODE
59
+ * set and one run by Codex has CODEX_THREAD_ID set. Neither or both is not guessed at; OpenCode is
60
+ * not detected.
61
+ */
62
+ export declare function defaultEvaluatorOrder(tools: readonly EvaluatorTool[], env: Readonly<Record<string, string | undefined>>): readonly EvaluatorTool[];
63
+ /** Finds and probes each listed tool in order; one that is missing or too old is passed over and recorded. */
64
+ export declare function locateEvaluators(tools: readonly EvaluatorTool[], directories: readonly string[]): Promise<EvaluatorChain>;
65
+ /**
66
+ * Runs the first tool in the chain that answers. A tool that cannot run here (missing, signed out,
67
+ * rate-limited, timed out) is recorded and the next is tried; an answer that cannot be trusted is refused
68
+ * outright, since trying another tool then would shop for a verdict.
69
+ */
70
+ export declare function evaluate(request: EvaluationRequest): Promise<EvaluationResult>;
71
+ export {};
@@ -0,0 +1,403 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { sha256Hex } from '../sha256.js';
3
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
4
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { delimiter, dirname, isAbsolute, join } from 'node:path';
7
+ import { parseDocument } from 'yaml';
8
+ import { ENVIRONMENT_CODES, errorCause, parseJsonText, RefusalError } from '../environment/contract.js';
9
+ import { resolveExecutablePath } from '../environment/executable.js';
10
+ import { authenticatedExecutionOutcome, executeProbe } from '../environment-exec/execute.js';
11
+ import { EVALUATOR_COMMANDS, runningEvaluatorTool } from '../schema.js';
12
+ const MAX_DOCUMENT_BYTES = 8 * 1_048_576;
13
+ // The Codex CLI's and OpenCode's JSON streams carry every tool call and its output; a larger stream
14
+ // fails naming this bound rather than being cut short of the session identity or the final answer.
15
+ const STDOUT_LIMIT_BYTES = 8 * 1_048_576;
16
+ const STDERR_LIMIT_BYTES = 65_536;
17
+ const ANSWER_LIMIT_BYTES = 65_536;
18
+ const PROBE_TIMEOUT_MS = 30_000;
19
+ const CAUSE_CHARACTERS = 2_048;
20
+ // The tool ran and said why it gave no answer (signed out, rate-limited), so another tool may be tried.
21
+ class ToolReportedFailure extends RefusalError {
22
+ }
23
+ // A tool that resumed a session it had already answered in could carry another run's context.
24
+ const answeredSessions = new Set();
25
+ function parseJson(text, subject, expected) {
26
+ const parsed = parseJsonText(text, subject, expected);
27
+ if ('refusal' in parsed)
28
+ throw new RefusalError(parsed.refusal);
29
+ return parsed.value;
30
+ }
31
+ function strictJson(text, subject, expected) {
32
+ if (parseDocument(text, { uniqueKeys: true }).errors.length)
33
+ throw new RefusalError(`${subject} is not strict duplicate-free JSON`);
34
+ return parseJson(text, subject, expected);
35
+ }
36
+ function jsonLines(stdout, subject) {
37
+ return stdout.split('\n').filter(Boolean).map(line => parseJson(line, subject, 'one JSON event per line'));
38
+ }
39
+ // A failed run's stdout can interleave non-JSON lines, so only the lines that parse are read for a cause.
40
+ function reportedEvents(stdout) {
41
+ return stdout.split('\n').flatMap(line => {
42
+ try {
43
+ const event = JSON.parse(line);
44
+ return event && typeof event === 'object' && !Array.isArray(event) ? [event] : [];
45
+ }
46
+ catch (error) {
47
+ if (error instanceof SyntaxError)
48
+ return [];
49
+ throw error;
50
+ }
51
+ });
52
+ }
53
+ function opencodeCause(stdout, stderr) {
54
+ const failed = reportedEvents(stdout).find(event => event.type === 'error')?.error;
55
+ const reported = typeof failed?.data?.message === 'string' ? failed.data.message : typeof failed?.name === 'string' ? failed.name : undefined;
56
+ const logged = [...stderr.matchAll(/ error(?:\.error)?="([^"]+)"/g)].at(-1)?.[1];
57
+ return reported && logged ? `${reported} (${logged})` : reported ?? logged;
58
+ }
59
+ // Each read-only mechanism was measured on 2026-11-03 beside a control without it: Codex 0.166.0 refused a
60
+ // shell write under --sandbox read-only and made it under workspace-write; Claude Code 2.1.317 had no tool
61
+ // to write with under --tools Read,Grep,Glob and wrote the file once Write was added; OpenCode 1.19.5
62
+ // listed only glob, grep, read and list under these denials and offered write once edit was not denied.
63
+ const TOOLS = Object.freeze({
64
+ codexcli: Object.freeze({
65
+ name: 'the Codex CLI', install: 'npm install -g @openai/codex', signIn: 'codex login',
66
+ help: ['exec', '--help'], flags: ['--ephemeral', '--ignore-user-config', '--skip-git-repo-check', '--sandbox', '--cd', '--json', '--output-schema', '--output-last-message'],
67
+ environment: ['HOME', 'CODEX_HOME'], fixedEnvironment: {},
68
+ restriction: 'codex exec --sandbox read-only',
69
+ // Codex reads its working root as a project, and outside a Git checkout only with --skip-git-repo-check.
70
+ argv: (run) => ['exec', '--ephemeral', '--ignore-user-config', '--skip-git-repo-check', '--sandbox', 'read-only', '--cd', run.runRoot, '--json',
71
+ '--output-schema', run.schemaPath, '--output-last-message', run.responsePath, run.prompt],
72
+ read: (stdout, run) => {
73
+ let sessionId;
74
+ for (const event of jsonLines(stdout, 'a Codex CLI event line')) {
75
+ const candidate = event.type === 'thread.started' && typeof event.thread_id === 'string' ? event.thread_id
76
+ : event.type === 'thread.started' && typeof event.threadId === 'string' ? event.threadId : undefined;
77
+ if (candidate && sessionId)
78
+ throw new RefusalError('the Codex CLI emitted duplicate thread identities');
79
+ sessionId ??= candidate;
80
+ }
81
+ if (!sessionId)
82
+ throw new RefusalError('the Codex CLI named no thread identity');
83
+ const stat = lstatSync(run.responsePath);
84
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > ANSWER_LIMIT_BYTES || realpathSync(run.responsePath) !== run.responsePath) {
85
+ throw new RefusalError('the Codex CLI answer is not a contained bounded file');
86
+ }
87
+ const bytes = readFileSync(run.responsePath);
88
+ if (bytes.length > ANSWER_LIMIT_BYTES)
89
+ throw new RefusalError('the Codex CLI answer grew past its bound');
90
+ return { sessionId, answer: new TextDecoder('utf-8', { fatal: true }).decode(bytes) };
91
+ },
92
+ // A failed run (an API rejection, a missing login) is reported as a JSON event on stdout.
93
+ cause: (stdout) => reportedEvents(stdout).reduce((cause, event) => event.type === 'error' && typeof event.message === 'string' ? event.message
94
+ : event.type === 'turn.failed' && typeof event.error?.message === 'string' ? event.error.message : cause, undefined),
95
+ }),
96
+ claudecode: Object.freeze({
97
+ name: 'Claude Code', install: 'npm install -g @anthropic-ai/claude-code', signIn: 'claude auth login',
98
+ help: ['--help'], flags: ['--print', '--output-format', '--json-schema', '--no-session-persistence', '--safe-mode', '--strict-mcp-config', '--tools', '--permission-mode', '--add-dir'],
99
+ // On macOS the sign-in is a keychain item kept under the user's name; without USER it reads as signed out.
100
+ environment: ['HOME', 'USER', 'CLAUDE_CONFIG_DIR'], fixedEnvironment: {},
101
+ restriction: 'claude --tools Read,Grep,Glob',
102
+ // --safe-mode skips the hooks, plugins, skills, MCP servers and CLAUDE.md the user configured;
103
+ // dontAsk denies what would prompt, so the repository is readable only because --add-dir names it.
104
+ // --tools and --add-dir take several values, so -- ends them before the prompt.
105
+ argv: (run) => ['--print', '--output-format', 'json', '--json-schema', run.schema, '--no-session-persistence', '--safe-mode',
106
+ '--strict-mcp-config', '--tools', 'Read,Grep,Glob', '--permission-mode', 'dontAsk', '--add-dir', run.repository, '--', run.prompt],
107
+ read: (stdout) => {
108
+ const result = strictJson(stdout, 'the Claude Code result', 'one JSON result object');
109
+ if (result.type !== 'result' || result.is_error !== false || typeof result.session_id !== 'string' || !result.session_id
110
+ || typeof result.structured_output !== 'object' || result.structured_output === null) {
111
+ throw new ToolReportedFailure(`Claude Code returned no structured result${typeof result.result === 'string' && result.result ? `: ${result.result.slice(-CAUSE_CHARACTERS)}` : ''}`);
112
+ }
113
+ return { sessionId: result.session_id, answer: JSON.stringify(result.structured_output) };
114
+ },
115
+ cause: (stdout) => {
116
+ const result = reportedEvents(stdout).find(event => event.type === 'result')?.result;
117
+ return typeof result === 'string' && result ? result : undefined;
118
+ },
119
+ }),
120
+ opencode: Object.freeze({
121
+ name: 'OpenCode', install: 'npm install -g opencode-ai', signIn: 'opencode auth login',
122
+ help: ['run', '--help'], flags: ['--pure', '--format', '--print-logs', '--log-level'],
123
+ environment: ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME'],
124
+ // OpenCode merges this over the user's configuration last, and a denied tool is not offered at all.
125
+ // Each permission is replaced whole, so a user rule allowing part of it cannot survive; read, glob,
126
+ // grep and list keep the user's rules. An ask would wait for a person nobody is, so the
127
+ // repository, outside the working directory, is allowed outright. --pure skips external plugins.
128
+ fixedEnvironment: { OPENCODE_PERMISSION: JSON.stringify({ edit: 'deny', bash: 'deny', task: 'deny', todowrite: 'deny', question: 'deny', webfetch: 'deny',
129
+ websearch: 'deny', lsp: 'deny', doom_loop: 'deny', skill: 'deny', external_directory: 'allow' }) },
130
+ restriction: 'opencode run with OPENCODE_PERMISSION denying edit, bash, task, web, skill, question, todowrite and lsp',
131
+ // OpenCode's error event says only "Unexpected server error"; its error log on stderr says which.
132
+ argv: (run) => ['run', '--pure', '--format', 'json', '--print-logs', '--log-level', 'ERROR', run.prompt],
133
+ read: (stdout) => {
134
+ const events = jsonLines(stdout, 'an OpenCode event line');
135
+ if (events.some(event => event.type === 'error'))
136
+ throw new ToolReportedFailure(`OpenCode reported an error: ${opencodeCause(stdout, '') ?? 'no message'}`);
137
+ const sessions = new Set(events.map(event => event.sessionID));
138
+ const [sessionId] = sessions;
139
+ if (sessions.size !== 1 || typeof sessionId !== 'string' || !sessionId)
140
+ throw new RefusalError('OpenCode named no single session identity');
141
+ const text = events.filter(event => event.type === 'text').at(-1)?.part;
142
+ if (typeof text?.text !== 'string')
143
+ throw new RefusalError('OpenCode returned no final answer');
144
+ return { sessionId, answer: text.text };
145
+ },
146
+ cause: opencodeCause,
147
+ }),
148
+ });
149
+ // Every value the output schema admits, checked here for every tool: OpenCode has no flag that
150
+ // constrains its answer, and the schema a tool enforced is only as good as that tool's own check.
151
+ function schemaViolation(schema, value, path) {
152
+ const types = [].concat(schema.type ?? []);
153
+ const matches = (type) => type === 'null' ? value === null : type === 'array' ? Array.isArray(value)
154
+ : type === 'object' ? typeof value === 'object' && value !== null && !Array.isArray(value)
155
+ : type === 'integer' ? Number.isSafeInteger(value) : typeof value === type;
156
+ if (!types.some(matches))
157
+ return `${path} is not ${types.join(' or ')}`;
158
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value))
159
+ return `${path} is not one of ${schema.enum.join(', ')}`;
160
+ if (Array.isArray(value)) {
161
+ for (const [index, item] of value.entries()) {
162
+ const found = schemaViolation(schema.items, item, `${path}[${index}]`);
163
+ if (found)
164
+ return found;
165
+ }
166
+ }
167
+ else if (typeof value === 'object' && value !== null) {
168
+ const properties = schema.properties;
169
+ const extra = Object.keys(value).find(key => !Object.hasOwn(properties, key));
170
+ if (extra !== undefined)
171
+ return `${path} has the undeclared field ${extra}`;
172
+ for (const [key, property] of Object.entries(properties)) {
173
+ if (!Object.hasOwn(value, key))
174
+ return `${path} lacks ${key}`;
175
+ const found = schemaViolation(property, value[key], `${path}.${key}`);
176
+ if (found)
177
+ return found;
178
+ }
179
+ }
180
+ return undefined;
181
+ }
182
+ // `reason` is the first cause other than the file simply not being there; it is absent when no directory held the tool.
183
+ async function findExecutable(directories, name) {
184
+ let reason;
185
+ const refuse = (cause) => { if (!/\bENOENT\b|\bENOTDIR\b/.test(cause))
186
+ reason ??= cause; };
187
+ for (const directory of directories) {
188
+ if (!isAbsolute(directory))
189
+ continue;
190
+ const resolved = await resolveExecutablePath(join(directory, name));
191
+ if (resolved.state !== 'available') {
192
+ refuse(resolved.reason);
193
+ continue;
194
+ }
195
+ let bytes;
196
+ try {
197
+ bytes = lstatSync(resolved.value.canonicalPath).size;
198
+ }
199
+ catch (error) {
200
+ refuse(`Executable ${name} is unavailable: ${errorCause(error)}`);
201
+ continue;
202
+ }
203
+ return { state: 'available', value: { path: resolved.value.canonicalPath, sha256: resolved.value.sha256, bytes } };
204
+ }
205
+ return reason === undefined ? { state: 'unavailable' } : { state: 'unavailable', reason };
206
+ }
207
+ const missingMessage = (tool, reason) => {
208
+ const spec = TOOLS[tool];
209
+ return `${spec.name} (${EVALUATOR_COMMANDS[tool]}) is not on PATH${reason === undefined ? '' : ` (${reason})`}; install it with ${spec.install} and sign in with ${spec.signIn}`;
210
+ };
211
+ /** The listed tools found on the search path, without running them: what doctor reports. */
212
+ export async function findEvaluators(tools, directories) {
213
+ const found = [];
214
+ const missing = [];
215
+ for (const tool of tools) {
216
+ const located = await findExecutable(directories, EVALUATOR_COMMANDS[tool]);
217
+ if (located.state === 'available')
218
+ found.push(tool);
219
+ else
220
+ missing.push({ tool, code: ENVIRONMENT_CODES.EVALUATOR_UNAVAILABLE, message: missingMessage(tool, located.reason) });
221
+ }
222
+ return { found, missing };
223
+ }
224
+ /** No listed evaluator can run here; one literal fix so the diagnostic inventory captures it. */
225
+ export function evaluatorUnavailable(message) {
226
+ return {
227
+ code: 'EVALUATOR_UNAVAILABLE',
228
+ message,
229
+ fix: 'install and sign in to an agent tool work.evaluators lists (codexcli: npm install -g @openai/codex, then codex login; claudecode: npm install -g @anthropic-ai/claude-code, then claude auth login; opencode: npm install -g opencode-ai, then opencode auth login), or list one you have, then run npx --no cockpit doctor --config <your cockpit.toml>',
230
+ };
231
+ }
232
+ async function runStep(executable, args, options) {
233
+ const argv = [executable.path, ...args];
234
+ const step = { id: `evaluator-${randomUUID()}`, argv, argv0: executable.path, cwd: options.cwd, env: { ...options.env },
235
+ timeoutMs: options.timeoutMs, spawnNotAfter: new Date(Date.now() + options.timeoutMs).toISOString(), stdoutLimitBytes: options.stdoutLimitBytes, stderrLimitBytes: STDERR_LIMIT_BYTES,
236
+ paths: [...options.paths ?? []], ports: [], labels: { ...options.labels }, expected: [],
237
+ consumedFiles: [{ path: executable.path, sha256: executable.sha256, maxBytes: executable.bytes, argvIndexes: [0], executable: true, executionMode: 'verified-path' },
238
+ ...options.consumed ?? []],
239
+ filesystemChecks: [{ path: options.cwd, expected: 'directory' }], compensation: [] };
240
+ const outcome = authenticatedExecutionOutcome(await executeProbe(step), step);
241
+ if (!outcome)
242
+ return { kind: 'failed', code: 'UNAUTHENTICATED', stdout: '', stderr: '' };
243
+ const decode = (bytes) => new TextDecoder('utf-8').decode(bytes);
244
+ if (outcome.kind === 'complete')
245
+ return { kind: 'complete', stdout: decode(outcome.stdout), stderr: decode(outcome.stderr) };
246
+ return { kind: 'failed', code: outcome.code, ...outcome.code === ENVIRONMENT_CODES.EXECUTION_STEP_FAILED ? { exitCode: outcome.execution.exitCode } : {},
247
+ stdout: decode(outcome.stdout), stderr: decode(outcome.stderr) };
248
+ }
249
+ // A probe's result holds for the executable bytes it ran, so a tool is probed once per process.
250
+ const probed = new Map();
251
+ async function probe(tool, executable, directories) {
252
+ const key = `${tool}\0${executable.path}\0${executable.sha256}`;
253
+ if (probed.has(key))
254
+ return probed.get(key);
255
+ const spec = TOOLS[tool];
256
+ const cwd = realpathSync(tmpdir());
257
+ const env = { PATH: directories.join(delimiter), TMPDIR: tmpdir() };
258
+ const options = { cwd, env, timeoutMs: PROBE_TIMEOUT_MS, stdoutLimitBytes: STDERR_LIMIT_BYTES, labels: { owner: 'independent-evaluator-probe', tool } };
259
+ const version = await runStep(executable, ['--version'], options);
260
+ const help = await runStep(executable, spec.help, options);
261
+ let problem = null;
262
+ if (version.kind !== 'complete' || !version.stdout.trim()) {
263
+ problem = `${spec.name} at ${executable.path} did not report its version: ${version.kind === 'complete' ? 'no output' : version.exitCode === undefined ? version.code : `exit ${version.exitCode}`}`;
264
+ }
265
+ else {
266
+ // OpenCode prints its help on stderr.
267
+ const text = help.kind === 'complete' ? `${help.stdout}\n${help.stderr}` : '';
268
+ const missing = spec.flags.filter(flag => !text.includes(flag));
269
+ if (missing.length)
270
+ problem = `${spec.name} at ${executable.path} does not offer ${[EVALUATOR_COMMANDS[tool], ...spec.help.slice(0, -1)].join(' ')} ${missing.join(' ')}; update it, then rerun`;
271
+ }
272
+ probed.set(key, problem);
273
+ return problem;
274
+ }
275
+ /**
276
+ * The default order puts the tool running this command last, so work done in one tool is graded by
277
+ * another first and the same tool, in a fresh headless session, is only the fallback. Measured
278
+ * 2026-11-03 (Claude Code 2.1.317, Codex 0.166.0): a shell command run by Claude Code has CLAUDECODE
279
+ * set and one run by Codex has CODEX_THREAD_ID set. Neither or both is not guessed at; OpenCode is
280
+ * not detected.
281
+ */
282
+ export function defaultEvaluatorOrder(tools, env) {
283
+ const runner = runningEvaluatorTool(env);
284
+ if (runner === undefined)
285
+ return tools;
286
+ return [...tools.filter(tool => tool !== runner), ...tools.filter(tool => tool === runner)];
287
+ }
288
+ /** Finds and probes each listed tool in order; one that is missing or too old is passed over and recorded. */
289
+ export async function locateEvaluators(tools, directories) {
290
+ const located = [];
291
+ const skipped = [];
292
+ for (const tool of tools) {
293
+ const found = await findExecutable(directories, EVALUATOR_COMMANDS[tool]);
294
+ if (found.state !== 'available') {
295
+ skipped.push({ tool, code: ENVIRONMENT_CODES.EVALUATOR_UNAVAILABLE, message: missingMessage(tool, found.reason) });
296
+ continue;
297
+ }
298
+ const problem = await probe(tool, found.value, directories);
299
+ if (problem)
300
+ skipped.push({ tool, code: ENVIRONMENT_CODES.EVALUATOR_UNAVAILABLE, message: problem });
301
+ else
302
+ located.push({ tool, executable: found.value });
303
+ }
304
+ return { located, skipped };
305
+ }
306
+ async function attempt(tool, executable, request) {
307
+ const spec = TOOLS[tool];
308
+ const observed = await findExecutable([dirname(executable.path)], executable.path.slice(dirname(executable.path).length + 1));
309
+ if (observed.state !== 'available' || JSON.stringify(observed.value) !== JSON.stringify(executable)) {
310
+ return { kind: 'unavailable', message: `${spec.name} at ${executable.path} is unavailable or changed since cockpit inspected it${observed.state === 'unavailable' ? ` (${observed.reason})` : ''}` };
311
+ }
312
+ const documentBytes = JSON.stringify({ ...request.document, restriction: spec.restriction });
313
+ if (Buffer.byteLength(documentBytes) > MAX_DOCUMENT_BYTES)
314
+ return { kind: 'invalid', message: 'the evaluation request exceeds its bound' };
315
+ const runRoot = await mkdtemp(join(realpathSync(tmpdir()), 'cockpit-work-actor-'));
316
+ const requestPath = join(runRoot, 'request.json');
317
+ const schemaPath = join(runRoot, 'schema.json');
318
+ const responsePath = join(runRoot, 'response.json');
319
+ try {
320
+ const staging = lstatSync(runRoot);
321
+ if (!staging.isDirectory() || staging.isSymbolicLink() || realpathSync(runRoot) !== runRoot || (staging.mode & 0o777) !== 0o700) {
322
+ return { kind: 'invalid', message: 'the evaluator run directory is not a private canonical directory' };
323
+ }
324
+ const schema = JSON.stringify(request.schema);
325
+ await writeFile(requestPath, documentBytes, { flag: 'wx', mode: 0o600 });
326
+ await writeFile(schemaPath, schema, { flag: 'wx', mode: 0o600 });
327
+ const requestDigest = sha256Hex(readFileSync(requestPath));
328
+ const prompt = `Read exactly the request JSON at ${requestPath} with SHA-256 ${requestDigest}; follow its fixed evaluator instruction and reply with only one JSON object, and no other text, that matches the JSON Schema at ${schemaPath}.`;
329
+ // Agent tools load instruction files and project configuration from their working root, and in
330
+ // the repository those are files the graded agent can write. The private run directory holds
331
+ // only the request and schema, so the repository is named in the request instead.
332
+ const run = { runRoot, repository: request.repository, schemaPath, schema, responsePath, prompt };
333
+ const args = spec.argv(run);
334
+ const env = { ...spec.fixedEnvironment, PATH: request.executableDirectories.join(delimiter) };
335
+ for (const name of spec.environment) {
336
+ const value = request.environment[name];
337
+ if (value)
338
+ env[name] = value;
339
+ }
340
+ const schemaIndex = args.indexOf(schemaPath);
341
+ const outcome = await runStep(executable, args, { cwd: runRoot, env, timeoutMs: request.timeoutMs, stdoutLimitBytes: STDOUT_LIMIT_BYTES, paths: [responsePath],
342
+ labels: { owner: 'independent-evaluator', role: request.role, tool, requestDigest },
343
+ consumed: schemaIndex === -1 ? [] : [{ path: schemaPath, sha256: sha256Hex(schema), maxBytes: ANSWER_LIMIT_BYTES, argvIndexes: [schemaIndex + 1], executable: false }] });
344
+ if (outcome.kind === 'failed') {
345
+ const reported = spec.cause(outcome.stdout, outcome.stderr);
346
+ if (outcome.code === ENVIRONMENT_CODES.EXECUTION_TIMEOUT) {
347
+ // OpenCode keeps retrying a rejected request (a free model's usage limit among them) until stopped.
348
+ return { kind: 'unavailable', message: `${spec.name} timed out after ${request.timeoutMs / 1_000} seconds${reported ? ` after reporting: ${reported.slice(-CAUSE_CHARACTERS)}` : ''}; raise ${request.timeoutField}` };
349
+ }
350
+ if (outcome.code === ENVIRONMENT_CODES.EXECUTION_STDOUT_LIMIT || outcome.code === ENVIRONMENT_CODES.EXECUTION_STDERR_LIMIT) {
351
+ return { kind: 'invalid', message: `${spec.name} output exceeded its ${outcome.code === ENVIRONMENT_CODES.EXECUTION_STDOUT_LIMIT ? STDOUT_LIMIT_BYTES : STDERR_LIMIT_BYTES}-byte capture` };
352
+ }
353
+ if (outcome.exitCode === undefined)
354
+ return { kind: 'unavailable', message: `${spec.name} did not run to completion: ${outcome.code}` };
355
+ const cause = (reported ?? outcome.stderr).trim().slice(-CAUSE_CHARACTERS) || 'no error event or stderr output';
356
+ return { kind: 'unavailable', message: `${spec.name} exited with status ${outcome.exitCode}: ${cause}; if it is not signed in, run ${spec.signIn}, then rerun` };
357
+ }
358
+ const { sessionId, answer } = spec.read(outcome.stdout, run);
359
+ if (answeredSessions.has(sessionId))
360
+ return { kind: 'invalid', message: `${spec.name} answered in session ${sessionId}, which already answered another evaluation` };
361
+ answeredSessions.add(sessionId);
362
+ if (Buffer.byteLength(answer) > ANSWER_LIMIT_BYTES)
363
+ return { kind: 'invalid', message: 'the evaluator answer exceeds its bound' };
364
+ const value = strictJson(answer, 'the evaluator answer', 'the object its output schema declares');
365
+ const violation = schemaViolation(request.schema, value, 'answer');
366
+ if (violation)
367
+ return { kind: 'invalid', message: `the evaluator answer does not match its output schema: ${violation}` };
368
+ return { kind: 'answer', value: value, sessionId };
369
+ }
370
+ catch (error) {
371
+ if (error instanceof ToolReportedFailure)
372
+ return { kind: 'unavailable', message: error.message };
373
+ return { kind: 'invalid', message: `${spec.name} answer could not be read: ${errorCause(error)}` };
374
+ }
375
+ finally {
376
+ await rm(runRoot, { recursive: true });
377
+ }
378
+ }
379
+ /**
380
+ * Runs the first tool in the chain that answers. A tool that cannot run here (missing, signed out,
381
+ * rate-limited, timed out) is recorded and the next is tried; an answer that cannot be trusted is refused
382
+ * outright, since trying another tool then would shop for a verdict.
383
+ */
384
+ export async function evaluate(request) {
385
+ const attempts = [...request.chain.skipped];
386
+ for (const { tool, executable } of request.chain.located) {
387
+ const result = await attempt(tool, executable, request);
388
+ if (result.kind === 'answer')
389
+ return { kind: 'answer', value: result.value, sessionId: result.sessionId, tool, fallbacks: attempts };
390
+ if (result.kind === 'invalid') {
391
+ return { kind: 'refusal', attempts: [...attempts, { tool, code: ENVIRONMENT_CODES.EVALUATOR_ANSWER_INVALID, message: result.message }],
392
+ code: 'EVALUATOR_ANSWER_INVALID', message: result.message,
393
+ fix: 'rerun the command; if the answer is refused again, list another evaluator first in work.evaluators' };
394
+ }
395
+ attempts.push({ tool, code: ENVIRONMENT_CODES.EVALUATOR_RUN_FAILED, message: result.message });
396
+ }
397
+ const message = attempts.map(row => row.message).join('; then ') || 'work.evaluators lists no evaluator';
398
+ if (attempts.every(row => row.code === ENVIRONMENT_CODES.EVALUATOR_UNAVAILABLE))
399
+ return { kind: 'refusal', attempts, ...evaluatorUnavailable(message) };
400
+ return { kind: 'refusal', attempts, message,
401
+ code: 'EVALUATOR_RUN_FAILED',
402
+ fix: 'read the message for the cause each evaluator reported: sign in to it, wait out its usage limit, or raise work.evaluator_timeout_seconds, then rerun the command' };
403
+ }
@@ -0,0 +1,33 @@
1
+ import type { WorkOrderCriterion } from '../work-orders/contract.js';
2
+ import type { CheckResult } from '../work-orders/store.js';
3
+ export interface ChangedFile {
4
+ readonly path: string;
5
+ readonly status: string;
6
+ readonly diff: string;
7
+ }
8
+ export interface GradingSubject {
9
+ readonly objective?: string;
10
+ readonly criteria: readonly WorkOrderCriterion[];
11
+ readonly nonGoals: readonly string[];
12
+ readonly scope: readonly string[];
13
+ readonly checks: readonly CheckResult[];
14
+ readonly changes: readonly ChangedFile[];
15
+ readonly outOfScope: readonly string[];
16
+ }
17
+ export type Verdict = Readonly<{
18
+ id: string;
19
+ verdict: 'met' | 'unmet' | 'unclear';
20
+ note: string;
21
+ }>;
22
+ export declare function gradingDocument(instruction: string, repositoryRoot: string, subject: GradingSubject): Readonly<Record<string, unknown>>;
23
+ export declare function gradingSchema(criteria: readonly WorkOrderCriterion[]): Readonly<Record<string, unknown>>;
24
+ /**
25
+ * The grade an answer supports. Each criterion must be answered once; a `met` verdict stands only
26
+ * when every quote it cites is found in the cited change (`<path>`) or check output (`check:<id>`),
27
+ * and otherwise reads as unclear with the reason.
28
+ */
29
+ export declare function readGradingAnswer(answer: Readonly<Record<string, unknown>>, subject: GradingSubject): Readonly<{
30
+ criteria: readonly Verdict[];
31
+ }> | Readonly<{
32
+ problem: string;
33
+ }>;
@@ -0,0 +1,57 @@
1
+ export function gradingDocument(instruction, repositoryRoot, subject) {
2
+ return {
3
+ protocol: 'cockpit-independent-grade/v1', instruction, repositoryRoot,
4
+ // No evaluator command line wires a browser, so a criterion about what a person sees cannot be observed.
5
+ browser: 'unavailable',
6
+ order: { objective: subject.objective ?? null, criteria: subject.criteria.map(row => ({ id: row.id, text: row.text, check: row.check ?? null })),
7
+ nonGoals: subject.nonGoals, scope: subject.scope },
8
+ checks: subject.checks, changes: subject.changes, outOfScope: subject.outOfScope,
9
+ };
10
+ }
11
+ export function gradingSchema(criteria) {
12
+ const string = { type: 'string' };
13
+ return { type: 'object', additionalProperties: false, required: ['criteria'], properties: {
14
+ criteria: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['id', 'verdict', 'note', 'evidence'], properties: {
15
+ id: { type: 'string', enum: criteria.map(row => row.id) },
16
+ verdict: { type: 'string', enum: ['met', 'unmet', 'unclear'] },
17
+ note: string,
18
+ evidence: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['source', 'quote'], properties: { source: string, quote: string } } },
19
+ } } },
20
+ } };
21
+ }
22
+ // A quote is evidence only when it is specific: at least this long, unless it is the whole text, and
23
+ // found exactly once in what it cites. A short or repeated quote matches almost anything, and would
24
+ // let a grader claim support it never observed.
25
+ const MIN_QUOTE_LENGTH = 8;
26
+ function quoted(textCited, quote) {
27
+ const trimmed = quote.trim();
28
+ if (!trimmed || trimmed.length < MIN_QUOTE_LENGTH && trimmed !== textCited.trim())
29
+ return false;
30
+ const first = textCited.indexOf(quote);
31
+ return first >= 0 && textCited.indexOf(quote, first + 1) < 0;
32
+ }
33
+ /**
34
+ * The grade an answer supports. Each criterion must be answered once; a `met` verdict stands only
35
+ * when every quote it cites is found in the cited change (`<path>`) or check output (`check:<id>`),
36
+ * and otherwise reads as unclear with the reason.
37
+ */
38
+ export function readGradingAnswer(answer, subject) {
39
+ const rows = answer.criteria;
40
+ const ids = rows.map(row => row.id);
41
+ if (new Set(ids).size !== ids.length || subject.criteria.some(row => !ids.includes(row.id))) {
42
+ return { problem: 'the grader did not answer each criterion exactly once' };
43
+ }
44
+ const cited = (source) => source.startsWith('check:')
45
+ ? subject.checks.find(row => row.criterion === source.slice('check:'.length))?.output
46
+ : subject.changes.find(row => row.path === source)?.diff;
47
+ return { criteria: subject.criteria.map(criterion => {
48
+ const row = rows.find(candidate => candidate.id === criterion.id);
49
+ if (row.verdict !== 'met')
50
+ return { id: row.id, verdict: row.verdict, note: row.note };
51
+ const unsupported = row.evidence.length === 0 ? 'cites no evidence'
52
+ : row.evidence.find(item => { const textCited = cited(item.source); return textCited === undefined || !quoted(textCited, item.quote); });
53
+ return unsupported === undefined ? { id: row.id, verdict: 'met', note: row.note }
54
+ : { id: row.id, verdict: 'unclear', note: `the grader said met but ${typeof unsupported === 'string' ? unsupported
55
+ : `its quote from ${unsupported.source} is not found exactly once there`}: ${row.note}` };
56
+ }) };
57
+ }