@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
|
@@ -0,0 +1,2179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely inventory and remove machine-global agent host wiring (Claude, Codex,
|
|
3
|
+
* Cursor, OpenCode, Kimi, shell rc blocks, LaunchAgents)
|
|
4
|
+
* that enact-vps does not ship and that should have been repository-local.
|
|
5
|
+
*
|
|
6
|
+
* Discovery is broad. Authorization is narrow: apply mode accepts only a
|
|
7
|
+
* reviewed plan emitted by this module, matches every operation against the
|
|
8
|
+
* built-in surface allowlist, and verifies the target's SHA-256 fingerprint
|
|
9
|
+
* immediately before changing it. Discovery alone never authorizes deletion.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import {
|
|
15
|
+
chmodSync,
|
|
16
|
+
chownSync,
|
|
17
|
+
cpSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
lstatSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
readlinkSync,
|
|
25
|
+
renameSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from 'node:fs'
|
|
29
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
import { BEGIN as KIMI_BEGIN, END as KIMI_END } from './kimi-hooks.ts'
|
|
32
|
+
import { declaredLaunchAgentPlists } from '../../../packages/fleet/src/launch-agents.ts'
|
|
33
|
+
|
|
34
|
+
const LAUNCH_AGENTS_DIR = 'Library/LaunchAgents'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* LaunchAgent scope is agent tooling only. A `~/Library/LaunchAgents` entry is
|
|
38
|
+
* a sweep candidate when its file name or its raw plist bytes (Label,
|
|
39
|
+
* Program/ProgramArguments, environment -- XML and binary plists both store
|
|
40
|
+
* these strings verbatim) contain, case-insensitively, one of these agent
|
|
41
|
+
* tool tokens. Everything else (GoogleUpdater, Keystone, other application
|
|
42
|
+
* updaters) is not an agent surface and is neither a candidate nor reported.
|
|
43
|
+
* LaunchAgents enact-vps declares in fleet/manifest.json are always in
|
|
44
|
+
* scope and protected by identity (see loadProtectionList).
|
|
45
|
+
*/
|
|
46
|
+
export const AGENT_TOOL_TOKENS = [
|
|
47
|
+
'lean-ctx',
|
|
48
|
+
'leanctx',
|
|
49
|
+
'adl',
|
|
50
|
+
'jcode',
|
|
51
|
+
'claude',
|
|
52
|
+
'codex',
|
|
53
|
+
'cursor',
|
|
54
|
+
'kimi',
|
|
55
|
+
'opencode',
|
|
56
|
+
'copilot',
|
|
57
|
+
'orca',
|
|
58
|
+
] as const
|
|
59
|
+
|
|
60
|
+
export function isAgentToolingLaunchAgent(name: string, content: string): boolean {
|
|
61
|
+
const haystack = `${name}\n${content}`.toLowerCase()
|
|
62
|
+
return AGENT_TOOL_TOKENS.some((token) => haystack.includes(token))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PreservedRegion {
|
|
66
|
+
readonly begin: string
|
|
67
|
+
readonly end: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SweepTarget =
|
|
71
|
+
/** Entries of a JSON object/array. `key` may be a `/`-separated nested path, e.g. `permissions/allow`. */
|
|
72
|
+
| { readonly kind: 'json-map'; readonly file: string; readonly key: string }
|
|
73
|
+
/**
|
|
74
|
+
* A hooks-shaped map: `document[key]` is an object keyed by event name,
|
|
75
|
+
* each value an array of hook groups (Claude/Codex: `{matcher?, hooks:
|
|
76
|
+
* [...]}`; Cursor: the group IS the leaf hook, `{command, timeout}`).
|
|
77
|
+
* Unlike `json-map` (which treats a whole per-event array as one
|
|
78
|
+
* candidate), this enumerates one candidate per GROUP — selector
|
|
79
|
+
* `/key/eventName/index` — so a single event can mix a still-sweepable
|
|
80
|
+
* entry with an entry that must never be a candidate (Orca's own
|
|
81
|
+
* user-level hook, 2026-09-27 decision: "Orca is allowed for adl", the
|
|
82
|
+
* managed layer calls it, keep the lock on) without the two fighting for
|
|
83
|
+
* the same removal unit. An event whose every group is removed loses the
|
|
84
|
+
* key entirely, matching `json-map`'s own convention.
|
|
85
|
+
*/
|
|
86
|
+
| { readonly kind: 'json-hook-map'; readonly file: string; readonly key: string }
|
|
87
|
+
| { readonly kind: 'json-root'; readonly file: string }
|
|
88
|
+
| {
|
|
89
|
+
readonly kind: 'toml-tables'
|
|
90
|
+
readonly file: string
|
|
91
|
+
readonly prefixes: readonly string[]
|
|
92
|
+
/** A delimited region owned by another enact-vps deployer; never inspected or changed. */
|
|
93
|
+
readonly preserve?: PreservedRegion
|
|
94
|
+
}
|
|
95
|
+
/** Tool-injected marker-delimited regions (`# >>> x >>>`, `# x — begin`, `<!-- x -->`). */
|
|
96
|
+
| { readonly kind: 'text-blocks'; readonly file: string }
|
|
97
|
+
| {
|
|
98
|
+
readonly kind: 'dir-entries'
|
|
99
|
+
readonly dir: string
|
|
100
|
+
readonly ignore?: readonly string[]
|
|
101
|
+
readonly descend?: readonly string[]
|
|
102
|
+
/** Scheduled enforcement never removes these; they are reported as pending explicit approval. */
|
|
103
|
+
readonly requiresApproval?: boolean
|
|
104
|
+
}
|
|
105
|
+
| { readonly kind: 'path'; readonly path: string }
|
|
106
|
+
/**
|
|
107
|
+
* Directly-contained regular files of `dir` whose content contains `marker`
|
|
108
|
+
* as an exact full line within the first 5 lines. Unlike `dir-entries`, this
|
|
109
|
+
* is content-addressed, not membership-addressed: `dir` may hold many
|
|
110
|
+
* unrelated files (and subdirectories, symlinks) that must never be
|
|
111
|
+
* candidates just for living there.
|
|
112
|
+
*/
|
|
113
|
+
| { readonly kind: 'marker-files'; readonly dir: string; readonly marker: string }
|
|
114
|
+
|
|
115
|
+
const SHELL_RC_FILES = ['.zshenv', '.zprofile', '.zshrc', '.zlogin', '.bashenv', '.bashrc', '.bash_profile', '.profile']
|
|
116
|
+
|
|
117
|
+
export const SWEEP_TARGETS: readonly SweepTarget[] = [
|
|
118
|
+
{ kind: 'json-hook-map', file: '.claude/settings.json', key: 'hooks' },
|
|
119
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'mcpServers' },
|
|
120
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'enabledPlugins' },
|
|
121
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'extraKnownMarketplaces' },
|
|
122
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'permissions/allow' },
|
|
123
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'permissions/deny' },
|
|
124
|
+
{ kind: 'json-map', file: '.claude/settings.json', key: 'permissions/ask' },
|
|
125
|
+
{ kind: 'text-blocks', file: '.claude/CLAUDE.md' },
|
|
126
|
+
{ kind: 'json-map', file: '.claude.json', key: 'mcpServers' },
|
|
127
|
+
{ kind: 'dir-entries', dir: '.claude/plugins/cache' },
|
|
128
|
+
{ kind: 'dir-entries', dir: '.claude/skills' },
|
|
129
|
+
{ kind: 'dir-entries', dir: '.claude/commands' },
|
|
130
|
+
{ kind: 'dir-entries', dir: '.claude/agents' },
|
|
131
|
+
{ kind: 'dir-entries', dir: '.claude/plugins/repos' },
|
|
132
|
+
{ kind: 'dir-entries', dir: '.claude/plugins/marketplaces' },
|
|
133
|
+
{ kind: 'json-map', file: '.claude/plugins/installed_plugins.json', key: 'plugins' },
|
|
134
|
+
{ kind: 'json-root', file: '.claude/plugins/known_marketplaces.json' },
|
|
135
|
+
{ kind: 'json-hook-map', file: '.codex/hooks.json', key: 'hooks' },
|
|
136
|
+
{ kind: 'toml-tables', file: '.codex/config.toml', prefixes: ['mcp_servers', 'hooks'] },
|
|
137
|
+
{ kind: 'text-blocks', file: '.codex/AGENTS.md' },
|
|
138
|
+
{ kind: 'dir-entries', dir: '.codex/rules' },
|
|
139
|
+
{ kind: 'dir-entries', dir: '.codex/skills' },
|
|
140
|
+
{ kind: 'dir-entries', dir: '.codex/plugins', descend: ['cache'] },
|
|
141
|
+
{ kind: 'dir-entries', dir: '.codex/plugins/cache' },
|
|
142
|
+
{ kind: 'dir-entries', dir: '.codex/marketplaces' },
|
|
143
|
+
{
|
|
144
|
+
kind: 'dir-entries',
|
|
145
|
+
dir: '.codex/cache',
|
|
146
|
+
descend: [
|
|
147
|
+
'bundled_plugin_exclusions',
|
|
148
|
+
'codex_app_directory',
|
|
149
|
+
'codex_apps_server_info',
|
|
150
|
+
'codex_apps_tools',
|
|
151
|
+
'remote_plugin_catalog',
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{ kind: 'dir-entries', dir: '.codex/cache/bundled_plugin_exclusions' },
|
|
155
|
+
{ kind: 'dir-entries', dir: '.codex/cache/codex_app_directory' },
|
|
156
|
+
{ kind: 'dir-entries', dir: '.codex/cache/codex_apps_server_info' },
|
|
157
|
+
{ kind: 'dir-entries', dir: '.codex/cache/codex_apps_tools' },
|
|
158
|
+
{ kind: 'dir-entries', dir: '.codex/cache/remote_plugin_catalog' },
|
|
159
|
+
{ kind: 'dir-entries', dir: '.codex/vendor_imports' },
|
|
160
|
+
{ kind: 'json-hook-map', file: '.cursor/hooks.json', key: 'hooks' },
|
|
161
|
+
{ kind: 'json-map', file: '.cursor/mcp.json', key: 'mcpServers' },
|
|
162
|
+
{ kind: 'dir-entries', dir: '.cursor/skills' },
|
|
163
|
+
{ kind: 'dir-entries', dir: '.cursor/skills-cursor' },
|
|
164
|
+
{ kind: 'dir-entries', dir: '.cursor/commands' },
|
|
165
|
+
{ kind: 'dir-entries', dir: '.cursor/agents' },
|
|
166
|
+
{ kind: 'dir-entries', dir: '.cursor/plugins' },
|
|
167
|
+
{ kind: 'dir-entries', dir: '.cursor/hooks' },
|
|
168
|
+
{ kind: 'dir-entries', dir: '.cursor/rules' },
|
|
169
|
+
{ kind: 'text-blocks', file: '.cursor/AGENTS.md' },
|
|
170
|
+
{ kind: 'dir-entries', dir: '.agents', ignore: ['GLOBAL-AGENTS.md'], descend: ['skills'] },
|
|
171
|
+
{ kind: 'dir-entries', dir: '.agents/skills' },
|
|
172
|
+
...['.config/opencode/opencode.json', '.config/opencode/opencode.jsonc'].flatMap((file): SweepTarget[] => [
|
|
173
|
+
{ kind: 'json-map', file, key: 'mcp' },
|
|
174
|
+
{ kind: 'json-map', file, key: 'permission' },
|
|
175
|
+
{ kind: 'json-map', file, key: 'plugin' },
|
|
176
|
+
]),
|
|
177
|
+
{ kind: 'dir-entries', dir: '.config/opencode/plugins' },
|
|
178
|
+
{ kind: 'dir-entries', dir: '.config/opencode/skills' },
|
|
179
|
+
{ kind: 'text-blocks', file: '.config/opencode/AGENTS.md' },
|
|
180
|
+
{
|
|
181
|
+
kind: 'toml-tables',
|
|
182
|
+
file: '.kimi-code/config.toml',
|
|
183
|
+
prefixes: ['hooks'],
|
|
184
|
+
preserve: { begin: KIMI_BEGIN, end: KIMI_END },
|
|
185
|
+
},
|
|
186
|
+
{ kind: 'json-map', file: '.kimi-code/mcp.json', key: 'mcpServers' },
|
|
187
|
+
{ kind: 'dir-entries', dir: '.kimi-code/skills' },
|
|
188
|
+
{ kind: 'dir-entries', dir: '.kimi-code/plugins' },
|
|
189
|
+
{ kind: 'text-blocks', file: '.kimi-code/AGENTS.md' },
|
|
190
|
+
...SHELL_RC_FILES.map((file): SweepTarget => ({ kind: 'text-blocks', file })),
|
|
191
|
+
{ kind: 'dir-entries', dir: 'Library/LaunchAgents', requiresApproval: true },
|
|
192
|
+
{
|
|
193
|
+
kind: 'marker-files',
|
|
194
|
+
dir: '.local/bin',
|
|
195
|
+
marker: '# Generated by .agents/setup/setup.sh -- do not edit.',
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
function managedFileTarget(file: string): boolean {
|
|
200
|
+
return SWEEP_TARGETS.some(
|
|
201
|
+
(target) =>
|
|
202
|
+
(target.kind === 'json-map' ||
|
|
203
|
+
target.kind === 'json-hook-map' ||
|
|
204
|
+
target.kind === 'json-root' ||
|
|
205
|
+
target.kind === 'toml-tables' ||
|
|
206
|
+
target.kind === 'text-blocks') &&
|
|
207
|
+
target.file === file,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function preservedRegionFor(file: string): PreservedRegion | undefined {
|
|
212
|
+
for (const target of SWEEP_TARGETS) {
|
|
213
|
+
if (target.kind === 'toml-tables' && target.file === file && target.preserve !== undefined) return target.preserve
|
|
214
|
+
}
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function operationRequiresApproval(path: string): boolean {
|
|
219
|
+
return SWEEP_TARGETS.some(
|
|
220
|
+
(target) => target.kind === 'dir-entries' && target.requiresApproval === true && dirname(path) === target.dir,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type RemovalItem =
|
|
225
|
+
| {
|
|
226
|
+
readonly kind: 'json-entry'
|
|
227
|
+
readonly target: string
|
|
228
|
+
readonly selector: string
|
|
229
|
+
readonly beforeSha256: string
|
|
230
|
+
}
|
|
231
|
+
| {
|
|
232
|
+
readonly kind: 'toml-table'
|
|
233
|
+
readonly target: string
|
|
234
|
+
readonly header: string
|
|
235
|
+
readonly beforeSha256: string
|
|
236
|
+
}
|
|
237
|
+
| {
|
|
238
|
+
readonly kind: 'toml-key'
|
|
239
|
+
readonly target: string
|
|
240
|
+
readonly key: string
|
|
241
|
+
readonly beforeSha256: string
|
|
242
|
+
}
|
|
243
|
+
| {
|
|
244
|
+
readonly kind: 'text-block'
|
|
245
|
+
readonly target: string
|
|
246
|
+
readonly name: string
|
|
247
|
+
readonly beforeSha256: string
|
|
248
|
+
}
|
|
249
|
+
| { readonly kind: 'path'; readonly target: string; readonly beforeSha256: string }
|
|
250
|
+
|
|
251
|
+
export interface ProtectionList {
|
|
252
|
+
readonly schema: 1
|
|
253
|
+
readonly paths: readonly { readonly path: string; readonly reason: string }[]
|
|
254
|
+
readonly jsonEntries: readonly {
|
|
255
|
+
readonly target: string
|
|
256
|
+
readonly selector: string
|
|
257
|
+
readonly reason: string
|
|
258
|
+
}[]
|
|
259
|
+
readonly tomlTables: readonly {
|
|
260
|
+
readonly target: string
|
|
261
|
+
readonly header: string
|
|
262
|
+
readonly reason: string
|
|
263
|
+
}[]
|
|
264
|
+
/** Exact marker-block identities (file + block name) that are shipped/owned and must be retained. */
|
|
265
|
+
readonly textBlocks: readonly {
|
|
266
|
+
readonly target: string
|
|
267
|
+
readonly name: string
|
|
268
|
+
readonly reason: string
|
|
269
|
+
}[]
|
|
270
|
+
/**
|
|
271
|
+
* Declared shipped baseline of top-level keys for a settings file. When a
|
|
272
|
+
* baseline exists for a file, every other top-level JSON key (or TOML root
|
|
273
|
+
* key / table whose first segment is undeclared) is foreign and swept.
|
|
274
|
+
* Files without a baseline never lose root keys.
|
|
275
|
+
*/
|
|
276
|
+
readonly rootKeys: readonly {
|
|
277
|
+
readonly target: string
|
|
278
|
+
readonly keys: readonly string[]
|
|
279
|
+
readonly reason: string
|
|
280
|
+
}[]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface ProtectedItem {
|
|
284
|
+
readonly removal: RemovalItem
|
|
285
|
+
readonly reason: string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const EMPTY_PROTECTION: ProtectionList = {
|
|
289
|
+
schema: 1,
|
|
290
|
+
paths: [],
|
|
291
|
+
jsonEntries: [],
|
|
292
|
+
tomlTables: [],
|
|
293
|
+
textBlocks: [],
|
|
294
|
+
rootKeys: [],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export const DEFAULT_PROTECTION_PATH = fileURLToPath(new URL('../protected.json', import.meta.url))
|
|
298
|
+
export const DEFAULT_MANIFEST_PATH = fileURLToPath(new URL('../../fleet/manifest.json', import.meta.url))
|
|
299
|
+
|
|
300
|
+
export interface SweepNote {
|
|
301
|
+
readonly level: 'warn' | 'ok'
|
|
302
|
+
readonly text: string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type SweepOperation =
|
|
306
|
+
| {
|
|
307
|
+
readonly id: string
|
|
308
|
+
readonly kind: 'write-file'
|
|
309
|
+
readonly path: string
|
|
310
|
+
readonly absolutePath: string
|
|
311
|
+
readonly beforeSha256: string
|
|
312
|
+
readonly removals: readonly RemovalItem[]
|
|
313
|
+
readonly content: string
|
|
314
|
+
readonly mode: number
|
|
315
|
+
readonly uid: number
|
|
316
|
+
readonly gid: number
|
|
317
|
+
readonly requiresApproval: false
|
|
318
|
+
}
|
|
319
|
+
| {
|
|
320
|
+
readonly id: string
|
|
321
|
+
readonly kind: 'remove-path'
|
|
322
|
+
readonly path: string
|
|
323
|
+
readonly absolutePath: string
|
|
324
|
+
readonly beforeSha256: string
|
|
325
|
+
readonly removals: readonly RemovalItem[]
|
|
326
|
+
readonly requiresApproval: boolean
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface ApprovedOperation {
|
|
330
|
+
readonly id: string
|
|
331
|
+
readonly kind: SweepOperation['kind']
|
|
332
|
+
readonly path: string
|
|
333
|
+
readonly beforeSha256: string
|
|
334
|
+
readonly removals: readonly RemovalItem[]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface ApprovalPlan {
|
|
338
|
+
readonly schema: 1
|
|
339
|
+
readonly home: string
|
|
340
|
+
readonly generatedAt: string
|
|
341
|
+
readonly operations: readonly ApprovedOperation[]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface SweepPlan {
|
|
345
|
+
readonly operations: readonly SweepOperation[]
|
|
346
|
+
readonly protected: readonly ProtectedItem[]
|
|
347
|
+
readonly notes: readonly SweepNote[]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface ApplyResult {
|
|
351
|
+
readonly applied: readonly ApprovedOperation[]
|
|
352
|
+
readonly alreadyClean: readonly string[]
|
|
353
|
+
readonly unapproved: readonly ApprovedOperation[]
|
|
354
|
+
readonly backupRoot: string | null
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface EnforceResult extends ApplyResult {
|
|
358
|
+
readonly protectedItems: number
|
|
359
|
+
/** Candidates scheduled enforcement must not remove; they need a reviewed plan + apply. */
|
|
360
|
+
readonly pendingApproval: readonly ApprovedOperation[]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface ReviewCopyResult {
|
|
364
|
+
readonly destination: string
|
|
365
|
+
readonly copiedPaths: number
|
|
366
|
+
readonly copiedConfigFiles: number
|
|
367
|
+
readonly candidateItems: number
|
|
368
|
+
readonly protectedItems: number
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
interface DraftFile {
|
|
372
|
+
readonly path: string
|
|
373
|
+
readonly relativePath: string
|
|
374
|
+
readonly original: string
|
|
375
|
+
content: string
|
|
376
|
+
readonly mode: number
|
|
377
|
+
readonly uid: number
|
|
378
|
+
readonly gid: number
|
|
379
|
+
readonly removals: RemovalItem[]
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function clearJsonMap(
|
|
383
|
+
document: unknown,
|
|
384
|
+
key: string,
|
|
385
|
+
): { readonly document: unknown; readonly cleared: readonly string[] } | null {
|
|
386
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document)) return null
|
|
387
|
+
const root = document as Record<string, unknown>
|
|
388
|
+
const value = root[key]
|
|
389
|
+
if (value === null || typeof value !== 'object') return null
|
|
390
|
+
|
|
391
|
+
const cleared = Array.isArray(value) ? value.map((_, index) => String(index)) : Object.keys(value)
|
|
392
|
+
if (cleared.length === 0) return null
|
|
393
|
+
|
|
394
|
+
return { document: { ...root, [key]: Array.isArray(value) ? [] : {} }, cleared }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function dropTomlTables(
|
|
398
|
+
text: string,
|
|
399
|
+
prefixes: readonly string[],
|
|
400
|
+
): { readonly text: string; readonly dropped: readonly string[] } {
|
|
401
|
+
const kept: string[] = []
|
|
402
|
+
const dropped: string[] = []
|
|
403
|
+
let dropping = false
|
|
404
|
+
|
|
405
|
+
for (const line of text.split('\n')) {
|
|
406
|
+
const trimmed = line.trim()
|
|
407
|
+
if (trimmed.startsWith('[')) {
|
|
408
|
+
const name = trimmed.slice(trimmed.startsWith('[[') ? 2 : 1).replace(/\]+$/, '')
|
|
409
|
+
dropping = prefixes.some((prefix) => name === prefix || name.startsWith(`${prefix}.`))
|
|
410
|
+
if (dropping) dropped.push(trimmed)
|
|
411
|
+
}
|
|
412
|
+
if (!dropping) kept.push(line)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (dropped.length === 0) return { text, dropped }
|
|
416
|
+
return { text: kept.join('\n').replace(/\n{3,}/g, '\n\n'), dropped }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function messageOf(cause: unknown): string {
|
|
420
|
+
return cause instanceof Error ? cause.message : String(cause)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function pathExists(path: string): boolean {
|
|
424
|
+
try {
|
|
425
|
+
lstatSync(path)
|
|
426
|
+
return true
|
|
427
|
+
} catch (cause) {
|
|
428
|
+
const code =
|
|
429
|
+
cause !== null && typeof cause === 'object' && 'code' in cause
|
|
430
|
+
? (cause as { readonly code?: unknown }).code
|
|
431
|
+
: undefined
|
|
432
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return false
|
|
433
|
+
throw cause
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function relativeInside(root: string, path: string): string {
|
|
438
|
+
const value = relative(root, path)
|
|
439
|
+
if (value.length === 0 || value === '..' || value.startsWith(`..${sep}`) || isAbsolute(value)) {
|
|
440
|
+
throw new Error(`path escapes its allowed root: ${path}`)
|
|
441
|
+
}
|
|
442
|
+
return value.split(sep).join('/')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function insideHome(home: string, relativePath: string): string {
|
|
446
|
+
if (relativePath.length === 0 || isAbsolute(relativePath)) {
|
|
447
|
+
throw new Error(`expected a non-empty path relative to home: ${relativePath}`)
|
|
448
|
+
}
|
|
449
|
+
const path = resolve(home, relativePath)
|
|
450
|
+
relativeInside(home, path)
|
|
451
|
+
return path
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function fingerprintPath(path: string): string {
|
|
455
|
+
const hash = createHash('sha256')
|
|
456
|
+
|
|
457
|
+
const visit = (current: string, label: string): void => {
|
|
458
|
+
const stat = lstatSync(current)
|
|
459
|
+
if (stat.isSymbolicLink()) {
|
|
460
|
+
hash.update(`link\0${label}\0${readlinkSync(current)}\0`)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
if (stat.isDirectory()) {
|
|
464
|
+
hash.update(`dir\0${label}\0`)
|
|
465
|
+
for (const name of readdirSync(current).sort()) visit(join(current, name), `${label}/${name}`)
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
if (stat.isFile()) {
|
|
469
|
+
hash.update(`file\0${label}\0${stat.mode & 0o777}\0`)
|
|
470
|
+
hash.update(readFileSync(current))
|
|
471
|
+
hash.update('\0')
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
hash.update(`other\0${label}\0${stat.mode}\0`)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
visit(path, '.')
|
|
478
|
+
return `sha256:${hash.digest('hex')}`
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function jsonPointerPart(value: string): string {
|
|
482
|
+
return value.replaceAll('~', '~0').replaceAll('/', '~1')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function assertRelativeProtectionPath(value: string, label: string): void {
|
|
486
|
+
if (
|
|
487
|
+
value.length === 0 ||
|
|
488
|
+
isAbsolute(value) ||
|
|
489
|
+
value === '..' ||
|
|
490
|
+
value.startsWith('../') ||
|
|
491
|
+
value.includes('/../') ||
|
|
492
|
+
value.endsWith('/..')
|
|
493
|
+
) {
|
|
494
|
+
throw new Error(`${label} must be a path relative to home: ${value}`)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function decodeProtectionList(value: unknown): ProtectionList {
|
|
499
|
+
if (
|
|
500
|
+
!isRecord(value) ||
|
|
501
|
+
value.schema !== 1 ||
|
|
502
|
+
!Array.isArray(value.paths) ||
|
|
503
|
+
!Array.isArray(value.json_entries) ||
|
|
504
|
+
!Array.isArray(value.toml_tables)
|
|
505
|
+
) {
|
|
506
|
+
throw new Error('protection list must have schema 1 and paths, json_entries, and toml_tables arrays')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const paths = value.paths.map((raw) => {
|
|
510
|
+
if (
|
|
511
|
+
!isRecord(raw) ||
|
|
512
|
+
typeof raw.path !== 'string' ||
|
|
513
|
+
typeof raw.reason !== 'string' ||
|
|
514
|
+
raw.reason.length === 0
|
|
515
|
+
) {
|
|
516
|
+
throw new Error('protection list contains an invalid path entry')
|
|
517
|
+
}
|
|
518
|
+
assertRelativeProtectionPath(raw.path, 'protected path')
|
|
519
|
+
if (raw.sha256 !== undefined || raw.mutable_file !== undefined) {
|
|
520
|
+
throw new Error('protected paths are identity-only and must not contain fingerprints')
|
|
521
|
+
}
|
|
522
|
+
return { path: raw.path, reason: raw.reason }
|
|
523
|
+
})
|
|
524
|
+
const jsonEntries = value.json_entries.map((raw) => {
|
|
525
|
+
if (
|
|
526
|
+
!isRecord(raw) ||
|
|
527
|
+
typeof raw.target !== 'string' ||
|
|
528
|
+
typeof raw.selector !== 'string' ||
|
|
529
|
+
!raw.selector.startsWith('/') ||
|
|
530
|
+
typeof raw.reason !== 'string' ||
|
|
531
|
+
raw.reason.length === 0
|
|
532
|
+
) {
|
|
533
|
+
throw new Error('protection list contains an invalid JSON entry')
|
|
534
|
+
}
|
|
535
|
+
assertRelativeProtectionPath(raw.target, 'protected JSON target')
|
|
536
|
+
if (raw.sha256 !== undefined) {
|
|
537
|
+
throw new Error('protected JSON entries are identity-only and must not contain fingerprints')
|
|
538
|
+
}
|
|
539
|
+
return { target: raw.target, selector: raw.selector, reason: raw.reason }
|
|
540
|
+
})
|
|
541
|
+
const tomlTables = value.toml_tables.map((raw) => {
|
|
542
|
+
if (
|
|
543
|
+
!isRecord(raw) ||
|
|
544
|
+
typeof raw.target !== 'string' ||
|
|
545
|
+
typeof raw.header !== 'string' ||
|
|
546
|
+
!raw.header.startsWith('[') ||
|
|
547
|
+
typeof raw.reason !== 'string' ||
|
|
548
|
+
raw.reason.length === 0
|
|
549
|
+
) {
|
|
550
|
+
throw new Error('protection list contains an invalid TOML table entry')
|
|
551
|
+
}
|
|
552
|
+
assertRelativeProtectionPath(raw.target, 'protected TOML target')
|
|
553
|
+
if (raw.sha256 !== undefined) {
|
|
554
|
+
throw new Error('protected TOML tables are identity-only and must not contain fingerprints')
|
|
555
|
+
}
|
|
556
|
+
return { target: raw.target, header: raw.header, reason: raw.reason }
|
|
557
|
+
})
|
|
558
|
+
if (value.text_blocks !== undefined && !Array.isArray(value.text_blocks)) {
|
|
559
|
+
throw new Error('protection list text_blocks must be an array')
|
|
560
|
+
}
|
|
561
|
+
const textBlocks = ((value.text_blocks ?? []) as unknown[]).map((raw) => {
|
|
562
|
+
if (
|
|
563
|
+
!isRecord(raw) ||
|
|
564
|
+
typeof raw.target !== 'string' ||
|
|
565
|
+
typeof raw.name !== 'string' ||
|
|
566
|
+
raw.name.length === 0 ||
|
|
567
|
+
typeof raw.reason !== 'string' ||
|
|
568
|
+
raw.reason.length === 0
|
|
569
|
+
) {
|
|
570
|
+
throw new Error('protection list contains an invalid text block entry')
|
|
571
|
+
}
|
|
572
|
+
assertRelativeProtectionPath(raw.target, 'protected text block target')
|
|
573
|
+
return { target: raw.target, name: raw.name, reason: raw.reason }
|
|
574
|
+
})
|
|
575
|
+
if (value.root_keys !== undefined && !Array.isArray(value.root_keys)) {
|
|
576
|
+
throw new Error('protection list root_keys must be an array')
|
|
577
|
+
}
|
|
578
|
+
const rootKeys = ((value.root_keys ?? []) as unknown[]).map((raw) => {
|
|
579
|
+
if (
|
|
580
|
+
!isRecord(raw) ||
|
|
581
|
+
typeof raw.target !== 'string' ||
|
|
582
|
+
!Array.isArray(raw.keys) ||
|
|
583
|
+
raw.keys.length === 0 ||
|
|
584
|
+
!raw.keys.every((key) => typeof key === 'string' && key.length > 0) ||
|
|
585
|
+
new Set(raw.keys).size !== raw.keys.length ||
|
|
586
|
+
typeof raw.reason !== 'string' ||
|
|
587
|
+
raw.reason.length === 0
|
|
588
|
+
) {
|
|
589
|
+
throw new Error('protection list contains an invalid root key baseline')
|
|
590
|
+
}
|
|
591
|
+
assertRelativeProtectionPath(raw.target, 'root key baseline target')
|
|
592
|
+
return { target: raw.target, keys: raw.keys as string[], reason: raw.reason }
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const identities = [
|
|
596
|
+
...paths.map((entry) => `path:${entry.path}`),
|
|
597
|
+
...jsonEntries.map((entry) => `json:${entry.target}#${entry.selector}`),
|
|
598
|
+
...tomlTables.map((entry) => `toml:${entry.target}#${entry.header}`),
|
|
599
|
+
...textBlocks.map((entry) => `block:${entry.target}#${entry.name}`),
|
|
600
|
+
...rootKeys.map((entry) => `root-keys:${entry.target}`),
|
|
601
|
+
]
|
|
602
|
+
if (new Set(identities).size !== identities.length) throw new Error('protection list contains duplicate entries')
|
|
603
|
+
|
|
604
|
+
return { schema: 1, paths, jsonEntries, tomlTables, textBlocks, rootKeys }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The checked-in protection list plus every LaunchAgent plist enact-vps
|
|
609
|
+
* itself ships (declared under `launch_agents` in fleet/manifest.json).
|
|
610
|
+
* Shipped-job protection is derived from the manifest, never hand-listed.
|
|
611
|
+
*/
|
|
612
|
+
export function loadProtectionList(path = DEFAULT_PROTECTION_PATH, manifestPath = DEFAULT_MANIFEST_PATH): ProtectionList {
|
|
613
|
+
const list = decodeProtectionList(JSON.parse(readFileSync(path, 'utf8')))
|
|
614
|
+
const declared = declaredLaunchAgentPlists(JSON.parse(readFileSync(manifestPath, 'utf8')))
|
|
615
|
+
const listed = new Set(list.paths.map((entry) => entry.path))
|
|
616
|
+
const derived = declared
|
|
617
|
+
.filter((entry) => !listed.has(entry.path))
|
|
618
|
+
.map((entry) => ({ path: entry.path, reason: `LaunchAgent shipped by enact-vps (fleet/manifest.json ${entry.declaredAt})` }))
|
|
619
|
+
return { ...list, paths: [...list.paths, ...derived] }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function tomlHeaderName(header: string): string {
|
|
623
|
+
const trimmed = header.trim()
|
|
624
|
+
return trimmed.slice(trimmed.startsWith('[[') ? 2 : 1).replace(/\]+$/, '')
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function fingerprintBytes(value: string): string {
|
|
628
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function fingerprintJsonValue(value: unknown): string {
|
|
632
|
+
return fingerprintBytes(JSON.stringify(value))
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Orca (2026-09-27 decision: "keep the orca hooks, let orca manage it, Orca
|
|
637
|
+
* is allowed for adl"; the MANAGED layer calls Orca, the managed-hooks-only
|
|
638
|
+
* lock stays on) installs its OWN user-level hooks on every hook-capable
|
|
639
|
+
* surface: Claude `~/.claude/settings.json` (`hooks` + `statusLine`), Codex
|
|
640
|
+
* `~/.codex/hooks.json`, Kimi `~/.kimi-code/config.toml` (`[[hooks]]`), and
|
|
641
|
+
* Cursor `~/.cursor/hooks.json`. Those entries must never be swept as
|
|
642
|
+
* "third-party wiring that should have been repo-local" the way a genuine
|
|
643
|
+
* foreign tool's hook is.
|
|
644
|
+
*
|
|
645
|
+
* Identity is by EXACT reference to the real install path, ground-truthed
|
|
646
|
+
* against this Mac's own live files (`~/.claude/settings.json`,
|
|
647
|
+
* `~/.codex/hooks.json`, `~/.cursor/hooks.json`, `~/.kimi-code/config.toml`,
|
|
648
|
+
* fetched 2026-09-27): Orca renders the path as `${HOME-}/.orca/agent-hooks/`
|
|
649
|
+
* (Claude), or resolves it to an absolute `<home>/.orca/agent-hooks/` at
|
|
650
|
+
* install time (Codex, Cursor, Kimi) — never a fixed literal across
|
|
651
|
+
* machines. The one substring every real form shares is the literal path
|
|
652
|
+
* segment `.orca/agent-hooks/`, so that is the marker, not a loose token
|
|
653
|
+
* match on "orca" alone: `/opt/orca/kimi-hook.sh` (a look-alike that merely
|
|
654
|
+
* mentions "orca" — see `third-party-wiring.test.ts`'s `KIMI_ORCA_HOOK`
|
|
655
|
+
* fixture) does NOT contain this segment and stays a genuine sweep
|
|
656
|
+
* candidate.
|
|
657
|
+
*/
|
|
658
|
+
const ORCA_HOOK_REFERENCE_MARKER = '.orca/agent-hooks/'
|
|
659
|
+
|
|
660
|
+
const ORCA_HOOK_PROTECTION_REASON =
|
|
661
|
+
"Orca's own user-level agent hook (exact reference to .orca/agent-hooks/); Orca is allowed for adl (Tarun, 2026-09-27) — the managed layer calls it, this user-level copy is not swept"
|
|
662
|
+
|
|
663
|
+
/** Recursively true iff any string reachable from `value` contains the exact Orca hook path segment. */
|
|
664
|
+
function containsOrcaHookReference(value: unknown): boolean {
|
|
665
|
+
if (typeof value === 'string') return value.includes(ORCA_HOOK_REFERENCE_MARKER)
|
|
666
|
+
if (Array.isArray(value)) return value.some(containsOrcaHookReference)
|
|
667
|
+
if (isRecord(value)) return Object.values(value).some(containsOrcaHookReference)
|
|
668
|
+
return false
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Same as `protectionFor`, but checks the live Orca-content identity first, ahead of the static, selector-based `protection` list. */
|
|
672
|
+
function protectionForValue(removal: RemovalItem, value: unknown, protection: ProtectionList): string | null {
|
|
673
|
+
if (containsOrcaHookReference(value)) return ORCA_HOOK_PROTECTION_REASON
|
|
674
|
+
return protectionFor(removal, protection)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function protectionFor(
|
|
678
|
+
removal: RemovalItem,
|
|
679
|
+
protection: ProtectionList,
|
|
680
|
+
): string | null {
|
|
681
|
+
if (removal.kind === 'path') {
|
|
682
|
+
const match = protection.paths.find((entry) => removal.target === entry.path)
|
|
683
|
+
return match?.reason ?? null
|
|
684
|
+
}
|
|
685
|
+
if (removal.kind === 'json-entry') {
|
|
686
|
+
const match = protection.jsonEntries.find(
|
|
687
|
+
(entry) => entry.target === removal.target && entry.selector === removal.selector,
|
|
688
|
+
)
|
|
689
|
+
return match?.reason ?? null
|
|
690
|
+
}
|
|
691
|
+
if (removal.kind === 'text-block') {
|
|
692
|
+
const match = protection.textBlocks.find((entry) => entry.target === removal.target && entry.name === removal.name)
|
|
693
|
+
return match?.reason ?? null
|
|
694
|
+
}
|
|
695
|
+
if (removal.kind === 'toml-key') return null
|
|
696
|
+
const match = protection.tomlTables.find(
|
|
697
|
+
(entry) => entry.target === removal.target && entry.header === removal.header,
|
|
698
|
+
)
|
|
699
|
+
return match?.reason ?? null
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function valueAtPath(document: Record<string, unknown>, path: readonly string[]): unknown {
|
|
703
|
+
let current: unknown = document
|
|
704
|
+
for (const part of path) {
|
|
705
|
+
if (!isRecord(current) || !Object.hasOwn(current, part)) return undefined
|
|
706
|
+
current = current[part]
|
|
707
|
+
}
|
|
708
|
+
return current
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function withValueAtPath(
|
|
712
|
+
document: Record<string, unknown>,
|
|
713
|
+
path: readonly string[],
|
|
714
|
+
value: unknown,
|
|
715
|
+
): Record<string, unknown> {
|
|
716
|
+
const [head, ...rest] = path
|
|
717
|
+
if (head === undefined) throw new Error('empty JSON key path')
|
|
718
|
+
if (rest.length === 0) return { ...document, [head]: value }
|
|
719
|
+
const child = document[head]
|
|
720
|
+
if (!isRecord(child)) throw new Error(`JSON key path is not an object: ${path.join('/')}`)
|
|
721
|
+
return { ...document, [head]: withValueAtPath(child, rest, value) }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function filterJsonMap(
|
|
725
|
+
document: unknown,
|
|
726
|
+
key: string,
|
|
727
|
+
target: string,
|
|
728
|
+
protection: ProtectionList,
|
|
729
|
+
): {
|
|
730
|
+
readonly document: unknown
|
|
731
|
+
readonly removals: readonly RemovalItem[]
|
|
732
|
+
readonly protected: readonly ProtectedItem[]
|
|
733
|
+
} | null {
|
|
734
|
+
if (!isRecord(document)) return null
|
|
735
|
+
const path = key.split('/')
|
|
736
|
+
const value = valueAtPath(document, path)
|
|
737
|
+
if (value === null || typeof value !== 'object') return null
|
|
738
|
+
const pointer = path.map(jsonPointerPart).join('/')
|
|
739
|
+
|
|
740
|
+
const removals: RemovalItem[] = []
|
|
741
|
+
const protectedItems: ProtectedItem[] = []
|
|
742
|
+
if (Array.isArray(value)) {
|
|
743
|
+
const kept: unknown[] = []
|
|
744
|
+
for (const [index, entry] of value.entries()) {
|
|
745
|
+
const removal: RemovalItem = {
|
|
746
|
+
kind: 'json-entry',
|
|
747
|
+
target,
|
|
748
|
+
selector: `/${pointer}/${index}`,
|
|
749
|
+
beforeSha256: fingerprintJsonValue(entry),
|
|
750
|
+
}
|
|
751
|
+
const reason = protectionForValue(removal, entry, protection)
|
|
752
|
+
if (reason === null) removals.push(removal)
|
|
753
|
+
else {
|
|
754
|
+
kept.push(entry)
|
|
755
|
+
protectedItems.push({ removal, reason })
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (removals.length === 0 && protectedItems.length === 0) return null
|
|
759
|
+
return { document: removals.length === 0 ? document : withValueAtPath(document, path, kept), removals, protected: protectedItems }
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const kept: Record<string, unknown> = {}
|
|
763
|
+
for (const [name, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
764
|
+
const removal: RemovalItem = {
|
|
765
|
+
kind: 'json-entry',
|
|
766
|
+
target,
|
|
767
|
+
selector: `/${pointer}/${jsonPointerPart(name)}`,
|
|
768
|
+
beforeSha256: fingerprintJsonValue(entry),
|
|
769
|
+
}
|
|
770
|
+
const reason = protectionForValue(removal, entry, protection)
|
|
771
|
+
if (reason === null) removals.push(removal)
|
|
772
|
+
else {
|
|
773
|
+
kept[name] = entry
|
|
774
|
+
protectedItems.push({ removal, reason })
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (removals.length === 0 && protectedItems.length === 0) return null
|
|
778
|
+
return { document: removals.length === 0 ? document : withValueAtPath(document, path, kept), removals, protected: protectedItems }
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* `json-hook-map` filter: `document[key]` is an object keyed by event name,
|
|
783
|
+
* each value an array of hook groups. One candidate per GROUP (selector
|
|
784
|
+
* `/key/eventName/index`), not per event — see the `SweepTarget` union
|
|
785
|
+
* member's own doc comment for why. An event that loses every group loses
|
|
786
|
+
* the key entirely (same convention as `filterJsonMap`'s object branch); a
|
|
787
|
+
* malformed event whose value is not an array falls back to whole-value
|
|
788
|
+
* removal so a hand-edited file never throws instead of sweeping.
|
|
789
|
+
*/
|
|
790
|
+
interface FilteredEntry {
|
|
791
|
+
readonly kept: unknown
|
|
792
|
+
readonly removals: readonly RemovalItem[]
|
|
793
|
+
readonly protected: readonly ProtectedItem[]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** One event's array of hook groups: one candidate per group (selector `.../<index>`). Extracted from `filterJsonHookMap` to keep both functions under the cognitive-complexity limit. */
|
|
797
|
+
function filterHookGroups(groups: readonly unknown[], selectorPrefix: string, target: string, protection: ProtectionList): FilteredEntry {
|
|
798
|
+
const removals: RemovalItem[] = []
|
|
799
|
+
const protectedItems: ProtectedItem[] = []
|
|
800
|
+
const kept: unknown[] = []
|
|
801
|
+
for (const [index, group] of groups.entries()) {
|
|
802
|
+
const removal: RemovalItem = {
|
|
803
|
+
kind: 'json-entry',
|
|
804
|
+
target,
|
|
805
|
+
selector: `${selectorPrefix}/${index}`,
|
|
806
|
+
beforeSha256: fingerprintJsonValue(group),
|
|
807
|
+
}
|
|
808
|
+
const reason = protectionForValue(removal, group, protection)
|
|
809
|
+
if (reason === null) removals.push(removal)
|
|
810
|
+
else {
|
|
811
|
+
kept.push(group)
|
|
812
|
+
protectedItems.push({ removal, reason })
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return { kept, removals, protected: protectedItems }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function filterJsonHookMap(
|
|
819
|
+
document: unknown,
|
|
820
|
+
key: string,
|
|
821
|
+
target: string,
|
|
822
|
+
protection: ProtectionList,
|
|
823
|
+
): {
|
|
824
|
+
readonly document: unknown
|
|
825
|
+
readonly removals: readonly RemovalItem[]
|
|
826
|
+
readonly protected: readonly ProtectedItem[]
|
|
827
|
+
} | null {
|
|
828
|
+
if (!isRecord(document)) return null
|
|
829
|
+
const path = key.split('/')
|
|
830
|
+
const value = valueAtPath(document, path)
|
|
831
|
+
if (!isRecord(value)) return null
|
|
832
|
+
const pointer = path.map(jsonPointerPart).join('/')
|
|
833
|
+
|
|
834
|
+
const removals: RemovalItem[] = []
|
|
835
|
+
const protectedItems: ProtectedItem[] = []
|
|
836
|
+
const kept: Record<string, unknown> = {}
|
|
837
|
+
for (const [eventName, groups] of Object.entries(value)) {
|
|
838
|
+
const selectorPrefix = `/${pointer}/${jsonPointerPart(eventName)}`
|
|
839
|
+
if (!Array.isArray(groups)) {
|
|
840
|
+
const removal: RemovalItem = { kind: 'json-entry', target, selector: selectorPrefix, beforeSha256: fingerprintJsonValue(groups) }
|
|
841
|
+
const reason = protectionForValue(removal, groups, protection)
|
|
842
|
+
if (reason === null) removals.push(removal)
|
|
843
|
+
else {
|
|
844
|
+
kept[eventName] = groups
|
|
845
|
+
protectedItems.push({ removal, reason })
|
|
846
|
+
}
|
|
847
|
+
continue
|
|
848
|
+
}
|
|
849
|
+
const filtered = filterHookGroups(groups, selectorPrefix, target, protection)
|
|
850
|
+
removals.push(...filtered.removals)
|
|
851
|
+
protectedItems.push(...filtered.protected)
|
|
852
|
+
if ((filtered.kept as unknown[]).length > 0) kept[eventName] = filtered.kept
|
|
853
|
+
}
|
|
854
|
+
if (removals.length === 0 && protectedItems.length === 0) return null
|
|
855
|
+
return { document: removals.length === 0 ? document : withValueAtPath(document, path, kept), removals, protected: protectedItems }
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function filterJsonRoot(
|
|
859
|
+
document: unknown,
|
|
860
|
+
target: string,
|
|
861
|
+
protection: ProtectionList,
|
|
862
|
+
): {
|
|
863
|
+
readonly document: unknown
|
|
864
|
+
readonly removals: readonly RemovalItem[]
|
|
865
|
+
readonly protected: readonly ProtectedItem[]
|
|
866
|
+
} | null {
|
|
867
|
+
if (!isRecord(document)) return null
|
|
868
|
+
const kept: Record<string, unknown> = {}
|
|
869
|
+
const removals: RemovalItem[] = []
|
|
870
|
+
const protectedItems: ProtectedItem[] = []
|
|
871
|
+
for (const [name, entry] of Object.entries(document)) {
|
|
872
|
+
const removal: RemovalItem = {
|
|
873
|
+
kind: 'json-entry',
|
|
874
|
+
target,
|
|
875
|
+
selector: `/${jsonPointerPart(name)}`,
|
|
876
|
+
beforeSha256: fingerprintJsonValue(entry),
|
|
877
|
+
}
|
|
878
|
+
const reason = protectionForValue(removal, entry, protection)
|
|
879
|
+
if (reason === null) removals.push(removal)
|
|
880
|
+
else {
|
|
881
|
+
kept[name] = entry
|
|
882
|
+
protectedItems.push({ removal, reason })
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (removals.length === 0 && protectedItems.length === 0) return null
|
|
886
|
+
return { document: removals.length === 0 ? document : kept, removals, protected: protectedItems }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
interface TomlBlock {
|
|
890
|
+
readonly header: string | null
|
|
891
|
+
readonly lines: readonly string[]
|
|
892
|
+
/** True for a region owned by another deployer (kept verbatim, never inspected). */
|
|
893
|
+
readonly preserved: boolean
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function splitTomlBlocks(text: string, preserve?: PreservedRegion): readonly TomlBlock[] {
|
|
897
|
+
const blocks: { header: string | null; lines: string[]; preserved: boolean }[] = []
|
|
898
|
+
let current: { header: string | null; lines: string[]; preserved: boolean } = {
|
|
899
|
+
header: null,
|
|
900
|
+
lines: [],
|
|
901
|
+
preserved: false,
|
|
902
|
+
}
|
|
903
|
+
let inRegion = false
|
|
904
|
+
for (const line of text.split('\n')) {
|
|
905
|
+
const trimmed = line.trim()
|
|
906
|
+
if (inRegion) {
|
|
907
|
+
current.lines.push(line)
|
|
908
|
+
if (trimmed === preserve?.end) {
|
|
909
|
+
inRegion = false
|
|
910
|
+
blocks.push(current)
|
|
911
|
+
current = { header: null, lines: [], preserved: false }
|
|
912
|
+
}
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
if (preserve !== undefined && trimmed === preserve.begin) {
|
|
916
|
+
if (current.lines.length > 0 || current.header !== null) blocks.push(current)
|
|
917
|
+
current = { header: null, lines: [line], preserved: true }
|
|
918
|
+
inRegion = true
|
|
919
|
+
continue
|
|
920
|
+
}
|
|
921
|
+
if (trimmed.startsWith('[')) {
|
|
922
|
+
if (current.lines.length > 0) blocks.push(current)
|
|
923
|
+
current = { header: trimmed, lines: [line], preserved: false }
|
|
924
|
+
} else current.lines.push(line)
|
|
925
|
+
}
|
|
926
|
+
if (inRegion) throw new Error(`unterminated preserved region: ${preserve?.begin}`)
|
|
927
|
+
blocks.push(current)
|
|
928
|
+
return blocks
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function tomlFirstSegment(header: string): string {
|
|
932
|
+
const name = tomlHeaderName(header).trim()
|
|
933
|
+
const quote = name[0]
|
|
934
|
+
if (quote === '"' || quote === "'") {
|
|
935
|
+
const close = name.indexOf(quote, 1)
|
|
936
|
+
return close === -1 ? name : name.slice(1, close)
|
|
937
|
+
}
|
|
938
|
+
return name.split('.')[0]!.trim()
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
interface TomlRootKey {
|
|
942
|
+
readonly key: string
|
|
943
|
+
/** Line indexes inside the root block, inclusive. */
|
|
944
|
+
readonly start: number
|
|
945
|
+
readonly end: number
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Top-level `key = value` statements before the first table, including multi-line arrays/strings. */
|
|
949
|
+
function tomlRootKeys(lines: readonly string[]): readonly TomlRootKey[] {
|
|
950
|
+
const keys: TomlRootKey[] = []
|
|
951
|
+
let index = 0
|
|
952
|
+
while (index < lines.length) {
|
|
953
|
+
const match = /^\s*("[^"]*"|'[^']*'|[A-Za-z0-9_-]+)\s*(?:\.[^=]*)?=(.*)$/.exec(lines[index]!)
|
|
954
|
+
if (match === null) {
|
|
955
|
+
index += 1
|
|
956
|
+
continue
|
|
957
|
+
}
|
|
958
|
+
const rawKey = match[1]!
|
|
959
|
+
const key = rawKey.startsWith('"') || rawKey.startsWith("'") ? rawKey.slice(1, -1) : rawKey
|
|
960
|
+
const start = index
|
|
961
|
+
let depth = 0
|
|
962
|
+
let multiline: '"""' | "'''" | null = null
|
|
963
|
+
let text = match[2]!
|
|
964
|
+
for (;;) {
|
|
965
|
+
let position = 0
|
|
966
|
+
while (position < text.length) {
|
|
967
|
+
if (multiline !== null) {
|
|
968
|
+
const close = text.indexOf(multiline, position)
|
|
969
|
+
if (close === -1) {
|
|
970
|
+
position = text.length
|
|
971
|
+
break
|
|
972
|
+
}
|
|
973
|
+
position = close + 3
|
|
974
|
+
multiline = null
|
|
975
|
+
continue
|
|
976
|
+
}
|
|
977
|
+
const character = text[position]!
|
|
978
|
+
if (text.startsWith('"""', position) || text.startsWith("'''", position)) {
|
|
979
|
+
multiline = text.slice(position, position + 3) as '"""' | "'''"
|
|
980
|
+
position += 3
|
|
981
|
+
} else if (character === '"' || character === "'") {
|
|
982
|
+
let close = position + 1
|
|
983
|
+
while (close < text.length && text[close] !== character) close += character === '"' && text[close] === '\\' ? 2 : 1
|
|
984
|
+
position = close + 1
|
|
985
|
+
} else if (character === '#') {
|
|
986
|
+
break
|
|
987
|
+
} else {
|
|
988
|
+
if (character === '[' || character === '{') depth += 1
|
|
989
|
+
if (character === ']' || character === '}') depth -= 1
|
|
990
|
+
position += 1
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if ((depth <= 0 && multiline === null) || index + 1 >= lines.length) break
|
|
994
|
+
index += 1
|
|
995
|
+
text = lines[index]!
|
|
996
|
+
}
|
|
997
|
+
keys.push({ key, start, end: index })
|
|
998
|
+
index += 1
|
|
999
|
+
}
|
|
1000
|
+
return keys
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export interface TextBlock {
|
|
1004
|
+
readonly name: string
|
|
1005
|
+
/** Line indexes, inclusive. */
|
|
1006
|
+
readonly start: number
|
|
1007
|
+
readonly end: number
|
|
1008
|
+
readonly text: string
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const TEXT_BLOCK_SYNTAXES: readonly { readonly open: RegExp; readonly close: (name: string) => string }[] = [
|
|
1012
|
+
{ open: /^# >>> (.+?) >>>$/, close: (name) => `# <<< ${name} <<<` },
|
|
1013
|
+
{ open: /^# (.+?) — begin$/, close: (name) => `# ${name} — end` },
|
|
1014
|
+
{ open: /^<!-- (?!\/)(.+?) -->$/, close: (name) => `<!-- /${name} -->` },
|
|
1015
|
+
]
|
|
1016
|
+
|
|
1017
|
+
/** Outermost tool-injected marker-delimited regions. Unterminated openers are ordinary lines. */
|
|
1018
|
+
export function findTextBlocks(text: string): readonly TextBlock[] {
|
|
1019
|
+
const lines = text.split('\n')
|
|
1020
|
+
const blocks: TextBlock[] = []
|
|
1021
|
+
let index = 0
|
|
1022
|
+
while (index < lines.length) {
|
|
1023
|
+
const trimmed = lines[index]!.trim()
|
|
1024
|
+
let found: TextBlock | null = null
|
|
1025
|
+
for (const syntax of TEXT_BLOCK_SYNTAXES) {
|
|
1026
|
+
const match = syntax.open.exec(trimmed)
|
|
1027
|
+
if (match === null) continue
|
|
1028
|
+
const name = match[1]!
|
|
1029
|
+
const closer = syntax.close(name)
|
|
1030
|
+
const end = lines.findIndex((line, candidate) => candidate > index && line.trim() === closer)
|
|
1031
|
+
if (end === -1) continue
|
|
1032
|
+
found = { name, start: index, end, text: lines.slice(index, end + 1).join('\n') }
|
|
1033
|
+
break
|
|
1034
|
+
}
|
|
1035
|
+
if (found === null) {
|
|
1036
|
+
index += 1
|
|
1037
|
+
continue
|
|
1038
|
+
}
|
|
1039
|
+
blocks.push(found)
|
|
1040
|
+
index = found.end + 1
|
|
1041
|
+
}
|
|
1042
|
+
return blocks
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function filterTextBlocks(
|
|
1046
|
+
text: string,
|
|
1047
|
+
target: string,
|
|
1048
|
+
protection: ProtectionList,
|
|
1049
|
+
): { readonly text: string; readonly removals: readonly RemovalItem[]; readonly protected: readonly ProtectedItem[] } {
|
|
1050
|
+
const lines = text.split('\n')
|
|
1051
|
+
const drop = new Set<number>()
|
|
1052
|
+
const removals: RemovalItem[] = []
|
|
1053
|
+
const protectedItems: ProtectedItem[] = []
|
|
1054
|
+
for (const block of findTextBlocks(text)) {
|
|
1055
|
+
const removal: RemovalItem = { kind: 'text-block', target, name: block.name, beforeSha256: fingerprintBytes(block.text) }
|
|
1056
|
+
const reason = protectionFor(removal, protection)
|
|
1057
|
+
if (reason !== null) {
|
|
1058
|
+
protectedItems.push({ removal, reason })
|
|
1059
|
+
continue
|
|
1060
|
+
}
|
|
1061
|
+
removals.push(removal)
|
|
1062
|
+
for (let line = block.start; line <= block.end; line += 1) drop.add(line)
|
|
1063
|
+
let previous = block.start - 1
|
|
1064
|
+
while (previous >= 0 && drop.has(previous)) previous -= 1
|
|
1065
|
+
const next = block.end + 1
|
|
1066
|
+
if (next < lines.length && lines[next]!.trim() === '' && (previous < 0 || lines[previous]!.trim() === '')) {
|
|
1067
|
+
drop.add(next)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (removals.length === 0) return { text, removals, protected: protectedItems }
|
|
1071
|
+
return { text: lines.filter((_, index) => !drop.has(index)).join('\n'), removals, protected: protectedItems }
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function filterJsonRootKeys(
|
|
1075
|
+
document: unknown,
|
|
1076
|
+
target: string,
|
|
1077
|
+
allowed: readonly string[],
|
|
1078
|
+
protection: ProtectionList,
|
|
1079
|
+
): { readonly document: unknown; readonly removals: readonly RemovalItem[] } | null {
|
|
1080
|
+
if (!isRecord(document)) return null
|
|
1081
|
+
const kept: Record<string, unknown> = {}
|
|
1082
|
+
const removals: RemovalItem[] = []
|
|
1083
|
+
for (const [name, entry] of Object.entries(document)) {
|
|
1084
|
+
const removal: RemovalItem = {
|
|
1085
|
+
kind: 'json-entry',
|
|
1086
|
+
target,
|
|
1087
|
+
selector: `/${jsonPointerPart(name)}`,
|
|
1088
|
+
beforeSha256: fingerprintJsonValue(entry),
|
|
1089
|
+
}
|
|
1090
|
+
if (allowed.includes(name) || protectionForValue(removal, entry, protection) !== null) kept[name] = entry
|
|
1091
|
+
else removals.push(removal)
|
|
1092
|
+
}
|
|
1093
|
+
if (removals.length === 0) return null
|
|
1094
|
+
return { document: kept, removals }
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function filterTomlRootKeys(
|
|
1098
|
+
text: string,
|
|
1099
|
+
target: string,
|
|
1100
|
+
allowed: readonly string[],
|
|
1101
|
+
protection: ProtectionList,
|
|
1102
|
+
): { readonly text: string; readonly removals: readonly RemovalItem[] } {
|
|
1103
|
+
const blocks = splitTomlBlocks(text, preservedRegionFor(target))
|
|
1104
|
+
const kept: string[][] = []
|
|
1105
|
+
const removals: RemovalItem[] = []
|
|
1106
|
+
for (const [position, block] of blocks.entries()) {
|
|
1107
|
+
if (block.preserved) {
|
|
1108
|
+
kept.push([...block.lines])
|
|
1109
|
+
continue
|
|
1110
|
+
}
|
|
1111
|
+
if (block.header === null) {
|
|
1112
|
+
if (position !== 0) {
|
|
1113
|
+
kept.push([...block.lines])
|
|
1114
|
+
continue
|
|
1115
|
+
}
|
|
1116
|
+
const drop = new Set<number>()
|
|
1117
|
+
for (const rootKey of tomlRootKeys(block.lines)) {
|
|
1118
|
+
if (allowed.includes(rootKey.key)) continue
|
|
1119
|
+
const removed = block.lines.slice(rootKey.start, rootKey.end + 1)
|
|
1120
|
+
removals.push({ kind: 'toml-key', target, key: rootKey.key, beforeSha256: fingerprintBytes(removed.join('\n')) })
|
|
1121
|
+
for (let line = rootKey.start; line <= rootKey.end; line += 1) drop.add(line)
|
|
1122
|
+
}
|
|
1123
|
+
kept.push(block.lines.filter((_, index) => !drop.has(index)))
|
|
1124
|
+
continue
|
|
1125
|
+
}
|
|
1126
|
+
const removal: RemovalItem = {
|
|
1127
|
+
kind: 'toml-table',
|
|
1128
|
+
target,
|
|
1129
|
+
header: block.header,
|
|
1130
|
+
beforeSha256: fingerprintBytes(block.lines.join('\n')),
|
|
1131
|
+
}
|
|
1132
|
+
if (
|
|
1133
|
+
allowed.includes(tomlFirstSegment(block.header)) ||
|
|
1134
|
+
protectionForValue(removal, block.lines.join('\n'), protection) !== null
|
|
1135
|
+
) {
|
|
1136
|
+
kept.push([...block.lines])
|
|
1137
|
+
} else removals.push(removal)
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
text: removals.length === 0 ? text : kept.flat().join('\n').replace(/\n{3,}/g, '\n\n'),
|
|
1141
|
+
removals,
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function filterTomlTables(
|
|
1146
|
+
text: string,
|
|
1147
|
+
prefixes: readonly string[],
|
|
1148
|
+
target: string,
|
|
1149
|
+
protection: ProtectionList,
|
|
1150
|
+
): { readonly text: string; readonly removals: readonly RemovalItem[]; readonly protected: readonly ProtectedItem[] } {
|
|
1151
|
+
const blocks = splitTomlBlocks(text, preservedRegionFor(target))
|
|
1152
|
+
const kept: string[][] = []
|
|
1153
|
+
const removals: RemovalItem[] = []
|
|
1154
|
+
const protectedItems: ProtectedItem[] = []
|
|
1155
|
+
for (const block of blocks) {
|
|
1156
|
+
if (block.header === null || block.preserved) {
|
|
1157
|
+
kept.push([...block.lines])
|
|
1158
|
+
continue
|
|
1159
|
+
}
|
|
1160
|
+
const name = tomlHeaderName(block.header)
|
|
1161
|
+
const selected = prefixes.some((prefix) => name === prefix || name.startsWith(`${prefix}.`))
|
|
1162
|
+
if (!selected) {
|
|
1163
|
+
kept.push([...block.lines])
|
|
1164
|
+
continue
|
|
1165
|
+
}
|
|
1166
|
+
const removal: RemovalItem = {
|
|
1167
|
+
kind: 'toml-table',
|
|
1168
|
+
target,
|
|
1169
|
+
header: block.header,
|
|
1170
|
+
beforeSha256: fingerprintBytes(block.lines.join('\n')),
|
|
1171
|
+
}
|
|
1172
|
+
const reason = protectionForValue(removal, block.lines.join('\n'), protection)
|
|
1173
|
+
if (reason === null) removals.push(removal)
|
|
1174
|
+
else {
|
|
1175
|
+
kept.push([...block.lines])
|
|
1176
|
+
protectedItems.push({ removal, reason })
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return {
|
|
1181
|
+
text: removals.length === 0 ? text : kept.flat().join('\n').replace(/\n{3,}/g, '\n\n'),
|
|
1182
|
+
removals,
|
|
1183
|
+
protected: protectedItems,
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function entriesOf(dir: string, notes: SweepNote[]): readonly string[] {
|
|
1188
|
+
if (!pathExists(dir)) return []
|
|
1189
|
+
try {
|
|
1190
|
+
if (!lstatSync(dir).isDirectory()) {
|
|
1191
|
+
notes.push({ level: 'warn', text: `${dir} is not a directory; left untouched` })
|
|
1192
|
+
return []
|
|
1193
|
+
}
|
|
1194
|
+
return readdirSync(dir).filter((name) => name !== '.DS_Store')
|
|
1195
|
+
} catch (cause) {
|
|
1196
|
+
notes.push({ level: 'warn', text: `${dir} could not be read; left untouched (${messageOf(cause)})` })
|
|
1197
|
+
return []
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** True iff `marker` appears as an exact, standalone line within the first 5 lines of `content`. */
|
|
1202
|
+
function hasMarkerLine(content: string, marker: string): boolean {
|
|
1203
|
+
return content.split('\n').slice(0, 5).includes(marker)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function launchAgentInScope(path: string, name: string): boolean {
|
|
1207
|
+
const stat = lstatSync(path)
|
|
1208
|
+
return isAgentToolingLaunchAgent(name, stat.isFile() ? readFileSync(path).toString('latin1') : '')
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function editableFile(
|
|
1212
|
+
home: string,
|
|
1213
|
+
relativePath: string,
|
|
1214
|
+
drafts: Map<string, DraftFile>,
|
|
1215
|
+
notes: SweepNote[],
|
|
1216
|
+
): DraftFile | null {
|
|
1217
|
+
const path = insideHome(home, relativePath)
|
|
1218
|
+
const existing = drafts.get(path)
|
|
1219
|
+
if (existing !== undefined) return existing
|
|
1220
|
+
if (!pathExists(path)) return null
|
|
1221
|
+
|
|
1222
|
+
const stat = lstatSync(path)
|
|
1223
|
+
if (!stat.isFile()) {
|
|
1224
|
+
notes.push({ level: 'warn', text: `${path} is not a regular file; left untouched` })
|
|
1225
|
+
return null
|
|
1226
|
+
}
|
|
1227
|
+
const content = readFileSync(path, 'utf8')
|
|
1228
|
+
const draft: DraftFile = {
|
|
1229
|
+
path,
|
|
1230
|
+
relativePath,
|
|
1231
|
+
original: content,
|
|
1232
|
+
content,
|
|
1233
|
+
mode: stat.mode & 0o777,
|
|
1234
|
+
uid: stat.uid,
|
|
1235
|
+
gid: stat.gid,
|
|
1236
|
+
removals: [],
|
|
1237
|
+
}
|
|
1238
|
+
drafts.set(path, draft)
|
|
1239
|
+
return draft
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export function planGlobalSweep(
|
|
1243
|
+
homeInput: string,
|
|
1244
|
+
protection: ProtectionList = loadProtectionList(),
|
|
1245
|
+
): SweepPlan {
|
|
1246
|
+
if (!isAbsolute(homeInput)) throw new Error(`--home must be absolute: ${homeInput}`)
|
|
1247
|
+
const home = resolve(homeInput)
|
|
1248
|
+
const drafts = new Map<string, DraftFile>()
|
|
1249
|
+
const removals = new Map<string, SweepOperation>()
|
|
1250
|
+
const protectedItems: ProtectedItem[] = []
|
|
1251
|
+
const notes: SweepNote[] = []
|
|
1252
|
+
|
|
1253
|
+
// Declared shipped baselines run first, so a wholly foreign container key is
|
|
1254
|
+
// reported once as a root removal instead of entry-by-entry.
|
|
1255
|
+
for (const baseline of protection.rootKeys) {
|
|
1256
|
+
if (!managedFileTarget(baseline.target)) {
|
|
1257
|
+
throw new Error(`root key baseline targets an unmanaged file: ${baseline.target}`)
|
|
1258
|
+
}
|
|
1259
|
+
const draft = editableFile(home, baseline.target, drafts, notes)
|
|
1260
|
+
if (draft === null) continue
|
|
1261
|
+
if (baseline.target.endsWith('.toml')) {
|
|
1262
|
+
const result = filterTomlRootKeys(draft.content, baseline.target, baseline.keys, protection)
|
|
1263
|
+
if (result.removals.length === 0) continue
|
|
1264
|
+
draft.content = result.text
|
|
1265
|
+
draft.removals.push(...result.removals)
|
|
1266
|
+
continue
|
|
1267
|
+
}
|
|
1268
|
+
let parsed: unknown
|
|
1269
|
+
try {
|
|
1270
|
+
parsed = JSON.parse(draft.content)
|
|
1271
|
+
} catch {
|
|
1272
|
+
notes.push({ level: 'warn', text: `${draft.path} is not valid JSON; left untouched` })
|
|
1273
|
+
continue
|
|
1274
|
+
}
|
|
1275
|
+
const result = filterJsonRootKeys(parsed, baseline.target, baseline.keys, protection)
|
|
1276
|
+
if (result === null) continue
|
|
1277
|
+
draft.content = `${JSON.stringify(result.document, null, 2)}\n`
|
|
1278
|
+
draft.removals.push(...result.removals)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
for (const target of SWEEP_TARGETS) {
|
|
1282
|
+
if (target.kind === 'text-blocks') {
|
|
1283
|
+
const draft = editableFile(home, target.file, drafts, notes)
|
|
1284
|
+
if (draft === null) continue
|
|
1285
|
+
const result = filterTextBlocks(draft.content, target.file, protection)
|
|
1286
|
+
protectedItems.push(...result.protected)
|
|
1287
|
+
if (result.removals.length === 0) continue
|
|
1288
|
+
draft.content = result.text
|
|
1289
|
+
draft.removals.push(...result.removals)
|
|
1290
|
+
continue
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (target.kind === 'json-map' || target.kind === 'json-hook-map' || target.kind === 'json-root') {
|
|
1294
|
+
const draft = editableFile(home, target.file, drafts, notes)
|
|
1295
|
+
if (draft === null) continue
|
|
1296
|
+
let parsed: unknown
|
|
1297
|
+
try {
|
|
1298
|
+
parsed = JSON.parse(draft.content)
|
|
1299
|
+
} catch {
|
|
1300
|
+
notes.push({ level: 'warn', text: `${draft.path} is not valid JSON; left untouched` })
|
|
1301
|
+
continue
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
if (target.kind === 'json-map') {
|
|
1305
|
+
const result = filterJsonMap(parsed, target.key, target.file, protection)
|
|
1306
|
+
if (result === null) continue
|
|
1307
|
+
protectedItems.push(...result.protected)
|
|
1308
|
+
if (result.removals.length === 0) continue
|
|
1309
|
+
draft.content = `${JSON.stringify(result.document, null, 2)}\n`
|
|
1310
|
+
draft.removals.push(...result.removals)
|
|
1311
|
+
} else if (target.kind === 'json-hook-map') {
|
|
1312
|
+
const result = filterJsonHookMap(parsed, target.key, target.file, protection)
|
|
1313
|
+
if (result === null) continue
|
|
1314
|
+
protectedItems.push(...result.protected)
|
|
1315
|
+
if (result.removals.length === 0) continue
|
|
1316
|
+
draft.content = `${JSON.stringify(result.document, null, 2)}\n`
|
|
1317
|
+
draft.removals.push(...result.removals)
|
|
1318
|
+
} else {
|
|
1319
|
+
const result = filterJsonRoot(parsed, target.file, protection)
|
|
1320
|
+
if (result === null) continue
|
|
1321
|
+
protectedItems.push(...result.protected)
|
|
1322
|
+
if (result.removals.length === 0) continue
|
|
1323
|
+
draft.content = `${JSON.stringify(result.document, null, 2)}\n`
|
|
1324
|
+
draft.removals.push(...result.removals)
|
|
1325
|
+
}
|
|
1326
|
+
continue
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (target.kind === 'toml-tables') {
|
|
1330
|
+
const draft = editableFile(home, target.file, drafts, notes)
|
|
1331
|
+
if (draft === null) continue
|
|
1332
|
+
const result = filterTomlTables(draft.content, target.prefixes, target.file, protection)
|
|
1333
|
+
protectedItems.push(...result.protected)
|
|
1334
|
+
if (result.removals.length === 0) continue
|
|
1335
|
+
draft.content = result.text
|
|
1336
|
+
draft.removals.push(...result.removals)
|
|
1337
|
+
continue
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (target.kind === 'dir-entries') {
|
|
1341
|
+
const dir = insideHome(home, target.dir)
|
|
1342
|
+
for (const name of entriesOf(dir, notes)) {
|
|
1343
|
+
const path = join(dir, name)
|
|
1344
|
+
if (target.ignore?.includes(name) === true) continue
|
|
1345
|
+
if (target.descend?.includes(name) === true && lstatSync(path).isDirectory()) continue
|
|
1346
|
+
const relativePath = relativeInside(home, path)
|
|
1347
|
+
const beforeSha256 = fingerprintPath(path)
|
|
1348
|
+
const removal: RemovalItem = { kind: 'path', target: relativePath, beforeSha256 }
|
|
1349
|
+
const reason = protectionFor(removal, protection)
|
|
1350
|
+
if (reason !== null) {
|
|
1351
|
+
protectedItems.push({ removal, reason })
|
|
1352
|
+
continue
|
|
1353
|
+
}
|
|
1354
|
+
if (target.dir === LAUNCH_AGENTS_DIR && !launchAgentInScope(path, name)) continue
|
|
1355
|
+
removals.set(relativePath, {
|
|
1356
|
+
id: `remove-path:${relativePath}`,
|
|
1357
|
+
kind: 'remove-path',
|
|
1358
|
+
path: relativePath,
|
|
1359
|
+
absolutePath: path,
|
|
1360
|
+
beforeSha256,
|
|
1361
|
+
removals: [removal],
|
|
1362
|
+
requiresApproval: target.requiresApproval === true,
|
|
1363
|
+
})
|
|
1364
|
+
}
|
|
1365
|
+
continue
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (target.kind === 'marker-files') {
|
|
1369
|
+
const dir = insideHome(home, target.dir)
|
|
1370
|
+
for (const name of entriesOf(dir, notes)) {
|
|
1371
|
+
const path = join(dir, name)
|
|
1372
|
+
if (!lstatSync(path).isFile()) continue
|
|
1373
|
+
if (!hasMarkerLine(readFileSync(path, 'utf8'), target.marker)) continue
|
|
1374
|
+
const relativePath = relativeInside(home, path)
|
|
1375
|
+
const beforeSha256 = fingerprintPath(path)
|
|
1376
|
+
const removal: RemovalItem = { kind: 'path', target: relativePath, beforeSha256 }
|
|
1377
|
+
const reason = protectionFor(removal, protection)
|
|
1378
|
+
if (reason !== null) {
|
|
1379
|
+
protectedItems.push({ removal, reason })
|
|
1380
|
+
continue
|
|
1381
|
+
}
|
|
1382
|
+
removals.set(relativePath, {
|
|
1383
|
+
id: `remove-path:${relativePath}`,
|
|
1384
|
+
kind: 'remove-path',
|
|
1385
|
+
path: relativePath,
|
|
1386
|
+
absolutePath: path,
|
|
1387
|
+
beforeSha256,
|
|
1388
|
+
removals: [removal],
|
|
1389
|
+
requiresApproval: false,
|
|
1390
|
+
})
|
|
1391
|
+
}
|
|
1392
|
+
continue
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const path = insideHome(home, target.path)
|
|
1396
|
+
if (!pathExists(path)) continue
|
|
1397
|
+
const relativePath = relativeInside(home, path)
|
|
1398
|
+
const beforeSha256 = fingerprintPath(path)
|
|
1399
|
+
const removal: RemovalItem = { kind: 'path', target: relativePath, beforeSha256 }
|
|
1400
|
+
const reason = protectionFor(removal, protection)
|
|
1401
|
+
if (reason !== null) {
|
|
1402
|
+
protectedItems.push({ removal, reason })
|
|
1403
|
+
continue
|
|
1404
|
+
}
|
|
1405
|
+
removals.set(relativePath, {
|
|
1406
|
+
id: `remove-path:${relativePath}`,
|
|
1407
|
+
kind: 'remove-path',
|
|
1408
|
+
path: relativePath,
|
|
1409
|
+
absolutePath: path,
|
|
1410
|
+
beforeSha256,
|
|
1411
|
+
removals: [removal],
|
|
1412
|
+
requiresApproval: false,
|
|
1413
|
+
})
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const writes: SweepOperation[] = []
|
|
1417
|
+
for (const draft of drafts.values()) {
|
|
1418
|
+
if (draft.content === draft.original) continue
|
|
1419
|
+
writes.push({
|
|
1420
|
+
id: `write-file:${draft.relativePath}`,
|
|
1421
|
+
kind: 'write-file',
|
|
1422
|
+
path: draft.relativePath,
|
|
1423
|
+
absolutePath: draft.path,
|
|
1424
|
+
beforeSha256: fingerprintPath(draft.path),
|
|
1425
|
+
removals: [...draft.removals].sort((a, b) => describeRemoval(a).localeCompare(describeRemoval(b))),
|
|
1426
|
+
content: draft.content,
|
|
1427
|
+
mode: draft.mode,
|
|
1428
|
+
uid: draft.uid,
|
|
1429
|
+
gid: draft.gid,
|
|
1430
|
+
requiresApproval: false,
|
|
1431
|
+
})
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const operations = [...writes, ...removals.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
1435
|
+
protectedItems.sort((a, b) => describeRemoval(a.removal).localeCompare(describeRemoval(b.removal)))
|
|
1436
|
+
if (operations.length === 0 && notes.length === 0) {
|
|
1437
|
+
notes.push({
|
|
1438
|
+
level: 'ok',
|
|
1439
|
+
text:
|
|
1440
|
+
protectedItems.length === 0
|
|
1441
|
+
? 'global agent surfaces are already clean'
|
|
1442
|
+
: 'no unprotected global agent surface candidates found',
|
|
1443
|
+
})
|
|
1444
|
+
}
|
|
1445
|
+
const uniqueNotes = notes.filter(
|
|
1446
|
+
(note, index) => notes.findIndex((other) => other.level === note.level && other.text === note.text) === index,
|
|
1447
|
+
)
|
|
1448
|
+
return { operations, protected: protectedItems, notes: uniqueNotes }
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function publicOperation(operation: SweepOperation): ApprovedOperation {
|
|
1452
|
+
return {
|
|
1453
|
+
id: operation.id,
|
|
1454
|
+
kind: operation.kind,
|
|
1455
|
+
path: operation.path,
|
|
1456
|
+
beforeSha256: operation.beforeSha256,
|
|
1457
|
+
removals: operation.removals,
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export function createApprovalPlan(
|
|
1462
|
+
home: string,
|
|
1463
|
+
generatedAt = new Date().toISOString(),
|
|
1464
|
+
protection: ProtectionList = loadProtectionList(),
|
|
1465
|
+
): ApprovalPlan {
|
|
1466
|
+
const resolvedHome = resolve(home)
|
|
1467
|
+
return {
|
|
1468
|
+
schema: 1,
|
|
1469
|
+
home: resolvedHome,
|
|
1470
|
+
generatedAt,
|
|
1471
|
+
operations: planGlobalSweep(resolvedHome, protection).operations.map(publicOperation),
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1476
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function decodeRemovalItem(value: unknown): RemovalItem {
|
|
1480
|
+
if (
|
|
1481
|
+
!isRecord(value) ||
|
|
1482
|
+
typeof value.target !== 'string' ||
|
|
1483
|
+
typeof value.beforeSha256 !== 'string' ||
|
|
1484
|
+
!/^sha256:[0-9a-f]{64}$/.test(value.beforeSha256)
|
|
1485
|
+
) {
|
|
1486
|
+
throw new Error('invalid removal item')
|
|
1487
|
+
}
|
|
1488
|
+
if (value.kind === 'path') {
|
|
1489
|
+
return { kind: 'path', target: value.target, beforeSha256: value.beforeSha256 }
|
|
1490
|
+
}
|
|
1491
|
+
if (value.kind === 'json-entry' && typeof value.selector === 'string') {
|
|
1492
|
+
return {
|
|
1493
|
+
kind: 'json-entry',
|
|
1494
|
+
target: value.target,
|
|
1495
|
+
selector: value.selector,
|
|
1496
|
+
beforeSha256: value.beforeSha256,
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (value.kind === 'toml-key' && typeof value.key === 'string') {
|
|
1500
|
+
return { kind: 'toml-key', target: value.target, key: value.key, beforeSha256: value.beforeSha256 }
|
|
1501
|
+
}
|
|
1502
|
+
if (value.kind === 'text-block' && typeof value.name === 'string') {
|
|
1503
|
+
return { kind: 'text-block', target: value.target, name: value.name, beforeSha256: value.beforeSha256 }
|
|
1504
|
+
}
|
|
1505
|
+
if (value.kind === 'toml-table' && typeof value.header === 'string') {
|
|
1506
|
+
return {
|
|
1507
|
+
kind: 'toml-table',
|
|
1508
|
+
target: value.target,
|
|
1509
|
+
header: value.header,
|
|
1510
|
+
beforeSha256: value.beforeSha256,
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
throw new Error('invalid removal item')
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export function decodeApprovalPlan(value: unknown): ApprovalPlan {
|
|
1517
|
+
if (!isRecord(value) || value.schema !== 1 || typeof value.home !== 'string' || !isAbsolute(value.home)) {
|
|
1518
|
+
throw new Error('approval plan must have schema 1 and an absolute home')
|
|
1519
|
+
}
|
|
1520
|
+
if (typeof value.generatedAt !== 'string' || !Array.isArray(value.operations)) {
|
|
1521
|
+
throw new Error('approval plan is missing generatedAt or operations')
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
const operations: ApprovedOperation[] = []
|
|
1525
|
+
const ids = new Set<string>()
|
|
1526
|
+
for (const raw of value.operations) {
|
|
1527
|
+
if (
|
|
1528
|
+
!isRecord(raw) ||
|
|
1529
|
+
(raw.kind !== 'write-file' && raw.kind !== 'remove-path') ||
|
|
1530
|
+
typeof raw.path !== 'string' ||
|
|
1531
|
+
typeof raw.id !== 'string' ||
|
|
1532
|
+
raw.id !== `${raw.kind}:${raw.path}` ||
|
|
1533
|
+
typeof raw.beforeSha256 !== 'string' ||
|
|
1534
|
+
!/^sha256:[0-9a-f]{64}$/.test(raw.beforeSha256) ||
|
|
1535
|
+
!Array.isArray(raw.removals)
|
|
1536
|
+
) {
|
|
1537
|
+
throw new Error('approval plan contains an invalid operation')
|
|
1538
|
+
}
|
|
1539
|
+
if (ids.has(raw.id)) throw new Error(`approval plan contains duplicate operation: ${raw.id}`)
|
|
1540
|
+
ids.add(raw.id)
|
|
1541
|
+
operations.push({
|
|
1542
|
+
id: raw.id,
|
|
1543
|
+
kind: raw.kind,
|
|
1544
|
+
path: raw.path,
|
|
1545
|
+
beforeSha256: raw.beforeSha256,
|
|
1546
|
+
removals: raw.removals.map(decodeRemovalItem),
|
|
1547
|
+
})
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return { schema: 1, home: resolve(value.home), generatedAt: value.generatedAt, operations }
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function knownOperation(operation: ApprovedOperation): boolean {
|
|
1554
|
+
if (operation.kind === 'write-file') return managedFileTarget(operation.path)
|
|
1555
|
+
return SWEEP_TARGETS.some(
|
|
1556
|
+
(target) =>
|
|
1557
|
+
(target.kind === 'path' && target.path === operation.path) ||
|
|
1558
|
+
(target.kind === 'dir-entries' && dirname(operation.path) === target.dir) ||
|
|
1559
|
+
(target.kind === 'marker-files' && dirname(operation.path) === target.dir),
|
|
1560
|
+
)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function sameRemovals(left: readonly RemovalItem[], right: readonly RemovalItem[]): boolean {
|
|
1564
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function atomicWrite(path: string, content: string, mode: number, uid?: number, gid?: number): void {
|
|
1568
|
+
const temporary = join(dirname(path), `.global-surface-sweep.${process.pid}.${Date.now()}.tmp`)
|
|
1569
|
+
try {
|
|
1570
|
+
writeFileSync(temporary, content, { mode })
|
|
1571
|
+
chmodSync(temporary, mode)
|
|
1572
|
+
if (uid !== undefined && gid !== undefined) {
|
|
1573
|
+
const temporaryStat = lstatSync(temporary)
|
|
1574
|
+
if (temporaryStat.uid !== uid || temporaryStat.gid !== gid) chownSync(temporary, uid, gid)
|
|
1575
|
+
}
|
|
1576
|
+
renameSync(temporary, path)
|
|
1577
|
+
} catch (cause) {
|
|
1578
|
+
rmSync(temporary, { force: true })
|
|
1579
|
+
throw cause
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Best-effort unload of a LaunchAgent immediately before its plist is removed
|
|
1585
|
+
* (moved to backup), so an approved removal never leaves an orphaned running
|
|
1586
|
+
* job. `launchctl bootout` on a job that is not currently loaded is expected
|
|
1587
|
+
* and harmless (the overwhelmingly common case for a stale/foreign plist); we
|
|
1588
|
+
* only fail loud when the job is verifiably still loaded afterward, since
|
|
1589
|
+
* deleting the plist out from under a still-running job is the unsafe outcome
|
|
1590
|
+
* this exists to prevent. macOS-only: this directory only exists on Darwin.
|
|
1591
|
+
*/
|
|
1592
|
+
function bootOutLaunchAgentIfPresent(relativePath: string): void {
|
|
1593
|
+
if (process.platform !== 'darwin') return
|
|
1594
|
+
if (dirname(relativePath) !== LAUNCH_AGENTS_DIR) return
|
|
1595
|
+
const base = relativePath.slice(relativePath.lastIndexOf('/') + 1)
|
|
1596
|
+
const label = base.endsWith('.plist') ? base.slice(0, -'.plist'.length) : base
|
|
1597
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0
|
|
1598
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}/${label}`], { encoding: 'utf8' })
|
|
1599
|
+
const stillLoaded = spawnSync('launchctl', ['list', label], { encoding: 'utf8' })
|
|
1600
|
+
if (stillLoaded.status === 0) {
|
|
1601
|
+
throw new Error(`LaunchAgent ${label} is still loaded after bootout; refusing to remove ${relativePath}`)
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function backupPath(backupRoot: string, relativePath: string): string {
|
|
1606
|
+
const path = resolve(backupRoot, relativePath)
|
|
1607
|
+
relativeInside(backupRoot, path)
|
|
1608
|
+
return path
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* CLI-only reporting (Tarun, 2026-09-28): print the exact original -> backup
|
|
1613
|
+
* mapping for every file this run copied (write-file) or moved (remove-path)
|
|
1614
|
+
* into the backup root `applyApprovedSweep` already creates before touching
|
|
1615
|
+
* anything -- see the `selected` loop there, which backs up every operation
|
|
1616
|
+
* it applies and fails loudly (throws, and rolls back what it already
|
|
1617
|
+
* applied) if a backup step itself fails. When the run applied nothing
|
|
1618
|
+
* (report-only audit, or an already-clean apply/enforce), there is no
|
|
1619
|
+
* backup root at all, so this prints the explicit "no backups" line instead
|
|
1620
|
+
* of an empty block.
|
|
1621
|
+
*/
|
|
1622
|
+
function printBackupsThisRun(home: string, applied: readonly ApprovedOperation[], backupRoot: string | null): void {
|
|
1623
|
+
if (backupRoot === null || applied.length === 0) {
|
|
1624
|
+
console.log('global-surface-sweep: no files changed; no backups')
|
|
1625
|
+
return
|
|
1626
|
+
}
|
|
1627
|
+
console.log('global-surface-sweep: backups this run:')
|
|
1628
|
+
for (const operation of applied) {
|
|
1629
|
+
console.log(` ${resolve(home, operation.path)} -> ${backupPath(backupRoot, operation.path)}`)
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function reviewSurface(relativePath: string): { readonly surface: string; readonly path: string } {
|
|
1634
|
+
for (const [prefix, surface] of [
|
|
1635
|
+
['.agents/', 'agents'],
|
|
1636
|
+
['.claude/', 'claude'],
|
|
1637
|
+
['.codex/', 'codex'],
|
|
1638
|
+
['.cursor/', 'cursor'],
|
|
1639
|
+
['.config/opencode/', 'opencode'],
|
|
1640
|
+
['.kimi-code/', 'kimi'],
|
|
1641
|
+
['Library/LaunchAgents/', 'launchagents'],
|
|
1642
|
+
['.local/bin/', 'localbin'],
|
|
1643
|
+
] as const) {
|
|
1644
|
+
if (relativePath.startsWith(prefix)) return { surface, path: relativePath.slice(prefix.length) }
|
|
1645
|
+
}
|
|
1646
|
+
if (relativePath === '.claude.json') return { surface: 'claude', path: 'home-.claude.json' }
|
|
1647
|
+
if (SHELL_RC_FILES.includes(relativePath)) return { surface: 'shell', path: relativePath }
|
|
1648
|
+
throw new Error(`cannot classify review-copy surface: ${relativePath}`)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function reviewPath(destination: string, relativePath: string, category: string, suffix = ''): string {
|
|
1652
|
+
const classified = reviewSurface(relativePath)
|
|
1653
|
+
const path = resolve(destination, classified.surface, category, `${classified.path}${suffix}`)
|
|
1654
|
+
relativeInside(destination, path)
|
|
1655
|
+
return path
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function writeReviewFile(path: string, content: string, mode = 0o600): void {
|
|
1659
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 })
|
|
1660
|
+
writeFileSync(path, content, { mode })
|
|
1661
|
+
chmodSync(path, mode)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function jsonPointerValue(document: unknown, selector: string): unknown {
|
|
1665
|
+
let current = document
|
|
1666
|
+
for (const encoded of selector.split('/').slice(1)) {
|
|
1667
|
+
const key = encoded.replaceAll('~1', '/').replaceAll('~0', '~')
|
|
1668
|
+
if (current === null || typeof current !== 'object' || !(key in current)) {
|
|
1669
|
+
throw new Error(`JSON selector no longer exists: ${selector}`)
|
|
1670
|
+
}
|
|
1671
|
+
current = (current as Record<string, unknown>)[key]
|
|
1672
|
+
}
|
|
1673
|
+
return current
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
export function copyReviewBundle(
|
|
1677
|
+
homeInput: string,
|
|
1678
|
+
destinationInput: string,
|
|
1679
|
+
protection: ProtectionList = loadProtectionList(),
|
|
1680
|
+
generatedAt = new Date().toISOString(),
|
|
1681
|
+
): ReviewCopyResult {
|
|
1682
|
+
if (!isAbsolute(homeInput) || !isAbsolute(destinationInput)) {
|
|
1683
|
+
throw new Error('--home and --destination must be absolute')
|
|
1684
|
+
}
|
|
1685
|
+
const home = resolve(homeInput)
|
|
1686
|
+
const destination = resolve(destinationInput)
|
|
1687
|
+
if (destination === home) throw new Error('review-copy destination cannot be the home directory')
|
|
1688
|
+
if (pathExists(destination)) {
|
|
1689
|
+
if (!lstatSync(destination).isDirectory()) throw new Error('review-copy destination is not a directory')
|
|
1690
|
+
if (readdirSync(destination).length > 0) throw new Error('review-copy destination must be empty')
|
|
1691
|
+
} else mkdirSync(destination, { recursive: true, mode: 0o700 })
|
|
1692
|
+
chmodSync(destination, 0o700)
|
|
1693
|
+
|
|
1694
|
+
const plan = planGlobalSweep(home, protection)
|
|
1695
|
+
for (const operation of plan.operations) {
|
|
1696
|
+
if (destination === operation.absolutePath || destination.startsWith(`${operation.absolutePath}${sep}`)) {
|
|
1697
|
+
throw new Error(`review-copy destination is inside candidate: ${operation.path}`)
|
|
1698
|
+
}
|
|
1699
|
+
if (!pathExists(operation.absolutePath) || fingerprintPath(operation.absolutePath) !== operation.beforeSha256) {
|
|
1700
|
+
throw new Error(`candidate drifted before review copy: ${operation.path}`)
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
const copies: {
|
|
1705
|
+
readonly source: string
|
|
1706
|
+
readonly destination: string
|
|
1707
|
+
readonly kind: 'path' | 'config-original' | 'proposed-after' | 'removed-fragment'
|
|
1708
|
+
}[] = []
|
|
1709
|
+
let copiedPaths = 0
|
|
1710
|
+
let copiedConfigFiles = 0
|
|
1711
|
+
|
|
1712
|
+
for (const operation of plan.operations) {
|
|
1713
|
+
if (operation.kind === 'remove-path') {
|
|
1714
|
+
const copy = reviewPath(destination, operation.path, 'paths')
|
|
1715
|
+
mkdirSync(dirname(copy), { recursive: true, mode: 0o700 })
|
|
1716
|
+
cpSync(operation.absolutePath, copy, {
|
|
1717
|
+
recursive: true,
|
|
1718
|
+
dereference: false,
|
|
1719
|
+
errorOnExist: true,
|
|
1720
|
+
preserveTimestamps: true,
|
|
1721
|
+
verbatimSymlinks: true,
|
|
1722
|
+
})
|
|
1723
|
+
if (fingerprintPath(copy) !== operation.beforeSha256) {
|
|
1724
|
+
throw new Error(`review copy fingerprint mismatch: ${operation.path}`)
|
|
1725
|
+
}
|
|
1726
|
+
copies.push({ source: operation.path, destination: relativeInside(destination, copy), kind: 'path' })
|
|
1727
|
+
copiedPaths += 1
|
|
1728
|
+
continue
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const original = reviewPath(destination, operation.path, 'config-originals')
|
|
1732
|
+
mkdirSync(dirname(original), { recursive: true, mode: 0o700 })
|
|
1733
|
+
copyFileSync(operation.absolutePath, original)
|
|
1734
|
+
chmodSync(original, operation.mode)
|
|
1735
|
+
if (fingerprintPath(original) !== operation.beforeSha256) {
|
|
1736
|
+
throw new Error(`review config copy fingerprint mismatch: ${operation.path}`)
|
|
1737
|
+
}
|
|
1738
|
+
copies.push({
|
|
1739
|
+
source: operation.path,
|
|
1740
|
+
destination: relativeInside(destination, original),
|
|
1741
|
+
kind: 'config-original',
|
|
1742
|
+
})
|
|
1743
|
+
|
|
1744
|
+
const proposed = reviewPath(destination, operation.path, 'proposed-after')
|
|
1745
|
+
writeReviewFile(proposed, operation.content)
|
|
1746
|
+
copies.push({
|
|
1747
|
+
source: operation.path,
|
|
1748
|
+
destination: relativeInside(destination, proposed),
|
|
1749
|
+
kind: 'proposed-after',
|
|
1750
|
+
})
|
|
1751
|
+
|
|
1752
|
+
const jsonRemovals = operation.removals.filter(
|
|
1753
|
+
(removal): removal is Extract<RemovalItem, { readonly kind: 'json-entry' }> =>
|
|
1754
|
+
removal.kind === 'json-entry',
|
|
1755
|
+
)
|
|
1756
|
+
if (jsonRemovals.length > 0) {
|
|
1757
|
+
const document = JSON.parse(readFileSync(operation.absolutePath, 'utf8'))
|
|
1758
|
+
const entries = jsonRemovals.map((removal) => {
|
|
1759
|
+
const value = jsonPointerValue(document, removal.selector)
|
|
1760
|
+
if (fingerprintJsonValue(value) !== removal.beforeSha256) {
|
|
1761
|
+
throw new Error(`JSON removal drifted during review copy: ${operation.path}#${removal.selector}`)
|
|
1762
|
+
}
|
|
1763
|
+
return { selector: removal.selector, beforeSha256: removal.beforeSha256, value }
|
|
1764
|
+
})
|
|
1765
|
+
const removed = reviewPath(destination, operation.path, 'removed-fragments', '.removed.json')
|
|
1766
|
+
writeReviewFile(removed, `${JSON.stringify({ source: operation.path, entries }, null, 2)}\n`)
|
|
1767
|
+
copies.push({
|
|
1768
|
+
source: operation.path,
|
|
1769
|
+
destination: relativeInside(destination, removed),
|
|
1770
|
+
kind: 'removed-fragment',
|
|
1771
|
+
})
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const tomlRemovals = operation.removals.filter(
|
|
1775
|
+
(removal): removal is Extract<RemovalItem, { readonly kind: 'toml-table' | 'toml-key' }> =>
|
|
1776
|
+
removal.kind === 'toml-table' || removal.kind === 'toml-key',
|
|
1777
|
+
)
|
|
1778
|
+
if (tomlRemovals.length > 0) {
|
|
1779
|
+
const blocks = splitTomlBlocks(readFileSync(operation.absolutePath, 'utf8'), preservedRegionFor(operation.path))
|
|
1780
|
+
const rootBlock = blocks[0]?.header === null && blocks[0]?.preserved === false ? blocks[0] : null
|
|
1781
|
+
const rootKeyTexts = rootBlock === null
|
|
1782
|
+
? []
|
|
1783
|
+
: tomlRootKeys(rootBlock.lines).map((rootKey) => ({
|
|
1784
|
+
key: rootKey.key,
|
|
1785
|
+
text: rootBlock.lines.slice(rootKey.start, rootKey.end + 1).join('\n'),
|
|
1786
|
+
}))
|
|
1787
|
+
const removedBlocks = tomlRemovals.map((removal) => {
|
|
1788
|
+
if (removal.kind === 'toml-key') {
|
|
1789
|
+
const match = rootKeyTexts.find(
|
|
1790
|
+
(candidate) => candidate.key === removal.key && fingerprintBytes(candidate.text) === removal.beforeSha256,
|
|
1791
|
+
)
|
|
1792
|
+
if (match === undefined) {
|
|
1793
|
+
throw new Error(`TOML key removal drifted during review copy: ${operation.path} ${removal.key}`)
|
|
1794
|
+
}
|
|
1795
|
+
return match.text
|
|
1796
|
+
}
|
|
1797
|
+
const block = blocks.find(
|
|
1798
|
+
(candidate) =>
|
|
1799
|
+
candidate.header === removal.header &&
|
|
1800
|
+
fingerprintBytes(candidate.lines.join('\n')) === removal.beforeSha256,
|
|
1801
|
+
)
|
|
1802
|
+
if (block === undefined) {
|
|
1803
|
+
throw new Error(`TOML removal drifted during review copy: ${operation.path} ${removal.header}`)
|
|
1804
|
+
}
|
|
1805
|
+
return block.lines.join('\n')
|
|
1806
|
+
})
|
|
1807
|
+
const removed = reviewPath(destination, operation.path, 'removed-fragments', '.removed.toml')
|
|
1808
|
+
writeReviewFile(removed, removedBlocks.join('\n'))
|
|
1809
|
+
copies.push({
|
|
1810
|
+
source: operation.path,
|
|
1811
|
+
destination: relativeInside(destination, removed),
|
|
1812
|
+
kind: 'removed-fragment',
|
|
1813
|
+
})
|
|
1814
|
+
}
|
|
1815
|
+
const blockRemovals = operation.removals.filter(
|
|
1816
|
+
(removal): removal is Extract<RemovalItem, { readonly kind: 'text-block' }> => removal.kind === 'text-block',
|
|
1817
|
+
)
|
|
1818
|
+
if (blockRemovals.length > 0) {
|
|
1819
|
+
const blocks = findTextBlocks(readFileSync(operation.absolutePath, 'utf8'))
|
|
1820
|
+
const removedTexts = blockRemovals.map((removal) => {
|
|
1821
|
+
const block = blocks.find(
|
|
1822
|
+
(candidate) => candidate.name === removal.name && fingerprintBytes(candidate.text) === removal.beforeSha256,
|
|
1823
|
+
)
|
|
1824
|
+
if (block === undefined) {
|
|
1825
|
+
throw new Error(`text block removal drifted during review copy: ${operation.path} ${removal.name}`)
|
|
1826
|
+
}
|
|
1827
|
+
return block.text
|
|
1828
|
+
})
|
|
1829
|
+
const removed = reviewPath(destination, operation.path, 'removed-fragments', '.removed.txt')
|
|
1830
|
+
writeReviewFile(removed, `${removedTexts.join('\n\n')}\n`)
|
|
1831
|
+
copies.push({
|
|
1832
|
+
source: operation.path,
|
|
1833
|
+
destination: relativeInside(destination, removed),
|
|
1834
|
+
kind: 'removed-fragment',
|
|
1835
|
+
})
|
|
1836
|
+
}
|
|
1837
|
+
copiedConfigFiles += 1
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
for (const operation of plan.operations) {
|
|
1841
|
+
if (!pathExists(operation.absolutePath) || fingerprintPath(operation.absolutePath) !== operation.beforeSha256) {
|
|
1842
|
+
throw new Error(`candidate drifted during review copy: ${operation.path}`)
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const candidateItems = plan.operations.reduce((count, operation) => count + operation.removals.length, 0)
|
|
1847
|
+
writeReviewFile(
|
|
1848
|
+
join(destination, 'manifest.json'),
|
|
1849
|
+
`${JSON.stringify(
|
|
1850
|
+
{
|
|
1851
|
+
schema: 1,
|
|
1852
|
+
generatedAt,
|
|
1853
|
+
sourceHome: home,
|
|
1854
|
+
notice: 'Review copy only. No live agent surface was changed or deleted.',
|
|
1855
|
+
candidateOperations: plan.operations.length,
|
|
1856
|
+
candidateItems,
|
|
1857
|
+
protectedItems: plan.protected.length,
|
|
1858
|
+
copies,
|
|
1859
|
+
operations: plan.operations.map(publicOperation),
|
|
1860
|
+
protected: plan.protected,
|
|
1861
|
+
},
|
|
1862
|
+
null,
|
|
1863
|
+
2,
|
|
1864
|
+
)}\n`,
|
|
1865
|
+
)
|
|
1866
|
+
writeReviewFile(
|
|
1867
|
+
join(destination, 'README.md'),
|
|
1868
|
+
[
|
|
1869
|
+
'# Global surface sweep review copy',
|
|
1870
|
+
'',
|
|
1871
|
+
'This directory is a non-destructive snapshot. No live agent surface was changed or deleted.',
|
|
1872
|
+
'',
|
|
1873
|
+
`- Candidate operations: ${plan.operations.length}`,
|
|
1874
|
+
`- Candidate items: ${candidateItems}`,
|
|
1875
|
+
`- Protected items not copied as candidates: ${plan.protected.length}`,
|
|
1876
|
+
'',
|
|
1877
|
+
'Each surface contains `paths/` for exact filesystem candidates, `config-originals/` for the',
|
|
1878
|
+
'full source configuration, `proposed-after/` for the planned result, and `removed-fragments/`',
|
|
1879
|
+
'for the exact JSON values or TOML blocks selected for removal. `manifest.json` records hashes',
|
|
1880
|
+
'and source-to-copy mappings. Some removed fragments may contain credentials; keep this directory private.',
|
|
1881
|
+
'',
|
|
1882
|
+
].join('\n'),
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
return {
|
|
1886
|
+
destination,
|
|
1887
|
+
copiedPaths,
|
|
1888
|
+
copiedConfigFiles,
|
|
1889
|
+
candidateItems,
|
|
1890
|
+
protectedItems: plan.protected.length,
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
export function applyApprovedSweep(
|
|
1895
|
+
homeInput: string,
|
|
1896
|
+
approvalInput: ApprovalPlan,
|
|
1897
|
+
stateDirInput: string,
|
|
1898
|
+
now = new Date().toISOString(),
|
|
1899
|
+
protection: ProtectionList = loadProtectionList(),
|
|
1900
|
+
): ApplyResult {
|
|
1901
|
+
if (!isAbsolute(homeInput) || !isAbsolute(stateDirInput)) {
|
|
1902
|
+
throw new Error('--home and --state-dir must be absolute')
|
|
1903
|
+
}
|
|
1904
|
+
const home = resolve(homeInput)
|
|
1905
|
+
const stateDir = resolve(stateDirInput)
|
|
1906
|
+
const approval = decodeApprovalPlan(approvalInput)
|
|
1907
|
+
if (approval.home !== home) throw new Error(`approval plan is for ${approval.home}, not ${home}`)
|
|
1908
|
+
|
|
1909
|
+
const currentPlan = planGlobalSweep(home, protection)
|
|
1910
|
+
const currentById = new Map(currentPlan.operations.map((operation) => [operation.id, operation]))
|
|
1911
|
+
const approvedIds = new Set(approval.operations.map((operation) => operation.id))
|
|
1912
|
+
const selected: SweepOperation[] = []
|
|
1913
|
+
const alreadyClean: string[] = []
|
|
1914
|
+
|
|
1915
|
+
for (const approved of approval.operations) {
|
|
1916
|
+
if (!knownOperation(approved)) throw new Error(`approval operation is outside managed surfaces: ${approved.id}`)
|
|
1917
|
+
const current = currentById.get(approved.id)
|
|
1918
|
+
if (current === undefined) {
|
|
1919
|
+
const path = insideHome(home, approved.path)
|
|
1920
|
+
if (approved.kind === 'remove-path' && pathExists(path)) {
|
|
1921
|
+
throw new Error(`approved removal is no longer a direct managed entry: ${approved.path}`)
|
|
1922
|
+
}
|
|
1923
|
+
const unreadable = currentPlan.notes.some(
|
|
1924
|
+
(note) => note.level === 'warn' && note.text.startsWith(`${path} `) && note.text.includes('left untouched'),
|
|
1925
|
+
)
|
|
1926
|
+
if (unreadable) throw new Error(`approved target can no longer be read safely: ${approved.path}`)
|
|
1927
|
+
alreadyClean.push(approved.id)
|
|
1928
|
+
continue
|
|
1929
|
+
}
|
|
1930
|
+
if (
|
|
1931
|
+
current.kind !== approved.kind ||
|
|
1932
|
+
current.path !== approved.path ||
|
|
1933
|
+
current.beforeSha256 !== approved.beforeSha256 ||
|
|
1934
|
+
!sameRemovals(current.removals, approved.removals)
|
|
1935
|
+
) {
|
|
1936
|
+
throw new Error(`approved target drifted after review: ${approved.path}`)
|
|
1937
|
+
}
|
|
1938
|
+
selected.push(current)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const unapproved = currentPlan.operations
|
|
1942
|
+
.filter((operation) => !approvedIds.has(operation.id))
|
|
1943
|
+
.map(publicOperation)
|
|
1944
|
+
if (selected.length === 0) return { applied: [], alreadyClean, unapproved, backupRoot: null }
|
|
1945
|
+
|
|
1946
|
+
const stamp = now.replaceAll(/[^0-9A-Za-z_-]/g, '-')
|
|
1947
|
+
const backupParent = join(stateDir, 'global-surface-sweep', 'backups')
|
|
1948
|
+
for (const operation of selected) {
|
|
1949
|
+
const target = resolve(operation.absolutePath)
|
|
1950
|
+
if (stateDir === target || stateDir.startsWith(`${target}${sep}`)) {
|
|
1951
|
+
throw new Error(`state directory cannot be inside a removal target: ${operation.path}`)
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
mkdirSync(backupParent, { recursive: true, mode: 0o700 })
|
|
1955
|
+
const backupRoot = mkdtempSync(join(backupParent, `${stamp}-`))
|
|
1956
|
+
chmodSync(backupRoot, 0o700)
|
|
1957
|
+
|
|
1958
|
+
const applied: { readonly operation: SweepOperation; readonly backup: string }[] = []
|
|
1959
|
+
try {
|
|
1960
|
+
for (const operation of selected) {
|
|
1961
|
+
if (!pathExists(operation.absolutePath) || fingerprintPath(operation.absolutePath) !== operation.beforeSha256) {
|
|
1962
|
+
throw new Error(`approved target drifted immediately before apply: ${operation.path}`)
|
|
1963
|
+
}
|
|
1964
|
+
const backup = backupPath(backupRoot, operation.path)
|
|
1965
|
+
mkdirSync(dirname(backup), { recursive: true, mode: 0o700 })
|
|
1966
|
+
if (operation.kind === 'write-file') {
|
|
1967
|
+
copyFileSync(operation.absolutePath, backup)
|
|
1968
|
+
chmodSync(backup, operation.mode)
|
|
1969
|
+
const backupStat = lstatSync(backup)
|
|
1970
|
+
if (backupStat.uid !== operation.uid || backupStat.gid !== operation.gid) {
|
|
1971
|
+
chownSync(backup, operation.uid, operation.gid)
|
|
1972
|
+
}
|
|
1973
|
+
atomicWrite(operation.absolutePath, operation.content, operation.mode, operation.uid, operation.gid)
|
|
1974
|
+
} else {
|
|
1975
|
+
bootOutLaunchAgentIfPresent(operation.path)
|
|
1976
|
+
renameSync(operation.absolutePath, backup)
|
|
1977
|
+
}
|
|
1978
|
+
applied.push({ operation, backup })
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
atomicWrite(
|
|
1982
|
+
join(backupRoot, 'applied-plan.json'),
|
|
1983
|
+
`${JSON.stringify(
|
|
1984
|
+
{
|
|
1985
|
+
schema: 1,
|
|
1986
|
+
appliedAt: now,
|
|
1987
|
+
home,
|
|
1988
|
+
operations: applied.map(({ operation }) => publicOperation(operation)),
|
|
1989
|
+
},
|
|
1990
|
+
null,
|
|
1991
|
+
2,
|
|
1992
|
+
)}\n`,
|
|
1993
|
+
0o600,
|
|
1994
|
+
)
|
|
1995
|
+
} catch (cause) {
|
|
1996
|
+
for (const { operation, backup } of [...applied].reverse()) {
|
|
1997
|
+
if (operation.kind === 'write-file') {
|
|
1998
|
+
atomicWrite(
|
|
1999
|
+
operation.absolutePath,
|
|
2000
|
+
readFileSync(backup, 'utf8'),
|
|
2001
|
+
operation.mode,
|
|
2002
|
+
operation.uid,
|
|
2003
|
+
operation.gid,
|
|
2004
|
+
)
|
|
2005
|
+
} else {
|
|
2006
|
+
mkdirSync(dirname(operation.absolutePath), { recursive: true })
|
|
2007
|
+
renameSync(backup, operation.absolutePath)
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
throw cause
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
return {
|
|
2014
|
+
applied: applied.map(({ operation }) => publicOperation(operation)),
|
|
2015
|
+
alreadyClean,
|
|
2016
|
+
unapproved,
|
|
2017
|
+
backupRoot,
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
export function enforceGlobalSweep(
|
|
2022
|
+
homeInput: string,
|
|
2023
|
+
stateDirInput: string,
|
|
2024
|
+
now = new Date().toISOString(),
|
|
2025
|
+
protection: ProtectionList = loadProtectionList(),
|
|
2026
|
+
): EnforceResult {
|
|
2027
|
+
if (!isAbsolute(homeInput) || !isAbsolute(stateDirInput)) {
|
|
2028
|
+
throw new Error('--home and --state-dir must be absolute')
|
|
2029
|
+
}
|
|
2030
|
+
const home = resolve(homeInput)
|
|
2031
|
+
const plan = planGlobalSweep(home, protection)
|
|
2032
|
+
const result = applyApprovedSweep(
|
|
2033
|
+
home,
|
|
2034
|
+
{
|
|
2035
|
+
schema: 1,
|
|
2036
|
+
home,
|
|
2037
|
+
generatedAt: now,
|
|
2038
|
+
operations: plan.operations.filter((operation) => !operation.requiresApproval).map(publicOperation),
|
|
2039
|
+
},
|
|
2040
|
+
resolve(stateDirInput),
|
|
2041
|
+
now,
|
|
2042
|
+
protection,
|
|
2043
|
+
)
|
|
2044
|
+
const pendingApproval = result.unapproved.filter((operation) => operationRequiresApproval(operation.path))
|
|
2045
|
+
return { ...result, protectedItems: plan.protected.length, pendingApproval }
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
export function describeRemoval(removal: RemovalItem): string {
|
|
2049
|
+
if (removal.kind === 'json-entry') {
|
|
2050
|
+
return `JSON ${removal.target}#${removal.selector} (${removal.beforeSha256})`
|
|
2051
|
+
}
|
|
2052
|
+
if (removal.kind === 'toml-table') {
|
|
2053
|
+
return `TOML ${removal.target} ${removal.header} (${removal.beforeSha256})`
|
|
2054
|
+
}
|
|
2055
|
+
if (removal.kind === 'toml-key') {
|
|
2056
|
+
return `TOML-KEY ${removal.target} ${removal.key} (${removal.beforeSha256})`
|
|
2057
|
+
}
|
|
2058
|
+
if (removal.kind === 'text-block') {
|
|
2059
|
+
return `BLOCK ${removal.target} «${removal.name}» (${removal.beforeSha256})`
|
|
2060
|
+
}
|
|
2061
|
+
return `PATH ${removal.target} (${removal.beforeSha256})`
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function usage(): never {
|
|
2065
|
+
throw new Error(
|
|
2066
|
+
'usage: global-surface-sweep --home ABSOLUTE_PATH --mode audit|plan|copy-review|apply|enforce ' +
|
|
2067
|
+
'[--protection FILE --destination ABSOLUTE_PATH --approval PLAN.json --state-dir ABSOLUTE_PATH]',
|
|
2068
|
+
)
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
export function main(argv: readonly string[]): void {
|
|
2072
|
+
let home = ''
|
|
2073
|
+
let mode = 'audit'
|
|
2074
|
+
let approvalPath = ''
|
|
2075
|
+
let stateDir = '/var/lib/adl'
|
|
2076
|
+
let protectionPath = DEFAULT_PROTECTION_PATH
|
|
2077
|
+
let destination = ''
|
|
2078
|
+
|
|
2079
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2080
|
+
const argument = argv[index]
|
|
2081
|
+
if (argument === '--home') home = argv[++index] ?? ''
|
|
2082
|
+
else if (argument === '--mode') mode = argv[++index] ?? ''
|
|
2083
|
+
else if (argument === '--approval') approvalPath = argv[++index] ?? ''
|
|
2084
|
+
else if (argument === '--state-dir') stateDir = argv[++index] ?? ''
|
|
2085
|
+
else if (argument === '--protection') protectionPath = argv[++index] ?? ''
|
|
2086
|
+
else if (argument === '--destination') destination = argv[++index] ?? ''
|
|
2087
|
+
else usage()
|
|
2088
|
+
}
|
|
2089
|
+
if (home.length === 0 || !['audit', 'plan', 'copy-review', 'apply', 'enforce'].includes(mode)) usage()
|
|
2090
|
+
const protection = loadProtectionList(protectionPath)
|
|
2091
|
+
|
|
2092
|
+
if (mode === 'plan') {
|
|
2093
|
+
console.log(JSON.stringify(createApprovalPlan(home, new Date().toISOString(), protection), null, 2))
|
|
2094
|
+
return
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (mode === 'copy-review') {
|
|
2098
|
+
if (destination.length === 0) throw new Error('copy-review mode requires --destination')
|
|
2099
|
+
const result = copyReviewBundle(home, destination, protection)
|
|
2100
|
+
console.log(`review copy: ${result.destination}`)
|
|
2101
|
+
console.log(`copied path candidates: ${result.copiedPaths}`)
|
|
2102
|
+
console.log(`copied configuration originals: ${result.copiedConfigFiles}`)
|
|
2103
|
+
console.log(`candidate items: ${result.candidateItems}`)
|
|
2104
|
+
console.log(`protected items not copied as candidates: ${result.protectedItems}`)
|
|
2105
|
+
console.log('global-surface-sweep: review copy complete; no live files changed')
|
|
2106
|
+
return
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (mode === 'enforce') {
|
|
2110
|
+
const result = enforceGlobalSweep(home, stateDir, new Date().toISOString(), protection)
|
|
2111
|
+
for (const operation of result.applied) {
|
|
2112
|
+
console.log(`applied: ${operation.id}`)
|
|
2113
|
+
for (const removal of operation.removals) console.log(` removed: ${describeRemoval(removal)}`)
|
|
2114
|
+
}
|
|
2115
|
+
for (const operation of result.pendingApproval) {
|
|
2116
|
+
console.log(`pending approval: ${operation.id}`)
|
|
2117
|
+
for (const removal of operation.removals) console.log(` retained until approved: ${describeRemoval(removal)}`)
|
|
2118
|
+
}
|
|
2119
|
+
printBackupsThisRun(home, result.applied, result.backupRoot)
|
|
2120
|
+
console.log(
|
|
2121
|
+
`global-surface-sweep: enforced ${result.applied.length} operation(s); ` +
|
|
2122
|
+
`${result.protectedItems} exact protected item(s) retained; ` +
|
|
2123
|
+
`${result.pendingApproval.length} operation(s) pending approval`,
|
|
2124
|
+
)
|
|
2125
|
+
if (result.pendingApproval.length > 0) {
|
|
2126
|
+
console.error(
|
|
2127
|
+
`global-surface-sweep: WARNING ${result.pendingApproval.length} removal operation(s) pending explicit approval ` +
|
|
2128
|
+
'(generate with --mode plan, review, then --mode apply --approval PLAN.json)',
|
|
2129
|
+
)
|
|
2130
|
+
}
|
|
2131
|
+
return
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const plan = planGlobalSweep(home, protection)
|
|
2135
|
+
if (mode === 'audit') {
|
|
2136
|
+
for (const note of plan.notes) console.log(`${note.level}: ${note.text}`)
|
|
2137
|
+
for (const item of plan.protected) {
|
|
2138
|
+
console.log(`protected: ${describeRemoval(item.removal)} — ${item.reason}`)
|
|
2139
|
+
}
|
|
2140
|
+
for (const operation of plan.operations) {
|
|
2141
|
+
const suffix = operation.requiresApproval ? ' [requires explicit approval]' : ''
|
|
2142
|
+
for (const removal of operation.removals) console.log(`remove: ${describeRemoval(removal)}${suffix}`)
|
|
2143
|
+
}
|
|
2144
|
+
// Audit is report-only: it never writes or moves a file, so there is
|
|
2145
|
+
// never a backup root to point at (Tarun, 2026-09-28).
|
|
2146
|
+
printBackupsThisRun(home, [], null)
|
|
2147
|
+
console.log(
|
|
2148
|
+
`global-surface-sweep: ${plan.operations.length} candidate(s), ` +
|
|
2149
|
+
`${plan.protected.length} exact protected item(s); no changes applied`,
|
|
2150
|
+
)
|
|
2151
|
+
return
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (approvalPath.length === 0) {
|
|
2155
|
+
throw new Error('apply mode requires --approval; discovery alone never authorizes deletion')
|
|
2156
|
+
}
|
|
2157
|
+
const approval = decodeApprovalPlan(JSON.parse(readFileSync(approvalPath, 'utf8')))
|
|
2158
|
+
const result = applyApprovedSweep(home, approval, stateDir, new Date().toISOString(), protection)
|
|
2159
|
+
for (const operation of result.applied) {
|
|
2160
|
+
console.log(`applied: ${operation.id}`)
|
|
2161
|
+
for (const removal of operation.removals) console.log(` removed: ${describeRemoval(removal)}`)
|
|
2162
|
+
}
|
|
2163
|
+
for (const id of result.alreadyClean) console.log(`already clean: ${id}`)
|
|
2164
|
+
for (const operation of result.unapproved) {
|
|
2165
|
+
console.log(`left unapproved: ${operation.id}`)
|
|
2166
|
+
for (const removal of operation.removals) console.log(` retained: ${describeRemoval(removal)}`)
|
|
2167
|
+
}
|
|
2168
|
+
printBackupsThisRun(home, result.applied, result.backupRoot)
|
|
2169
|
+
console.log('global-surface-sweep: apply complete')
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
if (import.meta.main) {
|
|
2173
|
+
try {
|
|
2174
|
+
main(process.argv.slice(2))
|
|
2175
|
+
} catch (cause) {
|
|
2176
|
+
console.error(`global-surface-sweep: ${messageOf(cause)}`)
|
|
2177
|
+
process.exitCode = 1
|
|
2178
|
+
}
|
|
2179
|
+
}
|