@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.
- package/bin/adl +4 -0
- package/bin/cursor-settings +14 -0
- package/bin/default-agent-file +14 -0
- package/bin/git +670 -0
- package/bin/git-ado +425 -0
- package/bin/global-agents +14 -0
- package/bin/global-surface-sweep +14 -0
- package/bin/health-check +38 -0
- package/bin/kimi-hooks +14 -0
- package/bin/lean-ctx-observe-strip +361 -0
- package/bin/lib/bun-compile-build.sh +34 -0
- package/bin/lib/lean-ctx-observe-hooks.sh +634 -0
- package/bin/managed-settings +14 -0
- package/bin/opencode-hooks +14 -0
- package/bin/reconcile +2648 -0
- package/bin/scratch-reap +14 -0
- package/bin/shell-path-policy +64 -0
- package/bin/surface-policy +14 -0
- package/bin/sync-and-reconcile +215 -0
- package/bin/update-azure-agent-pool +29 -0
- package/bin/validate-manifest +171 -0
- package/bin/vscode-settings +14 -0
- package/package.json +44 -0
- package/packages/agent-hooks/README.md +411 -0
- package/packages/agent-hooks/assets/hooks.json +97 -0
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +224 -0
- package/packages/agent-hooks/bunfig.toml +27 -0
- package/packages/agent-hooks/deploy-profile.json +53 -0
- package/packages/agent-hooks/deploy-profile.schema.json +120 -0
- package/packages/agent-hooks/package.json +23 -0
- package/packages/agent-hooks/src/cli/args.ts +99 -0
- package/packages/agent-hooks/src/cli/doctor.ts +241 -0
- package/packages/agent-hooks/src/cli/rules.ts +133 -0
- package/packages/agent-hooks/src/deploy/deploy.ts +181 -0
- package/packages/agent-hooks/src/deploy/doctor.ts +158 -0
- package/packages/agent-hooks/src/deploy/index.ts +17 -0
- package/packages/agent-hooks/src/deploy/json-merge.ts +115 -0
- package/packages/agent-hooks/src/deploy/legacy-lock.ts +213 -0
- package/packages/agent-hooks/src/deploy/list.ts +117 -0
- package/packages/agent-hooks/src/deploy/lock.ts +65 -0
- package/packages/agent-hooks/src/deploy/opencode-template.ts +74 -0
- package/packages/agent-hooks/src/deploy/profile.ts +117 -0
- package/packages/agent-hooks/src/deploy/render.ts +162 -0
- package/packages/agent-hooks/src/deploy/source.ts +84 -0
- package/packages/agent-hooks/src/gating/config.ts +514 -0
- package/packages/agent-hooks/src/gating/root.ts +117 -0
- package/packages/agent-hooks/src/hooks/briefing/handler.ts +22 -0
- package/packages/agent-hooks/src/hooks/briefing/index.ts +320 -0
- package/packages/agent-hooks/src/hooks/contract.ts +112 -0
- package/packages/agent-hooks/src/hooks/crg/handler.ts +22 -0
- package/packages/agent-hooks/src/hooks/crg/index.ts +209 -0
- package/packages/agent-hooks/src/hooks/lean-ctx/handler.ts +22 -0
- package/packages/agent-hooks/src/hooks/lean-ctx/index.ts +173 -0
- package/packages/agent-hooks/src/hooks/lean-ctx/mapping.ts +121 -0
- package/packages/agent-hooks/src/hooks/lean-ctx/scope-rewrite.ts +207 -0
- package/packages/agent-hooks/src/hooks/log.ts +233 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/config.ts +58 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/constants.ts +149 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/handler.ts +72 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/index.ts +203 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/match.ts +212 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/skill-roots.ts +73 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/skills.ts +98 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/tokenizer.ts +115 -0
- package/packages/agent-hooks/src/hooks/user-prompt-submit/vector.ts +174 -0
- package/packages/agent-hooks/src/hosts/impl/claude.ts +47 -0
- package/packages/agent-hooks/src/hosts/impl/codex.ts +47 -0
- package/packages/agent-hooks/src/hosts/impl/cursor.ts +40 -0
- package/packages/agent-hooks/src/hosts/impl/kimi.ts +47 -0
- package/packages/agent-hooks/src/hosts/impl/opencode.ts +54 -0
- package/packages/agent-hooks/src/hosts/registry.ts +29 -0
- package/packages/agent-hooks/src/hosts/spec.ts +113 -0
- package/packages/agent-hooks/src/lean-ctx/env.ts +106 -0
- package/packages/agent-hooks/src/lean-ctx/mcp.ts +140 -0
- package/packages/agent-hooks/src/lib/spawn-with-timeout.ts +120 -0
- package/packages/agent-hooks/src/log/writer.ts +230 -0
- package/packages/agent-hooks/src/main.ts +269 -0
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +549 -0
- package/packages/agent-hooks/src/rules/engine.ts +293 -0
- package/packages/agent-hooks/src/rules/impl/adl-build-run-denied.ts +51 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-credential-bypass.ts +320 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-identity.ts +249 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-push-policy.ts +258 -0
- package/packages/agent-hooks/src/rules/impl/adl-human-azdo-bypass.ts +731 -0
- package/packages/agent-hooks/src/rules/impl/adl-identity.ts +128 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +179 -0
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +65 -0
- package/packages/agent-hooks/src/rules/impl/adl-pr-review-role.ts +156 -0
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +121 -0
- package/packages/agent-hooks/src/rules/impl/adl-session-required.ts +403 -0
- package/packages/agent-hooks/src/rules/impl/adl-token-minting-role.ts +92 -0
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +74 -0
- package/packages/agent-hooks/src/rules/impl/advisory-coverage.ts +43 -0
- package/packages/agent-hooks/src/rules/impl/advisory-finder.ts +62 -0
- package/packages/agent-hooks/src/rules/impl/agent-secrets-stay-unread.ts +173 -0
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +508 -0
- package/packages/agent-hooks/src/rules/impl/fs-canonical-checkout.ts +86 -0
- package/packages/agent-hooks/src/rules/impl/fs-git-internals.ts +75 -0
- package/packages/agent-hooks/src/rules/impl/fs-mv.ts +66 -0
- package/packages/agent-hooks/src/rules/impl/fs-rm.ts +83 -0
- package/packages/agent-hooks/src/rules/impl/fs-trash.ts +140 -0
- package/packages/agent-hooks/src/rules/impl/git-core.ts +101 -0
- package/packages/agent-hooks/src/rules/impl/git-helpers.ts +408 -0
- package/packages/agent-hooks/src/rules/impl/git-history.ts +77 -0
- package/packages/agent-hooks/src/rules/impl/git-paths.ts +139 -0
- package/packages/agent-hooks/src/rules/impl/git-plumbing.ts +167 -0
- package/packages/agent-hooks/src/rules/impl/git-protected-tree.ts +132 -0
- package/packages/agent-hooks/src/rules/impl/git-transitions.ts +292 -0
- package/packages/agent-hooks/src/rules/impl/git-verify.ts +95 -0
- package/packages/agent-hooks/src/rules/impl/git-worktree.ts +265 -0
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +76 -0
- package/packages/agent-hooks/src/rules/impl/messages.ts +128 -0
- package/packages/agent-hooks/src/rules/impl/parse-uncertain.ts +143 -0
- package/packages/agent-hooks/src/rules/impl/pr-completion.ts +35 -0
- package/packages/agent-hooks/src/rules/impl/shell-remote-script-exec.ts +63 -0
- package/packages/agent-hooks/src/rules/impl/tool-azure-devops.ts +283 -0
- package/packages/agent-hooks/src/rules/impl/tool-bun-test.ts +81 -0
- package/packages/agent-hooks/src/rules/impl/tool-cargo.ts +66 -0
- package/packages/agent-hooks/src/rules/impl/tool-chmod-hook.ts +42 -0
- package/packages/agent-hooks/src/rules/impl/tool-find.ts +82 -0
- package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +83 -0
- package/packages/agent-hooks/src/rules/impl/tool-grep.ts +93 -0
- package/packages/agent-hooks/src/rules/impl/tool-lean-ctx-global.ts +594 -0
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +77 -0
- package/packages/agent-hooks/src/rules/ports.ts +178 -0
- package/packages/agent-hooks/src/rules/registry.ts +233 -0
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +1415 -0
- package/packages/agent-hooks/src/rules/shell/git-internals.ts +186 -0
- package/packages/agent-hooks/src/rules/shell/invocation.ts +1083 -0
- package/packages/agent-hooks/src/rules/shell/piped-script.ts +201 -0
- package/packages/agent-hooks/src/rules/shell/prefixes.ts +214 -0
- package/packages/agent-hooks/src/rules/shell/protected-carrier-writes.ts +108 -0
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +149 -0
- package/packages/agent-hooks/src/rules/shell/split.ts +179 -0
- package/packages/agent-hooks/src/rules/shell/substitution.ts +394 -0
- package/packages/agent-hooks/src/rules/shell/token.ts +180 -0
- package/packages/agent-hooks/src/rules/shell/xargs.ts +81 -0
- package/packages/agent-hooks/src/rules/spec.ts +152 -0
- package/packages/agent-hooks/src/surface-policy.ts +37 -0
- package/packages/agent-hooks/tsconfig.json +23 -0
- package/packages/agent-settings/assets/GLOBAL-AGENTS.md +107 -0
- package/packages/agent-settings/assets/agents/architect.toml +30 -0
- package/packages/agent-settings/assets/agents/code-reviewer.toml +27 -0
- package/packages/agent-settings/assets/agents/critic.toml +35 -0
- package/packages/agent-settings/assets/agents/executor.toml +28 -0
- package/packages/agent-settings/assets/agents/explore.toml +25 -0
- package/packages/agent-settings/assets/agents/planner.toml +29 -0
- package/packages/agent-settings/assets/lean-ctx/config.toml +39 -0
- package/packages/agent-settings/assets/lean-ctx/gitignore +6 -0
- package/packages/agent-settings/assets/lean-ctx/layout.toml +5 -0
- package/packages/agent-settings/assets/mcp.json +20 -0
- package/packages/agent-settings/managed/README.md +331 -0
- package/packages/agent-settings/managed/claude-managed-settings.json +150 -0
- package/packages/agent-settings/managed/codex-requirements.toml +105 -0
- package/packages/agent-settings/protected.json +470 -0
- package/packages/agent-settings/src/deploy/agent-render.ts +94 -0
- package/packages/agent-settings/src/deploy/agent-toml.ts +76 -0
- package/packages/agent-settings/src/deploy/agents-deploy.ts +129 -0
- package/packages/agent-settings/src/deploy/doctor.ts +201 -0
- package/packages/agent-settings/src/deploy/index.ts +33 -0
- package/packages/agent-settings/src/deploy/json-pretty.ts +66 -0
- package/packages/agent-settings/src/deploy/lean-ctx-deploy.ts +138 -0
- package/packages/agent-settings/src/deploy/legacy-lock.ts +286 -0
- package/packages/agent-settings/src/deploy/list.ts +135 -0
- package/packages/agent-settings/src/deploy/lock.ts +112 -0
- package/packages/agent-settings/src/deploy/mcp-deploy.ts +175 -0
- package/packages/agent-settings/src/deploy/mcp-inspect.ts +56 -0
- package/packages/agent-settings/src/deploy/mcp-merge.ts +164 -0
- package/packages/agent-settings/src/deploy/mcp-render.ts +80 -0
- package/packages/agent-settings/src/deploy/mcp-source.ts +43 -0
- package/packages/agent-settings/src/deploy/profile.ts +296 -0
- package/packages/agent-settings/src/deploy/settings.ts +65 -0
- package/packages/agent-settings/src/global-agents.ts +230 -0
- package/packages/agent-settings/src/global-surface-sweep.ts +2179 -0
- package/packages/agent-settings/src/kimi-hooks.ts +455 -0
- package/packages/agent-settings/src/opencode-hooks.ts +76 -0
- package/packages/agent-settings/src/surface-policy.ts +441 -0
- package/packages/agent-settings/surface-policy.json +20 -0
- package/packages/agent-skills/assets/code-review/SKILL.md +100 -0
- package/packages/agent-skills/assets/codebase-design/SKILL.md +98 -0
- package/packages/agent-skills/assets/codebase-design/references/DEEPENING.md +37 -0
- package/packages/agent-skills/assets/codebase-design/references/DESIGN-IT-TWICE.md +44 -0
- package/packages/agent-skills/assets/codebase-design/references/TESTABILITY.md +29 -0
- package/packages/agent-skills/assets/debug-issue/SKILL.md +40 -0
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +100 -0
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +34 -0
- package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/packages/agent-skills/assets/domain-modeling/SKILL.md +86 -0
- package/packages/agent-skills/assets/domain-modeling/references/ADR-FORMAT.md +47 -0
- package/packages/agent-skills/assets/domain-modeling/references/CONTEXT-FORMAT.md +60 -0
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +41 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +28 -0
- package/packages/agent-skills/assets/implement/SKILL.md +28 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +44 -0
- package/packages/agent-skills/assets/review-changes/SKILL.md +45 -0
- package/packages/agent-skills/assets/tdd/SKILL.md +49 -0
- package/packages/agent-skills/assets/tdd/references/mocking.md +59 -0
- package/packages/agent-skills/assets/tdd/references/tests.md +77 -0
- package/packages/agent-skills/assets/testing/SKILL.md +54 -0
- package/packages/agent-skills/assets/to-feature/SKILL.md +91 -0
- package/packages/agent-skills/assets/to-feature/references/FEATURE-SPEC-TEMPLATE.md +88 -0
- package/packages/agent-skills/assets/to-workitems/SKILL.md +90 -0
- package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +118 -0
- package/packages/agent-skills/assets/to-workitems/references/TICKET-TEMPLATES.md +70 -0
- package/packages/agent-skills/assets/wayfinder/SKILL.md +100 -0
- package/packages/agent-skills/assets/wayfinder/references/TEMPLATES.md +35 -0
- package/packages/agent-skills/assets/wayfinder/references/TICKET-TYPES.md +8 -0
- package/packages/agent-skills/deploy-profile.json +9 -0
- package/packages/agent-skills/deploy-profile.schema.json +45 -0
- package/packages/agent-skills/dist/deploy.d.ts +27 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -0
- package/packages/agent-skills/dist/deploy.js +186 -0
- package/packages/agent-skills/dist/deploy.js.map +1 -0
- package/packages/agent-skills/dist/doctor.d.ts +24 -0
- package/packages/agent-skills/dist/doctor.d.ts.map +1 -0
- package/packages/agent-skills/dist/doctor.js +141 -0
- package/packages/agent-skills/dist/doctor.js.map +1 -0
- package/packages/agent-skills/dist/fs-utils.d.ts +8 -0
- package/packages/agent-skills/dist/fs-utils.d.ts.map +1 -0
- package/packages/agent-skills/dist/fs-utils.js +40 -0
- package/packages/agent-skills/dist/fs-utils.js.map +1 -0
- package/packages/agent-skills/dist/index.d.ts +8 -0
- package/packages/agent-skills/dist/index.d.ts.map +1 -0
- package/packages/agent-skills/dist/index.js +8 -0
- package/packages/agent-skills/dist/index.js.map +1 -0
- package/packages/agent-skills/dist/legacy-lock.d.ts +44 -0
- package/packages/agent-skills/dist/legacy-lock.d.ts.map +1 -0
- package/packages/agent-skills/dist/legacy-lock.js +121 -0
- package/packages/agent-skills/dist/legacy-lock.js.map +1 -0
- package/packages/agent-skills/dist/list.d.ts +23 -0
- package/packages/agent-skills/dist/list.d.ts.map +1 -0
- package/packages/agent-skills/dist/list.js +75 -0
- package/packages/agent-skills/dist/list.js.map +1 -0
- package/packages/agent-skills/dist/manifest.d.ts +15 -0
- package/packages/agent-skills/dist/manifest.d.ts.map +1 -0
- package/packages/agent-skills/dist/manifest.js +57 -0
- package/packages/agent-skills/dist/manifest.js.map +1 -0
- package/packages/agent-skills/dist/profile.d.ts +41 -0
- package/packages/agent-skills/dist/profile.d.ts.map +1 -0
- package/packages/agent-skills/dist/profile.js +86 -0
- package/packages/agent-skills/dist/profile.js.map +1 -0
- package/packages/agent-skills/dist/skill-config.d.ts +24 -0
- package/packages/agent-skills/dist/skill-config.d.ts.map +1 -0
- package/packages/agent-skills/dist/skill-config.js +68 -0
- package/packages/agent-skills/dist/skill-config.js.map +1 -0
- package/packages/agent-skills/dist/skill-meta.d.ts +17 -0
- package/packages/agent-skills/dist/skill-meta.d.ts.map +1 -0
- package/packages/agent-skills/dist/skill-meta.js +67 -0
- package/packages/agent-skills/dist/skill-meta.js.map +1 -0
- package/packages/agent-skills/package.json +31 -0
- package/packages/agent-skills/src/deploy.ts +222 -0
- package/packages/agent-skills/src/doctor.ts +178 -0
- package/packages/agent-skills/src/fs-utils.ts +40 -0
- package/packages/agent-skills/src/index.ts +29 -0
- package/packages/agent-skills/src/legacy-lock.ts +152 -0
- package/packages/agent-skills/src/list.ts +94 -0
- package/packages/agent-skills/src/manifest.ts +66 -0
- package/packages/agent-skills/src/profile.ts +128 -0
- package/packages/agent-skills/src/skill-config.ts +75 -0
- package/packages/agent-skills/src/skill-meta.ts +78 -0
- package/packages/agent-skills/tsconfig.build.json +13 -0
- package/packages/agent-skills/tsconfig.json +23 -0
- package/packages/azdo/README.md +111 -0
- package/packages/azdo/package.json +21 -0
- package/packages/azdo/src/azdo-pl.ts +275 -0
- package/packages/azdo/src/azdo-pr-review.ts +160 -0
- package/packages/azdo/src/azdo-pr.ts +289 -0
- package/packages/azdo/src/azdo-repo-doctor.ts +839 -0
- package/packages/azdo/src/azdo-wit.ts +145 -0
- package/packages/azdo/src/cli.ts +251 -0
- package/packages/azdo/src/lib/agent-identity.ts +198 -0
- package/packages/azdo/src/lib/agent-token.ts +167 -0
- package/packages/azdo/src/lib/client.ts +259 -0
- package/packages/azdo/src/lib/graph-identity.ts +153 -0
- package/packages/azdo/src/lib/validate-pr-metadata.ts +57 -0
- package/packages/azdo/src/mcp-server.ts +209 -0
- package/packages/azdo/src/tool-dispatch.ts +204 -0
- package/packages/azdo/tsconfig.json +13 -0
- package/packages/fleet/jobs/git-maintain/src/main.ts +588 -0
- package/packages/fleet/manifest.json +207 -0
- package/packages/fleet/manifest.schema.json +283 -0
- package/packages/fleet/src/default-agent-file.ts +144 -0
- package/packages/fleet/src/install-adl-package.ts +178 -0
- package/packages/fleet/src/install-embedding-model.ts +412 -0
- package/packages/fleet/src/launch-agents.ts +649 -0
- package/packages/fleet/src/managed-settings.ts +278 -0
- package/packages/fleet/src/scratch-reap.ts +601 -0
- package/packages/fleet/src/vscode-settings.ts +705 -0
- package/packages/fleet/systemd/adl-azure-agent-update.service +13 -0
- package/packages/fleet/systemd/adl-azure-agent-update.timer +10 -0
- package/packages/repo-controls/AGENTS.md +96 -0
- package/packages/repo-controls/CLAUDE.md +1 -0
- package/packages/repo-controls/LICENSE +20 -0
- package/packages/repo-controls/README.md +493 -0
- package/packages/repo-controls/assets/enable-integration-auto-complete.mjs +79 -0
- package/packages/repo-controls/assets/promote.ts +371 -0
- package/packages/repo-controls/assets/version-bump.sh +189 -0
- package/packages/repo-controls/azure-pipelines.yml +274 -0
- package/packages/repo-controls/bin/adl-repo-controls.ts +10 -0
- package/packages/repo-controls/bunfig.toml +58 -0
- package/packages/repo-controls/package/adl-compliance/AGENTS.md +163 -0
- package/packages/repo-controls/package/adl-compliance/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/conftest.py +78 -0
- package/packages/repo-controls/package/adl-compliance/pyproject.toml +31 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/__init__.py +50 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +98 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +210 -0
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/fixtures/adr-lifecycle-clean/docs/architecture/overview.md +8 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +127 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/check.py +91 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/architecture/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/compliance/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/decisions/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/index.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/operations/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/research/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/stray-root-file.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/fixtures/docs/totally-made-up/nonsense.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +106 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +291 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/clean/backend/clean.rs +1 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/violation/backend/escaped.rs +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/violation/backend/raw.rs +1 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +193 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +140 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-clean/docs/architecture/foo.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-clean/docs/architecture/index.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-violation/docs/architecture/foo.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/fixtures/index-updated-violation/docs/architecture/index.md +9 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +98 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +378 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +192 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/index.md +14 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/one.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs/two.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-clean/docs-knowledge-indexes.yaml +20 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/index.md +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/one.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs/two.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-duplicate/docs-knowledge-indexes.yaml +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/index.md +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/one.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs/two.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/fixtures/knowledge-indexes-missing-entry/docs-knowledge-indexes.yaml +16 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +102 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/check.py +387 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-bad-date/plans/LEDGER.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-clean/plans/LEDGER.md +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-empty-table/plans/LEDGER.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-long-event/plans/LEDGER.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/fixtures/ledger-violation/plans/LEDGER.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +427 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/check.py +84 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-fence/docs/valid.md +5 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-bad-frontmatter/docs/valid.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/fixtures/markdown-structure-clean/docs/valid.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/markdown_ast/test_check.py +126 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/check.py +210 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +119 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs/architecture/demo.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-clean/docs-okf-profiles.yaml +8 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-violation/docs-okf-profiles.yaml +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +49 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +159 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +578 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/architecture/backend/crate-overview.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/architecture/team/team-overview.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/compliance/controls/control-matrix.md +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/compliance/controls/control-matrix.yaml +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-clean/docs/research/benchmarks/note.md +24 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/backend/crate-overview.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/stable-delivery.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/stable-recommendation.md +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/product/target-narration.md +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/team/team-overview.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/compliance/controls/control-matrix.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/compliance/controls/control-matrix.yaml +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/research/benchmarks/bad-research.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +461 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/check.py +124 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/.workflows/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/.workflows/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/CLAUDE.md +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/INVARIANTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/bad-pointer/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/bad-pointer/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/docs/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/docs/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/missing-pair/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/owned/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/fixtures/owned/CLAUDE.md +2 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/agents_layout/test_check.py +116 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/__init__.py +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/check.py +380 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +274 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +227 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +6 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +6 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +138 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +240 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-clean/backend/AGENTS.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-clean/docs/page.md +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-stray/backend/notes.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/fixtures/markdown-trees-stray/docs/page.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +220 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/__init__.py +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +225 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +330 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +240 -0
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/mobile/assets/images/icon.png +1 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/public/pwa/icon-192.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/frontend/resources/product.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-stray/apps/legacy/file.ts +1 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-stray/frontend/unapproved/screenshot.png +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +230 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +53 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/__init__.py +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +438 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/AGENTS.md +1 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/templates/feature.md +1 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +461 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/check.py +62 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/fixtures/migration-filenames/backend/crates/demo/NotAMigration.sql +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/profile_data/test_check.py +116 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +188 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/README.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/product/links.md +20 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/README.md +8 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/product/links.md +20 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +166 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +135 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.agents/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.changeset/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.claude/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.code-review-graph/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.code-review-graph_old/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.codex/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.cursor/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.cursorrules +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.dockerignore +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.docs/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.donors/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.gitattributes +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.kimi-code/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.lfsconfig +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.mcp.json +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.opencode/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.workflows/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CHANGELOG.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CLAUDE-INVARIANTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/CLAUDE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/INDEPENDENT-VERIFICATION.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/INVARIANTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/LICENSE +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/NOTICE +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/README.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/RELEASE.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/VERSION +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/backend/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/compliance/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/dev/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/docker/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/docs/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/frontend/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/infra/declared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/justfile +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/opencode.jsonc +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/rule-tests/undeclared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/rules/undeclared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/unapproved-root/undeclared.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/undeclared-root-file.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +122 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +290 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.plans/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.specs/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/.workflows/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/AGENTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/CHANGELOG.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/CLAUDE.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/INDEPENDENT-VERIFICATION.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/INVARIANTS.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/README.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/RELEASE.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/STRAY.md +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/docs/AGENTS.md +3 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-clean/frontend/README.md +3 -0
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/AGENTS.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/fixtures/root-markdown-missing-guide/README.md +1 -0
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +329 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +127 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +163 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +169 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/attribute-without-conversion/.gitattributes +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/attribute-without-conversion/large file.bin +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/.gitattributes +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/large file.bin +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/clean/small.txt +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/missing-filter/large file.bin +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/unmatched-extension/.gitattributes +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/fixtures/unmatched-extension/payload.dat +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +133 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/check.py +129 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/disjoint/gate-registry.yaml +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/empty/gate-registry.yaml +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/fixtures/overlapping/gate-registry.yaml +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/pipeline/phase_disjoint/test_check.py +109 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/check.py +149 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/fixtures/clean/docs/readme.md +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/fixtures/relevant/VERSION +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/changeset_plan/test_check.py +151 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/check.py +202 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/.workflows/changesets/package.json +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/.workflows/package.json +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/CHANGELOG.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/aligned/VERSION +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/.workflows/changesets/package.json +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/.workflows/package.json +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/CHANGELOG.md +5 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/fixtures/mismatched/VERSION +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/release/version_release/test_check.py +162 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/check.py +103 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/elsewhere/loose-git.sh +6 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/fixtures/no-banned-commands/elsewhere/loose-grep.sh +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +117 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/__init__.py +13 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/check.py +265 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/fixtures/ignored-paths/keep.log +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/fixtures/ignored-paths/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/ignored_paths/test_check.py +180 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/__init__.py +12 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/check.py +84 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-clean/LICENSE +10 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-mismatch/LICENSE +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/fixtures/license-declared-missing/README.md +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/license_declared/test_check.py +66 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +167 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/backend/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/frontend/bun.lock +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/Cargo.lock +7 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/Cargo.toml +4 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/rust-toolchain.toml +3 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/backend/src/main.rs +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/frontend/bun.lock +0 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +109 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +483 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +199 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/batch.py +52 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +303 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +106 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/context.py +99 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/errors.py +32 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/finding.py +65 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/models.py +40 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +63 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scope.py +82 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/verdict.py +161 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/__init__.py +1 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +131 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +30 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +65 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +791 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.schema.yaml +176 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +791 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/grants-registry.yaml +112 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/grants.py +129 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +281 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +250 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/severity-policy.yaml +114 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/providers/__init__.py +11 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +371 -0
- package/packages/repo-controls/package/adl-compliance/uv.lock +199 -0
- package/packages/repo-controls/package.json +33 -0
- package/packages/repo-controls/scripts/hooks/commit-msg +63 -0
- package/packages/repo-controls/scripts/hooks/lib/hook-log.sh +62 -0
- package/packages/repo-controls/scripts/hooks/post-checkout +15 -0
- package/packages/repo-controls/scripts/hooks/post-commit +18 -0
- package/packages/repo-controls/scripts/hooks/pre-commit +82 -0
- package/packages/repo-controls/scripts/hooks/pre-push +144 -0
- package/packages/repo-controls/src/azdo-push.ts +463 -0
- package/packages/repo-controls/src/azure-devops-governance.ts +248 -0
- package/packages/repo-controls/src/azure-devops-remote.ts +61 -0
- package/packages/repo-controls/src/cli.ts +188 -0
- package/packages/repo-controls/src/compliance-config.ts +200 -0
- package/packages/repo-controls/src/config.ts +690 -0
- package/packages/repo-controls/src/constants.ts +97 -0
- package/packages/repo-controls/src/coverage-ratchet.ts +153 -0
- package/packages/repo-controls/src/doc-freshness.ts +172 -0
- package/packages/repo-controls/src/doctor.ts +304 -0
- package/packages/repo-controls/src/findings-ratchet.ts +163 -0
- package/packages/repo-controls/src/gitignore-block.ts +253 -0
- package/packages/repo-controls/src/hook-log.ts +72 -0
- package/packages/repo-controls/src/hook.ts +694 -0
- package/packages/repo-controls/src/install.ts +506 -0
- package/packages/repo-controls/src/integrity.ts +104 -0
- package/packages/repo-controls/src/promote.ts +25 -0
- package/packages/repo-controls/src/runtime.ts +110 -0
- package/packages/repo-controls/src/workflows-drift.ts +106 -0
- package/packages/repo-controls/src/worktree-guard.ts +272 -0
- package/packages/repo-controls/src/worktree-limit.ts +11 -0
- package/packages/repo-controls/tsconfig.json +14 -0
- 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
|