@amsterdamdatalabs/adl-agent-control-darwin-arm64 0.1.4

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 (741) hide show
  1. package/bin/adl +4 -0
  2. package/bin/cursor-settings +14 -0
  3. package/bin/default-agent-file +14 -0
  4. package/bin/git +670 -0
  5. package/bin/git-ado +425 -0
  6. package/bin/global-agents +14 -0
  7. package/bin/global-surface-sweep +14 -0
  8. package/bin/health-check +38 -0
  9. package/bin/kimi-hooks +14 -0
  10. package/bin/lean-ctx-observe-strip +361 -0
  11. package/bin/lib/bun-compile-build.sh +34 -0
  12. package/bin/lib/lean-ctx-observe-hooks.sh +634 -0
  13. package/bin/managed-settings +14 -0
  14. package/bin/opencode-hooks +14 -0
  15. package/bin/reconcile +2648 -0
  16. package/bin/scratch-reap +14 -0
  17. package/bin/shell-path-policy +64 -0
  18. package/bin/surface-policy +14 -0
  19. package/bin/sync-and-reconcile +215 -0
  20. package/bin/update-azure-agent-pool +29 -0
  21. package/bin/validate-manifest +171 -0
  22. package/bin/vscode-settings +14 -0
  23. package/package.json +44 -0
  24. package/packages/agent-hooks/README.md +411 -0
  25. package/packages/agent-hooks/assets/hooks.json +97 -0
  26. package/packages/agent-hooks/assets/opencode-plugin.template.ts +224 -0
  27. package/packages/agent-hooks/bunfig.toml +27 -0
  28. package/packages/agent-hooks/deploy-profile.json +53 -0
  29. package/packages/agent-hooks/deploy-profile.schema.json +120 -0
  30. package/packages/agent-hooks/package.json +23 -0
  31. package/packages/agent-hooks/src/cli/args.ts +99 -0
  32. package/packages/agent-hooks/src/cli/doctor.ts +241 -0
  33. package/packages/agent-hooks/src/cli/rules.ts +133 -0
  34. package/packages/agent-hooks/src/deploy/deploy.ts +181 -0
  35. package/packages/agent-hooks/src/deploy/doctor.ts +158 -0
  36. package/packages/agent-hooks/src/deploy/index.ts +17 -0
  37. package/packages/agent-hooks/src/deploy/json-merge.ts +115 -0
  38. package/packages/agent-hooks/src/deploy/legacy-lock.ts +213 -0
  39. package/packages/agent-hooks/src/deploy/list.ts +117 -0
  40. package/packages/agent-hooks/src/deploy/lock.ts +65 -0
  41. package/packages/agent-hooks/src/deploy/opencode-template.ts +74 -0
  42. package/packages/agent-hooks/src/deploy/profile.ts +117 -0
  43. package/packages/agent-hooks/src/deploy/render.ts +162 -0
  44. package/packages/agent-hooks/src/deploy/source.ts +84 -0
  45. package/packages/agent-hooks/src/gating/config.ts +514 -0
  46. package/packages/agent-hooks/src/gating/root.ts +117 -0
  47. package/packages/agent-hooks/src/hooks/briefing/handler.ts +22 -0
  48. package/packages/agent-hooks/src/hooks/briefing/index.ts +320 -0
  49. package/packages/agent-hooks/src/hooks/contract.ts +112 -0
  50. package/packages/agent-hooks/src/hooks/crg/handler.ts +22 -0
  51. package/packages/agent-hooks/src/hooks/crg/index.ts +209 -0
  52. package/packages/agent-hooks/src/hooks/lean-ctx/handler.ts +22 -0
  53. package/packages/agent-hooks/src/hooks/lean-ctx/index.ts +173 -0
  54. package/packages/agent-hooks/src/hooks/lean-ctx/mapping.ts +121 -0
  55. package/packages/agent-hooks/src/hooks/lean-ctx/scope-rewrite.ts +207 -0
  56. package/packages/agent-hooks/src/hooks/log.ts +233 -0
  57. package/packages/agent-hooks/src/hooks/user-prompt-submit/config.ts +58 -0
  58. package/packages/agent-hooks/src/hooks/user-prompt-submit/constants.ts +149 -0
  59. package/packages/agent-hooks/src/hooks/user-prompt-submit/handler.ts +72 -0
  60. package/packages/agent-hooks/src/hooks/user-prompt-submit/index.ts +203 -0
  61. package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +212 -0
  62. package/packages/agent-hooks/src/hooks/user-prompt-submit/skill-roots.ts +73 -0
  63. package/packages/agent-hooks/src/hooks/user-prompt-submit/skills.ts +98 -0
  64. package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +115 -0
  65. package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +174 -0
  66. package/packages/agent-hooks/src/hosts/impl/claude.ts +47 -0
  67. package/packages/agent-hooks/src/hosts/impl/codex.ts +47 -0
  68. package/packages/agent-hooks/src/hosts/impl/cursor.ts +40 -0
  69. package/packages/agent-hooks/src/hosts/impl/kimi.ts +47 -0
  70. package/packages/agent-hooks/src/hosts/impl/opencode.ts +54 -0
  71. package/packages/agent-hooks/src/hosts/registry.ts +29 -0
  72. package/packages/agent-hooks/src/hosts/spec.ts +113 -0
  73. package/packages/agent-hooks/src/lean-ctx/env.ts +106 -0
  74. package/packages/agent-hooks/src/lean-ctx/mcp.ts +140 -0
  75. package/packages/agent-hooks/src/lib/spawn-with-timeout.ts +120 -0
  76. package/packages/agent-hooks/src/log/writer.ts +230 -0
  77. package/packages/agent-hooks/src/main.ts +269 -0
  78. package/packages/agent-hooks/src/pre-tool-use/handler.ts +549 -0
  79. package/packages/agent-hooks/src/rules/engine.ts +293 -0
  80. package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +51 -0
  81. package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +320 -0
  82. package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +249 -0
  83. package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +258 -0
  84. package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +731 -0
  85. package/packages/agent-hooks/src/rules/impl/adl-identity.ts +128 -0
  86. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +179 -0
  87. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +65 -0
  88. package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +156 -0
  89. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +121 -0
  90. package/packages/agent-hooks/src/rules/impl/adl-session-required.ts +403 -0
  91. package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +92 -0
  92. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +74 -0
  93. package/packages/agent-hooks/src/rules/impl/advisory-coverage.ts +43 -0
  94. package/packages/agent-hooks/src/rules/impl/advisory-finder.ts +62 -0
  95. package/packages/agent-hooks/src/rules/impl/agent-secrets-stay-unread.ts +173 -0
  96. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +508 -0
  97. package/packages/agent-hooks/src/rules/impl/fs-canonical-checkout.ts +86 -0
  98. package/packages/agent-hooks/src/rules/impl/fs-git-internals.ts +75 -0
  99. package/packages/agent-hooks/src/rules/impl/fs-mv.ts +66 -0
  100. package/packages/agent-hooks/src/rules/impl/fs-rm.ts +83 -0
  101. package/packages/agent-hooks/src/rules/impl/fs-trash.ts +140 -0
  102. package/packages/agent-hooks/src/rules/impl/git-core.ts +101 -0
  103. package/packages/agent-hooks/src/rules/impl/git-helpers.ts +408 -0
  104. package/packages/agent-hooks/src/rules/impl/git-history.ts +77 -0
  105. package/packages/agent-hooks/src/rules/impl/git-paths.ts +139 -0
  106. package/packages/agent-hooks/src/rules/impl/git-plumbing.ts +167 -0
  107. package/packages/agent-hooks/src/rules/impl/git-protected-tree.ts +132 -0
  108. package/packages/agent-hooks/src/rules/impl/git-transitions.ts +292 -0
  109. package/packages/agent-hooks/src/rules/impl/git-verify.ts +95 -0
  110. package/packages/agent-hooks/src/rules/impl/git-worktree.ts +265 -0
  111. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +76 -0
  112. package/packages/agent-hooks/src/rules/impl/messages.ts +128 -0
  113. package/packages/agent-hooks/src/rules/impl/parse-uncertain.ts +143 -0
  114. package/packages/agent-hooks/src/rules/impl/pr-completion.ts +35 -0
  115. package/packages/agent-hooks/src/rules/impl/shell-remote-script-exec.ts +63 -0
  116. package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +283 -0
  117. package/packages/agent-hooks/src/rules/impl/tool-bun-test.ts +81 -0
  118. package/packages/agent-hooks/src/rules/impl/tool-cargo.ts +66 -0
  119. package/packages/agent-hooks/src/rules/impl/tool-chmod-hook.ts +42 -0
  120. package/packages/agent-hooks/src/rules/impl/tool-find.ts +82 -0
  121. package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +83 -0
  122. package/packages/agent-hooks/src/rules/impl/tool-grep.ts +93 -0
  123. package/packages/agent-hooks/src/rules/impl/tool-lean-ctx-global.ts +594 -0
  124. package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +77 -0
  125. package/packages/agent-hooks/src/rules/ports.ts +178 -0
  126. package/packages/agent-hooks/src/rules/registry.ts +233 -0
  127. package/packages/agent-hooks/src/rules/shell/git-identity.ts +1415 -0
  128. package/packages/agent-hooks/src/rules/shell/git-internals.ts +186 -0
  129. package/packages/agent-hooks/src/rules/shell/invocation.ts +1083 -0
  130. package/packages/agent-hooks/src/rules/shell/piped-script.ts +201 -0
  131. package/packages/agent-hooks/src/rules/shell/prefixes.ts +214 -0
  132. package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +108 -0
  133. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +149 -0
  134. package/packages/agent-hooks/src/rules/shell/split.ts +179 -0
  135. package/packages/agent-hooks/src/rules/shell/substitution.ts +394 -0
  136. package/packages/agent-hooks/src/rules/shell/token.ts +180 -0
  137. package/packages/agent-hooks/src/rules/shell/xargs.ts +81 -0
  138. package/packages/agent-hooks/src/rules/spec.ts +152 -0
  139. package/packages/agent-hooks/src/surface-policy.ts +37 -0
  140. package/packages/agent-hooks/tsconfig.json +23 -0
  141. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +107 -0
  142. package/packages/agent-settings/assets/agents/architect.toml +30 -0
  143. package/packages/agent-settings/assets/agents/code-reviewer.toml +27 -0
  144. package/packages/agent-settings/assets/agents/critic.toml +35 -0
  145. package/packages/agent-settings/assets/agents/executor.toml +28 -0
  146. package/packages/agent-settings/assets/agents/explore.toml +25 -0
  147. package/packages/agent-settings/assets/agents/planner.toml +29 -0
  148. package/packages/agent-settings/assets/lean-ctx/config.toml +39 -0
  149. package/packages/agent-settings/assets/lean-ctx/gitignore +6 -0
  150. package/packages/agent-settings/assets/lean-ctx/layout.toml +5 -0
  151. package/packages/agent-settings/assets/mcp.json +20 -0
  152. package/packages/agent-settings/managed/README.md +331 -0
  153. package/packages/agent-settings/managed/claude-managed-settings.json +150 -0
  154. package/packages/agent-settings/managed/codex-requirements.toml +105 -0
  155. package/packages/agent-settings/protected.json +470 -0
  156. package/packages/agent-settings/src/deploy/agent-render.ts +94 -0
  157. package/packages/agent-settings/src/deploy/agent-toml.ts +76 -0
  158. package/packages/agent-settings/src/deploy/agents-deploy.ts +129 -0
  159. package/packages/agent-settings/src/deploy/doctor.ts +201 -0
  160. package/packages/agent-settings/src/deploy/index.ts +33 -0
  161. package/packages/agent-settings/src/deploy/json-pretty.ts +66 -0
  162. package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +138 -0
  163. package/packages/agent-settings/src/deploy/legacy-lock.ts +286 -0
  164. package/packages/agent-settings/src/deploy/list.ts +135 -0
  165. package/packages/agent-settings/src/deploy/lock.ts +112 -0
  166. package/packages/agent-settings/src/deploy/mcp-deploy.ts +175 -0
  167. package/packages/agent-settings/src/deploy/mcp-inspect.ts +56 -0
  168. package/packages/agent-settings/src/deploy/mcp-merge.ts +164 -0
  169. package/packages/agent-settings/src/deploy/mcp-render.ts +80 -0
  170. package/packages/agent-settings/src/deploy/mcp-source.ts +43 -0
  171. package/packages/agent-settings/src/deploy/profile.ts +296 -0
  172. package/packages/agent-settings/src/deploy/settings.ts +65 -0
  173. package/packages/agent-settings/src/global-agents.ts +230 -0
  174. package/packages/agent-settings/src/global-surface-sweep.ts +2179 -0
  175. package/packages/agent-settings/src/kimi-hooks.ts +455 -0
  176. package/packages/agent-settings/src/opencode-hooks.ts +76 -0
  177. package/packages/agent-settings/src/surface-policy.ts +441 -0
  178. package/packages/agent-settings/surface-policy.json +20 -0
  179. package/packages/agent-skills/assets/code-review/SKILL.md +100 -0
  180. package/packages/agent-skills/assets/codebase-design/SKILL.md +98 -0
  181. package/packages/agent-skills/assets/codebase-design/references/DEEPENING.md +37 -0
  182. package/packages/agent-skills/assets/codebase-design/references/DESIGN-IT-TWICE.md +44 -0
  183. package/packages/agent-skills/assets/codebase-design/references/TESTABILITY.md +29 -0
  184. package/packages/agent-skills/assets/debug-issue/SKILL.md +40 -0
  185. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +100 -0
  186. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +34 -0
  187. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  188. package/packages/agent-skills/assets/domain-modeling/SKILL.md +86 -0
  189. package/packages/agent-skills/assets/domain-modeling/references/ADR-FORMAT.md +47 -0
  190. package/packages/agent-skills/assets/domain-modeling/references/CONTEXT-FORMAT.md +60 -0
  191. package/packages/agent-skills/assets/explore-codebase/SKILL.md +41 -0
  192. package/packages/agent-skills/assets/handoff/SKILL.md +28 -0
  193. package/packages/agent-skills/assets/implement/SKILL.md +28 -0
  194. package/packages/agent-skills/assets/refactor-safely/SKILL.md +44 -0
  195. package/packages/agent-skills/assets/review-changes/SKILL.md +45 -0
  196. package/packages/agent-skills/assets/tdd/SKILL.md +49 -0
  197. package/packages/agent-skills/assets/tdd/references/mocking.md +59 -0
  198. package/packages/agent-skills/assets/tdd/references/tests.md +77 -0
  199. package/packages/agent-skills/assets/testing/SKILL.md +54 -0
  200. package/packages/agent-skills/assets/to-feature/SKILL.md +91 -0
  201. package/packages/agent-skills/assets/to-feature/references/FEATURE-SPEC-TEMPLATE.md +88 -0
  202. package/packages/agent-skills/assets/to-workitems/SKILL.md +90 -0
  203. package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +118 -0
  204. package/packages/agent-skills/assets/to-workitems/references/TICKET-TEMPLATES.md +70 -0
  205. package/packages/agent-skills/assets/wayfinder/SKILL.md +100 -0
  206. package/packages/agent-skills/assets/wayfinder/references/TEMPLATES.md +35 -0
  207. package/packages/agent-skills/assets/wayfinder/references/TICKET-TYPES.md +8 -0
  208. package/packages/agent-skills/deploy-profile.json +9 -0
  209. package/packages/agent-skills/deploy-profile.schema.json +45 -0
  210. package/packages/agent-skills/dist/deploy.d.ts +27 -0
  211. package/packages/agent-skills/dist/deploy.d.ts.map +1 -0
  212. package/packages/agent-skills/dist/deploy.js +186 -0
  213. package/packages/agent-skills/dist/deploy.js.map +1 -0
  214. package/packages/agent-skills/dist/doctor.d.ts +24 -0
  215. package/packages/agent-skills/dist/doctor.d.ts.map +1 -0
  216. package/packages/agent-skills/dist/doctor.js +141 -0
  217. package/packages/agent-skills/dist/doctor.js.map +1 -0
  218. package/packages/agent-skills/dist/fs-utils.d.ts +8 -0
  219. package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -0
  220. package/packages/agent-skills/dist/fs-utils.js +40 -0
  221. package/packages/agent-skills/dist/fs-utils.js.map +1 -0
  222. package/packages/agent-skills/dist/index.d.ts +8 -0
  223. package/packages/agent-skills/dist/index.d.ts.map +1 -0
  224. package/packages/agent-skills/dist/index.js +8 -0
  225. package/packages/agent-skills/dist/index.js.map +1 -0
  226. package/packages/agent-skills/dist/legacy-lock.d.ts +44 -0
  227. package/packages/agent-skills/dist/legacy-lock.d.ts.map +1 -0
  228. package/packages/agent-skills/dist/legacy-lock.js +121 -0
  229. package/packages/agent-skills/dist/legacy-lock.js.map +1 -0
  230. package/packages/agent-skills/dist/list.d.ts +23 -0
  231. package/packages/agent-skills/dist/list.d.ts.map +1 -0
  232. package/packages/agent-skills/dist/list.js +75 -0
  233. package/packages/agent-skills/dist/list.js.map +1 -0
  234. package/packages/agent-skills/dist/manifest.d.ts +15 -0
  235. package/packages/agent-skills/dist/manifest.d.ts.map +1 -0
  236. package/packages/agent-skills/dist/manifest.js +57 -0
  237. package/packages/agent-skills/dist/manifest.js.map +1 -0
  238. package/packages/agent-skills/dist/profile.d.ts +41 -0
  239. package/packages/agent-skills/dist/profile.d.ts.map +1 -0
  240. package/packages/agent-skills/dist/profile.js +86 -0
  241. package/packages/agent-skills/dist/profile.js.map +1 -0
  242. package/packages/agent-skills/dist/skill-config.d.ts +24 -0
  243. package/packages/agent-skills/dist/skill-config.d.ts.map +1 -0
  244. package/packages/agent-skills/dist/skill-config.js +68 -0
  245. package/packages/agent-skills/dist/skill-config.js.map +1 -0
  246. package/packages/agent-skills/dist/skill-meta.d.ts +17 -0
  247. package/packages/agent-skills/dist/skill-meta.d.ts.map +1 -0
  248. package/packages/agent-skills/dist/skill-meta.js +67 -0
  249. package/packages/agent-skills/dist/skill-meta.js.map +1 -0
  250. package/packages/agent-skills/package.json +31 -0
  251. package/packages/agent-skills/src/deploy.ts +222 -0
  252. package/packages/agent-skills/src/doctor.ts +178 -0
  253. package/packages/agent-skills/src/fs-utils.ts +40 -0
  254. package/packages/agent-skills/src/index.ts +29 -0
  255. package/packages/agent-skills/src/legacy-lock.ts +152 -0
  256. package/packages/agent-skills/src/list.ts +94 -0
  257. package/packages/agent-skills/src/manifest.ts +66 -0
  258. package/packages/agent-skills/src/profile.ts +128 -0
  259. package/packages/agent-skills/src/skill-config.ts +75 -0
  260. package/packages/agent-skills/src/skill-meta.ts +78 -0
  261. package/packages/agent-skills/tsconfig.build.json +13 -0
  262. package/packages/agent-skills/tsconfig.json +23 -0
  263. package/packages/azdo/README.md +111 -0
  264. package/packages/azdo/package.json +21 -0
  265. package/packages/azdo/src/azdo-pl.ts +275 -0
  266. package/packages/azdo/src/azdo-pr-review.ts +160 -0
  267. package/packages/azdo/src/azdo-pr.ts +289 -0
  268. package/packages/azdo/src/azdo-repo-doctor.ts +839 -0
  269. package/packages/azdo/src/azdo-wit.ts +145 -0
  270. package/packages/azdo/src/cli.ts +251 -0
  271. package/packages/azdo/src/lib/agent-identity.ts +198 -0
  272. package/packages/azdo/src/lib/agent-token.ts +167 -0
  273. package/packages/azdo/src/lib/client.ts +259 -0
  274. package/packages/azdo/src/lib/graph-identity.ts +153 -0
  275. package/packages/azdo/src/lib/validate-pr-metadata.ts +57 -0
  276. package/packages/azdo/src/mcp-server.ts +209 -0
  277. package/packages/azdo/src/tool-dispatch.ts +204 -0
  278. package/packages/azdo/tsconfig.json +13 -0
  279. package/packages/fleet/jobs/git-maintain/src/main.ts +588 -0
  280. package/packages/fleet/manifest.json +207 -0
  281. package/packages/fleet/manifest.schema.json +283 -0
  282. package/packages/fleet/src/default-agent-file.ts +144 -0
  283. package/packages/fleet/src/install-adl-package.ts +178 -0
  284. package/packages/fleet/src/install-embedding-model.ts +412 -0
  285. package/packages/fleet/src/launch-agents.ts +649 -0
  286. package/packages/fleet/src/managed-settings.ts +278 -0
  287. package/packages/fleet/src/scratch-reap.ts +601 -0
  288. package/packages/fleet/src/vscode-settings.ts +705 -0
  289. package/packages/fleet/systemd/adl-azure-agent-update.service +13 -0
  290. package/packages/fleet/systemd/adl-azure-agent-update.timer +10 -0
  291. package/packages/repo-controls/AGENTS.md +96 -0
  292. package/packages/repo-controls/CLAUDE.md +1 -0
  293. package/packages/repo-controls/LICENSE +20 -0
  294. package/packages/repo-controls/README.md +493 -0
  295. package/packages/repo-controls/assets/enable-integration-auto-complete.mjs +79 -0
  296. package/packages/repo-controls/assets/promote.ts +371 -0
  297. package/packages/repo-controls/assets/version-bump.sh +189 -0
  298. package/packages/repo-controls/azure-pipelines.yml +274 -0
  299. package/packages/repo-controls/bin/adl-repo-controls.ts +10 -0
  300. package/packages/repo-controls/bunfig.toml +58 -0
  301. package/packages/repo-controls/package/adl-compliance/AGENTS.md +163 -0
  302. package/packages/repo-controls/package/adl-compliance/CLAUDE.md +1 -0
  303. package/packages/repo-controls/package/adl-compliance/conftest.py +78 -0
  304. package/packages/repo-controls/package/adl-compliance/pyproject.toml +31 -0
  305. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/__init__.py +50 -0
  306. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +98 -0
  307. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/__init__.py +1 -0
  308. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/__init__.py +12 -0
  309. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +210 -0
  310. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-bad-status/docs/architecture/overview.md +8 -0
  311. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-bad-status/docs/decisions/adr-901-proposed.md +10 -0
  312. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-broken-backlink/docs/decisions/adr-902-dangling.md +11 -0
  313. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-clean/docs/architecture/overview.md +8 -0
  314. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-clean/docs/decisions/adr-900-example.md +10 -0
  315. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-frontmatter-mismatch/.plans/b_committed/26-08-08-lifecycle-fixture.md +3 -0
  316. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-frontmatter-mismatch/.specs/demo/lifecycle.md +7 -0
  317. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-frontmatter-mismatch/docs/decisions/adr-903-frontmatter-mismatch.md +11 -0
  318. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-inactive-plan/.plans/c_done/26-08-08-completed-fixture.md +3 -0
  319. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-inactive-plan/.specs/demo/lifecycle.md +7 -0
  320. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-inactive-plan/docs/decisions/adr-904-completed-plan.md +11 -0
  321. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +127 -0
  322. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/__init__.py +7 -0
  323. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/check.py +91 -0
  324. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/AGENTS.md +1 -0
  325. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/CLAUDE.md +1 -0
  326. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/architecture/declared.txt +1 -0
  327. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/compliance/declared.txt +1 -0
  328. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/decisions/declared.txt +1 -0
  329. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/index.md +1 -0
  330. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/operations/declared.txt +1 -0
  331. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/research/declared.txt +1 -0
  332. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/stray-root-file.md +1 -0
  333. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/totally-made-up/nonsense.md +1 -0
  334. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +106 -0
  335. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/__init__.py +7 -0
  336. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +291 -0
  337. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/clean/backend/clean.rs +1 -0
  338. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/frontend/mobile/src/i18n/locales/zh-CN/strings.ts +1 -0
  339. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/violation/backend/escaped.rs +2 -0
  340. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/violation/backend/raw.rs +1 -0
  341. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/violation/backend//346/226/207/344/273/266.rs +1 -0
  342. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +193 -0
  343. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/__init__.py +12 -0
  344. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +140 -0
  345. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-clean/docs/architecture/foo.md +7 -0
  346. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-clean/docs/architecture/index.md +10 -0
  347. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-violation/docs/architecture/foo.md +7 -0
  348. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-violation/docs/architecture/index.md +9 -0
  349. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +98 -0
  350. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/__init__.py +12 -0
  351. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +378 -0
  352. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +192 -0
  353. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/index.md +14 -0
  354. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/one.md +7 -0
  355. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/two.md +7 -0
  356. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs-knowledge-indexes.yaml +20 -0
  357. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/index.md +15 -0
  358. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/one.md +7 -0
  359. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/two.md +7 -0
  360. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs-knowledge-indexes.yaml +16 -0
  361. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/index.md +13 -0
  362. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/one.md +7 -0
  363. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/two.md +7 -0
  364. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs-knowledge-indexes.yaml +16 -0
  365. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +102 -0
  366. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/__init__.py +12 -0
  367. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/check.py +387 -0
  368. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-bad-date/plans/LEDGER.md +5 -0
  369. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-clean/plans/LEDGER.md +6 -0
  370. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-empty-table/plans/LEDGER.md +4 -0
  371. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-long-event/plans/LEDGER.md +5 -0
  372. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-violation/plans/LEDGER.md +5 -0
  373. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +427 -0
  374. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/__init__.py +11 -0
  375. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/check.py +84 -0
  376. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-fence/docs/bad-fence.md +2 -0
  377. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-fence/docs/valid.md +5 -0
  378. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-frontmatter/docs/bad-front-matter.md +2 -0
  379. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-frontmatter/docs/valid.md +5 -0
  380. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-clean/docs/valid.md +7 -0
  381. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/test_check.py +126 -0
  382. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/__init__.py +12 -0
  383. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +210 -0
  384. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +119 -0
  385. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +17 -0
  386. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +8 -0
  387. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs/architecture/bad-status.md +17 -0
  388. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +8 -0
  389. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +49 -0
  390. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +159 -0
  391. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/__init__.py +12 -0
  392. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +578 -0
  393. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/architecture/backend/crate-overview.md +8 -0
  394. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/architecture/team/team-overview.md +8 -0
  395. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/compliance/controls/control-matrix.md +12 -0
  396. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/compliance/controls/control-matrix.yaml +10 -0
  397. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/research/benchmarks/note.md +24 -0
  398. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/backend/crate-overview.md +8 -0
  399. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/stable-delivery.md +10 -0
  400. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/stable-recommendation.md +12 -0
  401. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/target-narration.md +10 -0
  402. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/team/team-overview.md +8 -0
  403. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/compliance/controls/control-matrix.md +8 -0
  404. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/compliance/controls/control-matrix.yaml +17 -0
  405. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/research/benchmarks/bad-research.md +8 -0
  406. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +461 -0
  407. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/__init__.py +1 -0
  408. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/__init__.py +12 -0
  409. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/check.py +124 -0
  410. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/.workflows/AGENTS.md +1 -0
  411. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/.workflows/CLAUDE.md +1 -0
  412. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/AGENTS.md +3 -0
  413. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/CLAUDE.md +2 -0
  414. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/INVARIANTS.md +3 -0
  415. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/bad-pointer/AGENTS.md +3 -0
  416. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/bad-pointer/CLAUDE.md +1 -0
  417. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/docs/AGENTS.md +1 -0
  418. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/docs/CLAUDE.md +1 -0
  419. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/missing-pair/AGENTS.md +3 -0
  420. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/owned/AGENTS.md +1 -0
  421. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/owned/CLAUDE.md +2 -0
  422. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/test_check.py +116 -0
  423. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/__init__.py +13 -0
  424. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/check.py +380 -0
  425. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/fixtures/redeclared-workflows-tree/automation/pipelines/pr-validation.yaml +1 -0
  426. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +274 -0
  427. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +12 -0
  428. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +227 -0
  429. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  430. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +6 -0
  431. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/fresh/fresh.ts +1 -0
  432. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub1/only.ts +1 -0
  433. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub10/only.ts +1 -0
  434. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub11/only.ts +1 -0
  435. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub2/only.ts +1 -0
  436. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub3/only.ts +1 -0
  437. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub4/only.ts +1 -0
  438. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub5/only.ts +1 -0
  439. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub6/only.ts +1 -0
  440. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub7/only.ts +1 -0
  441. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub8/only.ts +1 -0
  442. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/many/sub9/only.ts +1 -0
  443. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/services/i18n/locales/de/common.json +1 -0
  444. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  445. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/frontend/packages/desktop/src/vendored-crate/Cargo.toml +2 -0
  446. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +6 -0
  447. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/fresh/fresh.ts +1 -0
  448. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub1/only.ts +1 -0
  449. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub10/only.ts +1 -0
  450. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub11/only.ts +1 -0
  451. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub2/only.ts +1 -0
  452. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub3/only.ts +1 -0
  453. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub4/only.ts +1 -0
  454. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub5/only.ts +1 -0
  455. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub6/only.ts +1 -0
  456. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub7/only.ts +1 -0
  457. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub8/only.ts +1 -0
  458. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/many/sub9/only.ts +1 -0
  459. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/frontend/packages/desktop/src/renderer/solo/item.ts +1 -0
  460. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +6 -0
  461. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +138 -0
  462. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/__init__.py +12 -0
  463. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +240 -0
  464. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-clean/.plans/a_new/plan.md +7 -0
  465. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-clean/backend/AGENTS.md +4 -0
  466. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-clean/docs/page.md +7 -0
  467. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-stray/backend/notes.md +4 -0
  468. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-stray/docs/page.md +8 -0
  469. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +220 -0
  470. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/__init__.py +5 -0
  471. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +225 -0
  472. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/GAPS-AND-FINDINGS.md +5 -0
  473. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/spec.md +10 -0
  474. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +330 -0
  475. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/__init__.py +12 -0
  476. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +240 -0
  477. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/agent/crates/enact-providers/src/snapshots/README.md +1 -0
  478. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/backend/crates/enact-app/assets/builtin-assistants/avatars/assistant.jpg +1 -0
  479. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/backend/crates/enact-assets/assets/logos/tools/coding/tool.png +1 -0
  480. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/backend/crates/enact-session/src/backend/codex_init_prompt.md +1 -0
  481. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/mobile/assets/images/icon.png +1 -0
  482. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/packages/desktop/src/renderer/assets/logos/brand/app.png +1 -0
  483. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/packages/desktop/src/renderer/assets/themes/default-theme.png +1 -0
  484. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/public/pwa/icon-192.png +1 -0
  485. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/resources/product.png +1 -0
  486. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-stray/apps/legacy/file.ts +1 -0
  487. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-stray/backend/crates/enact-session/src/backend/README.md +1 -0
  488. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-stray/frontend/unapproved/screenshot.png +1 -0
  489. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +230 -0
  490. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +53 -0
  491. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/__init__.py +7 -0
  492. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +438 -0
  493. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/AGENTS.md +1 -0
  494. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +9 -0
  495. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/spec.md +12 -0
  496. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/templates/feature.md +1 -0
  497. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +9 -0
  498. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/spec.md +12 -0
  499. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +461 -0
  500. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/__init__.py +11 -0
  501. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/check.py +62 -0
  502. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/NotAMigration.sql +1 -0
  503. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/0001_long_prefix.sql +1 -0
  504. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/001_init.sql +1 -0
  505. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/002_add_index.sql +1 -0
  506. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/003-dashed-slug.sql +1 -0
  507. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/004_MixedCase.sql +1 -0
  508. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/005_trailing space.sql +1 -0
  509. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/01_short_prefix.sql +1 -0
  510. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/migrations/not-a-migration.sql +1 -0
  511. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/test_check.py +116 -0
  512. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/__init__.py +11 -0
  513. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +188 -0
  514. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/.plans/c_done/26-07-27-history.md +3 -0
  515. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/README.md +8 -0
  516. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/product/links.md +20 -0
  517. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/product/route-target.md +8 -0
  518. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/.plans/c_done/26-07-27-history.md +3 -0
  519. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/README.md +8 -0
  520. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/product/links.md +20 -0
  521. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/product/route-target.md +8 -0
  522. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +166 -0
  523. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/__init__.py +11 -0
  524. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +135 -0
  525. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.agents/declared.txt +1 -0
  526. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.changeset/declared.txt +1 -0
  527. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.claude/declared.txt +1 -0
  528. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.code-review-graph/declared.txt +1 -0
  529. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.code-review-graph_old/declared.txt +1 -0
  530. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.codex/declared.txt +1 -0
  531. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.cursor/declared.txt +1 -0
  532. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.cursorrules +1 -0
  533. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.dockerignore +1 -0
  534. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.docs/declared.txt +1 -0
  535. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.donors/declared.txt +1 -0
  536. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.gitattributes +1 -0
  537. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.kimi-code/declared.txt +1 -0
  538. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.lfsconfig +1 -0
  539. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.mcp.json +1 -0
  540. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.opencode/declared.txt +1 -0
  541. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.workflows/declared.txt +1 -0
  542. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/AGENTS.md +1 -0
  543. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CHANGELOG.md +1 -0
  544. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CLAUDE-INVARIANTS.md +1 -0
  545. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CLAUDE.md +1 -0
  546. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/INDEPENDENT-VERIFICATION.md +1 -0
  547. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/INVARIANTS.md +1 -0
  548. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/LICENSE +1 -0
  549. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/NOTICE +1 -0
  550. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/README.md +1 -0
  551. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/RELEASE.md +1 -0
  552. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/VERSION +1 -0
  553. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/backend/declared.txt +1 -0
  554. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/compliance/declared.txt +1 -0
  555. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/dev/declared.txt +1 -0
  556. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/docker/declared.txt +1 -0
  557. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/docs/declared.txt +1 -0
  558. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/frontend/declared.txt +1 -0
  559. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/infra/declared.txt +1 -0
  560. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/justfile +1 -0
  561. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/opencode.jsonc +1 -0
  562. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/rule-tests/undeclared.txt +1 -0
  563. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/rules/undeclared.txt +1 -0
  564. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/unapproved-root/undeclared.txt +1 -0
  565. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/undeclared-root-file.txt +1 -0
  566. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +122 -0
  567. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/__init__.py +12 -0
  568. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +290 -0
  569. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.changeset/some-change.md +3 -0
  570. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.plans/AGENTS.md +3 -0
  571. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.specs/AGENTS.md +3 -0
  572. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.workflows/AGENTS.md +3 -0
  573. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/AGENTS.md +3 -0
  574. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/CHANGELOG.md +3 -0
  575. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/CLAUDE.md +3 -0
  576. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/INDEPENDENT-VERIFICATION.md +3 -0
  577. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/INVARIANTS.md +3 -0
  578. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/README.md +3 -0
  579. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/RELEASE.md +3 -0
  580. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/STRAY.md +3 -0
  581. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/docs/AGENTS.md +3 -0
  582. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/docs/architecture/note.md +3 -0
  583. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/frontend/README.md +3 -0
  584. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/.workflows/AGENTS.md +1 -0
  585. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/AGENTS.md +1 -0
  586. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/README.md +1 -0
  587. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/docs/overview.md +1 -0
  588. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/plans/AGENTS.md +1 -0
  589. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +329 -0
  590. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/__init__.py +12 -0
  591. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +127 -0
  592. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/a/b/c/d/at-limit.sh +3 -0
  593. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/a/b/c/d/e/too-deep.sh +3 -0
  594. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-01.sh +3 -0
  595. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-02.sh +3 -0
  596. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-03.sh +3 -0
  597. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-04.sh +3 -0
  598. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-05.sh +3 -0
  599. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-06.sh +3 -0
  600. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-07.sh +3 -0
  601. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-08.sh +3 -0
  602. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-09.sh +3 -0
  603. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-10.sh +3 -0
  604. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-11.sh +3 -0
  605. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-12.sh +3 -0
  606. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-13.sh +3 -0
  607. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-14.sh +3 -0
  608. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-15.sh +3 -0
  609. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/fixtures/scripts-layout/.workflows/scripts/direct-16.sh +3 -0
  610. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +163 -0
  611. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/__init__.py +1 -0
  612. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/__init__.py +11 -0
  613. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +169 -0
  614. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/attribute-without-conversion/.gitattributes +1 -0
  615. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/attribute-without-conversion/large file.bin +3 -0
  616. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/.gitattributes +1 -0
  617. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/large file.bin +3 -0
  618. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/small.txt +1 -0
  619. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/missing-filter/large file.bin +0 -0
  620. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/unmatched-extension/.gitattributes +1 -0
  621. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/unmatched-extension/payload.dat +0 -0
  622. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +133 -0
  623. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/__init__.py +1 -0
  624. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/__init__.py +12 -0
  625. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/check.py +129 -0
  626. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/disjoint/gate-registry.yaml +13 -0
  627. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/empty/gate-registry.yaml +7 -0
  628. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/overlapping/gate-registry.yaml +13 -0
  629. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/test_check.py +109 -0
  630. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/__init__.py +1 -0
  631. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/__init__.py +11 -0
  632. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/check.py +149 -0
  633. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/fixtures/clean/docs/readme.md +4 -0
  634. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/fixtures/relevant/VERSION +1 -0
  635. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/test_check.py +151 -0
  636. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/__init__.py +11 -0
  637. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/check.py +202 -0
  638. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/.workflows/changesets/package.json +5 -0
  639. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/.workflows/package.json +5 -0
  640. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/CHANGELOG.md +5 -0
  641. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/VERSION +1 -0
  642. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/.workflows/changesets/package.json +5 -0
  643. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/.workflows/package.json +5 -0
  644. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/CHANGELOG.md +5 -0
  645. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/VERSION +1 -0
  646. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/test_check.py +162 -0
  647. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/__init__.py +1 -0
  648. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +12 -0
  649. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/check.py +103 -0
  650. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/git-bad.sh +2 -0
  651. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/git-note.sh +3 -0
  652. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/good.sh +2 -0
  653. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/grep-bad.sh +2 -0
  654. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/grep-note.sh +3 -0
  655. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/grep-trailing.sh +2 -0
  656. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/compliance_py/checks/notes.md +1 -0
  657. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/elsewhere/loose-git.sh +6 -0
  658. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/elsewhere/loose-grep.sh +4 -0
  659. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +117 -0
  660. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/__init__.py +13 -0
  661. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +265 -0
  662. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/fixtures/ignored-paths/keep.log +1 -0
  663. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/fixtures/ignored-paths/src/main.rs +1 -0
  664. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +180 -0
  665. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/__init__.py +12 -0
  666. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/check.py +84 -0
  667. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-clean/LICENSE +10 -0
  668. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-mismatch/LICENSE +3 -0
  669. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-missing/README.md +1 -0
  670. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/test_check.py +66 -0
  671. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/__init__.py +11 -0
  672. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +167 -0
  673. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/Cargo.lock +7 -0
  674. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/Cargo.toml +4 -0
  675. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/rust-toolchain.toml +3 -0
  676. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/src/main.rs +1 -0
  677. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/frontend/bun.lock +1 -0
  678. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/Cargo.lock +7 -0
  679. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/Cargo.toml +4 -0
  680. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/rust-toolchain.toml +3 -0
  681. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/src/main.rs +1 -0
  682. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/frontend/bun.lock +0 -0
  683. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +109 -0
  684. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +483 -0
  685. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/__init__.py +1 -0
  686. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +199 -0
  687. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/batch.py +52 -0
  688. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +303 -0
  689. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +106 -0
  690. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +99 -0
  691. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/errors.py +32 -0
  692. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/finding.py +65 -0
  693. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/models.py +40 -0
  694. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +63 -0
  695. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scope.py +82 -0
  696. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/verdict.py +161 -0
  697. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/__init__.py +1 -0
  698. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +131 -0
  699. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +30 -0
  700. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +65 -0
  701. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +791 -0
  702. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.schema.yaml +176 -0
  703. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +791 -0
  704. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/grants-registry.yaml +112 -0
  705. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/grants.py +129 -0
  706. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +281 -0
  707. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +250 -0
  708. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/severity-policy.yaml +114 -0
  709. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/providers/__init__.py +11 -0
  710. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +371 -0
  711. package/packages/repo-controls/package/adl-compliance/uv.lock +199 -0
  712. package/packages/repo-controls/package.json +33 -0
  713. package/packages/repo-controls/scripts/hooks/commit-msg +63 -0
  714. package/packages/repo-controls/scripts/hooks/lib/hook-log.sh +62 -0
  715. package/packages/repo-controls/scripts/hooks/post-checkout +15 -0
  716. package/packages/repo-controls/scripts/hooks/post-commit +18 -0
  717. package/packages/repo-controls/scripts/hooks/pre-commit +82 -0
  718. package/packages/repo-controls/scripts/hooks/pre-push +144 -0
  719. package/packages/repo-controls/src/azdo-push.ts +463 -0
  720. package/packages/repo-controls/src/azure-devops-governance.ts +248 -0
  721. package/packages/repo-controls/src/azure-devops-remote.ts +61 -0
  722. package/packages/repo-controls/src/cli.ts +188 -0
  723. package/packages/repo-controls/src/compliance-config.ts +200 -0
  724. package/packages/repo-controls/src/config.ts +690 -0
  725. package/packages/repo-controls/src/constants.ts +97 -0
  726. package/packages/repo-controls/src/coverage-ratchet.ts +153 -0
  727. package/packages/repo-controls/src/doc-freshness.ts +172 -0
  728. package/packages/repo-controls/src/doctor.ts +304 -0
  729. package/packages/repo-controls/src/findings-ratchet.ts +163 -0
  730. package/packages/repo-controls/src/gitignore-block.ts +253 -0
  731. package/packages/repo-controls/src/hook-log.ts +72 -0
  732. package/packages/repo-controls/src/hook.ts +694 -0
  733. package/packages/repo-controls/src/install.ts +506 -0
  734. package/packages/repo-controls/src/integrity.ts +104 -0
  735. package/packages/repo-controls/src/promote.ts +25 -0
  736. package/packages/repo-controls/src/runtime.ts +110 -0
  737. package/packages/repo-controls/src/workflows-drift.ts +106 -0
  738. package/packages/repo-controls/src/worktree-guard.ts +272 -0
  739. package/packages/repo-controls/src/worktree-limit.ts +11 -0
  740. package/packages/repo-controls/tsconfig.json +14 -0
  741. package/packages/repo-controls/workflows/scripts/release/verify-npm-artifact.ts +119 -0
