@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,681 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, open, readlink, realpath } from 'node:fs/promises';
3
+ import { isAbsolute, join, posix, relative, resolve } from 'node:path';
4
+ import { errorCause, snapshotJson, RefusalError, hasErrorCode } from '../environment/contract.js';
5
+ import { SURFACE_TARGETS } from './contract.js';
6
+ import { GUARD_SURFACE_TARGETS as INSTRUCTION_TARGETS, HARNESS_REGISTRY } from './harness-registry.js';
7
+ import { INSTRUCTION_POINTER_MARKERS } from '../environment/instruction-pointer.js';
8
+ import { exactKeys } from '../exact-keys.js';
9
+ const GUIDE_BYTE_LIMIT = 65_536;
10
+ const GUIDE_COUNT_LIMIT = 256;
11
+ const OBSERVATION_CONTENT_BYTE_LIMIT = 786_432;
12
+ const PROJECTION_RESULT_ENTRY_LIMIT = 1_024;
13
+ const SHA256 = /^[a-f0-9]{64}$/;
14
+ const SLUG = /^[a-z0-9][a-z0-9-]*$/;
15
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
16
+ function isObject(value) {
17
+ return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+ function capturedValue(value) {
20
+ const captured = snapshotJson(value);
21
+ return captured === undefined ? undefined : captured;
22
+ }
23
+ const RESULT_BOUNDS_DIAGNOSTIC = Object.freeze({
24
+ code: 'PROJECTION_RESULT_BOUNDS',
25
+ message: 'sibling projection result exceeds the immutable snapshot bound',
26
+ });
27
+ const OBSERVATION_RESULT_BOUNDS = Object.freeze({
28
+ kind: 'refusal',
29
+ diagnostic: RESULT_BOUNDS_DIAGNOSTIC,
30
+ });
31
+ const PLANNER_RESULT_BOUNDS = Object.freeze({
32
+ kind: 'refusal',
33
+ diagnostic: RESULT_BOUNDS_DIAGNOSTIC,
34
+ });
35
+ function diagnostic(code, message, details = {}) {
36
+ return capturedValue({ code, message, ...details }) ?? RESULT_BOUNDS_DIAGNOSTIC;
37
+ }
38
+ function refuse(value) {
39
+ return capturedValue({ kind: 'refusal', diagnostic: value }) ?? PLANNER_RESULT_BOUNDS;
40
+ }
41
+ function observedOutcome(value) {
42
+ return capturedValue(value) ?? OBSERVATION_RESULT_BOUNDS;
43
+ }
44
+ function safeGuidePath(path) {
45
+ if (path.length === 0 || path.length > 4096 || path.startsWith('/') || /^[a-z]:[/\\]/i.test(path)
46
+ || path.includes('\\') || /[\x00-\x1f\x7f]/.test(path))
47
+ return false;
48
+ return path.split('/').every(segment => segment.length > 0 && segment !== '.' && segment !== '..');
49
+ }
50
+ function contained(root, path) {
51
+ const fromRoot = relative(root, path);
52
+ return fromRoot === '' || !fromRoot.startsWith('..') && !isAbsolute(fromRoot);
53
+ }
54
+ function snapshotRequests(input) {
55
+ const snapshot = snapshotJson(input);
56
+ if (!isObject(snapshot) || !exactKeys(snapshot, ['allowedRoots', 'requests']) || !Array.isArray(snapshot.requests)
57
+ || snapshot.requests.length > GUIDE_COUNT_LIMIT)
58
+ return undefined;
59
+ const allowedRoots = stringArray(snapshot.allowedRoots);
60
+ if (!allowedRoots || allowedRoots.length === 0 || allowedRoots.length > GUIDE_COUNT_LIMIT
61
+ || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some(root => !isAbsolute(root)))
62
+ return undefined;
63
+ const result = [];
64
+ const identities = new Set();
65
+ for (const request of snapshot.requests) {
66
+ if (!isObject(request) || !exactKeys(request, ['slug', 'root', 'sourcePath'])
67
+ || typeof request.slug !== 'string' || !SLUG.test(request.slug)
68
+ || typeof request.root !== 'string' || !isAbsolute(request.root)
69
+ || typeof request.sourcePath !== 'string' || !isAbsolute(request.sourcePath))
70
+ return undefined;
71
+ const sourcePath = request.sourcePath;
72
+ if (!allowedRoots.includes(request.root) || !contained(request.root, sourcePath))
73
+ return undefined;
74
+ const identity = `${request.slug}\0${sourcePath}`;
75
+ if (identities.has(identity))
76
+ return undefined;
77
+ identities.add(identity);
78
+ result.push({ slug: request.slug, root: request.root, sourcePath });
79
+ }
80
+ return { requests: result, allowedRoots };
81
+ }
82
+ function statIdentity(stat) {
83
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
84
+ }
85
+ function inodeIdentity(stat) {
86
+ return `${stat.dev}:${stat.ino}`;
87
+ }
88
+ function declaredRootIdentity(entry, linkTarget) {
89
+ return entry.isSymbolicLink() ? `${statIdentity(entry)}:${linkTarget ?? ''}` : inodeIdentity(entry);
90
+ }
91
+ async function rootEntryIdentity(path) {
92
+ const entry = await lstat(path, { bigint: true });
93
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
94
+ throw new RefusalError('sibling root is not a directory or symlink');
95
+ const linkTarget = entry.isSymbolicLink() ? await readlink(path) : undefined;
96
+ return { entry, identity: declaredRootIdentity(entry, linkTarget) };
97
+ }
98
+ async function captureRootAuthority(declared) {
99
+ let handle;
100
+ let failure;
101
+ try {
102
+ const before = await rootEntryIdentity(declared);
103
+ handle = await open(declared, 'r');
104
+ const directory = await handle.stat({ bigint: true });
105
+ const canonical = await realpath(declared);
106
+ const direct = await lstat(canonical, { bigint: true });
107
+ const after = await rootEntryIdentity(declared);
108
+ if (!directory.isDirectory() || !direct.isDirectory() || before.identity !== after.identity
109
+ || inodeIdentity(directory) !== inodeIdentity(direct)
110
+ || !before.entry.isSymbolicLink() && inodeIdentity(before.entry) !== inodeIdentity(directory)) {
111
+ failure = diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed while its authority was captured: ${declared}`, { target: declared });
112
+ }
113
+ else {
114
+ return {
115
+ declared,
116
+ canonical,
117
+ declaredIdentity: before.identity,
118
+ directoryIdentity: inodeIdentity(directory),
119
+ handle,
120
+ };
121
+ }
122
+ }
123
+ catch (error) {
124
+ failure = diagnostic('PROJECTION_GUIDE_UNAVAILABLE', `declared sibling root could not be observed: ${declared}: ${errorCause(error)}`, { target: declared });
125
+ }
126
+ try {
127
+ await handle?.close();
128
+ }
129
+ catch (error) {
130
+ return diagnostic('PROJECTION_GUIDE_CLOSE_FAILED', `sibling root authority descriptor close failed: ${declared}: ${errorCause(error)}`, { target: declared });
131
+ }
132
+ return failure ?? diagnostic('PROJECTION_GUIDE_UNAVAILABLE', `declared sibling root observation produced no result: ${declared}`, { target: declared });
133
+ }
134
+ async function rootAuthorityStable(root) {
135
+ try {
136
+ const directory = await root.handle.stat({ bigint: true });
137
+ const declared = await rootEntryIdentity(root.declared);
138
+ const canonical = await realpath(root.declared);
139
+ const direct = await lstat(canonical, { bigint: true });
140
+ return directory.isDirectory() && direct.isDirectory()
141
+ && root.declaredIdentity === declared.identity && root.canonical === canonical
142
+ && root.directoryIdentity === inodeIdentity(directory) && root.directoryIdentity === inodeIdentity(direct);
143
+ }
144
+ catch {
145
+ // an unobservable root is no longer stable; the caller reports the change.
146
+ return false;
147
+ }
148
+ }
149
+ async function finishRootObservation(roots, outcome) {
150
+ let closeFailure;
151
+ for (const root of roots) {
152
+ try {
153
+ await root.handle.close();
154
+ }
155
+ catch (error) {
156
+ closeFailure ??= diagnostic('PROJECTION_GUIDE_CLOSE_FAILED', `sibling root authority descriptor close failed: ${root.declared}: ${errorCause(error)}`, { target: root.declared });
157
+ }
158
+ }
159
+ return closeFailure ? observedOutcome({ kind: 'refusal', diagnostic: closeFailure }) : outcome;
160
+ }
161
+ async function pathIdentity(sourcePath) {
162
+ const entry = await lstat(sourcePath, { bigint: true });
163
+ if (!entry.isFile() && !entry.isSymbolicLink())
164
+ throw new RefusalError('guide entry type is unsupported');
165
+ const entryType = entry.isSymbolicLink() ? 'symlink' : 'file';
166
+ const linkTarget = entryType === 'symlink' ? await readlink(sourcePath) : undefined;
167
+ const contentPath = await realpath(sourcePath);
168
+ const target = await lstat(contentPath, { bigint: true });
169
+ return { entry, entryType, ...(linkTarget === undefined ? {} : { linkTarget }), contentPath, target };
170
+ }
171
+ async function readGuide(request, requestRoot) {
172
+ if (!await rootAuthorityStable(requestRoot)) {
173
+ return diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed before guide observation: ${request.root}`, { target: request.root });
174
+ }
175
+ let pathBefore;
176
+ try {
177
+ pathBefore = await pathIdentity(request.sourcePath);
178
+ }
179
+ catch (error) {
180
+ if (hasErrorCode(error, 'ENOENT'))
181
+ return 'missing';
182
+ return diagnostic('PROJECTION_GUIDE_UNAVAILABLE', `guide entry could not be observed: ${request.sourcePath}`, { target: request.sourcePath });
183
+ }
184
+ if (!await rootAuthorityStable(requestRoot)) {
185
+ return diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed during guide entry observation: ${request.root}`, { target: request.root });
186
+ }
187
+ if (!contained(requestRoot.canonical, pathBefore.contentPath)) {
188
+ return diagnostic('PROJECTION_GUIDE_ESCAPE', `guide resolves outside its declared sibling root: ${request.sourcePath}`, {
189
+ target: request.sourcePath, expected: requestRoot.canonical, actual: pathBefore.contentPath,
190
+ });
191
+ }
192
+ let handle;
193
+ let result;
194
+ try {
195
+ handle = await open(request.sourcePath, 'r');
196
+ }
197
+ catch (error) {
198
+ if (!await rootAuthorityStable(requestRoot)) {
199
+ return diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed before guide open: ${request.root}`, { target: request.root });
200
+ }
201
+ return diagnostic('PROJECTION_GUIDE_UNAVAILABLE', `guide content could not be opened: ${request.sourcePath}: ${errorCause(error)}`, { target: request.sourcePath });
202
+ }
203
+ try {
204
+ const before = await handle.stat({ bigint: true });
205
+ if (!await rootAuthorityStable(requestRoot)) {
206
+ result = diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed during guide open: ${request.root}`, { target: request.root });
207
+ }
208
+ else if (!before.isFile() || before.size > BigInt(GUIDE_BYTE_LIMIT)
209
+ || inodeIdentity(before) !== inodeIdentity(pathBefore.target)) {
210
+ result = diagnostic('PROJECTION_GUIDE_CHANGED', `guide changed before its bounded read: ${request.sourcePath}`, { target: request.sourcePath });
211
+ }
212
+ else {
213
+ const buffer = Buffer.alloc(Number(before.size) + 1);
214
+ let offset = 0;
215
+ let rootChanged = false;
216
+ while (offset < buffer.byteLength) {
217
+ if (!await rootAuthorityStable(requestRoot)) {
218
+ rootChanged = true;
219
+ break;
220
+ }
221
+ const read = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
222
+ if (!await rootAuthorityStable(requestRoot)) {
223
+ rootChanged = true;
224
+ break;
225
+ }
226
+ if (read.bytesRead === 0)
227
+ break;
228
+ offset += read.bytesRead;
229
+ }
230
+ const after = await handle.stat({ bigint: true });
231
+ let pathAfter;
232
+ try {
233
+ if (!await rootAuthorityStable(requestRoot))
234
+ rootChanged = true;
235
+ pathAfter = await pathIdentity(request.sourcePath);
236
+ if (!await rootAuthorityStable(requestRoot))
237
+ rootChanged = true;
238
+ }
239
+ catch {
240
+ // A pathname that disappears or becomes unreadable during the read is an identity change.
241
+ }
242
+ if (rootChanged) {
243
+ result = diagnostic('PROJECTION_GUIDE_CHANGED', `declared sibling root changed during guide read: ${request.root}`, { target: request.root });
244
+ }
245
+ else if (offset > GUIDE_BYTE_LIMIT || statIdentity(before) !== statIdentity(after) || !pathAfter
246
+ || statIdentity(pathBefore.entry) !== statIdentity(pathAfter.entry)
247
+ || pathBefore.entryType !== pathAfter.entryType || pathBefore.linkTarget !== pathAfter.linkTarget
248
+ || pathBefore.contentPath !== pathAfter.contentPath
249
+ || statIdentity(pathBefore.target) !== statIdentity(pathAfter.target)
250
+ || inodeIdentity(after) !== inodeIdentity(pathAfter.target)) {
251
+ result = diagnostic('PROJECTION_GUIDE_CHANGED', `guide changed during its bounded read: ${request.sourcePath}`, { target: request.sourcePath });
252
+ }
253
+ else if (!contained(requestRoot.canonical, pathAfter.contentPath)) {
254
+ result = diagnostic('PROJECTION_GUIDE_ESCAPE', `guide resolves outside its declared sibling root: ${request.sourcePath}`, {
255
+ target: request.sourcePath, expected: requestRoot.canonical, actual: pathAfter.contentPath,
256
+ });
257
+ }
258
+ else {
259
+ let content;
260
+ try {
261
+ content = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, offset));
262
+ }
263
+ catch {
264
+ // undecodable bytes are malformed input the caller refuses.
265
+ result = diagnostic('PROJECTION_GUIDE_UNAVAILABLE', `guide is not valid UTF-8: ${request.sourcePath}`, { target: request.sourcePath });
266
+ }
267
+ if (content !== undefined) {
268
+ result = capturedValue({
269
+ slug: request.slug,
270
+ sourcePath: request.sourcePath,
271
+ sourceEntryType: pathAfter.entryType,
272
+ ...(pathAfter.linkTarget === undefined ? {} : { sourceLinkTarget: pathAfter.linkTarget }),
273
+ contentPath: pathAfter.contentPath,
274
+ content,
275
+ contentBytes: offset,
276
+ contentSha256: createHash('sha256').update(buffer.subarray(0, offset)).digest('hex'),
277
+ contentIdentity: statIdentity(after),
278
+ }) ?? RESULT_BOUNDS_DIAGNOSTIC;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ catch (error) {
284
+ result = diagnostic('PROJECTION_GUIDE_READ_FAILED', `guide descriptor read failed: ${request.sourcePath}: ${errorCause(error)}`, { target: request.sourcePath });
285
+ }
286
+ try {
287
+ await handle.close();
288
+ }
289
+ catch (error) {
290
+ return diagnostic('PROJECTION_GUIDE_CLOSE_FAILED', `guide descriptor close failed: ${request.sourcePath}: ${errorCause(error)}`, { target: request.sourcePath });
291
+ }
292
+ return result ?? diagnostic('PROJECTION_GUIDE_READ_FAILED', `guide descriptor read produced no bounded result: ${request.sourcePath}`, { target: request.sourcePath });
293
+ }
294
+ export async function observeSiblingGuides(input) {
295
+ const captured = snapshotRequests(input);
296
+ if (!captured)
297
+ return observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_INPUT_INVALID', 'guide observation input is invalid') });
298
+ const allowedRoots = [];
299
+ for (const declared of captured.allowedRoots) {
300
+ const root = await captureRootAuthority(declared);
301
+ if ('code' in root) {
302
+ const kind = root.code === 'PROJECTION_GUIDE_CHANGED' || root.code === 'PROJECTION_GUIDE_CLOSE_FAILED' ? 'refusal' : 'unavailable';
303
+ return finishRootObservation(allowedRoots, observedOutcome({ kind, diagnostic: root }));
304
+ }
305
+ allowedRoots.push(root);
306
+ }
307
+ if (new Set(allowedRoots.map(root => root.canonical)).size !== allowedRoots.length) {
308
+ return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_ROOT_AMBIGUOUS', 'declared sibling roots resolve to the same canonical authority') }));
309
+ }
310
+ const observations = [];
311
+ const missing = [];
312
+ let contentBytes = 0;
313
+ for (const request of captured.requests) {
314
+ const mostSpecificRoot = allowedRoots.filter(root => contained(root.declared, request.sourcePath))
315
+ .sort((left, right) => {
316
+ const leftDepth = relative(left.declared, request.sourcePath).split(/[\\/]/).length;
317
+ const rightDepth = relative(right.declared, request.sourcePath).split(/[\\/]/).length;
318
+ return leftDepth - rightDepth || left.declared.localeCompare(right.declared);
319
+ })[0];
320
+ if (!mostSpecificRoot || request.root !== mostSpecificRoot.declared) {
321
+ return finishRootObservation(allowedRoots, observedOutcome({ kind: 'refusal', diagnostic: diagnostic('PROJECTION_INPUT_INVALID', `guide request is not bound to its most-specific sibling root: ${request.sourcePath}`, { target: request.sourcePath }) }));
322
+ }
323
+ const requestRoot = allowedRoots.find(root => root.declared === request.root);
324
+ const observed = await readGuide(request, requestRoot);
325
+ if (observed === 'missing')
326
+ missing.push(request.sourcePath);
327
+ else if ('code' in observed) {
328
+ const refusal = ['PROJECTION_GUIDE_CHANGED', 'PROJECTION_GUIDE_ESCAPE', 'PROJECTION_GUIDE_READ_FAILED',
329
+ 'PROJECTION_GUIDE_CLOSE_FAILED', 'PROJECTION_RESULT_BOUNDS'].includes(observed.code);
330
+ return finishRootObservation(allowedRoots, observedOutcome({ kind: refusal ? 'refusal' : 'unavailable', diagnostic: observed }));
331
+ }
332
+ else {
333
+ contentBytes += observed.contentBytes;
334
+ if (contentBytes > OBSERVATION_CONTENT_BYTE_LIMIT)
335
+ return finishRootObservation(allowedRoots, OBSERVATION_RESULT_BOUNDS);
336
+ observations.push(observed);
337
+ }
338
+ }
339
+ return finishRootObservation(allowedRoots, observedOutcome({ kind: 'complete', observations, missing }));
340
+ }
341
+ function target(value) {
342
+ return typeof value === 'string' && SURFACE_TARGETS.includes(value);
343
+ }
344
+ function stringArray(value) {
345
+ return Array.isArray(value) && value.every(item => typeof item === 'string') ? value : undefined;
346
+ }
347
+ function capturePlannerInput(input) {
348
+ const value = snapshotJson(input);
349
+ if (!isObject(value) || !exactKeys(value, [
350
+ 'canonicalMainCheckout', 'siblingsRoot', 'activeProfiles', 'guideCapLines', 'siblings',
351
+ 'mountedPaths', 'directoryGrants', 'guideObservations', 'standingSources', 'loadCharacterizations',
352
+ ], ['invocationCwd', 'targets'])
353
+ || typeof value.canonicalMainCheckout !== 'string' || !isAbsolute(value.canonicalMainCheckout)
354
+ || typeof value.siblingsRoot !== 'string' || value.siblingsRoot.length === 0
355
+ || typeof value.guideCapLines !== 'number' || !Number.isSafeInteger(value.guideCapLines) || value.guideCapLines <= 0
356
+ || !Array.isArray(value.siblings) || value.siblings.length > GUIDE_COUNT_LIMIT
357
+ || !Array.isArray(value.mountedPaths) || value.mountedPaths.length > GUIDE_COUNT_LIMIT
358
+ || !Array.isArray(value.directoryGrants) || !Array.isArray(value.guideObservations)
359
+ || !Array.isArray(value.standingSources) || !isObject(value.loadCharacterizations)
360
+ || value.directoryGrants.length > GUIDE_COUNT_LIMIT * SURFACE_TARGETS.length
361
+ || value.guideObservations.length > GUIDE_COUNT_LIMIT || value.standingSources.length > GUIDE_COUNT_LIMIT * SURFACE_TARGETS.length)
362
+ return undefined;
363
+ const targets = value.targets === undefined ? SURFACE_TARGETS : stringArray(value.targets);
364
+ if (!targets || targets.length === 0 || new Set(targets).size !== targets.length || !targets.every(item => target(item)))
365
+ return undefined;
366
+ const activeProfiles = stringArray(value.activeProfiles);
367
+ if (!activeProfiles || new Set(activeProfiles).size !== activeProfiles.length
368
+ || activeProfiles.some(profile => !/^[a-z0-9][a-z0-9.-]*$/.test(profile)))
369
+ return undefined;
370
+ const siblings = [];
371
+ const siblingSlugs = new Set();
372
+ for (const row of value.siblings) {
373
+ if (!isObject(row) || !exactKeys(row, ['slug', 'mount', 'directoryAuthority'], ['declaredOverride', 'guides'])
374
+ || typeof row.slug !== 'string' || !SLUG.test(row.slug) || siblingSlugs.has(row.slug)
375
+ || typeof row.mount !== 'string' || !/^(?:always|host|none|profile:[a-z0-9][a-z0-9-]*)$/.test(row.mount)
376
+ || typeof row.directoryAuthority !== 'string' || row.directoryAuthority.length === 0
377
+ || row.declaredOverride !== undefined && (typeof row.declaredOverride !== 'string' || !isAbsolute(row.declaredOverride)))
378
+ return undefined;
379
+ const guides = row.guides === undefined ? ['CLAUDE.md', 'AGENTS.md'] : stringArray(row.guides);
380
+ if (!guides || guides.length > GUIDE_COUNT_LIMIT || new Set(guides).size !== guides.length || guides.some(guide => !safeGuidePath(guide)))
381
+ return undefined;
382
+ siblingSlugs.add(row.slug);
383
+ siblings.push({
384
+ slug: row.slug,
385
+ mount: row.mount,
386
+ directoryAuthority: row.directoryAuthority,
387
+ ...(row.declaredOverride === undefined ? {} : { declaredOverride: row.declaredOverride }),
388
+ guides: [...guides],
389
+ });
390
+ }
391
+ const mountedPaths = [];
392
+ const mountIds = new Set();
393
+ for (const row of value.mountedPaths) {
394
+ if (!isObject(row) || !exactKeys(row, ['slug', 'path']) || typeof row.slug !== 'string' || !SLUG.test(row.slug)
395
+ || mountIds.has(row.slug) || typeof row.path !== 'string' || !isAbsolute(row.path))
396
+ return undefined;
397
+ mountIds.add(row.slug);
398
+ mountedPaths.push({ slug: row.slug, path: row.path });
399
+ }
400
+ const directoryGrants = [];
401
+ const grantIds = new Set();
402
+ for (const row of value.directoryGrants) {
403
+ if (!isObject(row) || !exactKeys(row, ['slug', 'target', 'path']) || typeof row.slug !== 'string' || !SLUG.test(row.slug)
404
+ || !target(row.target) || typeof row.path !== 'string' || !isAbsolute(row.path))
405
+ return undefined;
406
+ const id = `${row.slug}\0${row.target}`;
407
+ if (grantIds.has(id))
408
+ return undefined;
409
+ grantIds.add(id);
410
+ directoryGrants.push({ slug: row.slug, target: row.target, path: row.path });
411
+ }
412
+ const guideObservations = [];
413
+ const observationIds = new Set();
414
+ for (const row of value.guideObservations) {
415
+ if (!isObject(row) || !exactKeys(row, ['slug', 'sourcePath', 'sourceEntryType', 'contentPath', 'content', 'contentBytes', 'contentSha256', 'contentIdentity'], ['sourceLinkTarget'])
416
+ || typeof row.slug !== 'string' || !SLUG.test(row.slug) || typeof row.sourcePath !== 'string' || !isAbsolute(row.sourcePath)
417
+ || typeof row.contentPath !== 'string' || !isAbsolute(row.contentPath)
418
+ || row.sourceEntryType !== 'file' && row.sourceEntryType !== 'symlink'
419
+ || row.sourceLinkTarget !== undefined && typeof row.sourceLinkTarget !== 'string'
420
+ || typeof row.content !== 'string' || typeof row.contentBytes !== 'number' || !Number.isSafeInteger(row.contentBytes)
421
+ || Buffer.byteLength(row.content) !== row.contentBytes || row.contentBytes > GUIDE_BYTE_LIMIT
422
+ || typeof row.contentSha256 !== 'string' || !SHA256.test(row.contentSha256)
423
+ || createHash('sha256').update(row.content).digest('hex') !== row.contentSha256
424
+ || typeof row.contentIdentity !== 'string' || row.contentIdentity.length === 0
425
+ || observationIds.has(`${row.slug}\0${row.sourcePath}`))
426
+ return undefined;
427
+ observationIds.add(`${row.slug}\0${row.sourcePath}`);
428
+ guideObservations.push(row);
429
+ }
430
+ const standingSources = [];
431
+ const standingIds = new Set();
432
+ for (const row of value.standingSources) {
433
+ if (!isObject(row) || !exactKeys(row, ['target', 'path', 'content', 'scope']) || !target(row.target)
434
+ || typeof row.path !== 'string' || !safeGuidePath(row.path) || typeof row.content !== 'string'
435
+ || row.scope !== 'always' && row.scope !== 'scoped' || standingIds.has(`${row.target}\0${row.path}`))
436
+ return undefined;
437
+ standingIds.add(`${row.target}\0${row.path}`);
438
+ standingSources.push(row);
439
+ }
440
+ const loadCharacterizations = {};
441
+ if (Object.keys(value.loadCharacterizations).some(key => !INSTRUCTION_TARGETS.includes(key)))
442
+ return undefined;
443
+ for (const harness of INSTRUCTION_TARGETS) {
444
+ const row = value.loadCharacterizations[harness];
445
+ if (row === undefined || row === null)
446
+ continue;
447
+ if (!isObject(row) || !exactKeys(row, ['measuredOn', 'provenance', 'metric', 'scopeSemantics'])
448
+ || typeof row.measuredOn !== 'string' || !DATE.test(row.measuredOn)
449
+ || typeof row.provenance !== 'string' || row.provenance.length === 0
450
+ || row.metric !== 'lines' || row.scopeSemantics !== 'conditional' && row.scopeSemantics !== 'full')
451
+ return undefined;
452
+ loadCharacterizations[harness] = row;
453
+ }
454
+ return {
455
+ canonicalMainCheckout: value.canonicalMainCheckout,
456
+ siblingsRoot: value.siblingsRoot,
457
+ activeProfiles,
458
+ guideCapLines: value.guideCapLines,
459
+ siblings,
460
+ mountedPaths,
461
+ directoryGrants,
462
+ guideObservations,
463
+ standingSources,
464
+ loadCharacterizations,
465
+ targets: SURFACE_TARGETS.filter(item => targets.includes(item)),
466
+ };
467
+ }
468
+ function selected(row, profiles) {
469
+ return row.mount === 'always' || row.mount.startsWith('profile:') && profiles.has(row.mount.slice(8));
470
+ }
471
+ function siblingDirectory(input, row) {
472
+ if (row.declaredOverride !== undefined)
473
+ return resolve(row.declaredOverride);
474
+ if (isAbsolute(row.directoryAuthority))
475
+ return resolve(row.directoryAuthority);
476
+ return resolve(input.canonicalMainCheckout, input.siblingsRoot, row.directoryAuthority);
477
+ }
478
+ // The region and owner markers ownership parses in the root AGENTS.md.
479
+ const REGION_MARKER_PREFIXES = ['<!-- cockpit:instructions:', '<!-- cockpit:owner:'];
480
+ const POINTER_BLOCK = new RegExp(`${INSTRUCTION_POINTER_MARKERS.begin}[\\s\\S]*?${INSTRUCTION_POINTER_MARKERS.end}\\n?`, 'g');
481
+ const AGENTS_IMPORT_LINE = /^[ \t]*@(?:\.\/)?AGENTS\.md[ \t]*$/gm;
482
+ /**
483
+ * The text of a guide as the root AGENTS.md carries it. The pointer setup appends to a sibling guide
484
+ * points back at this cockpit, so the copy drops it; a CLAUDE.md line importing the sibling's own
485
+ * AGENTS.md is dropped when that AGENTS.md is copied too, since in the cockpit root the import would
486
+ * name the cockpit's AGENTS.md instead. Undefined when the guide holds cockpit's own region or owner
487
+ * markers, which would end or split the generated region.
488
+ */
489
+ function guideText(content, dropAgentsImport) {
490
+ if (REGION_MARKER_PREFIXES.some(marker => content.includes(marker)))
491
+ return undefined;
492
+ let text = content.replace(POINTER_BLOCK, '');
493
+ if (dropAgentsImport)
494
+ text = text.replace(AGENTS_IMPORT_LINE, '');
495
+ return text.replace(/^\s*\n/, '').trimEnd();
496
+ }
497
+ /** The marker records which guide, at which digest, the block was copied from, so check can name a stale copy. */
498
+ function siblingGuideMarker(sibling, guide, sha256) {
499
+ return `<!-- cockpit:sibling-guide sibling=${sibling} guide=${guide} sha256=${sha256} -->`;
500
+ }
501
+ export const SIBLING_GUIDE_MARKER = /<!-- cockpit:sibling-guide sibling=([a-z0-9][a-z0-9-]*) guide=(.+?) sha256=([a-f0-9]{64}) -->/g;
502
+ function guideBlock(sibling, guide, sha256, text) {
503
+ // Shared Markdown contributions are joined by one newline; the leading one separates the heading.
504
+ return `\n## Instructions for sibling \`${sibling}\` (its ${guide})\n\n${siblingGuideMarker(sibling, guide, sha256)}\n\n${text}`;
505
+ }
506
+ const GENERATED_ONLY_HARNESSES = HARNESS_REGISTRY.filter((row) => row.native.state === 'generated');
507
+ const INSTRUCTION_CAPABILITIES = Object.freeze([
508
+ // Each probe ran the tool headless in a cockpit beside two siblings. The declared sibling's guide
509
+ // gave a word the tool could state only if it had loaded the guide; an undeclared sibling's guide
510
+ // and a decoy file beside the guide gave words it must not state, the broken controls. Copying the
511
+ // guide into the root AGENTS.md is the one route every tool loaded; each route that left the guide
512
+ // in its sibling failed in Claude Code or Codex, so sync writes none.
513
+ Object.freeze({
514
+ target: 'claudecode', mechanism: 'root-agents-guide', capability: 'instructed', evidence: 'not_applicable',
515
+ characterizedOn: '2026-11-03', measuredVersion: '2.1.317',
516
+ provenance: 'in a workspace Claude Code had not trusted, a sibling guide copied into the root AGENTS.md that CLAUDE.md imports loaded, '
517
+ + 'with or without Claude Code\'s user option for reading AGENTS.md, while the undeclared sibling\'s guide and the decoy did not; '
518
+ + 'nothing loaded through a committed CLAUDE.md @-import of a link to the guide or of its relative path, through '
519
+ + 'permissions.additionalDirectories with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD in the settings env (ignored until a person trusts the workspace), '
520
+ + 'or through --add-dir with that variable set',
521
+ }),
522
+ Object.freeze({
523
+ target: 'codexcli', mechanism: 'root-agents-guide', capability: 'instructed', evidence: 'not_applicable',
524
+ characterizedOn: '2026-11-03', measuredVersion: '0.166.0',
525
+ provenance: 'a sibling guide copied into the root AGENTS.md loaded while the undeclared sibling\'s guide and the decoy did not; '
526
+ + 'nothing loaded through a line in AGENTS.md telling the agent to read a link to the guide, through --add-dir naming the sibling, '
527
+ + 'or through project_doc_fallback_filenames naming the link, and Codex dropped the end of a project AGENTS.md past 32 KiB',
528
+ }),
529
+ Object.freeze({
530
+ target: 'opencode', mechanism: 'root-agents-guide', capability: 'instructed', evidence: 'not_applicable',
531
+ characterizedOn: '2026-11-03', measuredVersion: '1.19.5',
532
+ provenance: 'a sibling guide copied into the root AGENTS.md loaded, as did a guide named through a link in the opencode.jsonc instructions list, '
533
+ + 'while the undeclared sibling\'s guide and the decoy did not',
534
+ }),
535
+ // Sync writes Cursor no rules file of its own for cockpit's instructions: they reach it only
536
+ // through the root AGENTS.md, and `.cursor/rules/` holds rules overlays alone.
537
+ ...GENERATED_ONLY_HARNESSES.map(row => ({
538
+ target: row.target, mechanism: 'rulesync-rules', capability: row.native.state, evidence: 'pending_evidence',
539
+ characterizedOn: row.native.measuredOn, measuredVersion: null,
540
+ provenance: row.target === 'cursor'
541
+ ? 'cockpit\'s instructions and the sibling guides it copies reach Cursor only through the root AGENTS.md, and .cursor/rules holds only rules overlays; no run of the tool beside a broken control has shown that it loads either'
542
+ : row.native.reason,
543
+ })),
544
+ ]);
545
+ export function shippedInstructionCapabilities() {
546
+ return INSTRUCTION_CAPABILITIES.map(row => ({ ...row }));
547
+ }
548
+ // `.codex/` sits one level below the cockpit root, so a root-relative path gains one `..`. This is
549
+ // string arithmetic on purpose: `relative('.codex', path)` resolves both against the process's
550
+ // working directory, and a sync run from inside a sibling then wrote a root that named the wrong place.
551
+ export function codexWritableRoot(rootRelativePath) {
552
+ return posix.join('..', rootRelativePath);
553
+ }
554
+ // The spelling each tool resolves to the sibling: Codex reads a relative root from `.codex/`, and
555
+ // OpenCode, which matches absolute paths only, reads the absolute directory `sync` writes locally.
556
+ function grantSpelling(targetName, path, slug) {
557
+ if (targetName === 'claudecode')
558
+ return `permissions.additionalDirectories:${path}`;
559
+ if (targetName === 'codexcli')
560
+ return `sandbox_workspace_write.writable_roots:${codexWritableRoot(path)}`;
561
+ if (targetName === 'opencode')
562
+ return `permission.external_directory:{file:.cockpit/local/opencode-grant-${slug}}/**=allow`;
563
+ if (targetName === 'cursor')
564
+ return `permission.read+write:${path}/**=allow`;
565
+ return `permission.read+edit:${path}/**=allow`;
566
+ }
567
+ function lines(content) {
568
+ if (content.length === 0)
569
+ return 0;
570
+ const matches = content.match(/\n/g)?.length ?? 0;
571
+ return matches + (content.endsWith('\n') ? 0 : 1);
572
+ }
573
+ export function planSiblingProjection(input) {
574
+ const captured = capturePlannerInput(input);
575
+ if (!captured)
576
+ return refuse(diagnostic('PROJECTION_INPUT_INVALID', 'sibling projection input is invalid'));
577
+ const profiles = new Set(captured.activeProfiles);
578
+ const active = captured.siblings.filter(row => selected(row, profiles));
579
+ const activeSlugs = new Set(active.map(row => row.slug));
580
+ if (captured.mountedPaths.length !== active.length || captured.mountedPaths.some(row => !activeSlugs.has(row.slug))
581
+ || captured.directoryGrants.length !== active.length * captured.targets.length
582
+ || captured.directoryGrants.some(row => !activeSlugs.has(row.slug))) {
583
+ return refuse(diagnostic('PROJECTION_INPUT_INVALID', 'mounted paths and grants must exactly cover active sibling rows'));
584
+ }
585
+ const guides = [];
586
+ const grants = [];
587
+ for (const sibling of active) {
588
+ const resolvedDirectory = siblingDirectory(captured, sibling);
589
+ const mounted = captured.mountedPaths.find(row => row.slug === sibling.slug);
590
+ if (mounted.path !== resolvedDirectory) {
591
+ return refuse(diagnostic('PROJECTION_MOUNT_MISMATCH', `projection and generated environment mount disagree for ${sibling.slug}`, {
592
+ target: sibling.slug, expected: resolvedDirectory, actual: mounted.path,
593
+ }));
594
+ }
595
+ for (const harness of captured.targets) {
596
+ const declared = captured.directoryGrants.find(row => row.slug === sibling.slug && row.target === harness);
597
+ if (!declared)
598
+ return refuse(diagnostic('PROJECTION_INPUT_INVALID', `directory grant coverage is incomplete for ${sibling.slug}:${harness}`));
599
+ if (declared.path !== resolvedDirectory) {
600
+ return refuse(diagnostic('PROJECTION_GRANT_MISMATCH', `projection and ${harness} directory grant disagree for ${sibling.slug}`, {
601
+ target: `${sibling.slug}:${harness}`, expected: resolvedDirectory, actual: declared.path,
602
+ }));
603
+ }
604
+ const portablePath = relative(captured.canonicalMainCheckout, resolvedDirectory) || '.';
605
+ grants.push({
606
+ slug: sibling.slug,
607
+ target: harness,
608
+ path: portablePath,
609
+ spelling: grantSpelling(harness, portablePath, sibling.slug),
610
+ capability: 'instructed',
611
+ evidence: 'pending_evidence',
612
+ });
613
+ }
614
+ const observed = (guide) => captured.guideObservations.find(row => row.slug === sibling.slug && row.sourcePath === join(resolvedDirectory, guide));
615
+ const declaredGuides = sibling.guides ?? ['CLAUDE.md', 'AGENTS.md'];
616
+ const agentsCopied = declaredGuides.includes('AGENTS.md') && observed('AGENTS.md') !== undefined;
617
+ const seenContent = new Set();
618
+ for (const guide of declaredGuides) {
619
+ const observation = observed(guide);
620
+ if (!observation || seenContent.has(observation.contentSha256))
621
+ continue;
622
+ seenContent.add(observation.contentSha256);
623
+ const text = guideText(observation.content, guide === 'CLAUDE.md' && agentsCopied);
624
+ if (text === undefined) {
625
+ return refuse(diagnostic('PROJECTION_INPUT_INVALID', `guide holds cockpit's own instruction region or owner markers: ${observation.sourcePath}`, { target: observation.sourcePath }));
626
+ }
627
+ if (text.length === 0)
628
+ continue;
629
+ guides.push({
630
+ sibling: sibling.slug,
631
+ guide,
632
+ sourceGuidePath: observation.sourcePath,
633
+ sourceContentIdentity: observation.contentIdentity,
634
+ sourceContentSha256: observation.contentSha256,
635
+ bytes: guideBlock(sibling.slug, guide, observation.contentSha256, text),
636
+ });
637
+ }
638
+ }
639
+ grants.sort((left, right) => left.slug.localeCompare(right.slug)
640
+ || SURFACE_TARGETS.indexOf(left.target) - SURFACE_TARGETS.indexOf(right.target));
641
+ if (guides.length + grants.length > PROJECTION_RESULT_ENTRY_LIMIT)
642
+ return PLANNER_RESULT_BOUNDS;
643
+ // Every instruction target loads the root AGENTS.md, so each pays for every copied guide.
644
+ const guideCost = guides.reduce((sum, guide) => sum + lines(guide.bytes), 0);
645
+ const costs = [];
646
+ for (const harness of INSTRUCTION_TARGETS.filter(item => captured.targets.includes(item))) {
647
+ const characterization = captured.loadCharacterizations[harness];
648
+ if (!characterization) {
649
+ costs.push({ target: harness, status: 'unavailable', reason: 'load cost is not characterized' });
650
+ continue;
651
+ }
652
+ let always = guideCost;
653
+ let scoped = 0;
654
+ for (const source of captured.standingSources.filter(source => source.target === harness)) {
655
+ if (source.scope === 'scoped' && characterization.scopeSemantics === 'conditional')
656
+ scoped += lines(source.content);
657
+ else
658
+ always += lines(source.content);
659
+ }
660
+ costs.push({
661
+ target: harness,
662
+ status: always > captured.guideCapLines ? 'exceeded' : 'within_cap',
663
+ metric: 'lines',
664
+ value: always,
665
+ conditionalValue: scoped,
666
+ cap: captured.guideCapLines,
667
+ measuredOn: characterization.measuredOn,
668
+ provenance: characterization.provenance,
669
+ scopeSemantics: characterization.scopeSemantics,
670
+ });
671
+ }
672
+ const available = costs.filter((cost) => cost.status !== 'unavailable');
673
+ const headlineCost = available.reduce((maximum, cost) => !maximum || cost.value > maximum.value ? cost : maximum, undefined);
674
+ return capturedValue({
675
+ kind: 'complete',
676
+ guides,
677
+ grants,
678
+ costs,
679
+ ...(headlineCost ? { headline: { metric: 'lines', value: headlineCost.value, target: headlineCost.target } } : {}),
680
+ }) ?? PLANNER_RESULT_BOUNDS;
681
+ }