package/bin/reconcile ADDED
@@ -0,0 +1,2648 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Bash version gate -- runs FIRST, before `set -euo pipefail`, before the
4
+ # PATH export, before any argument parsing, before any state change. This
5
+ # script relies on `"${arr[@]}"` expansions over arrays that are legitimately
6
+ # empty in production (e.g. system_root_args when RECONCILE_TEST_SYSTEM_ROOT
7
+ # is unset, which is every real run). Under bash < 4.4's `set -u`, expanding
8
+ # an EMPTY array with `[@]`/`[*]` throws "unbound variable" instead of
9
+ # expanding to nothing -- fixed in bash 4.4 (2016). macOS ships /bin/bash
10
+ # 3.2.57 (the last GPLv2 release Apple will ship) and never upgrades it;
11
+ # `sudo`'s secure_path can select that 3.2 over a newer Homebrew bash even
12
+ # when one is installed and on the invoking user's own PATH, so a real
13
+ # `sudo bin/reconcile ...` run can hit this after this run had already
14
+ # deleted state (manifest entries) -- a partial, not a clean, failure. Two
15
+ # independent fixes, belt-and-suspenders, deliberately not just one:
16
+ # 1. THIS gate: if the running bash is older than 4.4, re-exec the very
17
+ # same invocation (same $0, same args) under the first known-newer bash
18
+ # found on this machine, or fail loudly and exit BEFORE anything else
19
+ # runs if none is found -- never a silent continue under the old bash.
20
+ # 2. Every remaining empty-array expansion in this script is ALSO guarded
21
+ # with the `${a[@]+"${a[@]}"}` idiom (see e.g. system_root_args below),
22
+ # so behaviour is correct even if this gate were ever bypassed.
23
+ # RECONCILE_REEXEC_BASH marks "this process is already the re-exec'd child"
24
+ # so a re-exec under a bash that ALSO happens to be <4.4 (a broken/ancient
25
+ # Homebrew bash, say) can't loop forever -- it just proceeds to the normal
26
+ # version check failure below instead.
27
+ # RECONCILE_TEST_BASH_CANDIDATES (test-only, unset in every real run) is a
28
+ # space-separated override of the candidate list below, so a test can force
29
+ # either branch (found vs not-found) hermetically, without depending on
30
+ # whether THIS machine happens to have Homebrew bash installed.
31
+ if [[ -z "${RECONCILE_REEXEC_BASH:-}" ]]; then
32
+ reconcile_bash_major="${BASH_VERSINFO[0]:-0}"
33
+ reconcile_bash_minor="${BASH_VERSINFO[1]:-0}"
34
+ if [[ "$reconcile_bash_major" -lt 4 || ( "$reconcile_bash_major" -eq 4 && "$reconcile_bash_minor" -lt 4 ) ]]; then
35
+ reconcile_bash_candidates="${RECONCILE_TEST_BASH_CANDIDATES:-/opt/homebrew/bin/bash /usr/local/bin/bash /home/linuxbrew/.linuxbrew/bin/bash}"
36
+ reconcile_newer_bash=""
37
+ for reconcile_bash_candidate in $reconcile_bash_candidates; do
38
+ if [[ -x "$reconcile_bash_candidate" ]]; then
39
+ reconcile_newer_bash="$reconcile_bash_candidate"
40
+ break
41
+ fi
42
+ done
43
+ if [[ -n "$reconcile_newer_bash" ]]; then
44
+ export RECONCILE_REEXEC_BASH=1
45
+ exec "$reconcile_newer_bash" "$0" "$@"
46
+ fi
47
+ echo "bin/reconcile requires bash >= 4.4 (this run is bash ${BASH_VERSION:-unknown} -- empty-array expansions are unsafe under set -u before 4.4). No newer bash was found at: $reconcile_bash_candidates. Install one (e.g. 'brew install bash') and re-run, or put a newer bash earlier on PATH -- refusing to start rather than risk an 'unbound variable' abort mid-run after state has already changed." >&2
48
+ exit 1
49
+ fi
50
+ fi
51
+
52
+ set -euo pipefail
53
+
54
+ export PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
55
+
56
+ usage() {
57
+ echo "usage: reconcile --device NAME [--source npm|checkout] [--mode audit|plan|apply] [--manifest PATH] [--state-dir PATH] [--platform mac|linux]" >&2
58
+ exit 2
59
+ }
60
+
61
+ device=""
62
+ mode="audit"
63
+ manifest=""
64
+ state_dir="/var/lib/adl"
65
+ platform_override=""
66
+ source_mode="npm"
67
+ # A plain variable, not a hardcoded literal in write_one_repo_dir/
68
+ # remove_one_repo_dir themselves, so packages/fleet/tests/test-manifest.sh
69
+ # can point it at a disposable test_root directory (RECONCILE_SOURCE_ONLY=1)
70
+ # and exercise the real sync/removal mechanics without root or a real
71
+ # /usr/local. Matches packages/fleet/src/launch-agents.ts's
72
+ # DEFAULT_LIBEXEC_DIR in production.
73
+ repo_dir_libexec_root="/usr/local/libexec/adl"
74
+ # Test-only escape hatch for default-agent-file.ts / managed-settings.ts's own
75
+ # `--root` (both already accept it; see their headers). Unset in every real
76
+ # run -- production reconcile never sets this, so `--root` is never passed
77
+ # and behaviour is byte-identical. When set (packages/fleet/tests/
78
+ # test-manifest.sh only), forwarded as `--root <dir>` to every plan AND apply
79
+ # invocation of both scripts below, so a test never reads or writes the real
80
+ # `/Library/Application Support/...` or `/etc/...` paths.
81
+ reconcile_test_system_root_args=()
82
+ if [[ -n "${RECONCILE_TEST_SYSTEM_ROOT:-}" ]]; then
83
+ reconcile_test_system_root_args=(--root "$RECONCILE_TEST_SYSTEM_ROOT")
84
+ fi
85
+ while [[ "$#" -gt 0 ]]; do
86
+ case "$1" in
87
+ --device) device="${2:-}"; shift 2 ;;
88
+ --mode) mode="${2:-}"; shift 2 ;;
89
+ --manifest) manifest="${2:-}"; shift 2 ;;
90
+ --state-dir) state_dir="${2:-}"; shift 2 ;;
91
+ --platform) platform_override="${2:-}"; shift 2 ;;
92
+ --source) source_mode="${2:-}"; shift 2 ;;
93
+ *) usage ;;
94
+ esac
95
+ done
96
+ [[ -n "$device" ]] || usage
97
+ [[ "$mode" == audit || "$mode" == plan || "$mode" == apply ]] || usage
98
+ [[ "$source_mode" == npm || "$source_mode" == checkout ]] || usage
99
+
100
+ # Tool ids whose install/build failed in a way that was recorded and
101
+ # continued past, rather than aborting the whole reconciliation (see
102
+ # install_one_tool / build_one_bun_tool / install_one_embedding_model_tool /
103
+ # write_one_shim below). Populated only in apply mode -- every other mode's
104
+ # tool-loop bodies only print "would ..." and never fail.
105
+ failed_tools=()
106
+
107
+ # Every backup THIS run actually created (apply mode) or would create (plan
108
+ # mode), as "<original path> -> <backup path>" strings -- printed as one
109
+ # block at the very end, just before "global-tools: reconciliation complete"
110
+ # (Tarun, 2026-09-28). Collected from every step that backs something up
111
+ # before overwriting/removing a file: kimi-hooks (parsed from its own
112
+ # captured "backed up previous config to <path>" note), lean-ctx-observe-strip
113
+ # (2026-09-29, STATUS.md 12p item 13 final scope -- same note shape, parsed
114
+ # by collect_lean_ctx_observe_strip_backups), the global surface sweep
115
+ # (parsed from ITS OWN already-printed "global-surface-sweep: backups
116
+ # this run:" block -- never recomputed here, per Tarun's instruction not to
117
+ # duplicate that package's backup logic), and write_one_repo_script below
118
+ # (recorded directly; it runs in this same process, so no output-parsing is
119
+ # needed). managed-settings, default-agent-file, vscode-settings,
120
+ # surface-policy, and global-agents do not back anything up
121
+ # before overwriting -- see this file's own header comment on each call site,
122
+ # and packages/fleet/tests/test-manifest.sh's backups-summary tests, for the
123
+ # full accounting of which steps do and do not.
124
+ backups=()
125
+
126
+ record_backup() {
127
+ backups+=("$1 -> $2")
128
+ }
129
+
130
+ # Every backup path in this codebase has the shape
131
+ # "<original-path>.pre-<label>-<UTC compact timestamp>" (kimi-hooks:
132
+ # backupPath() in packages/agent-settings/src/kimi-hooks.ts;
133
+ # write_one_repo_script below uses the same shape). Recovering the original
134
+ # by stripping exactly that trailing segment means a backup-creating step's
135
+ # own note text never has to repeat the original path a second time just so
136
+ # this summary can report it.
137
+ strip_backup_suffix() {
138
+ local backup="$1"
139
+ if [[ "$backup" =~ ^(.*)\.pre-[a-z0-9-]+-[0-9]{8}T[0-9]{6}Z$ ]]; then
140
+ printf '%s\n' "${BASH_REMATCH[1]}"
141
+ else
142
+ printf '%s\n' "$backup"
143
+ fi
144
+ }
145
+
146
+ # Scans a captured step's stdout for kimi-hooks' own "... backed up ... to
147
+ # <path>" sentence and records it. A no-op on plan-mode output (kimi-hooks
148
+ # --mode plan never writes, so it never emits this note).
149
+ collect_kimi_hooks_backups() {
150
+ local text="$1" line backup
151
+ while IFS= read -r line; do
152
+ if [[ "$line" =~ ^kimi-hooks:\ backed\ up\ previous\ config\ to\ (.+)$ ]]; then
153
+ backup="${BASH_REMATCH[1]}"
154
+ record_backup "$(strip_backup_suffix "$backup")" "$backup"
155
+ fi
156
+ done <<<"$text"
157
+ }
158
+
159
+ # Same shape as collect_kimi_hooks_backups, for bin/lean-ctx-observe-strip's
160
+ # own "lean-ctx-observe-strip: backed up previous config to <path>" note --
161
+ # unlike kimi-hooks (always exactly one config file), this step can strip
162
+ # multiple JSON/shell-rc targets in one run, so the loop below (like
163
+ # collect_sweep_backups) records every match, not just the first.
164
+ #
165
+ # A skill-directory backup gets a SECOND, distinct line shape: "backed up
166
+ # previous skill directory <original> to <backup>" (final review finding
167
+ # 1: its backup path is a SIBLING of the scanned skills/ directory, e.g.
168
+ # ".codex/skills.pre-lean-ctx-observe-strip-<ts>/lean-ctx" -- it no longer
169
+ # ends in ".pre-<label>-<timestamp>", so strip_backup_suffix's regex can't
170
+ # recover the original path from it; that note states BOTH paths directly
171
+ # instead, and is matched here as its own case rather than forced through
172
+ # the single-path regex above).
173
+ collect_lean_ctx_observe_strip_backups() {
174
+ local text="$1" line backup original
175
+ while IFS= read -r line; do
176
+ if [[ "$line" =~ ^lean-ctx-observe-strip:\ backed\ up\ previous\ config\ to\ (.+)$ ]]; then
177
+ backup="${BASH_REMATCH[1]}"
178
+ record_backup "$(strip_backup_suffix "$backup")" "$backup"
179
+ elif [[ "$line" =~ ^lean-ctx-observe-strip:\ backed\ up\ previous\ skill\ directory\ (.+)\ to\ (.+)$ ]]; then
180
+ original="${BASH_REMATCH[1]}"
181
+ backup="${BASH_REMATCH[2]}"
182
+ record_backup "$original" "$backup"
183
+ fi
184
+ done <<<"$text"
185
+ }
186
+
187
+ # Folds the global-surface-sweep's OWN already-computed and already-printed
188
+ # backup lines into this run's combined summary, without recomputing
189
+ # backupPath() or any other sweep-owned logic (Tarun, 2026-09-28: a separate
190
+ # change added the sweep's own "global-surface-sweep: backups this run:" /
191
+ # "... no files changed; no backups" lines in
192
+ # packages/agent-settings/src/global-surface-sweep.ts). Its "backups this
193
+ # run:" block is followed by two-space-indented "<original> -> <backup>"
194
+ # lines -- already in exactly this file's own record_backup shape.
195
+ collect_sweep_backups() {
196
+ local text="$1" line in_block=0 original backup
197
+ while IFS= read -r line; do
198
+ if [[ "$line" == "global-surface-sweep: backups this run:" ]]; then
199
+ in_block=1
200
+ continue
201
+ fi
202
+ if [[ "$in_block" -eq 1 ]]; then
203
+ if [[ "$line" =~ ^\ \ (.+)\ -\>\ (.+)$ ]]; then
204
+ original="${BASH_REMATCH[1]}"
205
+ backup="${BASH_REMATCH[2]}"
206
+ record_backup "$original" "$backup"
207
+ continue
208
+ fi
209
+ in_block=0
210
+ fi
211
+ done <<<"$text"
212
+ }
213
+
214
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
215
+ repo_root="$(cd "$script_dir/.." && pwd)"
216
+ manifest="${manifest:-$repo_root/packages/fleet/manifest.json}"
217
+ # shellcheck source=lib/bun-compile-build.sh
218
+ source "$script_dir/lib/bun-compile-build.sh"
219
+ "$script_dir/validate-manifest" "$manifest" >/dev/null
220
+ if [[ "${RECONCILE_SOURCE_ONLY:-0}" != "1" ]]; then
221
+ if [[ "$source_mode" == npm && -e "$repo_root/.git" ]]; then
222
+ echo "npm source mode requires an installed package root; use --source checkout for a Git checkout" >&2
223
+ exit 2
224
+ fi
225
+ if [[ "$source_mode" == checkout && ! -e "$repo_root/.git" ]]; then
226
+ echo "checkout source mode requires a Git checkout" >&2
227
+ exit 2
228
+ fi
229
+ fi
230
+
231
+ case "$(uname -s)" in
232
+ Darwin) platform=mac; sha256_cmd=(shasum -a 256) ;;
233
+ Linux) platform=linux; sha256_cmd=(sha256sum) ;;
234
+ *) echo "unsupported operating system: $(uname -s)" >&2; exit 1 ;;
235
+ esac
236
+ if [[ -n "$platform_override" ]]; then
237
+ # Applying as a foreign platform would install the wrong platform's tools, so
238
+ # the override stays refused -- EXCEPT under RECONCILE_SOURCE_ONLY=1, which
239
+ # returns below before any loop or post-pass runs. Nothing is applied in that
240
+ # mode, so there is nothing for the guard to protect, and without this a test
241
+ # exercising a mac device in apply mode is silently macOS-only: on the Linux
242
+ # agents of the mixed CI pool it dies with "device m3 is not declared under
243
+ # linux" (build 4226).
244
+ if [[ "$mode" == apply && "${RECONCILE_SOURCE_ONLY:-0}" != "1" ]]; then
245
+ echo "--platform cannot override the host in apply mode" >&2
246
+ exit 2
247
+ fi
248
+ [[ "$platform_override" == mac || "$platform_override" == linux ]] || usage
249
+ platform="$platform_override"
250
+ fi
251
+
252
+ jq -e --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device]' "$manifest" >/dev/null || {
253
+ echo "device $device is not declared under $platform" >&2
254
+ exit 1
255
+ }
256
+
257
+ resolved_file="$(mktemp)"
258
+ previous_resolved_file="$(mktemp)"
259
+ trap 'rm -f "$resolved_file" "$previous_resolved_file"' EXIT
260
+ jq --arg platform "$platform" --arg device "$device" \
261
+ '.[$platform].common.tools * .[$platform].devices[$device].tools' "$manifest" >"$resolved_file"
262
+
263
+ applied_manifest="$state_dir/applied-manifest.json"
264
+ have_previous=0
265
+ if [[ -f "$applied_manifest" ]] && jq -e --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device]' "$applied_manifest" >/dev/null 2>&1; then
266
+ jq --arg platform "$platform" --arg device "$device" \
267
+ '.[$platform].common.tools * .[$platform].devices[$device].tools' "$applied_manifest" >"$previous_resolved_file"
268
+ have_previous=1
269
+ else
270
+ printf '{}\n' >"$previous_resolved_file"
271
+ fi
272
+
273
+ administrator="$(jq -r --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device].administrator' "$manifest")"
274
+ administrator_home="$(jq -r --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device].home' "$manifest")"
275
+ minimum_free_gib="$(jq -r --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device].minimum_free_gib' "$manifest")"
276
+ if [[ "$platform" == mac ]]; then
277
+ brew_bin=/opt/homebrew/bin/brew
278
+ else
279
+ brew_bin=/home/linuxbrew/.linuxbrew/bin/brew
280
+ fi
281
+
282
+ path_owner() {
283
+ local path="$1" owner
284
+ if owner="$(stat -f '%Su' "$path" 2>/dev/null)"; then
285
+ printf '%s\n' "$owner"
286
+ elif owner="$(stat -c '%U' "$path" 2>/dev/null)"; then
287
+ printf '%s\n' "$owner"
288
+ else
289
+ echo "cannot determine owner of Homebrew prefix: $path" >&2
290
+ return 1
291
+ fi
292
+ }
293
+
294
+ resolve_brew_owner() {
295
+ local target_platform="$1" target_mode="$2" prefix="$3" target_administrator="$4" owner
296
+ if [[ "$target_platform" == mac ]]; then
297
+ printf '%s\n' "$target_administrator"
298
+ return 0
299
+ fi
300
+ if [[ ! -d "$prefix" ]]; then
301
+ if [[ "$target_mode" == apply ]]; then
302
+ echo "Homebrew prefix missing: $prefix" >&2
303
+ return 1
304
+ fi
305
+ printf '%s\n' '<unresolved-linuxbrew-owner>'
306
+ return 0
307
+ fi
308
+ owner="$(path_owner "$prefix")" || return 1
309
+ if [[ "$owner" == root ]]; then
310
+ echo "Homebrew prefix must be owned by a non-root account: $prefix" >&2
311
+ return 1
312
+ fi
313
+ printf '%s\n' "$owner"
314
+ }
315
+
316
+ brew_owner="$(resolve_brew_owner "$platform" "$mode" "${brew_bin%/bin/brew}" "$administrator")" || exit 1
317
+ git_config_user="$administrator"
318
+
319
+ run_brew() {
320
+ if [[ "$mode" != apply ]]; then
321
+ printf 'would run brew as %s: %q' "$brew_owner" "$brew_bin"
322
+ printf ' %q' "$@"
323
+ printf '\n'
324
+ return 0
325
+ fi
326
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
327
+ (cd /tmp && sudo -u "$brew_owner" -H env NONINTERACTIVE=1 PATH="${brew_bin%/*}:/usr/bin:/bin" "$brew_bin" "$@" </dev/null)
328
+ }
329
+
330
+ # Portable command timeout: neither GNU coreutils' `timeout` nor its `gtimeout`
331
+ # alias is installed by default on a fresh mac (this manifest installs
332
+ # neither), so a real `command -v timeout` gate would silently do nothing on
333
+ # exactly the machine this exists to protect. Temporarily enable Bash job
334
+ # control so the backgrounded command gets its own process group, then send
335
+ # SIGTERM-then-SIGKILL to that whole group at the deadline. This covers child
336
+ # processes (brew helpers or self-updaters) as well as the wrapper subshell and
337
+ # prevents them from continuing after reconcile reports a timeout. Restore the
338
+ # caller's original job-control setting before returning. Exit 124 distinguishes
339
+ # a timeout from the command's own failure. Per-call-site timeouts let
340
+ # packages/fleet/tests/test-manifest.sh exercise this branch in real time
341
+ # (e.g. RECONCILE_BREW_UPGRADE_TIMEOUT_SECONDS=1) rather than waiting out the
342
+ # real 20-minute default below.
343
+ run_with_timeout() {
344
+ local timeout_seconds="$1"
345
+ shift
346
+ local status=0
347
+ local monitor_was_enabled=false
348
+ case $- in *m*) monitor_was_enabled=true ;; esac
349
+ set -m
350
+ ( "$@" ) &
351
+ local pid=$!
352
+ if [[ "$monitor_was_enabled" != true ]]; then
353
+ set +m
354
+ fi
355
+ local waited=0
356
+ while kill -0 "$pid" 2>/dev/null; do
357
+ if [[ "$waited" -ge "$timeout_seconds" ]]; then
358
+ kill -TERM -- "-$pid" 2>/dev/null || true
359
+ sleep 1
360
+ kill -KILL -- "-$pid" 2>/dev/null || true
361
+ wait "$pid" 2>/dev/null || :
362
+ return 124
363
+ fi
364
+ sleep 1
365
+ waited=$((waited + 1))
366
+ done
367
+ wait "$pid" || status=$?
368
+ return "$status"
369
+ }
370
+
371
+ # Timeouts for the two long-running, network-dependent steps this decision
372
+ # adds (brew upgrade, and each agent-surface CLI update below): generous
373
+ # defaults for a real run (20 minutes, 5 minutes), overridable per-call-site
374
+ # so test-manifest.sh can exercise the "hung command" branch in real time
375
+ # instead of waiting out either default for real.
376
+ BREW_UPGRADE_ALL_TIMEOUT_SECONDS="${RECONCILE_BREW_UPGRADE_TIMEOUT_SECONDS:-1200}"
377
+ SURFACE_UPDATE_TIMEOUT_SECONDS="${RECONCILE_SURFACE_UPDATE_TIMEOUT_SECONDS:-300}"
378
+
379
+ # Upgrades every outdated Homebrew formula AND cask in one call -- never a
380
+ # per-package list, so a formula outdated for reasons unrelated to this
381
+ # manifest (installed once by hand, or a transitive dependency of a manifest
382
+ # formula) still converges instead of accumulating outdated versions forever
383
+ # (item 1, 2026-09-28: `brew update` alone, the only brew-wide step that ran
384
+ # before this, never upgrades anything). Gated by the manifest's own
385
+ # brew_upgrade_all switch at the call site below, not inside this function --
386
+ # this function always does the real (or plan-mode preview) work when called.
387
+ run_brew_upgrade_all() {
388
+ if [[ "$mode" != apply ]]; then
389
+ echo "would run: brew upgrade (all outdated formulae and casks)"
390
+ return 0
391
+ fi
392
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
393
+ local outdated_formulae outdated_casks formulae_count casks_count
394
+ outdated_formulae="$(brew_query outdated --formula --quiet)" || {
395
+ echo "brew outdated --formula check failed" >&2
396
+ return 1
397
+ }
398
+ outdated_casks="$(brew_query outdated --cask --quiet)" || {
399
+ echo "brew outdated --cask check failed" >&2
400
+ return 1
401
+ }
402
+ formulae_count=0
403
+ [[ -z "$outdated_formulae" ]] || formulae_count="$(wc -l <<<"$outdated_formulae" | tr -d ' ')"
404
+ casks_count=0
405
+ [[ -z "$outdated_casks" ]] || casks_count="$(wc -l <<<"$outdated_casks" | tr -d ' ')"
406
+ if [[ "$formulae_count" -eq 0 && "$casks_count" -eq 0 ]]; then
407
+ echo "brew upgrade: nothing outdated"
408
+ return 0
409
+ fi
410
+ local upgrade_output upgrade_status=0
411
+ # Routed through run_brew itself (same owner/cwd/stdin wiring as every
412
+ # other brew mutation in this file), bounded by run_with_timeout so a
413
+ # network-stalled `brew upgrade` cannot hang the rest of this run forever.
414
+ upgrade_output="$(run_with_timeout "$BREW_UPGRADE_ALL_TIMEOUT_SECONDS" run_brew upgrade 2>&1)" || upgrade_status=$?
415
+ printf '%s\n' "$upgrade_output"
416
+ if [[ "$upgrade_status" -eq 124 ]]; then
417
+ echo "brew upgrade timed out after ${BREW_UPGRADE_ALL_TIMEOUT_SECONDS}s" >&2
418
+ return 1
419
+ elif [[ "$upgrade_status" -ne 0 ]]; then
420
+ echo "brew upgrade failed (exit $upgrade_status)" >&2
421
+ return 1
422
+ fi
423
+ echo "brew upgrade: $formulae_count formulae, $casks_count casks were outdated before upgrade"
424
+ return 0
425
+ }
426
+
427
+ run_apt() {
428
+ if [[ "$mode" != apply ]]; then
429
+ printf 'would run apt-get:'
430
+ printf ' %q' "$@"
431
+ printf '\n'
432
+ return 0
433
+ fi
434
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
435
+ env DEBIAN_FRONTEND=noninteractive apt-get "$@" </dev/null
436
+ }
437
+
438
+ # npm and pip both resolve through whichever PATH prefix brew's own tools use
439
+ # (node/npm are Homebrew formulas already; python3 ships with the OS or CLT on
440
+ # every device this reconciler targets), so they share brew_bin's directory
441
+ # rather than needing their own resolution.
442
+ run_npm() {
443
+ if [[ "$mode" != apply ]]; then
444
+ printf 'would run npm as %s: npm' "$administrator"
445
+ printf ' %q' "$@"
446
+ printf '\n'
447
+ return 0
448
+ fi
449
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
450
+ if [[ "$platform" == linux ]]; then
451
+ (cd /tmp && sudo -u "$administrator" -H env npm_config_prefix=/usr/local PATH="${brew_bin%/*}:/usr/bin:/bin" npm "$@" </dev/null)
452
+ else
453
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" npm "$@" </dev/null)
454
+ fi
455
+ }
456
+
457
+ # Whether a read-only, owner-scoped query (brew_query, npm_installed_version)
458
+ # can run without asking for privilege reconcile does not already have. Apply
459
+ # mode always runs as real root (checked elsewhere), which can sudo -u any
460
+ # administrator with no password. A plan/audit preview run has no such
461
+ # guarantee -- $administrator on a Linux VPS device is "root", and previewing
462
+ # that device's plan from an unprivileged account must not demand a sudo
463
+ # password it may not have; it falls back to the pre-2026-09-28 unconditional
464
+ # message instead. Self-administered devices (m3: administrator=tarunrana)
465
+ # need no escalation at all, so their preview can query for real.
466
+ can_query_as_administrator() {
467
+ [[ "$(id -un)" == "$administrator" ]] || [[ "$(id -u)" -eq 0 ]]
468
+ }
469
+
470
+ # Read-only Homebrew query, run exactly like run_brew's real mutations (same
471
+ # owner, cwd, and stdin) but never a mutation itself. A named function so
472
+ # tests can stub just this, instead of having to fake sudo's own argument
473
+ # parsing the way a raw inline `sudo -u ... "$brew_bin" ...` would require.
474
+ # Apply-mode only (see can_query_as_administrator): `brew list`/`brew
475
+ # outdated` have been observed to take minutes under real load on a
476
+ # contended box (Homebrew's own auto-update against a large git checkout),
477
+ # so a plan preview never calls this -- it would turn a "quick look" into a
478
+ # slow, load-dependent one for every brew formula on the device.
479
+ brew_query() {
480
+ (cd /tmp && sudo -u "$brew_owner" -H "$brew_bin" "$@" </dev/null)
481
+ }
482
+
483
+ # Read-only global npm version lookups, run with the same user/PATH (and, on
484
+ # Linux, the same npm_config_prefix) as run_npm's real mutations, so a lookup
485
+ # never disagrees with what an install would actually see. Both are named
486
+ # functions for the same stubbing reason as brew_query above.
487
+ #
488
+ # npm ls -g's own exit code is NOT a reliable failure signal: it exits 1 for
489
+ # the completely ordinary case of "this package is not installed" (valid,
490
+ # parseable JSON either way) -- treating that as an error would turn "not
491
+ # installed yet" into a false alarm. A REAL failure (npm itself could not
492
+ # run: missing binary, sudo denied, corrupt output) instead fails to produce
493
+ # parseable JSON at all, which is the actual signal this checks for.
494
+ npm_installed_version() {
495
+ local package="$1" json
496
+ if [[ "$platform" == linux ]]; then
497
+ json="$(cd /tmp && sudo -u "$administrator" -H env npm_config_prefix=/usr/local PATH="${brew_bin%/*}:/usr/bin:/bin" npm ls -g --depth=0 --json "$package" </dev/null 2>/dev/null)"
498
+ else
499
+ json="$(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" npm ls -g --depth=0 --json "$package" </dev/null 2>/dev/null)"
500
+ fi
501
+ jq -e . >/dev/null 2>&1 <<<"$json" || return 1
502
+ jq -r --arg pkg "$package" '.dependencies[$pkg].version // empty' <<<"$json"
503
+ }
504
+
505
+ # Unlike `npm ls -g`, `npm view <pkg> version` DOES fail (real, non-zero exit)
506
+ # for a genuinely missing/unpublished package -- verified live 2026-09-28
507
+ # (404 from the registry) -- so its own exit code is trusted directly here.
508
+ npm_latest_version() {
509
+ local package="$1"
510
+ if [[ "$platform" == linux ]]; then
511
+ (cd /tmp && sudo -u "$administrator" -H env npm_config_prefix=/usr/local PATH="${brew_bin%/*}:/usr/bin:/bin" npm view "$package" version </dev/null 2>/dev/null)
512
+ else
513
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" npm view "$package" version </dev/null 2>/dev/null)
514
+ fi
515
+ }
516
+
517
+ npm_global_root() {
518
+ [[ "$mode" == apply ]] || return 1
519
+ if [[ "$platform" == linux ]]; then
520
+ (cd /tmp && sudo -u "$administrator" -H env npm_config_prefix=/usr/local PATH="${brew_bin%/*}:/usr/bin:/bin" npm root -g </dev/null)
521
+ else
522
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" npm root -g </dev/null)
523
+ fi
524
+ }
525
+
526
+ npm_global_executable_path() {
527
+ local executable="$1"
528
+ sudo -u "$administrator" -H env PATH="/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" \
529
+ /bin/sh -c 'command -v "$1"' /bin/sh "$executable" 2>/dev/null
530
+ }
531
+
532
+ canonical_parent_path() {
533
+ local path="$1" parent base
534
+ parent="${path%/*}"
535
+ base="${path##*/}"
536
+ (cd -P "$parent" && printf '%s/%s\n' "$PWD" "$base")
537
+ }
538
+
539
+ # A successful npm exit code alone does not prove that the command now belongs
540
+ # to npm: Linux has both legacy Bun binaries and npm shims in /usr/local/bin.
541
+ # Confirm the package's declared bin target owns the command before accepting
542
+ # the install.
543
+ npm_package_owns_executable() {
544
+ local package="$1" executable="$2" npm_root package_manifest bin_relative actual_path actual_link expected_target actual_target
545
+ npm_root="$(npm_global_root)" || {
546
+ echo "could not resolve npm global root for $package" >&2
547
+ return 1
548
+ }
549
+ package_manifest="$npm_root/$package/package.json"
550
+ [[ -f "$package_manifest" ]] || {
551
+ echo "npm package metadata missing: $package_manifest" >&2
552
+ return 1
553
+ }
554
+ bin_relative="$(jq -r --arg executable "$executable" '
555
+ if (.bin | type) == "string" then .bin
556
+ elif (.bin | type) == "object" then .bin[$executable] // empty
557
+ else empty
558
+ end
559
+ ' "$package_manifest")"
560
+ [[ -n "$bin_relative" && "$bin_relative" != /* && "$bin_relative" != .. && "$bin_relative" != ../* && "$bin_relative" != */../* && "$bin_relative" != */.. ]] || {
561
+ echo "npm package $package does not declare a safe bin target for $executable" >&2
562
+ return 1
563
+ }
564
+ actual_path="$(npm_global_executable_path "$executable")" || {
565
+ echo "npm executable missing from global PATH: $executable" >&2
566
+ return 1
567
+ }
568
+ [[ -L "$actual_path" ]] || {
569
+ echo "npm executable is not an npm-managed symlink: $actual_path" >&2
570
+ return 1
571
+ }
572
+ expected_target="$(canonical_parent_path "$npm_root/$package/$bin_relative")" || return 1
573
+ [[ -f "$expected_target" && -x "$expected_target" ]] || {
574
+ echo "npm package $package bin target is not an executable file: $expected_target" >&2
575
+ return 1
576
+ }
577
+ actual_link="$(readlink "$actual_path")" || return 1
578
+ if [[ "$actual_link" == /* ]]; then
579
+ actual_target="$(canonical_parent_path "$actual_link")" || return 1
580
+ else
581
+ actual_target="$(canonical_parent_path "${actual_path%/*}/$actual_link")" || return 1
582
+ fi
583
+ [[ "$actual_target" == "$expected_target" ]] || {
584
+ echo "npm executable $executable is not owned by $package" >&2
585
+ return 1
586
+ }
587
+ }
588
+
589
+ run_pip() {
590
+ if [[ "$mode" != apply ]]; then
591
+ printf 'would run pip as %s: python3 -m pip' "$administrator"
592
+ printf ' %q' "$@"
593
+ printf '\n'
594
+ return 0
595
+ fi
596
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
597
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" python3 -m pip "$@" </dev/null)
598
+ }
599
+
600
+ pip_user_executable() {
601
+ local executable="$1"
602
+ [[ "$executable" != */* ]] || {
603
+ echo "pip executable must be a command name, not a path: $executable" >&2
604
+ exit 1
605
+ }
606
+ local user_base user_home
607
+ user_home="$(cd /tmp && sudo -u "$administrator" -H /bin/sh -c 'printf "%s\n" "$HOME"')"
608
+ user_base="$(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" python3 -m site --user-base)"
609
+ [[ "$user_base" == "$user_home" || "$user_base" == "$user_home"/* ]] || {
610
+ echo "pip user base is outside the declared administrator home: $user_base" >&2
611
+ exit 1
612
+ }
613
+ printf '%s/bin/%s\n' "${user_base%/}" "$executable"
614
+ }
615
+
616
+ ensure_global_pip_executable() {
617
+ local executable="$1"
618
+ local target="/usr/local/bin/$executable"
619
+ if [[ "$mode" != apply ]]; then
620
+ echo "would expose pip executable globally: $target"
621
+ return 0
622
+ fi
623
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
624
+ local source existing_source user_home
625
+ source="$(pip_user_executable "$executable")"
626
+ user_home="$(cd /tmp && sudo -u "$administrator" -H /bin/sh -c 'printf "%s\n" "$HOME"')"
627
+ [[ -x "$source" ]] || { echo "pip executable missing after install: $source" >&2; exit 1; }
628
+ if [[ -L "$target" ]]; then
629
+ existing_source="$(readlink "$target")"
630
+ if [[ "$existing_source" == "$source" ]]; then
631
+ echo "global pip executable already points to $source"
632
+ return 0
633
+ fi
634
+ case "$existing_source" in
635
+ "$user_home"/*/bin/"$executable") ;;
636
+ *) echo "refusing to replace unrelated symlink: $target -> $existing_source" >&2; exit 1 ;;
637
+ esac
638
+ elif [[ -e "$target" ]]; then
639
+ echo "refusing to replace unrelated global executable: $target" >&2
640
+ exit 1
641
+ fi
642
+ ln -sfn "$source" "$target"
643
+ echo "exposed pip executable globally: $target -> $source"
644
+ }
645
+
646
+ remove_global_pip_executable() {
647
+ local executable="$1"
648
+ local target="/usr/local/bin/$executable"
649
+ if [[ "$mode" != apply ]]; then
650
+ echo "would remove global pip executable: $target"
651
+ return 0
652
+ fi
653
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
654
+ if [[ ! -e "$target" && ! -L "$target" ]]; then
655
+ return 0
656
+ fi
657
+ [[ -L "$target" ]] || { echo "refusing to remove unrelated global executable: $target" >&2; exit 1; }
658
+ local source user_home
659
+ source="$(readlink "$target")"
660
+ user_home="$(cd /tmp && sudo -u "$administrator" -H /bin/sh -c 'printf "%s\n" "$HOME"')"
661
+ case "$source" in
662
+ "$user_home"/*/bin/"$executable") rm -f -- "$target" ;;
663
+ *) echo "refusing to remove unrelated symlink: $target -> $source" >&2; exit 1 ;;
664
+ esac
665
+ }
666
+
667
+ # Git as the checkout's owner: launchd runs apply as root against a
668
+ # user-owned checkout, and Git's ownership guard rejects that unless the
669
+ # lookup runs as its owner. Plan/audit runs already are that user.
670
+ repo_git() {
671
+ if [[ "$(id -u)" -eq 0 ]]; then
672
+ sudo -u "$administrator" -H git -C "$repo_root" "$@"
673
+ else
674
+ git -C "$repo_root" "$@"
675
+ fi
676
+ }
677
+
678
+ # az374 (npm-sourced reconcile): true only on a live Git clone of this
679
+ # repository (today's dev/CI shape, and every existing test fixture below).
680
+ # False once the fleet job's source is the published npm package --
681
+ # $repo_root is then the installed package directory itself, with no .git,
682
+ # and IS the release artifact rather than something to ask git about.
683
+ repo_root_is_git() {
684
+ [[ -e "$repo_root/.git" ]]
685
+ }
686
+
687
+ # Content identity for a source directory when there is no git to ask
688
+ # (repo_root_is_git is false): every tracked-equivalent file's path and bytes,
689
+ # hashed in a path-sorted, platform-independent order so the result does not
690
+ # depend on directory read order between Darwin and Linux. Deliberately plain
691
+ # `find | sort` (newline-delimited, no GNU-only `-print0`/`sort -z`) -- these
692
+ # are our own source trees, not adversarial input, and this runs on Darwin
693
+ # (BSD find/sort, no -z) as well as Linux.
694
+ content_tree_hash() {
695
+ local dir="$1" f
696
+ (
697
+ cd "$dir" && find . -type f | LC_ALL=C sort | while IFS= read -r f; do
698
+ "${sha256_cmd[@]+"${sha256_cmd[@]}"}" "$f"
699
+ done
700
+ ) | "${sha256_cmd[@]+"${sha256_cmd[@]}"}" | awk '{print $1}'
701
+ }
702
+
703
+ # The content identity of a bun-compile tool: the git tree hash of its
704
+ # source directory at HEAD. It changes whenever any tracked file under that
705
+ # directory changes -- code, package.json, and bun.lock (every bun-compile
706
+ # source keeps its lockfile inside its own directory) -- and never for
707
+ # commits elsewhere in the repository. HEAD, not the working tree: the
708
+ # scheduled wrapper resets the checkout to origin/main before reconcile begins.
709
+ bun_source_hash() {
710
+ local source_dir="$1" hash
711
+ if repo_root_is_git; then
712
+ hash="$(repo_git rev-parse --verify --quiet "HEAD:$source_dir")" || {
713
+ echo "bun-compile source is not tracked at HEAD: $source_dir" >&2
714
+ return 1
715
+ }
716
+ else
717
+ [[ -d "$repo_root/$source_dir" ]] || {
718
+ echo "bun-compile source missing from installed package: $repo_root/$source_dir" >&2
719
+ return 1
720
+ }
721
+ hash="$(content_tree_hash "$repo_root/$source_dir")"
722
+ fi
723
+ printf '%s\n' "$hash"
724
+ }
725
+
726
+ bun_source_hash_file() {
727
+ printf '%s/bun-compile/%s.source-hash\n' "$state_dir" "$1"
728
+ }
729
+
730
+ run_bun_compile() {
731
+ local package="$1"
732
+ local source_hash="$2"
733
+ local source_dir="$3"
734
+ local entry="$4"
735
+ local target="$5"
736
+ if [[ "$mode" != apply ]]; then
737
+ printf 'would build and install bun binary: %s -> %s (source %s)\n' "$package" "$target" "$source_hash"
738
+ return 0
739
+ fi
740
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
741
+ [[ "$target" == /usr/local/bin/* ]] || { echo "bun-compile target must be under /usr/local/bin: $target" >&2; exit 1; }
742
+ [[ "$source_dir" != /* && "$source_dir" != *..* ]] || {
743
+ echo "unsafe bun-compile source path: $source_dir" >&2
744
+ exit 1
745
+ }
746
+ local project_dir="$repo_root/$source_dir"
747
+ [[ -d "$project_dir" ]] || { echo "bun-compile source missing: $project_dir" >&2; exit 1; }
748
+ local bun_bin
749
+ bun_bin="$(command -v bun || true)"
750
+ [[ -n "$bun_bin" ]] || { echo "bun-compile requires bun on PATH; ensure bun installs before this tool" >&2; exit 1; }
751
+ (cd "$project_dir" && "$bun_bin" install --frozen-lockfile)
752
+ bun_compile_build "$bun_bin" "$project_dir/$entry" "$target"
753
+ }
754
+
755
+ run_embedding_model() {
756
+ local package="$1"
757
+ local spec_json="$2"
758
+ if [[ "$mode" != apply ]]; then
759
+ printf 'would install embedding model: %s as %s -> ~/.cache/adl-embedding/model\n' "$package" "$administrator"
760
+ return 0
761
+ fi
762
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
763
+ command -v bun >/dev/null 2>&1 || { echo "embedding-model requires bun; ensure bun installs before this tool" >&2; exit 1; }
764
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
765
+ bun "$repo_root/packages/fleet/src/install-embedding-model.ts" install --spec "$spec_json" </dev/null)
766
+ }
767
+
768
+ # Installs $src as $dest via a same-directory temp-file + rename, never a
769
+ # direct in-place write -- so a concurrent reader (including the next agent
770
+ # shell's `command -v $dest`, per the lean-ctx incident this fixed: an
771
+ # in-place npm postinstall rewrite left the shim executing but printing
772
+ # "binary not found" for ~44s on every scheduled reconcile) can never observe
773
+ # $dest existing but only partially written. `mv` within the same directory
774
+ # is a single rename(2) on both Darwin and Linux -- atomic by construction,
775
+ # not by scheduling luck.
776
+ atomic_install_executable() {
777
+ local src="$1"
778
+ local dest="$2"
779
+ install -m 0755 "$src" "$dest.new"
780
+ mv -f "$dest.new" "$dest"
781
+ }
782
+
783
+ install_surface_policy_file() {
784
+ local source="$repo_root/packages/agent-settings/surface-policy.json"
785
+ local target="/etc/adl/surface-policy.json"
786
+ if [[ "$mode" != apply ]]; then
787
+ echo "would install required surface policy: $source -> $target"
788
+ return 0
789
+ fi
790
+ [[ -f "$source" ]] || { echo "surface policy source missing: $source" >&2; return 1; }
791
+ jq -e '(.deny.paths | arrays) and (.deny.nativeTools | arrays)' "$source" >/dev/null || {
792
+ echo "surface policy source is invalid: $source" >&2
793
+ return 1
794
+ }
795
+ mkdir -p /etc/adl
796
+ install -m 0644 "$source" "$target.new"
797
+ mv -f "$target.new" "$target"
798
+ echo "installed required surface policy: $target"
799
+ }
800
+
801
+ # Resolves the release tag GitHub's own "latest" redirect currently points
802
+ # at, without touching the api.github.com REST API (unauthenticated calls
803
+ # there are rate-limited per source IP; this fleet's shared egress IP already
804
+ # exhausts that limit under routine use). `.../releases/latest` 302s to
805
+ # `.../releases/tag/vTAG`; the tag is just that redirect's last path segment.
806
+ resolve_latest_release_tag() {
807
+ local repo="$1"
808
+ local location
809
+ location="$(curl --proto '=https' --proto-redir '=https' -fsS -o /dev/null -w '%{redirect_url}' "https://github.com/$repo/releases/latest")"
810
+ [[ -n "$location" ]] || { echo "could not resolve latest release tag for $repo" >&2; exit 1; }
811
+ basename "$location"
812
+ }
813
+
814
+ run_release_archive() {
815
+ local package="$1"
816
+ local version="$2"
817
+ local url="$3"
818
+ local expected_sha256="$4"
819
+ local archive_executable="$5"
820
+ local target="$6"
821
+ if [[ "$mode" != apply ]]; then
822
+ printf 'would install pinned release archive: %s %s -> %s\n' "$package" "$version" "$target"
823
+ return 0
824
+ fi
825
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
826
+ [[ "$target" == /usr/local/bin/* ]] || { echo "release archive target must be under /usr/local/bin: $target" >&2; exit 1; }
827
+ [[ "$archive_executable" != /* && "$archive_executable" != *..* ]] || {
828
+ echo "unsafe archive executable path: $archive_executable" >&2
829
+ exit 1
830
+ }
831
+
832
+ (
833
+ local release_dir archive_file extract_dir actual_sha256
834
+ release_dir="$(mktemp -d)"
835
+ trap 'rm -rf "$release_dir"' EXIT
836
+ archive_file="$release_dir/release.tar.gz"
837
+ extract_dir="$release_dir/extract"
838
+ mkdir -p "$extract_dir"
839
+ curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 -o "$archive_file" "$url"
840
+ case "$(uname -s)" in
841
+ Darwin) actual_sha256="$(shasum -a 256 "$archive_file" | awk '{print $1}')" ;;
842
+ Linux) actual_sha256="$(sha256sum "$archive_file" | awk '{print $1}')" ;;
843
+ *) echo "unsupported operating system for release archive" >&2; exit 1 ;;
844
+ esac
845
+ [[ "$actual_sha256" == "$expected_sha256" ]] || {
846
+ echo "checksum mismatch for $package $version" >&2
847
+ exit 1
848
+ }
849
+ tar -xzf "$archive_file" -C "$extract_dir" "$archive_executable"
850
+ [[ -f "$extract_dir/$archive_executable" ]] || {
851
+ echo "archive executable missing: $archive_executable" >&2
852
+ exit 1
853
+ }
854
+ atomic_install_executable "$extract_dir/$archive_executable" "$target"
855
+ )
856
+ }
857
+
858
+ # Like run_release_archive, but never pins a version or checksum in the
859
+ # manifest: it always installs whatever GitHub's own "latest" alias currently
860
+ # serves, verified against the checksums file published *in that same
861
+ # release* (never a value carried over from a previous run or hardcoded
862
+ # here) -- fails loud, installs nothing, if that checksums asset is missing
863
+ # or doesn't list our asset. `.../releases/latest/download/<name>` is GitHub's
864
+ # own stable alias for "the matching asset of whatever release is latest
865
+ # right now", so the download itself never needs the resolved tag.
866
+ run_release_archive_latest() {
867
+ local package="$1"
868
+ local repo="$2"
869
+ local asset="$3"
870
+ local checksums_asset="$4"
871
+ local archive_executable="$5"
872
+ local target="$6"
873
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
874
+ [[ "$target" == /usr/local/bin/* ]] || { echo "release archive target must be under /usr/local/bin: $target" >&2; exit 1; }
875
+ [[ "$archive_executable" != /* && "$archive_executable" != *..* ]] || {
876
+ echo "unsafe archive executable path: $archive_executable" >&2
877
+ exit 1
878
+ }
879
+
880
+ (
881
+ local release_dir archive_file checksums_file extract_dir actual_sha256 expected_sha256
882
+ release_dir="$(mktemp -d)"
883
+ trap 'rm -rf "$release_dir"' EXIT
884
+ archive_file="$release_dir/release.tar.gz"
885
+ checksums_file="$release_dir/checksums"
886
+ extract_dir="$release_dir/extract"
887
+ mkdir -p "$extract_dir"
888
+ curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 \
889
+ -o "$checksums_file" "https://github.com/$repo/releases/latest/download/$checksums_asset"
890
+ expected_sha256="$(awk -v want="$asset" '$2 == want { print $1; found = 1 } END { exit !found }' "$checksums_file")" || {
891
+ echo "no published checksum for $asset in $repo's $checksums_asset -- refusing to install unverified bytes" >&2
892
+ exit 1
893
+ }
894
+ curl --proto '=https' --proto-redir '=https' -fsSL --retry 3 \
895
+ -o "$archive_file" "https://github.com/$repo/releases/latest/download/$asset"
896
+ case "$(uname -s)" in
897
+ Darwin) actual_sha256="$(shasum -a 256 "$archive_file" | awk '{print $1}')" ;;
898
+ Linux) actual_sha256="$(sha256sum "$archive_file" | awk '{print $1}')" ;;
899
+ *) echo "unsupported operating system for release archive" >&2; exit 1 ;;
900
+ esac
901
+ [[ "$actual_sha256" == "$expected_sha256" ]] || {
902
+ echo "checksum mismatch for $package (latest): expected $expected_sha256 from published $checksums_asset, got $actual_sha256" >&2
903
+ exit 1
904
+ }
905
+ tar -xzf "$archive_file" -C "$extract_dir" "$archive_executable"
906
+ [[ -f "$extract_dir/$archive_executable" ]] || {
907
+ echo "archive executable missing: $archive_executable" >&2
908
+ exit 1
909
+ }
910
+ atomic_install_executable "$extract_dir/$archive_executable" "$target"
911
+ )
912
+ }
913
+
914
+ # global_surface_sweep.remove_unprotected wiring: when false, the sweep only
915
+ # ever plans/reports (mode=audit) even during an apply run -- it never
916
+ # deletes. When true (the historical, still-default behavior), an apply run
917
+ # enforces (removes) unprotected candidates. Plan/audit reconcile runs are
918
+ # always audit-only regardless of this flag, since they never mutate anything.
919
+ resolve_surface_mode() {
920
+ local run_mode="$1" remove_unprotected="$2"
921
+ if [[ "$run_mode" == apply && "$remove_unprotected" == true ]]; then
922
+ printf 'enforce\n'
923
+ else
924
+ printf 'audit\n'
925
+ fi
926
+ }
927
+
928
+ # Shims run in their own pass, strictly after every real tool above has been
929
+ # installed, upgraded, and PATH-verified — never interleaved by key order.
930
+ # `keys[]` is alphabetical, and relying on a shim's id happening to sort after
931
+ # its target is exactly the kind of implicit-ordering accident this project
932
+ # elsewhere replaced with an explicit dependency graph; a real pass boundary
933
+ # does not need one.
934
+ #
935
+ # install_one_tool is called as the condition of `if ! install_one_tool ...`
936
+ # below, which -- under `set -e` -- exempts every command it runs (including
937
+ # ones nested in the functions it calls) from aborting the script on
938
+ # failure: a single failing tool (a flaky brew mirror, an npm 403, a
939
+ # yanked release asset) is recorded and the loop moves on to the next tool,
940
+ # so the sweep/kimi-hooks/global-agents/health post-passes below still run.
941
+ # A hard `exit` inside run_brew/run_npm/etc. (e.g. "apply mode must run as
942
+ # root") is a real environment fault, not a flaky install, and still
943
+ # aborts immediately -- `exit` is unconditional and is never caught here.
944
+ install_one_tool() {
945
+ local tool_id="$1" manager package executable previous_executable migrating_from_bun=0 rc=0 npm_version npm_spec npm_allow_scripts npm_target_version npm_installed_ver npm_plan_installed brew_outdated_output brew_installed_version
946
+ manager="$(jq -r --arg id "$tool_id" '.[$id].manager' "$resolved_file")"
947
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
948
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
949
+ if [[ "$tool_id" == adl && "$source_mode" == checkout ]]; then
950
+ echo "ensure Adl from explicit checkout source: $repo_root"
951
+ build_checkout_adl || rc=1
952
+ if [[ "$mode" == apply && "$rc" -eq 0 ]]; then
953
+ command -v "$executable" >/dev/null 2>&1 || { echo "checkout Adl executable missing from PATH: $executable" >&2; rc=1; }
954
+ fi
955
+ return "$rc"
956
+ fi
957
+ if [[ "$tool_id" == adl && "$source_mode" == npm ]]; then
958
+ if [[ "$mode" != apply ]]; then
959
+ echo "would use installed npm package payload: $repo_root"
960
+ return 0
961
+ fi
962
+ if is_in_place_bun_to_npm_migration "$tool_id"; then
963
+ prepare_in_place_bun_to_npm_migration "$tool_id" || return 1
964
+ fi
965
+ npm_package_owns_executable "$package" "$executable" || return 1
966
+ echo "using installed npm package payload: $repo_root"
967
+ return 0
968
+ fi
969
+ echo "ensure latest stable: $tool_id ($manager:$package)"
970
+ case "$manager" in
971
+ brew)
972
+ # Plan mode's message is intentionally unconditional (see brew_query's
973
+ # own comment for why: a real up-to-date check is slow enough on a
974
+ # loaded box to defeat the point of a quick preview). Apply mode pays
975
+ # that cost once, for real, and only when it would otherwise blindly
976
+ # run `brew upgrade` on an already-current formula -- which brew itself
977
+ # accepts silently (prints "Warning: <pkg> <ver> already installed" and
978
+ # exits 0), the exact per-run noise this replaces.
979
+ if [[ "$mode" != apply ]]; then
980
+ echo "would install or upgrade Homebrew formula: $package"
981
+ elif brew_query list --formula "$package" >/dev/null 2>&1; then
982
+ if brew_outdated_output="$(brew_query outdated --formula --quiet "$package")"; then
983
+ if [[ -z "$brew_outdated_output" ]]; then
984
+ brew_installed_version="$(brew_query list --formula --versions "$package" | awk '{print $2}')"
985
+ echo "already latest: $tool_id (brew:$package $brew_installed_version)"
986
+ else
987
+ run_brew upgrade "$package" || rc=1
988
+ fi
989
+ else
990
+ echo "$tool_id: brew outdated check failed for $package" >&2
991
+ rc=1
992
+ fi
993
+ else
994
+ run_brew install "$package" || rc=1
995
+ fi
996
+ ;;
997
+ apt)
998
+ run_apt install -y "$package" || rc=1
999
+ ;;
1000
+ npm)
1001
+ # An entry's optional .version pins the installed release (e.g.
1002
+ # adl-agent-id, Handoff 5 2026-09-26 H4) rather than tracking whatever
1003
+ # npm resolves as latest for update_policy=latest-stable. Ownership
1004
+ # verification below still uses the bare package name: npm's global
1005
+ # tree and package.json are keyed by package, never by the requested
1006
+ # version spec.
1007
+ npm_version="$(jq -r --arg id "$tool_id" '.[$id].version // empty' "$resolved_file")"
1008
+ npm_spec="$package"
1009
+ [[ -z "$npm_version" ]] || npm_spec="$package@$npm_version"
1010
+ # Only @amsterdamdatalabs/enact-docs-search's manifest entry carries
1011
+ # this (docs/operations/global-tools.md Topic 2, decided 2026-09-28):
1012
+ # its own dependencies' install scripts, explicitly reviewed and
1013
+ # allowed, never a global `npm config set allow-scripts`.
1014
+ npm_allow_scripts="$(jq -r --arg id "$tool_id" '(.[$id].allow_scripts // []) | join(",")' "$resolved_file")"
1015
+ if [[ "$mode" != apply ]]; then
1016
+ if is_in_place_bun_to_npm_migration "$tool_id"; then
1017
+ previous_executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
1018
+ echo "would remove retired Bun compilation before npm install: $tool_id ($previous_executable and $(bun_source_hash_file "$tool_id"))"
1019
+ fi
1020
+ # A pinned version is a pure local/offline fact: compare it against
1021
+ # the real global install (npm_installed_version, no network) and say
1022
+ # so honestly. An UNPINNED entry's "latest stable" is not knowable
1023
+ # without a per-package registry round trip (npm_latest_version) --
1024
+ # doing that here would make a plan preview slow and network-bound
1025
+ # for every unpinned npm tool on the device, so it keeps today's
1026
+ # conservative message instead of guessing. Either way, only query
1027
+ # when it needs no new sudo privilege (can_query_as_administrator) --
1028
+ # a preview run must not demand a password a different device's real
1029
+ # administrator would need.
1030
+ if [[ -n "$npm_version" ]] && can_query_as_administrator; then
1031
+ if npm_plan_installed="$(npm_installed_version "$package")"; then
1032
+ if [[ -n "$npm_plan_installed" && "$npm_plan_installed" == "$npm_version" ]]; then
1033
+ echo "already latest: $tool_id (npm:$package@$npm_plan_installed)"
1034
+ else
1035
+ echo "would install or upgrade npm package: $npm_spec"
1036
+ fi
1037
+ else
1038
+ echo "would install or upgrade npm package: $npm_spec (installed version not checked: npm ls -g failed)"
1039
+ fi
1040
+ else
1041
+ echo "would install or upgrade npm package: $npm_spec"
1042
+ fi
1043
+ else
1044
+ if is_in_place_bun_to_npm_migration "$tool_id"; then
1045
+ migrating_from_bun=1
1046
+ prepare_in_place_bun_to_npm_migration "$tool_id" || rc=1
1047
+ fi
1048
+ if [[ "$rc" -eq 0 ]]; then
1049
+ if [[ -n "$npm_version" ]]; then
1050
+ npm_target_version="$npm_version"
1051
+ elif ! npm_target_version="$(npm_latest_version "$package")"; then
1052
+ echo "$tool_id: npm view could not resolve the latest version of $package" >&2
1053
+ rc=1
1054
+ fi
1055
+ fi
1056
+ if [[ "$rc" -eq 0 ]]; then
1057
+ if ! npm_installed_ver="$(npm_installed_version "$package")"; then
1058
+ echo "$tool_id: npm ls -g could not inspect the global install of $package" >&2
1059
+ rc=1
1060
+ elif [[ -n "$npm_installed_ver" && "$npm_installed_ver" == "$npm_target_version" ]]; then
1061
+ echo "already latest: $tool_id (npm:$package@$npm_installed_ver)"
1062
+ elif [[ -n "$npm_allow_scripts" ]]; then
1063
+ run_npm install -g --no-audit --no-fund --allow-scripts="$npm_allow_scripts" "$npm_spec" || rc=1
1064
+ [[ "$rc" -ne 0 || "$migrating_from_bun" -eq 0 ]] || npm_package_owns_executable "$package" "$executable" || rc=1
1065
+ else
1066
+ run_npm install -g --no-audit --no-fund "$npm_spec" || rc=1
1067
+ [[ "$rc" -ne 0 || "$migrating_from_bun" -eq 0 ]] || npm_package_owns_executable "$package" "$executable" || rc=1
1068
+ fi
1069
+ fi
1070
+ fi
1071
+ ;;
1072
+ pip)
1073
+ if [[ "$mode" != apply ]]; then
1074
+ echo "would install or upgrade pip package: $package"
1075
+ elif run_pip show "$package" >/dev/null 2>&1; then
1076
+ run_pip install --user --break-system-packages --upgrade "$package" || rc=1
1077
+ else
1078
+ run_pip install --user --break-system-packages "$package" || rc=1
1079
+ fi
1080
+ [[ "$rc" -eq 0 ]] && { ensure_global_pip_executable "$executable" || rc=1; }
1081
+ ;;
1082
+ release-archive)
1083
+ local version url sha256 archive_executable
1084
+ version="$(jq -r --arg id "$tool_id" '.[$id].version' "$resolved_file")"
1085
+ url="$(jq -r --arg id "$tool_id" '.[$id].url' "$resolved_file")"
1086
+ sha256="$(jq -r --arg id "$tool_id" '.[$id].sha256' "$resolved_file")"
1087
+ archive_executable="$(jq -r --arg id "$tool_id" '.[$id].archive_executable' "$resolved_file")"
1088
+ if [[ "$mode" == apply && -x "$executable" ]] &&
1089
+ "$executable" version 2>/dev/null | head -n 1 | grep -Fq "v$version"; then
1090
+ echo "pinned release already installed: $package $version"
1091
+ else
1092
+ run_release_archive "$package" "$version" "$url" "$sha256" "$archive_executable" "$executable" || rc=1
1093
+ fi
1094
+ ;;
1095
+ release-archive-latest)
1096
+ local repo asset checksums_asset archive_executable latest_tag latest_version
1097
+ repo="$(jq -r --arg id "$tool_id" '.[$id].repo' "$resolved_file")"
1098
+ asset="$(jq -r --arg id "$tool_id" '.[$id].asset' "$resolved_file")"
1099
+ checksums_asset="$(jq -r --arg id "$tool_id" '.[$id].checksums_asset // "SHA256SUMS"' "$resolved_file")"
1100
+ archive_executable="$(jq -r --arg id "$tool_id" '.[$id].archive_executable' "$resolved_file")"
1101
+ if [[ "$mode" != apply ]]; then
1102
+ echo "would install latest release archive: $package ($repo) -> $executable"
1103
+ else
1104
+ latest_tag="$(resolve_latest_release_tag "$repo")" || { rc=1; latest_tag=""; }
1105
+ if [[ "$rc" -eq 0 ]]; then
1106
+ latest_version="${latest_tag#v}"
1107
+ if [[ -x "$executable" ]] && "$executable" --version 2>/dev/null | head -n 1 | grep -Fq " $latest_version "; then
1108
+ echo "latest release already installed: $package $latest_version"
1109
+ else
1110
+ run_release_archive_latest "$package" "$repo" "$asset" "$checksums_asset" "$archive_executable" "$executable" || rc=1
1111
+ fi
1112
+ fi
1113
+ fi
1114
+ ;;
1115
+ *) echo "unsupported manager: $manager" >&2; rc=1 ;;
1116
+ esac
1117
+ if [[ "$mode" == apply && "$rc" -eq 0 ]]; then
1118
+ if [[ "$executable" == /* ]]; then
1119
+ [[ -x "$executable" ]] || { echo "$tool_id executable missing: $executable" >&2; rc=1; }
1120
+ else
1121
+ local executable_path
1122
+ executable_path="$(sudo -u "$administrator" -H env PATH="/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" /bin/sh -c "command -v '$executable'" 2>/dev/null || true)"
1123
+ [[ -n "$executable_path" ]] || { echo "$tool_id executable missing from global PATH: $executable" >&2; rc=1; }
1124
+ fi
1125
+ fi
1126
+ return "$rc"
1127
+ }
1128
+
1129
+ # A retired Bun binary and an npm command can have the same logical tool id
1130
+ # and command name. The prior applied manifest is the only authority for this
1131
+ # one-time migration; never infer it from an executable that might already be
1132
+ # owned by npm.
1133
+ is_in_place_bun_to_npm_migration() {
1134
+ local tool_id="$1" previous_manager previous_executable current_manager current_executable current_basename
1135
+ [[ "$have_previous" -eq 1 ]] || return 1
1136
+ previous_manager="$(jq -r --arg id "$tool_id" '.[$id].manager // empty' "$previous_resolved_file")"
1137
+ previous_executable="$(jq -r --arg id "$tool_id" '.[$id].executable // empty' "$previous_resolved_file")"
1138
+ current_manager="$(jq -r --arg id "$tool_id" '.[$id].manager // empty' "$resolved_file")"
1139
+ current_executable="$(jq -r --arg id "$tool_id" '.[$id].executable // empty' "$resolved_file")"
1140
+ current_basename="${current_executable##*/}"
1141
+ [[ "$previous_manager" == bun-compile && "$current_manager" == npm &&
1142
+ "$previous_executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ &&
1143
+ "${previous_executable##*/}" == "$current_basename" ]]
1144
+ }
1145
+
1146
+ remove_retired_bun_compilation() {
1147
+ local tool_id="$1" executable="$2" hash_file
1148
+ [[ "$tool_id" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
1149
+ echo "unsafe retired Bun compilation tool id: $tool_id" >&2
1150
+ return 1
1151
+ }
1152
+ [[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
1153
+ echo "refusing unsafe retired Bun binary removal target: $executable" >&2
1154
+ return 1
1155
+ }
1156
+ hash_file="$(bun_source_hash_file "$tool_id")"
1157
+ rm -f -- "$executable" || return 1
1158
+ rm -f -- "$hash_file" || return 1
1159
+ echo "removed retired Bun compilation: $tool_id ($executable and $hash_file)"
1160
+ }
1161
+
1162
+ # A regular retired binary is removed before npm runs. If sync already installed
1163
+ # npm, preserve its verified symlink and remove only the old Bun source marker.
1164
+ prepare_in_place_bun_to_npm_migration() {
1165
+ local tool_id="$1" previous_executable migration_path package executable hash_file
1166
+ is_in_place_bun_to_npm_migration "$tool_id" || return 0
1167
+ previous_executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
1168
+ migration_path="$previous_executable"
1169
+ if [[ "${RECONCILE_SOURCE_ONLY:-0}" == 1 && -n "${RECONCILE_TEST_COMMAND_ROOT:-}" ]]; then
1170
+ migration_path="${RECONCILE_TEST_COMMAND_ROOT%/}/${previous_executable##*/}"
1171
+ fi
1172
+ if [[ -L "$migration_path" ]]; then
1173
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1174
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
1175
+ npm_package_owns_executable "$package" "$executable" || {
1176
+ echo "refusing to remove an executable symlink not owned by npm: $previous_executable" >&2
1177
+ return 1
1178
+ }
1179
+ hash_file="$(bun_source_hash_file "$tool_id")"
1180
+ rm -f -- "$hash_file" || return 1
1181
+ echo "retained npm-owned executable and removed retired Bun marker: $previous_executable ($hash_file)"
1182
+ else
1183
+ remove_retired_bun_compilation "$tool_id" "$previous_executable"
1184
+ fi
1185
+ }
1186
+
1187
+ failed_in_place_npm_migration() {
1188
+ local tool_id
1189
+ for tool_id in ${failed_tools[@]+"${failed_tools[@]}"}; do
1190
+ if is_in_place_bun_to_npm_migration "$tool_id"; then
1191
+ echo "global-tools: npm migration failed for $tool_id; retired Bun command was removed and reconciliation must stop" >&2
1192
+ return 0
1193
+ fi
1194
+ done
1195
+ return 1
1196
+ }
1197
+
1198
+ # bun-compile builds a standalone binary with `bun build --compile` from a
1199
+ # repo-relative TypeScript subproject. It runs in its own pass, strictly
1200
+ # after every brew/apt/npm/pip tool above (including bun itself) has been
1201
+ # installed and PATH-verified — an alphabetically-earlier tool ID (e.g.
1202
+ # "azure-devops-mcp" before "bun") must never race bun's own install, the
1203
+ # same ordering guarantee the shim pass already gives real tools below.
1204
+ # Same continue-on-failure contract as install_one_tool above: a failed
1205
+ # build (e.g. adl) is recorded, not fatal here -- the post-passes
1206
+ # that depend on it (kimi-hooks) discover it missing and fail loud there.
1207
+ #
1208
+ # Idempotency is content-addressed, never a manual version string: the
1209
+ # binary is rebuilt unless it exists AND the source tree hash recorded beside
1210
+ # it ($state_dir/bun-compile/<tool>.source-hash, written only after a
1211
+ # successful build) equals the current bun_source_hash. Plan mode makes the
1212
+ # same comparison, so it reports exactly what apply would do.
1213
+ build_one_bun_tool() {
1214
+ local tool_id="$1" package executable source_dir entry source_hash hash_file rc=0
1215
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1216
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
1217
+ source_dir="$(jq -r --arg id "$tool_id" '.[$id].source' "$resolved_file")"
1218
+ entry="$(jq -r --arg id "$tool_id" '.[$id].entry' "$resolved_file")"
1219
+ echo "ensure bun binary: $tool_id (bun-compile:$package)"
1220
+ source_hash="$(bun_source_hash "$source_dir")" || return 1
1221
+ hash_file="$(bun_source_hash_file "$tool_id")"
1222
+ if [[ -x "$executable" && -f "$hash_file" && "$(cat "$hash_file")" == "$source_hash" ]]; then
1223
+ echo "bun binary already up to date: $package (source $source_hash)"
1224
+ return 0
1225
+ fi
1226
+ run_bun_compile "$package" "$source_hash" "$source_dir" "$entry" "$executable" || rc=1
1227
+ if [[ "$mode" == apply && "$rc" -eq 0 ]]; then
1228
+ [[ -x "$executable" ]] || { echo "$tool_id executable missing: $executable" >&2; rc=1; }
1229
+ fi
1230
+ if [[ "$mode" == apply && "$rc" -eq 0 ]]; then
1231
+ mkdir -p "${hash_file%/*}"
1232
+ printf '%s\n' "$source_hash" >"$hash_file.new"
1233
+ mv -f "$hash_file.new" "$hash_file"
1234
+ echo "built bun binary: $package (source $source_hash)"
1235
+ fi
1236
+ return "$rc"
1237
+ }
1238
+
1239
+ build_checkout_adl() {
1240
+ local source_dir="apps/cli" entry="src/main.ts" target="/usr/local/bin/adl" source_hash hash_file rc=0
1241
+ source_hash="$(content_tree_hash "$repo_root/$source_dir")" || return 1
1242
+ hash_file="$(bun_source_hash_file adl)"
1243
+ if [[ -x "$target" && -f "$hash_file" && "$(cat "$hash_file")" == "$source_hash" ]]; then
1244
+ echo "checkout Adl binary already up to date (source $source_hash)"
1245
+ return 0
1246
+ fi
1247
+ run_bun_compile adl "$source_hash" "$source_dir" "$entry" "$target" || rc=1
1248
+ if [[ "$mode" == apply && "$rc" -eq 0 ]]; then
1249
+ [[ -x "$target" ]] || { echo "checkout Adl executable missing: $target" >&2; rc=1; }
1250
+ if [[ "$rc" -eq 0 ]]; then
1251
+ mkdir -p "${hash_file%/*}"
1252
+ printf '%s\n' "$source_hash" >"$hash_file.new"
1253
+ mv -f "$hash_file.new" "$hash_file"
1254
+ echo "built checkout Adl binary: $target (source $source_hash)"
1255
+ fi
1256
+ fi
1257
+ return "$rc"
1258
+ }
1259
+
1260
+ # embedding-model installs the adl-embedding static (Model2Vec) artifact
1261
+ # that adl hook's (the folded-in hook engine's) skill-match hook loads
1262
+ # from the administrator's cache.
1263
+ # It runs as the declared administrator (root on Linux) so the hook and
1264
+ # installer resolve the same HOME. It downloads only when the artifact does
1265
+ # not already verify against
1266
+ # the manifest's pinned SHA-256 hashes, so a healthy install is a fast no-op.
1267
+ # Same continue-on-failure contract as the passes above.
1268
+ install_one_embedding_model_tool() {
1269
+ local tool_id="$1" package spec_json rc=0
1270
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1271
+ spec_json="$(jq -c --arg id "$tool_id" '.[$id]' "$resolved_file")"
1272
+ echo "ensure embedding model: $tool_id (embedding-model:$package)"
1273
+ run_embedding_model "$package" "$spec_json" || rc=1
1274
+ return "$rc"
1275
+ }
1276
+
1277
+ # A shim is a thin wrapper at a fixed /usr/local/bin path that execs whatever
1278
+ # `package` (its target's real executable name) resolves to on the
1279
+ # administrator's PATH right now. It carries no version of its own — it only
1280
+ # exists so a short, stable alias (e.g. `crg`) survives however the
1281
+ # underlying tool is actually distributed. Same continue-on-failure contract:
1282
+ # a shim can legitimately fail if the tool it targets failed to install
1283
+ # above, and that should not stop the remaining shims or the post-passes.
1284
+ write_one_shim() {
1285
+ local tool_id="$1" package executable target_path content alias_name resolved_alias rc=0
1286
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1287
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
1288
+ echo "ensure shim: $tool_id (shim:$package -> $executable)"
1289
+ [[ "$executable" == /usr/local/bin/* ]] || {
1290
+ echo "refusing unsafe shim target: $executable" >&2
1291
+ return 1
1292
+ }
1293
+ if [[ "$mode" != apply ]]; then
1294
+ echo "would write shim: $executable -> $package"
1295
+ return 0
1296
+ fi
1297
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1298
+ target_path="$(sudo -u "$administrator" -H env PATH="/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" /bin/sh -c "command -v '$package'" 2>/dev/null || true)"
1299
+ [[ -n "$target_path" ]] || { echo "shim target not yet on PATH: $package" >&2; return 1; }
1300
+ content="#!/bin/sh
1301
+ # Generated by global-tools -- do not edit.
1302
+ exec '$target_path' \"\$@\"
1303
+ "
1304
+ if [[ -f "$executable" ]] && [[ "$(cat "$executable")" == "$content" ]]; then
1305
+ echo "shim already points to $target_path"
1306
+ else
1307
+ printf '%s' "$content" >"$executable"
1308
+ chmod 0755 "$executable"
1309
+ echo "wrote shim: $executable -> $target_path"
1310
+ fi
1311
+ # Writing the file to /usr/local/bin is not the same claim as "an agent can
1312
+ # type the bare alias and it runs" — the same standard of proof every real
1313
+ # tool above is held to, not a weaker one just because this is our own file.
1314
+ alias_name="$(basename "$executable")"
1315
+ resolved_alias="$(sudo -u "$administrator" -H env PATH="/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" /bin/sh -c "command -v '$alias_name'" 2>/dev/null || true)"
1316
+ [[ "$resolved_alias" == "$executable" ]] || {
1317
+ echo "$tool_id shim written but not resolving from global PATH as '$alias_name' (got: ${resolved_alias:-nothing})" >&2
1318
+ rc=1
1319
+ }
1320
+ return "$rc"
1321
+ }
1322
+
1323
+ managed_repo_script_marker='# Managed by adl global-tools.'
1324
+ # The rename from Enact to adl (2026-09-27) changed this marker line, but
1325
+ # left every already-installed /usr/local/bin script carrying the OLD line
1326
+ # untouched. Recognising only the current marker made reconcile treat those
1327
+ # real, previously-self-installed files as "unmanaged", so apply mode refused
1328
+ # to update them ("refusing to replace unmanaged or symlinked managed script
1329
+ # target") instead of replacing them with the renamed source -- seen live on
1330
+ # M3's `fleet-cutover.sh --apply` for /usr/local/bin/git and
1331
+ # /usr/local/bin/git-ado. A target carrying EITHER line is this writer's own
1332
+ # prior output and must still be treated as managed; a target carrying
1333
+ # neither (or a symlink) must keep being refused.
1334
+ managed_repo_script_legacy_marker='# Managed by Enact global-tools.'
1335
+
1336
+ is_managed_repo_script() {
1337
+ local executable="$1"
1338
+ [[ -f "$executable" && ! -L "$executable" ]] || return 1
1339
+ grep -Fqx "$managed_repo_script_marker" "$executable" ||
1340
+ grep -Fqx "$managed_repo_script_legacy_marker" "$executable"
1341
+ }
1342
+
1343
+ atomic_install_managed_repo_script() {
1344
+ local source="$1" executable="$2" temp
1345
+ temp="$(mktemp "$(dirname "$executable")/.$(basename "$executable").tmp.XXXXXX")"
1346
+ if ! install -m 0755 "$source" "$temp"; then
1347
+ rm -f -- "$temp"
1348
+ return 1
1349
+ fi
1350
+ mv -f -- "$temp" "$executable"
1351
+ }
1352
+
1353
+ # A repo-script is copied into /usr/local/bin rather than linked back to the
1354
+ # reconciler checkout. The checkout is forcibly reset by sync-and-reconcile,
1355
+ # so a symlink would make every interactive command depend on a mutable tree.
1356
+ write_one_repo_script() {
1357
+ local tool_id="$1" package executable source source_path alias_name resolved_alias rc=0
1358
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1359
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
1360
+ source="$(jq -r --arg id "$tool_id" '.[$id].source' "$resolved_file")"
1361
+ echo "ensure managed repo script: $tool_id (repo-script:$package -> $executable)"
1362
+ [[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
1363
+ echo "unsafe managed script target: $executable" >&2
1364
+ return 1
1365
+ }
1366
+ [[ "$source" != /* && "$source" != *..* ]] || {
1367
+ echo "unsafe managed script source: $source" >&2
1368
+ return 1
1369
+ }
1370
+ source_path="$repo_root/$source"
1371
+ [[ -f "$source_path" && ! -L "$source_path" && -x "$source_path" ]] || {
1372
+ echo "managed script source must be an executable regular non-symlink file: $source_path" >&2
1373
+ return 1
1374
+ }
1375
+ if [[ "$mode" != apply ]]; then
1376
+ echo "would install managed repo script: $source_path -> $executable"
1377
+ # 2026-09-28 (backups-this-run summary): preview the one new backup step
1378
+ # below, so a plan run's "would back up" list matches what apply would
1379
+ # really do -- only when there is an existing, DIFFERENT file to replace;
1380
+ # a fresh install backs up nothing, same as apply's own guard below.
1381
+ if [[ -e "$executable" ]] && ! cmp -s "$source_path" "$executable" 2>/dev/null; then
1382
+ echo "would back up managed repo script: $executable"
1383
+ fi
1384
+ return 0
1385
+ fi
1386
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1387
+ if [[ -e "$executable" || -L "$executable" ]]; then
1388
+ is_managed_repo_script "$executable" || {
1389
+ echo "refusing to replace unmanaged or symlinked managed script target: $executable" >&2
1390
+ return 1
1391
+ }
1392
+ fi
1393
+ if cmp -s "$source_path" "$executable"; then
1394
+ echo "managed repo script already current: $executable"
1395
+ else
1396
+ # 2026-09-28: this step previously overwrote a managed script in place
1397
+ # with no backup at all (bin/reconcile was the one place among the
1398
+ # backups-this-run audit that both created the overwrite AND was small
1399
+ # enough to fix here directly -- see docs cited in the final report for
1400
+ # every other step that still does not back up).
1401
+ if [[ -e "$executable" ]]; then
1402
+ local repo_script_backup
1403
+ repo_script_backup="${executable}.pre-repo-script-$(date -u +%Y%m%dT%H%M%SZ)"
1404
+ if cp -p "$executable" "$repo_script_backup"; then
1405
+ echo "backed up previous managed repo script to $repo_script_backup"
1406
+ record_backup "$executable" "$repo_script_backup"
1407
+ else
1408
+ echo "could not back up managed repo script before replacing it: $executable" >&2
1409
+ return 1
1410
+ fi
1411
+ fi
1412
+ atomic_install_managed_repo_script "$source_path" "$executable" || return 1
1413
+ echo "installed managed repo script: $source_path -> $executable"
1414
+ fi
1415
+ alias_name="$(basename "$executable")"
1416
+ resolved_alias="$(sudo -u "$administrator" -H env PATH="/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin" /bin/sh -c 'command -v "$1"' sh "$alias_name" 2>/dev/null || true)"
1417
+ [[ "$resolved_alias" == "$executable" ]] || {
1418
+ echo "$tool_id managed script installed but not resolving from global PATH as '$alias_name' (got: ${resolved_alias:-nothing})" >&2
1419
+ rc=1
1420
+ }
1421
+ return "$rc"
1422
+ }
1423
+
1424
+ remove_one_repo_script() {
1425
+ local executable="$1"
1426
+ [[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
1427
+ echo "refusing unsafe managed script removal target: $executable" >&2
1428
+ return 1
1429
+ }
1430
+ if [[ ! -e "$executable" && ! -L "$executable" ]]; then
1431
+ return 0
1432
+ fi
1433
+ is_managed_repo_script "$executable" || {
1434
+ echo "refusing to remove unmanaged or symlinked managed script target: $executable" >&2
1435
+ return 1
1436
+ }
1437
+ rm -f -- "$executable"
1438
+ }
1439
+
1440
+ # The content identity of a repo-dir source: the git tree hash of its source
1441
+ # directory at HEAD -- same idea as bun_source_hash above, kept as its own
1442
+ # small function only so a failure names "repo-dir", not "bun-compile".
1443
+ repo_dir_source_hash() {
1444
+ local source_dir="$1" hash
1445
+ if repo_root_is_git; then
1446
+ hash="$(repo_git rev-parse --verify --quiet "HEAD:$source_dir")" || {
1447
+ echo "repo-dir source is not tracked at HEAD: $source_dir" >&2
1448
+ return 1
1449
+ }
1450
+ else
1451
+ hash="$(content_tree_hash "$repo_root/$source_dir")"
1452
+ fi
1453
+ printf '%s\n' "$hash"
1454
+ }
1455
+
1456
+ managed_repo_dir_marker='.adl-managed-repo-dir'
1457
+
1458
+ # repo-dir deploys a whole tracked directory VERBATIM (adl-m3-finish item
1459
+ # 2/3): unlike a `bun-compile` binary, `adl repo controls` needs real files
1460
+ # on disk at a fixed path -- packages/repo-controls' own bundled hook
1461
+ # scripts, vendored CI assets, and uv-managed Python compliance engine, none
1462
+ # of which a single compiled binary can embed and still have readFileSync()
1463
+ # find (their paths are computed from `import.meta.dir`, meaningless once
1464
+ # embedded). Most repo-dir sources (repo-controls itself, and its one
1465
+ # cross-package runtime import into agent-hooks' log writer) are plain
1466
+ # TypeScript with zero external package imports at runtime (grepped
1467
+ # 2026-09-27 -- only `node:` and relative imports, one of which crosses into
1468
+ # a SIBLING repo-dir package, which is exactly why the deploy preserves each
1469
+ # source's position relative to its own `packages/` siblings rather than
1470
+ # flattening everything into one path) and so need no install step at all;
1471
+ # `npmInstall` below is for the ones that are not.
1472
+ #
1473
+ # Content-addressed like bun-compile: a marker file inside the deployed
1474
+ # directory records the source git-tree hash, so an unchanged source is
1475
+ # never re-synced. The sync itself is `git archive HEAD:<source> | tar -x`
1476
+ # into a fresh temp directory, then an atomic same-filesystem rename over
1477
+ # the previous target -- so a reader never observes a half-written tree, and
1478
+ # a failed sync never clobbers a working one.
1479
+ #
1480
+ # npmInstall (optional, default false): a source that DOES import real npm
1481
+ # packages at runtime (unlike repo-controls) needs them materialized after
1482
+ # the copy. Deliberately plain `bun install`, never `--frozen-lockfile`: the
1483
+ # archived copy is isolated from this monorepo's one workspace-root
1484
+ # `bun.lock` (repo-controls and every sibling package share it, none commits
1485
+ # its own), so there is no local lockfile here to freeze against -- this
1486
+ # resolves fresh from the registry, same "latest-stable, unpinned unless a
1487
+ # version is given" contract every plain `npm` manager entry in this
1488
+ # manifest already has. Also runs the package's own `build` script when it
1489
+ # declares one (see run_repo_dir_sync) -- a source like agent-skills ships
1490
+ # no `dist/` in git, so `bun install` alone is not enough to make it run.
1491
+
1492
+ # The actual sync action, factored out so tests can stub it -- same shape as
1493
+ # run_bun_compile above, and for the same reason: the real action needs root
1494
+ # (it writes under /usr/local/libexec) and, when npmInstall is set, the
1495
+ # network, neither of which a hash/marker/removal-orchestration test should
1496
+ # have to provide.
1497
+ run_repo_dir_sync() {
1498
+ local tool_id="$1" source="$2" build_dir="$3" npm_install="$4" bun_bin
1499
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1500
+ if repo_root_is_git; then
1501
+ repo_git archive "HEAD:$source" | tar -x -C "$build_dir"
1502
+ else
1503
+ # az374: no live Git clone -- repo_root IS the release artifact (the
1504
+ # npm-installed package), so the source directory already on disk is the
1505
+ # canonical content. Copy it verbatim instead of asking git for it.
1506
+ cp -R "$repo_root/$source/." "$build_dir/"
1507
+ fi
1508
+ if [[ "$npm_install" == true ]]; then
1509
+ bun_bin="$(command -v bun || true)"
1510
+ [[ -n "$bun_bin" ]] || { echo "repo-dir $tool_id requires bun on PATH for npmInstall; ensure bun installs before this tool" >&2; return 1; }
1511
+ (cd "$build_dir" && "$bun_bin" install) || { echo "repo-dir $tool_id: bun install failed" >&2; return 1; }
1512
+ # agent-skills (the only npmInstall:true source today) ships no `dist/`
1513
+ # in git -- every one of its script entry points imports from
1514
+ # `../../dist/index.js` (confirmed empirically, all four: run-sync,
1515
+ # run-validate, run-repo-install, run-repo-doctor), so `bun install`
1516
+ # alone leaves it unable to run ANY subcommand, not just the ones this
1517
+ # deploy happens to exercise. Building is therefore not optional: run
1518
+ # the package's own declared `build` script, exactly as a developer
1519
+ # would, whenever the synced package.json has one.
1520
+ if [[ "$(jq -r '.scripts.build // empty' "$build_dir/package.json" 2>/dev/null)" != "" ]]; then
1521
+ (cd "$build_dir" && "$bun_bin" run build) || { echo "repo-dir $tool_id: bun run build failed" >&2; return 1; }
1522
+ fi
1523
+ fi
1524
+ }
1525
+
1526
+ # The synced tree is code, not secrets, and must be readable/executable by
1527
+ # every user on the box, not just root -- write_one_repo_dir runs as root
1528
+ # (apply mode), and `mktemp -d` always creates its directory 0700 regardless
1529
+ # of the invoking shell's (or systemd unit's) umask, confirmed empirically
1530
+ # (`umask 022; mktemp -d` still yields drwx------). Left unfixed, the
1531
+ # deployed target directory stays root-only forever, and a normal user
1532
+ # cannot even `stat` the installed entry point -- exactly the "adl:
1533
+ # repo-controls is not installed" failure filed 2026-09-28.
1534
+ #
1535
+ # True (exit 0) when anything under $dir does not yet have the normalized
1536
+ # mode: directories 0755, files 0644, EXCEPT a file that already has any
1537
+ # executable bit (the entry script itself, a node_modules/.bin shebang
1538
+ # script, ...), which normalizes to 0755 instead of losing its executable
1539
+ # bit. Symlinks (e.g. node_modules/.bin) are left alone -- `find -type
1540
+ # d`/`-type f` do not match them, and their own permission bits are not what
1541
+ # gates access; the file they point at is walked (and normalized)
1542
+ # separately. Shared by normalize_repo_dir_permissions (apply) and
1543
+ # write_one_repo_dir's plan-mode report below, so the two never disagree on
1544
+ # what "needs fixing" means. `-print -quit` stops at the first offender --
1545
+ # this only needs a yes/no, not the full list.
1546
+ repo_dir_permissions_need_fix() {
1547
+ local dir="$1"
1548
+ [[ -n "$(find "$dir" -type d ! -perm 0755 -print -quit)" ]] && return 0
1549
+ [[ -n "$(find "$dir" -type f \( -perm -0100 -o -perm -0010 -o -perm -0001 \) ! -perm 0755 -print -quit)" ]] && return 0
1550
+ [[ -n "$(find "$dir" -type f ! \( -perm -0100 -o -perm -0010 -o -perm -0001 \) ! -perm 0644 -print -quit)" ]] && return 0
1551
+ return 1
1552
+ }
1553
+
1554
+ # Actually fixes the modes described above. `! -perm` in every clause means
1555
+ # an entry whose mode is already correct is never chmod'd -- cheap to call
1556
+ # even on a large, already-current tree, whether that tree is a brand-new
1557
+ # mktemp -d build (this fresh-sync path) or a long-installed target that
1558
+ # predates this fix (write_one_repo_dir's already-current path, below).
1559
+ # Silent when nothing needed fixing; a target that DID need repair -- the
1560
+ # 2026-09-28 bug, where agent-hooks/agent-skills stayed 0700 forever because
1561
+ # they were already current and this function only ever ran on a fresh sync
1562
+ # -- logs one line so a fleet run shows what it repaired.
1563
+ normalize_repo_dir_permissions() {
1564
+ local dir="$1"
1565
+ if repo_dir_permissions_need_fix "$dir"; then
1566
+ find "$dir" -type d ! -perm 0755 -exec chmod 0755 {} +
1567
+ find "$dir" -type f \( -perm -0100 -o -perm -0010 -o -perm -0001 \) ! -perm 0755 -exec chmod 0755 {} +
1568
+ find "$dir" -type f ! \( -perm -0100 -o -perm -0010 -o -perm -0001 \) ! -perm 0644 -exec chmod 0644 {} +
1569
+ echo "repo dir permissions normalized: $dir"
1570
+ fi
1571
+ }
1572
+
1573
+ write_one_repo_dir() {
1574
+ local tool_id="$1" package source target executable rc=0 source_hash marker_file installed_hash source_path build_dir npm_install
1575
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$resolved_file")"
1576
+ source="$(jq -r --arg id "$tool_id" '.[$id].source' "$resolved_file")"
1577
+ target="$(jq -r --arg id "$tool_id" '.[$id].target' "$resolved_file")"
1578
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$resolved_file")"
1579
+ npm_install="$(jq -r --arg id "$tool_id" '.[$id].npmInstall // false' "$resolved_file")"
1580
+ echo "ensure repo dir: $tool_id (repo-dir:$package -> $target)"
1581
+ [[ "$target" == "$repo_dir_libexec_root"/* ]] || {
1582
+ echo "unsafe repo-dir target: $target" >&2
1583
+ return 1
1584
+ }
1585
+ [[ "$source" != /* && "$source" != *..* ]] || {
1586
+ echo "unsafe repo-dir source: $source" >&2
1587
+ return 1
1588
+ }
1589
+ source_path="$repo_root/$source"
1590
+ [[ -d "$source_path" ]] || {
1591
+ echo "repo-dir source missing: $source_path" >&2
1592
+ return 1
1593
+ }
1594
+ source_hash="$(repo_dir_source_hash "$source")" || return 1
1595
+ # Whether a (re)sync is needed is read-only (marker file + hash compare,
1596
+ # both already computed without root above) -- decided up front, BEFORE
1597
+ # branching on mode, so plan mode can tell "would sync" apart from
1598
+ # "already current, but permissions need repair" instead of reporting
1599
+ # "would sync" unconditionally regardless of actual target state.
1600
+ marker_file="$target/$managed_repo_dir_marker"
1601
+ installed_hash=""
1602
+ [[ -f "$marker_file" ]] && installed_hash="$(cat "$marker_file")"
1603
+ if [[ "$installed_hash" != "$source_hash" || ! -e "$target/$executable" ]]; then
1604
+ if [[ "$mode" != apply ]]; then
1605
+ echo "would sync repo dir: $source -> $target (source $source_hash)"
1606
+ return 0
1607
+ fi
1608
+ build_dir="$(mktemp -d)"
1609
+ run_repo_dir_sync "$tool_id" "$source" "$build_dir" "$npm_install" || return 1
1610
+ printf '%s\n' "$source_hash" >"$build_dir/$managed_repo_dir_marker"
1611
+ normalize_repo_dir_permissions "$build_dir"
1612
+ mkdir -p "$(dirname "$target")"
1613
+ rm -rf -- "$target.new"
1614
+ mv "$build_dir" "$target.new"
1615
+ rm -rf -- "$target"
1616
+ mv "$target.new" "$target"
1617
+ echo "synced repo dir: $source -> $target ($source_hash)"
1618
+ else
1619
+ echo "repo dir already current: $target ($source_hash)"
1620
+ # 2026-09-28 bug: a target that is already current never went through
1621
+ # run_repo_dir_sync (and its normalize_repo_dir_permissions call) on
1622
+ # THIS run, so a mode wrong from before this fix existed -- 0700 from an
1623
+ # old mktemp -d that was never chmod'd -- stayed wrong forever, no
1624
+ # matter how many times reconcile ran. Every already-current target
1625
+ # must also get checked/repaired here, apply mode fixing it for real,
1626
+ # plan mode only reporting it, exactly like the sync-needed branch
1627
+ # above never touches disk in plan mode either.
1628
+ if [[ "$mode" == apply ]]; then
1629
+ normalize_repo_dir_permissions "$target"
1630
+ elif repo_dir_permissions_need_fix "$target"; then
1631
+ echo "would normalize repo dir permissions: $target"
1632
+ fi
1633
+ fi
1634
+ [[ -e "$target/$executable" ]] || {
1635
+ echo "$tool_id repo dir installed but entry missing: $target/$executable" >&2
1636
+ rc=1
1637
+ }
1638
+ return "$rc"
1639
+ }
1640
+
1641
+ remove_one_repo_dir() {
1642
+ local target="$1"
1643
+ [[ "$target" == "$repo_dir_libexec_root"/* ]] || {
1644
+ echo "refusing unsafe repo-dir removal target: $target" >&2
1645
+ return 1
1646
+ }
1647
+ [[ -e "$target" ]] || return 0
1648
+ [[ -f "$target/$managed_repo_dir_marker" ]] || {
1649
+ echo "refusing to remove unmanaged repo dir: $target" >&2
1650
+ return 1
1651
+ }
1652
+ rm -rf -- "$target"
1653
+ }
1654
+
1655
+ # config.git_global_config: global git config for the administrator (the
1656
+ # account whose HOME the per-user deployers below also write to). Apply runs git as
1657
+ # that user (sudo -u -H) so ~/.gitconfig stays user-owned; plan/audit only
1658
+ # read, as whoever invoked reconcile. Motivating entries: Azure DevOps SSH
1659
+ # has no Git LFS support, and git-lfs's default pure-SSH `git-lfs-transfer`
1660
+ # probe hangs forever against ssh.dev.azure.com, stalling every `git push` in
1661
+ # the LFS pre-push hook.
1662
+ run_git_config_global() {
1663
+ if [[ "$mode" == apply ]]; then
1664
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1665
+ (cd /tmp && sudo -u "$git_config_user" -H git config --global "$@" </dev/null)
1666
+ else
1667
+ git config --global "$@" </dev/null
1668
+ fi
1669
+ }
1670
+
1671
+ # One-release migration for Linux state written by the retired two-user
1672
+ # contract. The previous applied manifest is the authority for the former
1673
+ # account and home, so no legacy identity remains configured after a successful
1674
+ # run replaces applied-manifest.json with the current manifest. Only the two
1675
+ # Azure LFS keys owned by global-tools are removed; unrelated config survives.
1676
+ run_legacy_git_config() {
1677
+ local legacy_user="$1" config_file="$2"
1678
+ shift 2
1679
+ (cd /tmp && sudo -u "$legacy_user" -H git config --file "$config_file" "$@" </dev/null)
1680
+ }
1681
+
1682
+ cleanup_legacy_linux_git_config() {
1683
+ [[ "$platform" == linux && "$have_previous" -eq 1 ]] || return 0
1684
+ local legacy_user legacy_home config_file key rc
1685
+ legacy_user="$(jq -r --arg device "$device" '.linux.devices[$device].brew_user // empty' "$applied_manifest")"
1686
+ legacy_home="$(jq -r --arg device "$device" '.linux.devices[$device].brew_user_home // empty' "$applied_manifest")"
1687
+ [[ -n "$legacy_user" && -n "$legacy_home" ]] || return 0
1688
+ [[ "$legacy_home" == /* && "$legacy_home" != / ]] || {
1689
+ echo "invalid legacy per-user home in applied manifest: $legacy_home" >&2
1690
+ return 1
1691
+ }
1692
+ config_file="$legacy_home/.gitconfig"
1693
+ [[ -f "$config_file" ]] || return 0
1694
+ for key in \
1695
+ 'lfs.ssh://ssh.dev.azure.com/.sshtransfer' \
1696
+ 'lfs.https://ssh.dev.azure.com/.locksverify'; do
1697
+ rc=0
1698
+ run_legacy_git_config "$legacy_user" "$config_file" --get-all "$key" >/dev/null 2>&1 || rc=$?
1699
+ if [[ "$rc" -eq 1 ]]; then
1700
+ continue
1701
+ fi
1702
+ [[ "$rc" -eq 0 ]] || {
1703
+ echo "failed to inspect legacy global git config: $key in $config_file (exit $rc)" >&2
1704
+ return "$rc"
1705
+ }
1706
+ if [[ "$mode" != apply ]]; then
1707
+ echo "would remove legacy global git config: $key from $config_file"
1708
+ else
1709
+ run_legacy_git_config "$legacy_user" "$config_file" --unset-all "$key" || return 1
1710
+ echo "removed legacy global git config: $key from $config_file"
1711
+ fi
1712
+ done
1713
+ }
1714
+
1715
+ # Prints the current global value of $1, or nothing when unset. git config
1716
+ # --get exits 1 for a missing key; any other failure is real and propagates.
1717
+ git_global_config_value() {
1718
+ local key="$1" value rc=0
1719
+ value="$(run_git_config_global --get "$key")" || rc=$?
1720
+ if [[ "$rc" -eq 1 ]]; then
1721
+ return 0
1722
+ fi
1723
+ [[ "$rc" -eq 0 ]] || { echo "git config --global --get $key failed (exit $rc)" >&2; return "$rc"; }
1724
+ printf '%s\n' "$value"
1725
+ }
1726
+
1727
+ reconcile_git_global_config() {
1728
+ local key value current
1729
+ while IFS=$'\t' read -r key value; do
1730
+ [[ -n "$key" ]] || continue
1731
+ current="$(git_global_config_value "$key")" || return 1
1732
+ if [[ "$current" == "$value" ]]; then
1733
+ echo "global git config already set: $key=$value"
1734
+ elif [[ "$mode" != apply ]]; then
1735
+ echo "would set global git config: $key=$value"
1736
+ else
1737
+ run_git_config_global "$key" "$value" || return 1
1738
+ echo "set global git config: $key=$value"
1739
+ fi
1740
+ done < <(jq -r '.config.git_global_config | to_entries[] | [.key, .value] | @tsv' "$manifest")
1741
+
1742
+ [[ "$have_previous" -eq 1 ]] || return 0
1743
+ while IFS= read -r key; do
1744
+ [[ -n "$key" ]] || continue
1745
+ current="$(git_global_config_value "$key")" || return 1
1746
+ [[ -n "$current" ]] || continue
1747
+ if [[ "$mode" != apply ]]; then
1748
+ echo "would unset global git config (explicit manifest deletion): $key"
1749
+ else
1750
+ run_git_config_global --unset-all "$key" || return 1
1751
+ echo "explicit manifest deletion: unset global git config $key"
1752
+ fi
1753
+ done < <(jq -r --slurpfile current "$manifest" '
1754
+ (.config.git_global_config // {}) | keys[] as $key
1755
+ | select(($current[0].config.git_global_config | has($key)) | not)
1756
+ | $key
1757
+ ' "$applied_manifest")
1758
+ }
1759
+
1760
+ # default-agent-file.ts and managed-settings.ts (Claude/Codex) MUST run
1761
+ # before install_one_tool's tool loop further down has any chance to install
1762
+ # bun itself: test-manifest.sh pins the ordering ("default-agent file mode=plan"
1763
+ # prints before the first "ensure latest stable:" tool line) so a human reading
1764
+ # a plan top to bottom sees "who this machine is" before "what it installs" --
1765
+ # so moving this section after the tool loop is not an option. That means on a
1766
+ # host where bun genuinely is not on PATH yet (a fresh Debian 13 container,
1767
+ # 2026-09-28, before any tool has ever been installed), this section cannot
1768
+ # wait for bun to arrive. Fail loudly and by name instead of a raw
1769
+ # "bun: command not found" abort with no explanation.
1770
+ require_bun_for_identity_files() {
1771
+ command -v bun >/dev/null 2>&1 || {
1772
+ echo "global-tools: bun is required before default-agent-file/managed-settings; install bun first, then re-run reconcile" >&2
1773
+ return 1
1774
+ }
1775
+ }
1776
+
1777
+ # Agent surface CLIs (Tarun, 2026-09-28): claude, kimi, opencode, cursor's
1778
+ # `agent`, and Codex (Homebrew cask on Macs) are declared per device.
1779
+ # Native surfaces use the three helpers below; the cask path uses brew_query
1780
+ # and run_brew. Tests stub those boundaries so no real sudo, Homebrew, or
1781
+ # system path is touched.
1782
+ #
1783
+ # The PATH searched for a surface command intentionally extends past the
1784
+ # usual brew/system directories into the per-user install locations these
1785
+ # specific CLIs' own installers use (claude's and cursor's native installers,
1786
+ # kimi's and opencode's curl-script installers) -- none of those are on the
1787
+ # plain system PATH this file uses elsewhere for brew/npm/pip.
1788
+ surface_cli_search_path="$administrator_home/.local/bin:$administrator_home/.kimi-code/bin:$administrator_home/.opencode/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin"
1789
+
1790
+ # Resolves a surface to the real executable that will be run: an explicit
1791
+ # manifest `executable` (a fixed, known path -- e.g. hostinger's report-only
1792
+ # /root/codex, a loose binary of unknown install provenance never looked up
1793
+ # on PATH) wins when given; otherwise the command name is resolved on the
1794
+ # administrator's own PATH, exactly the way it would run interactively.
1795
+ # Prints nothing (not a failure) when nothing resolves -- the caller reports
1796
+ # "not installed" and moves on, the same non-fatal shape as write_one_shim's
1797
+ # own "shim target not yet on PATH" case.
1798
+ resolve_surface_executable() {
1799
+ local command_name="$1" executable_override="$2"
1800
+ if [[ -n "$executable_override" ]]; then
1801
+ [[ -x "$executable_override" ]] && printf '%s\n' "$executable_override"
1802
+ return 0
1803
+ fi
1804
+ [[ -n "$command_name" ]] || return 0
1805
+ sudo -u "$administrator" -H env PATH="$surface_cli_search_path" /bin/sh -c 'command -v "$1"' /bin/sh "$command_name" 2>/dev/null
1806
+ }
1807
+
1808
+ # First line of `<resolved> --version`, as the administrator -- every surface
1809
+ # CLI here (claude, codex, kimi, opencode, cursor's agent) supports --version.
1810
+ surface_cli_version() {
1811
+ local resolved="$1"
1812
+ sudo -u "$administrator" -H "$resolved" --version 2>/dev/null | head -n 1
1813
+ }
1814
+
1815
+ # The real update mutation: runs the resolved executable with its declared
1816
+ # update_args, as the administrator, on the same extended PATH surfaces are
1817
+ # resolved against (an update command may itself shell out to a sibling
1818
+ # tool -- opencode's own upgrade path does exactly this for its npm install
1819
+ # case, confirmed against the CLI's own documented self-detection).
1820
+ run_surface_update() {
1821
+ local resolved="$1"
1822
+ shift
1823
+ sudo -u "$administrator" -H env PATH="$surface_cli_search_path" "$resolved" "$@" </dev/null
1824
+ }
1825
+
1826
+ # Reconciles one declared surface_clis entry. report_only (hostinger's loose
1827
+ # /root/codex, unknown install method) is version-reported only -- run_surface_update
1828
+ # is never called for it, by construction (this branch returns before
1829
+ # reaching that call). A surface not resolved on PATH (or whose declared
1830
+ # `executable` is missing/not executable) is reported "not installed" and
1831
+ # treated as a non-failure: the same posture kimi-hooks and vscode-settings
1832
+ # already take toward a surface that was never set up on this host.
1833
+ reconcile_one_surface_cli() {
1834
+ local name="$1" command_name="$2" executable_override="$3" report_only="$4" manager="$5"
1835
+ shift 5
1836
+ local update_args=("$@")
1837
+ if [[ "$manager" == brew ]]; then
1838
+ if [[ "$mode" != apply ]]; then
1839
+ echo "would ensure Homebrew cask: $name (brew install --cask $name; brew upgrade --cask $name)"
1840
+ return 0
1841
+ fi
1842
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1843
+ local installed_casks query_status=0 cask_action=() update_output update_status=0
1844
+ installed_casks="$(brew_query list --cask --versions 2>&1)" || query_status=$?
1845
+ if [[ "$query_status" -ne 0 ]]; then
1846
+ printf '%s\n' "$installed_casks" >&2
1847
+ echo "brew list --cask --versions failed for surface $name (exit $query_status)" >&2
1848
+ return 1
1849
+ fi
1850
+ if awk -v cask="$name" '$1 == cask { found = 1 } END { exit !found }' <<<"$installed_casks"; then
1851
+ cask_action=(upgrade --cask "$name")
1852
+ else
1853
+ cask_action=(install --cask "$name")
1854
+ fi
1855
+ update_output="$(run_with_timeout "$SURFACE_UPDATE_TIMEOUT_SECONDS" run_brew ${cask_action[@]+"${cask_action[@]}"} 2>&1)" || update_status=$?
1856
+ printf '%s\n' "$update_output"
1857
+ if [[ "$update_status" -eq 124 ]]; then
1858
+ echo "surface update timed out: $name" >&2
1859
+ return 1
1860
+ elif [[ "$update_status" -ne 0 ]]; then
1861
+ echo "surface update failed: $name (exit $update_status)" >&2
1862
+ return 1
1863
+ fi
1864
+ if [[ "${cask_action[0]}" == install ]]; then
1865
+ echo "surface $name: Homebrew cask installed"
1866
+ else
1867
+ echo "surface $name: Homebrew cask upgrade command completed"
1868
+ fi
1869
+ return 0
1870
+ fi
1871
+ if [[ "$mode" != apply ]]; then
1872
+ if [[ "$report_only" == true ]]; then
1873
+ echo "would report surface (no update): $name"
1874
+ else
1875
+ echo "would check and update surface: $name ($command_name ${update_args[*]+"${update_args[*]}"})"
1876
+ fi
1877
+ return 0
1878
+ fi
1879
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
1880
+ local resolved
1881
+ resolved="$(resolve_surface_executable "$command_name" "$executable_override")"
1882
+ if [[ -z "$resolved" ]]; then
1883
+ echo "surface not installed: $name"
1884
+ return 0
1885
+ fi
1886
+ if [[ "$report_only" == true ]]; then
1887
+ local ver
1888
+ ver="$(surface_cli_version "$resolved")"
1889
+ echo "surface $name: report-only, currently ${ver:-unknown}"
1890
+ return 0
1891
+ fi
1892
+ local old_version new_version update_output update_status=0
1893
+ old_version="$(surface_cli_version "$resolved")"
1894
+ update_output="$(run_with_timeout "$SURFACE_UPDATE_TIMEOUT_SECONDS" run_surface_update "$resolved" ${update_args[@]+"${update_args[@]}"} 2>&1)" || update_status=$?
1895
+ printf '%s\n' "$update_output"
1896
+ if [[ "$update_status" -eq 124 ]]; then
1897
+ echo "surface update timed out: $name" >&2
1898
+ return 1
1899
+ elif [[ "$update_status" -ne 0 ]]; then
1900
+ echo "surface update failed: $name (exit $update_status)" >&2
1901
+ return 1
1902
+ fi
1903
+ new_version="$(surface_cli_version "$resolved")"
1904
+ if [[ "$old_version" == "$new_version" ]]; then
1905
+ echo "surface $name: already latest (${new_version:-unknown})"
1906
+ else
1907
+ echo "surface $name: ${old_version:-unknown} -> ${new_version:-unknown}"
1908
+ fi
1909
+ return 0
1910
+ }
1911
+
1912
+ # Loops over this device's declared surface_clis (absent or empty -> silently
1913
+ # does nothing, same convention as launch_agents/scratch_repos). A failure is
1914
+ # collected into failed_tools by id "surface-<name>", exactly like every other
1915
+ # apply-mode step's continue-on-failure contract in this file.
1916
+ reconcile_surface_clis() {
1917
+ local count
1918
+ count="$(jq -r --arg platform "$platform" --arg device "$device" \
1919
+ '((.[$platform].devices[$device].surface_clis // []) | length)' "$manifest")"
1920
+ [[ "$count" -gt 0 ]] || return 0
1921
+ local i name manager command_name executable_override report_only update_args
1922
+ for ((i = 0; i < count; i++)); do
1923
+ name="$(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1924
+ '.[$platform].devices[$device].surface_clis[$i].name' "$manifest")"
1925
+ manager="$(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1926
+ '.[$platform].devices[$device].surface_clis[$i].manager // "native"' "$manifest")"
1927
+ command_name="$(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1928
+ '.[$platform].devices[$device].surface_clis[$i].command // empty' "$manifest")"
1929
+ executable_override="$(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1930
+ '.[$platform].devices[$device].surface_clis[$i].executable // empty' "$manifest")"
1931
+ report_only="$(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1932
+ '.[$platform].devices[$device].surface_clis[$i].report_only // false' "$manifest")"
1933
+ update_args=()
1934
+ while IFS= read -r arg; do
1935
+ [[ -n "$arg" ]] && update_args+=("$arg")
1936
+ done < <(jq -r --argjson i "$i" --arg platform "$platform" --arg device "$device" \
1937
+ '.[$platform].devices[$device].surface_clis[$i].update_args // [] | .[]' "$manifest")
1938
+ if ! reconcile_one_surface_cli "$name" "$command_name" "$executable_override" "$report_only" "$manager" ${update_args[@]+"${update_args[@]}"}; then
1939
+ echo "global-tools: TOOL INSTALL FAILED: surface-$name" >&2
1940
+ failed_tools+=("surface-$name")
1941
+ fi
1942
+ done
1943
+ }
1944
+
1945
+ # Test-only escape hatch: source this script with RECONCILE_SOURCE_ONLY=1 to
1946
+ # load every function definition above (and the arg-parsing / manifest
1947
+ # validation / platform-and-tool-resolution state already computed by this
1948
+ # point) without running any of the real tool loops or post-passes below --
1949
+ # lets packages/fleet/tests/test-manifest.sh call install_one_tool,
1950
+ # build_one_bun_tool, install_one_embedding_model_tool, write_one_shim,
1951
+ # write_one_repo_script, write_one_repo_dir, require_bun_for_identity_files,
1952
+ # reconcile_one_surface_cli, run_brew_upgrade_all, and resolve_surface_mode
1953
+ # directly against a disposable fixture manifest, with run_brew/run_npm/etc.
1954
+ # stubbed out, and with no real root/brew/sudo access required.
1955
+ if [[ "${RECONCILE_SOURCE_ONLY:-0}" == "1" ]]; then
1956
+ return 0 2>/dev/null || exit 0
1957
+ fi
1958
+
1959
+ echo "global-tools: device=$device platform=$platform mode=$mode"
1960
+
1961
+ # Scratch reclamation used to run right here, before anything else -- moved
1962
+ # 2026-09-28 (see below, after the repo-script loop) because that position
1963
+ # broke every host's first-ever apply run. See that comment for why.
1964
+ scratch_reap_failed=0
1965
+
1966
+ available_kib="$(df -Pk / | awk 'NR == 2 {print $4}')"
1967
+ required_kib="$((minimum_free_gib * 1024 * 1024))"
1968
+ if [[ "$available_kib" -lt "$required_kib" ]]; then
1969
+ echo "global-tools: only $((available_kib / 1024 / 1024)) GiB free; $minimum_free_gib GiB required" >&2
1970
+ [[ "$mode" != apply ]] || exit 1
1971
+ fi
1972
+
1973
+ if [[ "$mode" == apply ]]; then
1974
+ mkdir -p "$state_dir"
1975
+ fi
1976
+
1977
+ if ! install_surface_policy_file; then
1978
+ echo "global-tools: TOOL INSTALL FAILED: surface-policy-file" >&2
1979
+ failed_tools+=("surface-policy-file")
1980
+ fi
1981
+
1982
+ require_bun_for_identity_files || exit 1
1983
+
1984
+ # The machine's default adl identity (docs/architecture/cli-design.md, "Machine default
1985
+ # agent — the one source"): one root-owned file, written before anything
1986
+ # below that could read it (kimi-hooks resolves adl the same way; this
1987
+ # is the identity every consumer -- `adl run`, the managed-settings render
1988
+ # below, and host automation's git-ado call -- reads instead of its own
1989
+ # copy). Runs for real in plan mode too (like launch-agents.ts): default-agent-file.ts's
1990
+ # own --mode plan never writes, so this is safe to run unconditionally.
1991
+ default_agent="$(jq -r --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device].default_agent' "$manifest")"
1992
+ if [[ "$mode" != apply ]]; then
1993
+ echo "global-tools: default-agent file mode=plan device=$device -> $default_agent"
1994
+ bun "$repo_root/packages/fleet/src/default-agent-file.ts" --agent "$default_agent" --platform "$platform" --mode plan ${reconcile_test_system_root_args[@]+"${reconcile_test_system_root_args[@]}"}
1995
+ else
1996
+ echo "global-tools: default-agent file mode=apply device=$device -> $default_agent"
1997
+ if ! bun "$repo_root/packages/fleet/src/default-agent-file.ts" --agent "$default_agent" --platform "$platform" --mode apply ${reconcile_test_system_root_args[@]+"${reconcile_test_system_root_args[@]}"}; then
1998
+ echo "global-tools: TOOL INSTALL FAILED: default-agent-file" >&2
1999
+ failed_tools+=("default-agent-file")
2000
+ fi
2001
+ fi
2002
+
2003
+ # The managed baseline hook artifacts (adl items 10/17): Claude's
2004
+ # managed-settings.json (rendered with this device's env.ADL_AGENT_ID_AGENT)
2005
+ # and Codex's requirements.toml + managed_config.toml. See
2006
+ # packages/fleet/src/managed-settings.ts for the full contract and why Codex
2007
+ # needs two files. Same continue-on-failure contract as the tool passes
2008
+ # below: recorded, not fatal, so the remaining passes still run.
2009
+ if [[ "$mode" != apply ]]; then
2010
+ echo "global-tools: managed settings mode=plan device=$device -> $default_agent"
2011
+ bun "$repo_root/packages/fleet/src/managed-settings.ts" --agent "$default_agent" --platform "$platform" --mode plan ${reconcile_test_system_root_args[@]+"${reconcile_test_system_root_args[@]}"}
2012
+ else
2013
+ echo "global-tools: managed settings mode=apply device=$device -> $default_agent"
2014
+ if ! bun "$repo_root/packages/fleet/src/managed-settings.ts" --agent "$default_agent" --platform "$platform" --mode apply ${reconcile_test_system_root_args[@]+"${reconcile_test_system_root_args[@]}"}; then
2015
+ echo "global-tools: TOOL INSTALL FAILED: managed-settings" >&2
2016
+ failed_tools+=("managed-settings")
2017
+ fi
2018
+ fi
2019
+
2020
+ if jq -e 'to_entries | any(.value.manager == "brew")' "$resolved_file" >/dev/null; then
2021
+ run_brew update
2022
+ # Tarun, 2026-09-28: `brew update` alone never upgrades anything -- it only
2023
+ # refreshes the formula/cask index. The live m3 run this decision came from
2024
+ # printed "You have 84 outdated formulae and 4 outdated casks installed" for
2025
+ # the FIRST time reconcile had ever surfaced that number, because nothing
2026
+ # downstream of `brew update` ever runs a blanket `brew upgrade`: the
2027
+ # per-tool loop below only upgrades a formula that is BOTH manifest-declared
2028
+ # AND already installed-but-outdated (install_one_tool's own brew branch),
2029
+ # so anything not named in the manifest just accumulates outdated versions
2030
+ # forever. Declared per device (brew_upgrade_all), not implied by having any
2031
+ # brew tools at all, so a device can opt out explicitly instead of by
2032
+ # omission.
2033
+ brew_upgrade_all="$(jq -r --arg platform "$platform" --arg device "$device" '.[$platform].devices[$device].brew_upgrade_all // false' "$manifest")"
2034
+ if [[ "$brew_upgrade_all" == true ]]; then
2035
+ if ! run_brew_upgrade_all; then
2036
+ echo "global-tools: TOOL INSTALL FAILED: brew-upgrade-all" >&2
2037
+ failed_tools+=("brew-upgrade-all")
2038
+ fi
2039
+ fi
2040
+ fi
2041
+
2042
+ if [[ "$platform" == linux && "$(jq -r '.linux.common.os_upgrade // false' "$manifest")" == true ]]; then
2043
+ run_apt update
2044
+ run_apt -y full-upgrade
2045
+ fi
2046
+
2047
+ while IFS= read -r tool_id; do
2048
+ if ! install_one_tool "$tool_id"; then
2049
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2050
+ failed_tools+=("$tool_id")
2051
+ fi
2052
+ done < <(jq -r 'to_entries[] | select(.value.manager != "shim" and .value.manager != "repo-script" and .value.manager != "repo-dir" and .value.manager != "bun-compile" and .value.manager != "embedding-model") | .key' "$resolved_file")
2053
+
2054
+ # An ordinary package failure can be reported after the remaining passes. An
2055
+ # in-place Bun-to-npm migration cannot: its old command path was deliberately
2056
+ # removed before npm ran, so stop before any hook deployer or state writer can
2057
+ # treat the failed transition as usable.
2058
+ if [[ "$mode" == apply ]] && failed_in_place_npm_migration; then
2059
+ exit 1
2060
+ fi
2061
+
2062
+ while IFS= read -r tool_id; do
2063
+ if ! build_one_bun_tool "$tool_id"; then
2064
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2065
+ failed_tools+=("$tool_id")
2066
+ fi
2067
+ done < <(jq -r 'to_entries[] | select(.value.manager == "bun-compile") | .key' "$resolved_file")
2068
+
2069
+ while IFS= read -r tool_id; do
2070
+ if ! install_one_embedding_model_tool "$tool_id"; then
2071
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2072
+ failed_tools+=("$tool_id")
2073
+ fi
2074
+ done < <(jq -r 'to_entries[] | select(.value.manager == "embedding-model") | .key' "$resolved_file")
2075
+
2076
+ while IFS= read -r tool_id; do
2077
+ if ! write_one_shim "$tool_id"; then
2078
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2079
+ failed_tools+=("$tool_id")
2080
+ fi
2081
+ done < <(jq -r 'to_entries[] | select(.value.manager == "shim") | .key' "$resolved_file")
2082
+
2083
+ while IFS= read -r tool_id; do
2084
+ if ! write_one_repo_script "$tool_id"; then
2085
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2086
+ failed_tools+=("$tool_id")
2087
+ fi
2088
+ done < <(jq -r 'to_entries[] | select(.value.manager == "repo-script") | .key' "$resolved_file")
2089
+
2090
+ # Scratch reclamation (Tarun, 2026-09-28: moved from right after "global-tools:
2091
+ # device=..." above, before ANYTHING else had run). That original position
2092
+ # broke every host's first-ever apply run: scratch-reap's own fetch check
2093
+ # needs a working identity to fetch with -- the default-agent file, plus
2094
+ # adl-agent-id/agent-vault/adl resolving on PATH (git-ado's own network path,
2095
+ # require_agent_identity in bin/git-ado:228-253, fails closed without all
2096
+ # three there; git-ado itself is invoked by a path relative to the checkout/
2097
+ # installed package, never a PATH lookup, so its own install position does
2098
+ # not matter) -- and on a brand-new machine none of those exist yet that
2099
+ # early in the run. Confirmed live on strato-vps-1's dry-run plan
2100
+ # (.scratchpad/cutover/strato-dryrun.log:52-57, 210): "scratch-reap:
2101
+ # default-agent file missing or empty; run reconcile once to install it
2102
+ # before fetching", and the whole plan exited non-zero as a result. Running
2103
+ # here instead -- after default-agent-file/managed-settings (the identity
2104
+ # steps, above) and after adl-agent-id/agent-vault/adl install (the main tool
2105
+ # loop above) -- means scratch-reap's fetch authenticates as THIS run's
2106
+ # identity, not a nonexistent prior one. bun (a brew-managed tool, installed
2107
+ # in the main tool loop above) is also this run's fresh install now, never a
2108
+ # stale one.
2109
+ #
2110
+ # Trade-off accepted with this move: this pass used to run BEFORE the
2111
+ # free-space gate near the top of this file, deliberately, so a host that
2112
+ # filled up with scratch build artifacts could still reclaim them through
2113
+ # THIS run even on a short disk (31 GB reclaimed this way on strato-vps-1,
2114
+ # 2026-09-15). Running after the gate instead means a host that is
2115
+ # critically low on disk no longer gets that same-run rescue; reconcile is
2116
+ # idempotent and safe to retry, so it still recovers on a LATER run once the
2117
+ # gate's own failure is investigated, just not within this one. Fixing every
2118
+ # host's first-ever run outright is judged the bigger win -- every
2119
+ # established host has already passed its first run and keeps behaving
2120
+ # exactly as before.
2121
+ #
2122
+ # Only devices that declare scratch_repos are touched. A developer machine's
2123
+ # scratch worktrees are working state; silence there is the correct default.
2124
+ if [[ "$(jq -r '.config.scratch_reap.enabled // false' "$manifest")" == true ]]; then
2125
+ scratch_repos=()
2126
+ while IFS= read -r scratch_repo; do
2127
+ if [[ -n "$scratch_repo" ]]; then
2128
+ scratch_repos+=("$scratch_repo")
2129
+ fi
2130
+ done < <(jq -r --arg platform "$platform" --arg device "$device" \
2131
+ '.[$platform].devices[$device].scratch_repos // [] | .[]' "$manifest")
2132
+
2133
+ if [[ "${#scratch_repos[@]}" -eq 0 ]]; then
2134
+ echo "global-tools: scratch reap skipped: $device declares no scratch_repos"
2135
+ elif ! command -v bun >/dev/null 2>&1; then
2136
+ echo "global-tools: scratch reap skipped: bun is not installed yet on this host"
2137
+ else
2138
+ scratch_reap_args=()
2139
+ for scratch_repo in "${scratch_repos[@]}"; do
2140
+ scratch_reap_args+=(--repo "$scratch_repo")
2141
+ done
2142
+ scratch_reap_mode=plan
2143
+ if [[ "$mode" == apply ]]; then
2144
+ scratch_reap_mode=apply
2145
+ fi
2146
+ scratch_reap_idle_days="$(jq -r '.config.scratch_reap.idle_days' "$manifest")"
2147
+ scratch_reap_days="$(jq -r '.config.scratch_reap.reap_days' "$manifest")"
2148
+ if [[ "$(jq -r '.config.scratch_reap.remove_worktrees' "$manifest")" == true ]]; then
2149
+ scratch_reap_args+=(--remove-worktrees)
2150
+ fi
2151
+ # First-time host, plan mode: the default-agent step above (always run
2152
+ # before this one) only WRITES the default-agent file in apply mode --
2153
+ # its own --mode plan never writes. So on a host that has never had a
2154
+ # real reconcile apply yet, the file genuinely does not exist when
2155
+ # scratch-reap's fetch needs it here, and would otherwise refuse the
2156
+ # whole plan over a file THIS SAME run's apply step would go on to
2157
+ # install. Tell scratch-reap that, so it can defer instead of refusing
2158
+ # -- but only when this device actually has a default_agent declared
2159
+ # (i.e. an apply run really would install one); apply mode is untouched
2160
+ # (the file already exists there by the time this step runs).
2161
+ if [[ "$scratch_reap_mode" == plan && -n "$default_agent" && "$default_agent" != null ]]; then
2162
+ scratch_reap_args+=(--default-agent-pending)
2163
+ fi
2164
+ echo "global-tools: scratch reap mode=$scratch_reap_mode repos=${scratch_repos[*]}"
2165
+ # Recorded and continued past, exactly like a failed tool install: the only
2166
+ # way this exits non-zero is a fetch it could not complete, which makes it
2167
+ # REFUSE to remove worktrees. That is the safe direction, so it must not
2168
+ # abort the fleet's update -- but it must not vanish either, or the
2169
+ # published-HEAD gate rots with nothing ever reporting it.
2170
+ "$script_dir/scratch-reap" \
2171
+ "${scratch_reap_args[@]}" \
2172
+ --mode "$scratch_reap_mode" \
2173
+ --idle-days "$scratch_reap_idle_days" \
2174
+ --reap-days "$scratch_reap_days" || scratch_reap_failed=1
2175
+ fi
2176
+ fi
2177
+
2178
+ while IFS= read -r tool_id; do
2179
+ if ! write_one_repo_dir "$tool_id"; then
2180
+ echo "global-tools: TOOL INSTALL FAILED: $tool_id" >&2
2181
+ failed_tools+=("$tool_id")
2182
+ fi
2183
+ done < <(jq -r 'to_entries[] | select(.value.manager == "repo-dir") | .key' "$resolved_file")
2184
+
2185
+ # Agent surface CLIs (Tarun, 2026-09-28): claude/kimi/opencode/cursor's
2186
+ # `agent`, plus Codex as a Homebrew cask on m3/m5, are declared per device in
2187
+ # surface_clis and updated in the same run. Runs after every real tool pass
2188
+ # above so a surface that shells
2189
+ # out to a just-installed tool (or that IS the just-upgraded tool, on a
2190
+ # device where brew_upgrade_all also fired) sees this run's state, not a
2191
+ # stale one.
2192
+ reconcile_surface_clis
2193
+
2194
+ if [[ "$have_previous" -eq 1 ]]; then
2195
+ while IFS= read -r tool_id; do
2196
+ [[ -n "$tool_id" ]] || continue
2197
+ if jq -e --arg id "$tool_id" '.config.protected_tools | index($id)' "$manifest" >/dev/null; then
2198
+ echo "refusing to remove protected bootstrap tool: $tool_id" >&2
2199
+ exit 1
2200
+ fi
2201
+ manager="$(jq -r --arg id "$tool_id" '.[$id].manager' "$previous_resolved_file")"
2202
+ package="$(jq -r --arg id "$tool_id" '.[$id].package' "$previous_resolved_file")"
2203
+ executable="$(jq -r --arg id "$tool_id" '.[$id].executable' "$previous_resolved_file")"
2204
+ if is_in_place_bun_to_npm_migration "$tool_id"; then
2205
+ if [[ "$mode" == apply ]]; then
2206
+ echo "retired Bun compilation already removed for npm migration: $tool_id"
2207
+ fi
2208
+ continue
2209
+ fi
2210
+ if jq -e --arg manager "$manager" --arg package "$package" 'to_entries | any(.value.manager == $manager and .value.package == $package)' "$resolved_file" >/dev/null; then
2211
+ echo "retain shared package after entry deletion: $manager:$package"
2212
+ continue
2213
+ fi
2214
+ echo "explicit manifest deletion: uninstall $tool_id ($manager:$package)"
2215
+ case "$manager" in
2216
+ brew) run_brew uninstall "$package" ;;
2217
+ apt) run_apt remove -y "$package" ;;
2218
+ npm) run_npm uninstall -g "$package" ;;
2219
+ pip)
2220
+ run_pip uninstall --break-system-packages -y "$package"
2221
+ remove_global_pip_executable "$executable"
2222
+ ;;
2223
+ release-archive)
2224
+ [[ "$executable" == /usr/local/bin/* ]] || {
2225
+ echo "refusing unsafe release archive removal target: $executable" >&2
2226
+ exit 1
2227
+ }
2228
+ if [[ "$mode" == apply ]]; then
2229
+ rm -f -- "$executable"
2230
+ else
2231
+ echo "would remove pinned release executable: $executable"
2232
+ fi
2233
+ ;;
2234
+ release-archive-latest)
2235
+ [[ "$executable" == /usr/local/bin/* ]] || {
2236
+ echo "refusing unsafe release archive removal target: $executable" >&2
2237
+ exit 1
2238
+ }
2239
+ if [[ "$mode" == apply ]]; then
2240
+ rm -f -- "$executable"
2241
+ else
2242
+ echo "would remove latest release executable: $executable"
2243
+ fi
2244
+ ;;
2245
+ shim)
2246
+ [[ "$executable" == /usr/local/bin/* ]] || {
2247
+ echo "refusing unsafe shim removal target: $executable" >&2
2248
+ exit 1
2249
+ }
2250
+ if [[ "$mode" == apply ]]; then
2251
+ rm -f -- "$executable"
2252
+ else
2253
+ echo "would remove shim: $executable"
2254
+ fi
2255
+ ;;
2256
+ repo-script)
2257
+ if [[ "$mode" == apply ]]; then
2258
+ remove_one_repo_script "$executable"
2259
+ else
2260
+ [[ "$executable" =~ ^/usr/local/bin/[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
2261
+ echo "refusing unsafe managed script removal target: $executable" >&2
2262
+ exit 1
2263
+ }
2264
+ echo "would remove managed repo script: $executable"
2265
+ fi
2266
+ ;;
2267
+ repo-dir)
2268
+ removed_repo_dir_target="$(jq -r --arg id "$tool_id" '.[$id].target' "$previous_resolved_file")"
2269
+ if [[ "$mode" == apply ]]; then
2270
+ remove_one_repo_dir "$removed_repo_dir_target"
2271
+ else
2272
+ [[ "$removed_repo_dir_target" == "$repo_dir_libexec_root"/* ]] || {
2273
+ echo "refusing unsafe repo-dir removal target: $removed_repo_dir_target" >&2
2274
+ exit 1
2275
+ }
2276
+ echo "would remove repo dir: $removed_repo_dir_target"
2277
+ fi
2278
+ ;;
2279
+ bun-compile)
2280
+ [[ "$executable" == /usr/local/bin/* ]] || {
2281
+ echo "refusing unsafe bun binary removal target: $executable" >&2
2282
+ exit 1
2283
+ }
2284
+ if [[ "$mode" == apply ]]; then
2285
+ rm -f -- "$executable"
2286
+ else
2287
+ echo "would remove bun binary: $executable"
2288
+ fi
2289
+ ;;
2290
+ embedding-model)
2291
+ if [[ "$mode" == apply ]]; then
2292
+ [[ "$(id -u)" -eq 0 ]] || { echo "apply mode must run as root" >&2; exit 1; }
2293
+ command -v bun >/dev/null 2>&1 || { echo "embedding-model requires bun; ensure bun installs before this tool" >&2; exit 1; }
2294
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2295
+ bun "$repo_root/packages/fleet/src/install-embedding-model.ts" uninstall </dev/null)
2296
+ else
2297
+ echo "would remove embedding model cache: ~/.cache/adl-embedding/model (as $administrator)"
2298
+ fi
2299
+ ;;
2300
+ *) echo "unsupported manager in previous manifest: $manager" >&2; exit 1 ;;
2301
+ esac
2302
+ # Selects an id when it was deleted outright (absent from the current
2303
+ # manifest) OR retained but migrated to a different manager/package (e.g.
2304
+ # lean-ctx moving from npm:lean-ctx-bin to release-archive-latest:lean-ctx)
2305
+ # -- a plain "id no longer present" check would silently leave the OLD
2306
+ # manager's install in place forever whenever an id survives a manager
2307
+ # change, which for lean-ctx meant the stale npm package (and the
2308
+ # /opt/homebrew/bin/lean-ctx shim it owns) would never be uninstalled, and
2309
+ # that shim's earlier position on PATH than /usr/local/bin would keep
2310
+ # `command -v lean-ctx` resolving to it instead of the new release binary
2311
+ # no matter how many times reconcile ran.
2312
+ done < <(jq -r --slurpfile current "$resolved_file" '
2313
+ keys[] as $id
2314
+ | ($current[0][$id]) as $cur
2315
+ | select(($cur == null) or ($cur.manager != .[$id].manager) or ($cur.package != .[$id].package))
2316
+ | $id
2317
+ ' "$previous_resolved_file")
2318
+ fi
2319
+
2320
+ project_adl_package_payload() {
2321
+ local adl_package bun_bin
2322
+ adl_package="$(jq -r '.adl.package // empty' "$resolved_file")"
2323
+ [[ "$adl_package" == @amsterdamdatalabs/adl-agent-control ]] || {
2324
+ echo "Adl package projection requires the published wrapper entry" >&2
2325
+ return 1
2326
+ }
2327
+ for failed_tool in ${failed_tools[@]+"${failed_tools[@]}"}; do
2328
+ if [[ "$failed_tool" == adl ]]; then
2329
+ echo "skipping Adl payload projection because Adl source verification failed" >&2
2330
+ return 1
2331
+ fi
2332
+ done
2333
+ if [[ "$mode" != apply ]]; then
2334
+ if [[ "$source_mode" == checkout ]]; then
2335
+ echo "would build agent-skills payload from checkout"
2336
+ fi
2337
+ echo "would project Adl package payload from $repo_root to /usr/local/libexec/adl and /usr/local/bin/git{,-ado}"
2338
+ return 0
2339
+ fi
2340
+ bun_bin="$(command -v bun || true)"
2341
+ [[ -n "$bun_bin" ]] || { echo "Adl package projection requires bun on PATH" >&2; return 1; }
2342
+ if [[ "$source_mode" == checkout ]]; then
2343
+ (cd "$repo_root/packages/agent-skills" && "$bun_bin" run build) || {
2344
+ echo "could not build agent-skills payload from checkout" >&2
2345
+ return 1
2346
+ }
2347
+ fi
2348
+ local system_root_args=()
2349
+ if [[ -n "${RECONCILE_TEST_SYSTEM_ROOT:-}" ]]; then
2350
+ system_root_args+=(--system-root "$RECONCILE_TEST_SYSTEM_ROOT")
2351
+ fi
2352
+ "$bun_bin" "$repo_root/packages/fleet/src/install-adl-package.ts" install \
2353
+ --package-root "$repo_root" ${system_root_args[@]+"${system_root_args[@]}"}
2354
+ }
2355
+
2356
+ if ! project_adl_package_payload; then
2357
+ echo "global-tools: TOOL INSTALL FAILED: adl-package-projection" >&2
2358
+ failed_tools+=("adl-package-projection")
2359
+ fi
2360
+
2361
+ # Same continue-on-failure contract as the tool passes above: runs after git
2362
+ # itself is installed/upgraded, and a failure is recorded, not fatal.
2363
+ if ! "$repo_root/bin/shell-path-policy" --home "$administrator_home" --platform "$platform" --mode "$mode" --owner "$administrator"; then
2364
+ echo "global-tools: TOOL INSTALL FAILED: shell-path-policy" >&2
2365
+ failed_tools+=("shell-path-policy")
2366
+ fi
2367
+
2368
+ if ! reconcile_git_global_config; then
2369
+ echo "global-tools: TOOL INSTALL FAILED: git-global-config" >&2
2370
+ failed_tools+=("git-global-config")
2371
+ fi
2372
+ cleanup_legacy_linux_git_config || {
2373
+ echo "global-tools: legacy Linux git config cleanup failed" >&2
2374
+ exit 1
2375
+ }
2376
+
2377
+ # launch_agents (mac only; validate-manifest rejects them on linux): user
2378
+ # LaunchAgents adl owns -- see packages/fleet/src/launch-agents.ts. Runs
2379
+ # before the global surface sweep, whose LaunchAgent protection is derived
2380
+ # from these same manifest declarations. Apply runs as root (it installs the
2381
+ # compiles each job into its own signed binary under
2382
+ # /usr/local/libexec/adl, writes the administrator's plist owned by
2383
+ # that user, and bootstraps gui/<uid>); plan only prints. Same
2384
+ # continue-on-failure contract as the tool passes above.
2385
+ if [[ "$platform" == mac ]] && jq -e --arg device "$device" \
2386
+ '((.mac.common.launch_agents // {}) + (.mac.devices[$device].launch_agents // {})) | length > 0' "$manifest" >/dev/null; then
2387
+ launch_agents_args=(--manifest "$manifest" --platform "$platform" --device "$device" --home "$administrator_home" --state-dir "$state_dir")
2388
+ if [[ "$have_previous" -eq 1 ]]; then
2389
+ launch_agents_args+=(--previous-manifest "$applied_manifest")
2390
+ fi
2391
+ if [[ "$mode" == apply ]]; then
2392
+ echo "global-tools: launch agents mode=apply home=$administrator_home (for $administrator)"
2393
+ if ! bun "$repo_root/packages/fleet/src/launch-agents.ts" --mode apply "${launch_agents_args[@]}"; then
2394
+ echo "global-tools: TOOL INSTALL FAILED: launch-agents" >&2
2395
+ failed_tools+=("launch-agents")
2396
+ fi
2397
+ else
2398
+ echo "global-tools: launch agents mode=plan home=$administrator_home"
2399
+ bun "$repo_root/packages/fleet/src/launch-agents.ts" --mode plan "${launch_agents_args[@]}"
2400
+ fi
2401
+ fi
2402
+
2403
+ if [[ "$mode" == apply ]]; then
2404
+ "$script_dir/health-check" "$device"
2405
+ fi
2406
+
2407
+ if [[ "$(jq -r '.config.global_surface_sweep.enabled' "$manifest")" == true ]]; then
2408
+ remove_unprotected="$(jq -r '.config.global_surface_sweep.remove_unprotected' "$manifest")"
2409
+ surface_mode="$(resolve_surface_mode "$mode" "$remove_unprotected")"
2410
+ echo "global-tools: global surface sweep mode=$surface_mode home=$administrator_home (remove_unprotected=$remove_unprotected)"
2411
+ # stdout captured (then re-printed) so this run's combined backups summary
2412
+ # (2026-09-28) can fold in the sweep's OWN already-computed "backups this
2413
+ # run:" lines -- collect_sweep_backups parses them verbatim; it never
2414
+ # recomputes the sweep's own backupPath() logic. stderr is left
2415
+ # unredirected so a real failure still streams live as before.
2416
+ sweep_status=0
2417
+ sweep_output="$("$script_dir/global-surface-sweep" \
2418
+ --home "$administrator_home" \
2419
+ --mode "$surface_mode" \
2420
+ --state-dir "$state_dir")" || sweep_status=$?
2421
+ printf '%s\n' "$sweep_output"
2422
+ collect_sweep_backups "$sweep_output"
2423
+ [[ "$sweep_status" -eq 0 ]] || exit "$sweep_status"
2424
+ fi
2425
+
2426
+ # Agent surfaces and other per-user state belong to the administrator. On the
2427
+ # Linux VPS devices that is root; the current manifest carries no second user
2428
+ # or home.
2429
+ #
2430
+ # Kimi hooks, surface policy, VS Code settings, and global agent instructions
2431
+ # are the machine-global per-user deployers below. They must land in the
2432
+ # administrator's real HOME, so apply runs privilege-dropped as
2433
+ # $administrator (`sudo -u -H`). On Linux that user is root already; it is
2434
+ # never a secondary account. Plan/audit modes only read and run directly
2435
+ # against the manifest-declared $administrator_home. global-surface-sweep just
2436
+ # above runs as the reconcile invoker but also targets administrator_home and
2437
+ # preserves each rewritten file's owner and mode.
2438
+ kimi_hooks_enabled="$(jq -r '.config.kimi_hooks.enabled // false' "$manifest")"
2439
+ surface_policy_enabled="$(jq -r '.config.surface_policy.enabled // false' "$manifest")"
2440
+ global_agents_enabled="$(jq -r '.config.global_agents.enabled // false' "$manifest")"
2441
+ vscode_settings_enabled="$(jq -r '.config.vscode_settings.enabled // false' "$manifest")"
2442
+
2443
+ if [[ "$kimi_hooks_enabled" == true ]]; then
2444
+ # Kimi is optional per host: skip loudly instead of creating a
2445
+ # ~/.kimi-code directory (and a config file inside it) on a machine that
2446
+ # never installed Kimi Code.
2447
+ kimi_present=0
2448
+ if command -v kimi >/dev/null 2>&1 || [[ -d "$administrator_home/.kimi-code" ]]; then
2449
+ kimi_present=1
2450
+ fi
2451
+ if [[ "$kimi_present" -eq 0 ]]; then
2452
+ echo "global-tools: kimi-hooks skipped: kimi not installed"
2453
+ elif [[ "$mode" != apply ]]; then
2454
+ echo "global-tools: kimi hooks mode=plan home=$administrator_home"
2455
+ "$script_dir/kimi-hooks" --home "$administrator_home" --mode plan
2456
+ else
2457
+ # Ordering: adl (bun-compile pass, above) must already resolve as
2458
+ # a bare command before we write TOML entries that reference it -- Kimi
2459
+ # spawns the hook commands verbatim ("adl hook pre-tool-use
2460
+ # --host=kimi", never an absolute path). `adl hook` folds in the old
2461
+ # standalone hook binary into the one `adl` binary (item 15/P4,
2462
+ # docs/specs/two-branch-standard-2026-09-27.md) -- hard cut, no compat
2463
+ # shim: there is no standalone hook executable to resolve any more.
2464
+ adl_executable="$(jq -r '."adl".executable' "$resolved_file")"
2465
+ if ! adl_resolved="$(sudo -u "$administrator" -H env PATH="/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin" /bin/sh -c "command -v '$adl_executable'" 2>/dev/null)"; then
2466
+ echo "global-tools: could not resolve adl from the administrator PATH" >&2
2467
+ exit 1
2468
+ fi
2469
+ [[ -n "$adl_resolved" && -x "$adl_resolved" ]] || {
2470
+ echo "global-tools: kimi-hooks requires adl on PATH ($adl_executable); bun-compile installation must succeed first" >&2
2471
+ exit 1
2472
+ }
2473
+ echo "global-tools: kimi hooks mode=apply home=$administrator_home (as $administrator)"
2474
+ # Never --force: an automated run must not overwrite a hand-edited
2475
+ # managed block. A drifted block makes kimi-hooks exit non-zero with a
2476
+ # clear message, which aborts this reconciliation exactly like a failure
2477
+ # in any apply-mode step above already does -- retryable, not silently
2478
+ # skipped or silently overwritten.
2479
+ #
2480
+ # stdout is captured (then re-printed below) so its own "backed up
2481
+ # previous config to ..." note (2026-09-28: bin/reconcile's own
2482
+ # backups-this-run summary, collect_kimi_hooks_backups) can be collected;
2483
+ # stderr is left unredirected, so a real failure's diagnostic still
2484
+ # streams live exactly as before.
2485
+ kimi_hooks_status=0
2486
+ kimi_hooks_output="$(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2487
+ "$script_dir/kimi-hooks" --home "$administrator_home" --mode apply)" || kimi_hooks_status=$?
2488
+ printf '%s\n' "$kimi_hooks_output"
2489
+ collect_kimi_hooks_backups "$kimi_hooks_output"
2490
+ [[ "$kimi_hooks_status" -eq 0 ]] || exit "$kimi_hooks_status"
2491
+ fi
2492
+ fi
2493
+
2494
+ # OpenCode now discovers the canonical hook carrier from each managed
2495
+ # project's `.opencode/plugins/` directory. Remove the superseded global
2496
+ # carrier on every reconciliation; it would otherwise be auto-loaded too and
2497
+ # run the same hook contract twice. The exact old path remains Enact-owned.
2498
+ if [[ "$mode" != apply ]]; then
2499
+ echo "global-tools: retired OpenCode carrier mode=plan home=$administrator_home"
2500
+ "$script_dir/opencode-hooks" --home "$administrator_home" --mode plan
2501
+ else
2502
+ echo "global-tools: retired OpenCode carrier mode=remove home=$administrator_home (as $administrator)"
2503
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2504
+ "$script_dir/opencode-hooks" --home "$administrator_home" --mode remove)
2505
+ fi
2506
+
2507
+ # STATUS.md 12p item 13, final scope (2026-09-29): lean-ctx hooks are "moved
2508
+ # to repo level" and must be REMOVED FROM GLOBAL -- the per-repo carrier
2509
+ # (`adl hook lean-ctx pre-tool-use|post-tool-use --host=<x>`, written by
2510
+ # `adl hooks deploy`) already provides them with the repo's own config, so
2511
+ # any raw, unwrapped `lean-ctx hook <subcommand>` entry left in a per-user
2512
+ # config (observe, rewrite, redirect, read-dedup, any other) is retracted
2513
+ # global wiring, never the per-repo carrier or Orca's own hooks -- see
2514
+ # bin/lib/lean-ctx-observe-hooks.sh's header for the full decision trail and
2515
+ # exactly which per-user files can carry one. Unconditional, like the
2516
+ # retired OpenCode carrier cleanup just above: not a manifest-gated optional
2517
+ # feature, a standing cleanup that runs every reconciliation so a
2518
+ # hand-edited or Orca-reinstalled per-user config never lets this creep back
2519
+ # in unnoticed.
2520
+ if [[ "$mode" != apply ]]; then
2521
+ echo "global-tools: lean-ctx observe/raw-hook strip mode=plan home=$administrator_home"
2522
+ "$script_dir/lean-ctx-observe-strip" --home "$administrator_home" --mode plan
2523
+ else
2524
+ echo "global-tools: lean-ctx observe/raw-hook strip mode=apply home=$administrator_home (as $administrator)"
2525
+ # stdout captured (then re-printed) so its own "backed up previous config
2526
+ # to ..." note(s) can be folded into this run's combined backups list,
2527
+ # exactly like kimi-hooks above; stderr left unredirected so a real
2528
+ # failure (a file that fails to parse) still streams live.
2529
+ lean_ctx_observe_strip_status=0
2530
+ lean_ctx_observe_strip_output="$(cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2531
+ "$script_dir/lean-ctx-observe-strip" --home "$administrator_home" --mode apply)" || lean_ctx_observe_strip_status=$?
2532
+ printf '%s\n' "$lean_ctx_observe_strip_output"
2533
+ collect_lean_ctx_observe_strip_backups "$lean_ctx_observe_strip_output"
2534
+ [[ "$lean_ctx_observe_strip_status" -eq 0 ]] || exit "$lean_ctx_observe_strip_status"
2535
+ fi
2536
+
2537
+ # surface-policy re-asserts each host's permission MODE from the single source
2538
+ # packages/agent-settings/surface-policy.json, so every machine converges on one
2539
+ # declared posture instead of drifting per-host (m5 ran Claude "acceptEdits"
2540
+ # against Codex "never"/"danger-full-access" before this existed).
2541
+ #
2542
+ # Same privilege-drop contract as kimi-hooks above: these are per-user config
2543
+ # files in the invoking user's own HOME, never root's.
2544
+ #
2545
+ # No "is this surface installed" pre-check is needed here, unlike kimi-hooks:
2546
+ # surface-policy never CREATES a config file. A surface with no config is
2547
+ # reported skipped and left alone, which is what keeps this from re-installing
2548
+ # a surface the global sweep spent its whole existence removing.
2549
+ #
2550
+ # Deliberately not fatal on drift: this writes one or two scalar keys rather
2551
+ # than a delimited managed block, so there is no "drifted region" to refuse --
2552
+ # a hand-edited mode is simply overwritten by the declared posture, which is
2553
+ # the entire point of re-asserting it every cycle.
2554
+ if [[ "$surface_policy_enabled" == true ]]; then
2555
+ if [[ "$mode" != apply ]]; then
2556
+ echo "global-tools: surface policy mode=plan home=$administrator_home"
2557
+ "$script_dir/surface-policy" --home "$administrator_home" --mode plan
2558
+ else
2559
+ echo "global-tools: surface policy mode=apply home=$administrator_home (as $administrator)"
2560
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2561
+ "$script_dir/surface-policy" --home "$administrator_home" --mode apply)
2562
+ fi
2563
+ fi
2564
+
2565
+ # VS Code keeps distinct settings files for the default profile and every
2566
+ # named profile. The deployer patches the two fleet-owned root booleans in all
2567
+ # of them without reserialising JSONC, and skips a host whose stable VS Code
2568
+ # User directory does not exist. Like every user-surface deployer, it targets
2569
+ # the administrator's HOME, never a Linux CI account.
2570
+ if [[ "$vscode_settings_enabled" == true ]]; then
2571
+ if [[ "$mode" != apply ]]; then
2572
+ echo "global-tools: VS Code settings mode=plan home=$administrator_home"
2573
+ "$script_dir/vscode-settings" --home "$administrator_home" --platform "$platform" --mode plan
2574
+ else
2575
+ echo "global-tools: VS Code settings mode=apply home=$administrator_home (as $administrator)"
2576
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2577
+ "$script_dir/vscode-settings" --home "$administrator_home" --platform "$platform" --mode apply)
2578
+ fi
2579
+ fi
2580
+
2581
+ if [[ "$global_agents_enabled" == true ]]; then
2582
+ if [[ "$mode" != apply ]]; then
2583
+ echo "global-tools: global agents mode=status home=$administrator_home"
2584
+ "$script_dir/global-agents" --home "$administrator_home" --mode status
2585
+ else
2586
+ echo "global-tools: global agents mode=deploy home=$administrator_home (as $administrator)"
2587
+ # The exact target files are authoritative global artifacts. Drift is
2588
+ # replaced from GLOBAL-AGENTS.md with no backup or leave-in-place branch.
2589
+ (cd /tmp && sudo -u "$administrator" -H env PATH="${brew_bin%/*}:/usr/bin:/bin" \
2590
+ "$script_dir/global-agents" --home "$administrator_home" --mode deploy)
2591
+ fi
2592
+ fi
2593
+
2594
+ # One combined block for every backup this run created (apply) or would
2595
+ # create (plan/audit) -- see backups=() and record_backup's own header
2596
+ # comment near the top of this file for what feeds it and what does not.
2597
+ if [[ "${#backups[@]}" -gt 0 ]]; then
2598
+ if [[ "$mode" == apply ]]; then
2599
+ echo "global-tools: backups this run:"
2600
+ else
2601
+ echo "global-tools: would back up this run:"
2602
+ fi
2603
+ for backup_entry in "${backups[@]}"; do
2604
+ echo " $backup_entry"
2605
+ done
2606
+ else
2607
+ echo "global-tools: no backups this run"
2608
+ fi
2609
+
2610
+ if [[ "$mode" == apply ]]; then
2611
+ if repo_root_is_git; then
2612
+ applied_commit="$(sudo -u "$administrator" -H git -C "$repo_root" rev-parse HEAD)" || {
2613
+ echo "could not resolve checkout commit for applied manifest identity" >&2
2614
+ exit 1
2615
+ }
2616
+ else
2617
+ # az374: the installed npm package's own version is the identity of what
2618
+ # was applied -- there is no commit to ask for.
2619
+ applied_npm_version="$(jq -er '.version | select(type == "string" and length > 0)' "$repo_root/package.json")" || {
2620
+ echo "installed Adl package has no valid version: $repo_root/package.json" >&2
2621
+ exit 1
2622
+ }
2623
+ applied_commit="npm:$applied_npm_version"
2624
+ fi
2625
+ install -m 0644 "$manifest" "$applied_manifest.new"
2626
+ mv "$applied_manifest.new" "$applied_manifest"
2627
+ printf '%s\n' "$applied_commit" >"$state_dir/applied-commit"
2628
+ printf '%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$state_dir/last-success"
2629
+ echo "global-tools: reconciliation complete"
2630
+ else
2631
+ echo "global-tools: $mode complete; no state changed"
2632
+ fi
2633
+
2634
+ # A recorded, continued-past tool failure (install_one_tool /
2635
+ # build_one_bun_tool / install_one_embedding_model_tool / write_one_shim)
2636
+ # still lets every post-pass above run -- sweep, kimi-hooks, global-agents,
2637
+ # and health-check all completed (or failed loud on their own terms) before
2638
+ # this point. The non-zero exit here is the only signal a caller (cron,
2639
+ # systemd, CI) needs that something needs attention.
2640
+ if [[ "${#failed_tools[@]}" -gt 0 ]]; then
2641
+ echo "global-tools: FAILED to install/build ${#failed_tools[@]} tool(s): ${failed_tools[*]}" >&2
2642
+ exit 1
2643
+ fi
2644
+
2645
+ if [[ "$scratch_reap_failed" -eq 1 ]]; then
2646
+ echo "global-tools: scratch reap could not verify remotes; worktree removal was refused" >&2
2647
+ exit 1
2648
+ fi