@paths.design/caws-cli 10.1.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -374
- package/dist/index.js +43 -756
- package/dist/shell/binding/resolve-binding.d.ts +4 -0
- package/dist/shell/binding/resolve-binding.d.ts.map +1 -0
- package/dist/shell/binding/resolve-binding.js +228 -0
- package/dist/shell/binding/resolve-binding.js.map +1 -0
- package/dist/shell/binding/types.d.ts +42 -0
- package/dist/shell/binding/types.d.ts.map +1 -0
- package/dist/shell/binding/types.js +21 -0
- package/dist/shell/binding/types.js.map +1 -0
- package/dist/shell/commands/claim.d.ts +14 -0
- package/dist/shell/commands/claim.d.ts.map +1 -0
- package/dist/shell/commands/claim.js +197 -0
- package/dist/shell/commands/claim.js.map +1 -0
- package/dist/shell/commands/doctor.d.ts +13 -0
- package/dist/shell/commands/doctor.d.ts.map +1 -0
- package/dist/shell/commands/doctor.js +97 -0
- package/dist/shell/commands/doctor.js.map +1 -0
- package/dist/shell/commands/evidence.d.ts +28 -0
- package/dist/shell/commands/evidence.d.ts.map +1 -0
- package/dist/shell/commands/evidence.js +166 -0
- package/dist/shell/commands/evidence.js.map +1 -0
- package/dist/shell/commands/gates.d.ts +19 -0
- package/dist/shell/commands/gates.d.ts.map +1 -0
- package/dist/shell/commands/gates.js +181 -0
- package/dist/shell/commands/gates.js.map +1 -0
- package/dist/shell/commands/init.d.ts +8 -0
- package/dist/shell/commands/init.d.ts.map +1 -0
- package/dist/shell/commands/init.js +64 -0
- package/dist/shell/commands/init.js.map +1 -0
- package/dist/shell/commands/scope.d.ts +11 -0
- package/dist/shell/commands/scope.d.ts.map +1 -0
- package/dist/shell/commands/scope.js +92 -0
- package/dist/shell/commands/scope.js.map +1 -0
- package/dist/shell/commands/status.d.ts +15 -0
- package/dist/shell/commands/status.d.ts.map +1 -0
- package/dist/shell/commands/status.js +106 -0
- package/dist/shell/commands/status.js.map +1 -0
- package/dist/shell/commands/waiver.d.ts +38 -0
- package/dist/shell/commands/waiver.d.ts.map +1 -0
- package/dist/shell/commands/waiver.js +240 -0
- package/dist/shell/commands/waiver.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts +23 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -0
- package/dist/shell/gates/disposition.js +87 -0
- package/dist/shell/gates/disposition.js.map +1 -0
- package/dist/shell/gates/gate-result-contract.d.ts +39 -0
- package/dist/shell/gates/gate-result-contract.d.ts.map +1 -0
- package/dist/shell/gates/gate-result-contract.js +150 -0
- package/dist/shell/gates/gate-result-contract.js.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts +55 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.js +161 -0
- package/dist/shell/gates/quality-gates-adapter.js.map +1 -0
- package/dist/shell/gates/waiver-filter.d.ts +58 -0
- package/dist/shell/gates/waiver-filter.d.ts.map +1 -0
- package/dist/shell/gates/waiver-filter.js +119 -0
- package/dist/shell/gates/waiver-filter.js.map +1 -0
- package/dist/shell/index.d.ts +50 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +73 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/register.d.ts +11 -0
- package/dist/shell/register.d.ts.map +1 -0
- package/dist/shell/register.js +274 -0
- package/dist/shell/register.js.map +1 -0
- package/dist/shell/render/claim.d.ts +22 -0
- package/dist/shell/render/claim.d.ts.map +1 -0
- package/dist/shell/render/claim.js +75 -0
- package/dist/shell/render/claim.js.map +1 -0
- package/dist/shell/render/decision.d.ts +15 -0
- package/dist/shell/render/decision.d.ts.map +1 -0
- package/dist/shell/render/decision.js +66 -0
- package/dist/shell/render/decision.js.map +1 -0
- package/dist/shell/render/diagnostic.d.ts +19 -0
- package/dist/shell/render/diagnostic.d.ts.map +1 -0
- package/dist/shell/render/diagnostic.js +76 -0
- package/dist/shell/render/diagnostic.js.map +1 -0
- package/dist/shell/render/finding.d.ts +15 -0
- package/dist/shell/render/finding.d.ts.map +1 -0
- package/dist/shell/render/finding.js +57 -0
- package/dist/shell/render/finding.js.map +1 -0
- package/dist/shell/render/gates.d.ts +3 -0
- package/dist/shell/render/gates.d.ts.map +1 -0
- package/dist/shell/render/gates.js +56 -0
- package/dist/shell/render/gates.js.map +1 -0
- package/dist/shell/render/init.d.ts +11 -0
- package/dist/shell/render/init.d.ts.map +1 -0
- package/dist/shell/render/init.js +32 -0
- package/dist/shell/render/init.js.map +1 -0
- package/dist/shell/render/status.d.ts +26 -0
- package/dist/shell/render/status.d.ts.map +1 -0
- package/dist/shell/render/status.js +143 -0
- package/dist/shell/render/status.js.map +1 -0
- package/dist/shell/render/waiver.d.ts +21 -0
- package/dist/shell/render/waiver.d.ts.map +1 -0
- package/dist/shell/render/waiver.js +94 -0
- package/dist/shell/render/waiver.js.map +1 -0
- package/dist/shell/rules.d.ts +37 -0
- package/dist/shell/rules.d.ts.map +1 -0
- package/dist/shell/rules.js +51 -0
- package/dist/shell/rules.js.map +1 -0
- package/dist/shell/session/actor.d.ts +14 -0
- package/dist/shell/session/actor.d.ts.map +1 -0
- package/dist/shell/session/actor.js +34 -0
- package/dist/shell/session/actor.js.map +1 -0
- package/dist/shell/session/resolve-session.d.ts +5 -0
- package/dist/shell/session/resolve-session.d.ts.map +1 -0
- package/dist/shell/session/resolve-session.js +239 -0
- package/dist/shell/session/resolve-session.js.map +1 -0
- package/dist/shell/session/types.d.ts +56 -0
- package/dist/shell/session/types.d.ts.map +1 -0
- package/dist/shell/session/types.js +15 -0
- package/dist/shell/session/types.js.map +1 -0
- package/dist/store/agents-store.d.ts +3 -0
- package/dist/store/agents-store.d.ts.map +1 -0
- package/dist/store/agents-store.js +63 -0
- package/dist/store/agents-store.js.map +1 -0
- package/dist/store/apply-patch.d.ts +16 -0
- package/dist/store/apply-patch.d.ts.map +1 -0
- package/dist/store/apply-patch.js +191 -0
- package/dist/store/apply-patch.js.map +1 -0
- package/dist/store/atomic-write.d.ts +16 -0
- package/dist/store/atomic-write.d.ts.map +1 -0
- package/dist/store/atomic-write.js +132 -0
- package/dist/store/atomic-write.js.map +1 -0
- package/dist/store/doctor-snapshot.d.ts +20 -0
- package/dist/store/doctor-snapshot.d.ts.map +1 -0
- package/dist/store/doctor-snapshot.js +176 -0
- package/dist/store/doctor-snapshot.js.map +1 -0
- package/dist/store/events-store.d.ts +33 -0
- package/dist/store/events-store.d.ts.map +1 -0
- package/dist/store/events-store.js +297 -0
- package/dist/store/events-store.js.map +1 -0
- package/dist/store/index.d.ts +21 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +47 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/init-store.d.ts +21 -0
- package/dist/store/init-store.d.ts.map +1 -0
- package/dist/store/init-store.js +295 -0
- package/dist/store/init-store.js.map +1 -0
- package/dist/store/json-store.d.ts +3 -0
- package/dist/store/json-store.d.ts.map +1 -0
- package/dist/store/json-store.js +65 -0
- package/dist/store/json-store.js.map +1 -0
- package/dist/store/policy-store.d.ts +3 -0
- package/dist/store/policy-store.d.ts.map +1 -0
- package/dist/store/policy-store.js +65 -0
- package/dist/store/policy-store.js.map +1 -0
- package/dist/store/repo-root.d.ts +46 -0
- package/dist/store/repo-root.d.ts.map +1 -0
- package/dist/store/repo-root.js +145 -0
- package/dist/store/repo-root.js.map +1 -0
- package/dist/store/rules.d.ts +53 -0
- package/dist/store/rules.d.ts.map +1 -0
- package/dist/store/rules.js +78 -0
- package/dist/store/rules.js.map +1 -0
- package/dist/store/specs-store.d.ts +3 -0
- package/dist/store/specs-store.d.ts.map +1 -0
- package/dist/store/specs-store.js +131 -0
- package/dist/store/specs-store.js.map +1 -0
- package/dist/store/types.d.ts +84 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +14 -0
- package/dist/store/types.js.map +1 -0
- package/dist/store/waivers-store.d.ts +25 -0
- package/dist/store/waivers-store.d.ts.map +1 -0
- package/dist/store/waivers-store.js +232 -0
- package/dist/store/waivers-store.js.map +1 -0
- package/dist/store/worktrees-store.d.ts +3 -0
- package/dist/store/worktrees-store.d.ts.map +1 -0
- package/dist/store/worktrees-store.js +62 -0
- package/dist/store/worktrees-store.js.map +1 -0
- package/dist/store/yaml-store.d.ts +9 -0
- package/dist/store/yaml-store.d.ts.map +1 -0
- package/dist/store/yaml-store.js +121 -0
- package/dist/store/yaml-store.js.map +1 -0
- package/package.json +15 -13
- package/dist/budget-derivation.js +0 -751
- package/dist/cicd-optimizer.js +0 -504
- package/dist/commands/archive.js +0 -500
- package/dist/commands/burnup.js +0 -198
- package/dist/commands/diagnose.js +0 -525
- package/dist/commands/evaluate.js +0 -314
- package/dist/commands/gates.js +0 -149
- package/dist/commands/init.js +0 -857
- package/dist/commands/iterate.js +0 -417
- package/dist/commands/mode.js +0 -269
- package/dist/commands/parallel.js +0 -242
- package/dist/commands/plan.js +0 -438
- package/dist/commands/provenance.js +0 -1143
- package/dist/commands/quality-monitor.js +0 -284
- package/dist/commands/scope.js +0 -264
- package/dist/commands/session.js +0 -312
- package/dist/commands/sidecar.js +0 -74
- package/dist/commands/specs.js +0 -1448
- package/dist/commands/status.js +0 -1151
- package/dist/commands/templates.js +0 -237
- package/dist/commands/tool.js +0 -136
- package/dist/commands/tutorial.js +0 -480
- package/dist/commands/validate.js +0 -357
- package/dist/commands/verify-acs.js +0 -443
- package/dist/commands/waivers.js +0 -599
- package/dist/commands/workflow.js +0 -243
- package/dist/commands/worktree.js +0 -386
- package/dist/config/lite-scope.js +0 -158
- package/dist/config/modes.js +0 -347
- package/dist/constants/spec-types.js +0 -65
- package/dist/gates/budget-limit.js +0 -121
- package/dist/gates/feedback.js +0 -260
- package/dist/gates/format.js +0 -179
- package/dist/gates/god-object.js +0 -117
- package/dist/gates/pipeline.js +0 -167
- package/dist/gates/scope-boundary.js +0 -93
- package/dist/gates/spec-completeness.js +0 -109
- package/dist/gates/todo-detection.js +0 -205
- package/dist/generators/jest-config-generator.js +0 -242
- package/dist/generators/working-spec.js +0 -237
- package/dist/minimal-cli.js +0 -88
- package/dist/parallel/parallel-manager.js +0 -433
- package/dist/policy/PolicyManager.js +0 -465
- package/dist/scaffold/claude-hooks.js +0 -443
- package/dist/scaffold/cursor-hooks.js +0 -177
- package/dist/scaffold/git-hooks.js +0 -928
- package/dist/scaffold/index.js +0 -794
- package/dist/session/session-manager.js +0 -653
- package/dist/sidecars/index.js +0 -33
- package/dist/sidecars/listeners.js +0 -40
- package/dist/sidecars/provenance-summary.js +0 -238
- package/dist/sidecars/quality-gaps.js +0 -258
- package/dist/sidecars/schema.js +0 -149
- package/dist/sidecars/spec-drift.js +0 -151
- package/dist/sidecars/waiver-draft.js +0 -176
- package/dist/spec/SpecFileManager.js +0 -419
- package/dist/templates/.caws/schemas/policy.schema.json +0 -112
- package/dist/templates/.caws/schemas/scope.schema.json +0 -52
- package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
- package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
- package/dist/templates/.caws/tools/README.md +0 -18
- package/dist/templates/.caws/tools/scope-guard.js +0 -203
- package/dist/templates/.caws/tools-allow.json +0 -331
- package/dist/templates/.caws/waivers.yml +0 -19
- package/dist/templates/.claude/README.md +0 -190
- package/dist/templates/.claude/hooks/audit.sh +0 -121
- package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/dist/templates/.claude/hooks/classify_command.py +0 -592
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/dist/templates/.claude/hooks/naming-check.sh +0 -100
- package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
- package/dist/templates/.claude/hooks/quality-check.sh +0 -81
- package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
- package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/dist/templates/.claude/hooks/session-log.sh +0 -634
- package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
- package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/dist/templates/.claude/rules/git-safety.md +0 -26
- package/dist/templates/.claude/rules/worktree-isolation.md +0 -83
- package/dist/templates/.claude/settings.json +0 -141
- package/dist/templates/.cursor/README.md +0 -299
- package/dist/templates/.cursor/hooks/audit.sh +0 -55
- package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/dist/templates/.cursor/hooks/format.sh +0 -38
- package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
- package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/dist/templates/.cursor/hooks/session-log.sh +0 -924
- package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/dist/templates/.cursor/hooks.json +0 -76
- package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/dist/templates/.cursor/rules/README.md +0 -148
- package/dist/templates/.github/copilot-instructions.md +0 -82
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/dist/templates/.junie/guidelines.md +0 -73
- package/dist/templates/.vscode/launch.json +0 -17
- package/dist/templates/.vscode/settings.json +0 -95
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/dist/templates/CLAUDE.md +0 -174
- package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
- package/dist/templates/OIDC_SETUP.md +0 -300
- package/dist/templates/agents.md +0 -145
- package/dist/templates/codemod/README.md +0 -1
- package/dist/templates/codemod/test.js +0 -93
- package/dist/templates/docs/README.md +0 -151
- package/dist/templates/scripts/new_feature.sh +0 -80
- package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
- package/dist/test-analysis.js +0 -786
- package/dist/tool-interface.js +0 -314
- package/dist/tool-loader.js +0 -303
- package/dist/tool-validator.js +0 -393
- package/dist/utils/agent-session.js +0 -202
- package/dist/utils/async-utils.js +0 -188
- package/dist/utils/command-wrapper.js +0 -200
- package/dist/utils/event-log.js +0 -584
- package/dist/utils/event-renderer.js +0 -521
- package/dist/utils/finalization.js +0 -230
- package/dist/utils/git-lock.js +0 -119
- package/dist/utils/gitignore-updater.js +0 -158
- package/dist/utils/ide-detection.js +0 -133
- package/dist/utils/lifecycle-events.js +0 -94
- package/dist/utils/project-analysis.js +0 -367
- package/dist/utils/promise-utils.js +0 -72
- package/dist/utils/quality-gates-errors.js +0 -520
- package/dist/utils/quality-gates-utils.js +0 -387
- package/dist/utils/schema-validator.js +0 -50
- package/dist/utils/spec-resolver.js +0 -711
- package/dist/utils/typescript-detector.js +0 -369
- package/dist/utils/working-state.js +0 -530
- package/dist/utils/yaml-validation.js +0 -156
- package/dist/validation/spec-validation.js +0 -921
- package/dist/waivers-manager.js +0 -732
- package/dist/worktree/worktree-manager.js +0 -1374
- package/templates/.caws/schemas/policy.schema.json +0 -112
- package/templates/.caws/schemas/scope.schema.json +0 -52
- package/templates/.caws/schemas/waivers.schema.json +0 -106
- package/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/templates/.caws/templates/working-spec.template.yml +0 -80
- package/templates/.caws/tools/README.md +0 -18
- package/templates/.caws/tools/scope-guard.js +0 -203
- package/templates/.caws/tools-allow.json +0 -331
- package/templates/.caws/waivers.yml +0 -19
- package/templates/.claude/README.md +0 -190
- package/templates/.claude/hooks/audit.sh +0 -121
- package/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/templates/.claude/hooks/classify_command.py +0 -592
- package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/templates/.claude/hooks/naming-check.sh +0 -100
- package/templates/.claude/hooks/protected-paths.sh +0 -39
- package/templates/.claude/hooks/quality-check.sh +0 -81
- package/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/templates/.claude/hooks/scope-guard.sh +0 -381
- package/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/templates/.claude/hooks/session-log.sh +0 -634
- package/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/templates/.claude/hooks/test_classify_command.py +0 -370
- package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/templates/.claude/hooks/validate-spec.sh +0 -76
- package/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/templates/.claude/rules/git-safety.md +0 -26
- package/templates/.claude/rules/worktree-isolation.md +0 -83
- package/templates/.claude/settings.json +0 -141
- package/templates/.cursor/README.md +0 -299
- package/templates/.cursor/hooks/audit.sh +0 -55
- package/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/templates/.cursor/hooks/format.sh +0 -38
- package/templates/.cursor/hooks/naming-check.sh +0 -64
- package/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/templates/.cursor/hooks/session-log.sh +0 -924
- package/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/templates/.cursor/hooks.json +0 -76
- package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/templates/.cursor/rules/README.md +0 -148
- package/templates/.github/copilot-instructions.md +0 -82
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/templates/.junie/guidelines.md +0 -73
- package/templates/.vscode/launch.json +0 -17
- package/templates/.vscode/settings.json +0 -95
- package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/templates/CLAUDE.md +0 -174
- package/templates/COMMIT_CONVENTIONS.md +0 -86
- package/templates/OIDC_SETUP.md +0 -300
- package/templates/agents.md +0 -145
- package/templates/codemod/README.md +0 -1
- package/templates/codemod/test.js +0 -93
- package/templates/docs/README.md +0 -151
- package/templates/scripts/new_feature.sh +0 -80
- package/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
|
@@ -1,592 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Command safety classifier for Claude Code PreToolUse hooks.
|
|
4
|
-
|
|
5
|
-
Segments shell commands, parses them individually, and classifies each
|
|
6
|
-
as allow / confirm / deny based on tiered policy.
|
|
7
|
-
|
|
8
|
-
Output: JSON object with keys:
|
|
9
|
-
decision: "allow" | "ask" | "deny"
|
|
10
|
-
reason: human-readable explanation (empty string for allow)
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
echo "$COMMAND" | python3 classify_command.py [--repo-root DIR] [--home DIR]
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
import re
|
|
21
|
-
import shlex
|
|
22
|
-
import sys
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# ---------------------------------------------------------------------------
|
|
27
|
-
# Configuration
|
|
28
|
-
# ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
# Paths that are safe targets for recursive deletion (relative to repo root).
|
|
31
|
-
# After normalization, if the resolved path starts with one of these, allow.
|
|
32
|
-
SAFE_DELETE_PREFIXES: list[str] = [
|
|
33
|
-
"target/",
|
|
34
|
-
"tmp/",
|
|
35
|
-
".pytest_cache/",
|
|
36
|
-
"node_modules/",
|
|
37
|
-
"__pycache__/",
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
# Pipeline-aware deny patterns: matched against the FULL raw command string
|
|
41
|
-
# BEFORE segmentation. These detect cross-pipeline dangers like curl|sh and
|
|
42
|
-
# fork bombs whose syntax spans segment boundaries.
|
|
43
|
-
DENY_PIPELINE_PATTERNS: list[tuple[str, str]] = [
|
|
44
|
-
# Pipe-to-shell (network exfiltration) — must match across | boundary
|
|
45
|
-
(r"\b(curl|wget)\b.*\|\s*(ba)?sh\b", "pipe-to-shell execution"),
|
|
46
|
-
# Fork bombs — special syntax that segmentation mangles
|
|
47
|
-
(r":\(\)\s*\{.*:\|:.*\}\s*;\s*:", "fork bomb"),
|
|
48
|
-
(r"\bwhile\s+true\b.*\bfork\b", "fork loop"),
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
# Segment-level regex patterns that are always hard-blocked.
|
|
52
|
-
# These are matched against individual parsed command segments, NOT the raw
|
|
53
|
-
# command string. Quoted literals in other segments will not trigger them.
|
|
54
|
-
DENY_SEGMENT_PATTERNS: list[tuple[str, str]] = [
|
|
55
|
-
# System destruction
|
|
56
|
-
(r"\bdd\b.*\bif=/dev/(zero|random)\b", "dd with destructive input"),
|
|
57
|
-
(r"\bmkfs\.", "filesystem format"),
|
|
58
|
-
(r"\bfdisk\b", "disk partitioning"),
|
|
59
|
-
(r">\s*/dev/sd", "raw device write"),
|
|
60
|
-
# Permission escalation
|
|
61
|
-
(r"\bchmod\b.*\+s\b", "setuid/setgid bit"),
|
|
62
|
-
# System control
|
|
63
|
-
(r"\b(shutdown|reboot)\b", "system shutdown/reboot"),
|
|
64
|
-
(r"\binit\s+[06]\b", "system runlevel change"),
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
# Segment-level regex patterns that require user confirmation.
|
|
68
|
-
CONFIRM_SEGMENT_PATTERNS: list[tuple[str, str]] = [
|
|
69
|
-
# Git destructive operations
|
|
70
|
-
(r"\bgit\s+reset\s+--hard\b", "git reset --hard"),
|
|
71
|
-
(r"\bgit\s+push\s+(-f\b|--force\b|--force-with-lease\b)", "git force push"),
|
|
72
|
-
(r"\bgit\s+clean\s+-[a-zA-Z]*f", "git clean with force"),
|
|
73
|
-
(r"\bgit\s+checkout\s+\.\s*$", "git checkout . (discard all changes)"),
|
|
74
|
-
(r"\bgit\s+restore\s+\.\s*$", "git restore . (discard all changes)"),
|
|
75
|
-
(r"\bgit\s+rebase\b", "git rebase (rewrites branch history)"),
|
|
76
|
-
(r"\bgit\s+cherry-pick\b", "git cherry-pick (replays commits across branches)"),
|
|
77
|
-
# chmod 777
|
|
78
|
-
(r"\bchmod\b.*\b777\b", "chmod 777"),
|
|
79
|
-
# History manipulation
|
|
80
|
-
(r"\bhistory\s+-c\b", "history clear"),
|
|
81
|
-
# sudo (not in allowed list)
|
|
82
|
-
(r"^sudo\s+(?!npm|yarn|pnpm|brew|apt-get|apt|dnf|yum)", "sudo command"),
|
|
83
|
-
# venv creation (sprawl prevention)
|
|
84
|
-
(r"\bpython3?\s+-m\s+venv\b", "virtual environment creation"),
|
|
85
|
-
(r"\bvirtualenv\s", "virtual environment creation"),
|
|
86
|
-
(r"\bconda\s+create\b", "conda environment creation"),
|
|
87
|
-
# git init (unless CAWS worktree context)
|
|
88
|
-
(r"\bgit\s+init\b", "git init"),
|
|
89
|
-
# Credential file reads
|
|
90
|
-
(r"\bcat\b.*\.(env|ssh/|aws/)", "credential file read"),
|
|
91
|
-
(r"\bcat\b.*/etc/(passwd|shadow)\b", "system credential read"),
|
|
92
|
-
(r"\bcat\b.*(id_rsa|credentials)\b", "credential file read"),
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# ---------------------------------------------------------------------------
|
|
97
|
-
# Command segmentation
|
|
98
|
-
# ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
def segment_command(raw: str) -> list[str]:
|
|
101
|
-
"""Split a shell command string on &&, ||, ;, | operators.
|
|
102
|
-
|
|
103
|
-
Respects quoted strings so that e.g. git commit -m "rm -rf /" does not
|
|
104
|
-
split inside the quotes. Returns individual command segments with
|
|
105
|
-
leading/trailing whitespace stripped.
|
|
106
|
-
|
|
107
|
-
This is intentionally conservative: if we cannot parse, we return
|
|
108
|
-
the entire string as one segment so it still gets classified.
|
|
109
|
-
"""
|
|
110
|
-
segments: list[str] = []
|
|
111
|
-
current: list[str] = []
|
|
112
|
-
i = 0
|
|
113
|
-
in_single = False
|
|
114
|
-
in_double = False
|
|
115
|
-
in_heredoc: str | None = None
|
|
116
|
-
heredoc_marker: str = ""
|
|
117
|
-
|
|
118
|
-
while i < len(raw):
|
|
119
|
-
ch = raw[i]
|
|
120
|
-
|
|
121
|
-
# ---- heredoc detection ----
|
|
122
|
-
# Look for <<EOF or <<'EOF' or <<"EOF" at segment level
|
|
123
|
-
if not in_single and not in_double and in_heredoc is None:
|
|
124
|
-
if raw[i:i+2] == "<<":
|
|
125
|
-
# Extract the delimiter
|
|
126
|
-
j = i + 2
|
|
127
|
-
while j < len(raw) and raw[j] in (' ', '\t'):
|
|
128
|
-
j += 1
|
|
129
|
-
# Strip optional quotes around delimiter
|
|
130
|
-
quote_char = None
|
|
131
|
-
if j < len(raw) and raw[j] in ("'", '"'):
|
|
132
|
-
quote_char = raw[j]
|
|
133
|
-
j += 1
|
|
134
|
-
k = j
|
|
135
|
-
while k < len(raw) and raw[k] not in (' ', '\t', '\n', "'", '"', ')'):
|
|
136
|
-
k += 1
|
|
137
|
-
if k > j:
|
|
138
|
-
heredoc_marker = raw[j:k]
|
|
139
|
-
in_heredoc = heredoc_marker
|
|
140
|
-
# Skip to end of this line
|
|
141
|
-
nl = raw.find('\n', i)
|
|
142
|
-
if nl >= 0:
|
|
143
|
-
current.append(raw[i:nl+1])
|
|
144
|
-
i = nl + 1
|
|
145
|
-
else:
|
|
146
|
-
current.append(raw[i:])
|
|
147
|
-
i = len(raw)
|
|
148
|
-
continue
|
|
149
|
-
|
|
150
|
-
# ---- inside heredoc: scan for closing marker ----
|
|
151
|
-
if in_heredoc is not None:
|
|
152
|
-
nl = raw.find('\n', i)
|
|
153
|
-
if nl < 0:
|
|
154
|
-
# No newline found, rest is heredoc content
|
|
155
|
-
current.append(raw[i:])
|
|
156
|
-
i = len(raw)
|
|
157
|
-
continue
|
|
158
|
-
line = raw[i:nl]
|
|
159
|
-
current.append(raw[i:nl+1])
|
|
160
|
-
i = nl + 1
|
|
161
|
-
if line.strip() == in_heredoc:
|
|
162
|
-
in_heredoc = None
|
|
163
|
-
continue
|
|
164
|
-
|
|
165
|
-
# ---- quoting ----
|
|
166
|
-
if ch == '\\' and not in_single:
|
|
167
|
-
current.append(raw[i:i+2])
|
|
168
|
-
i += 2
|
|
169
|
-
continue
|
|
170
|
-
if ch == "'" and not in_double:
|
|
171
|
-
in_single = not in_single
|
|
172
|
-
current.append(ch)
|
|
173
|
-
i += 1
|
|
174
|
-
continue
|
|
175
|
-
if ch == '"' and not in_single:
|
|
176
|
-
in_double = not in_double
|
|
177
|
-
current.append(ch)
|
|
178
|
-
i += 1
|
|
179
|
-
continue
|
|
180
|
-
|
|
181
|
-
# ---- segment separators (only outside quotes) ----
|
|
182
|
-
if not in_single and not in_double:
|
|
183
|
-
# && or ||
|
|
184
|
-
if raw[i:i+2] in ('&&', '||'):
|
|
185
|
-
seg = ''.join(current).strip()
|
|
186
|
-
if seg:
|
|
187
|
-
segments.append(seg)
|
|
188
|
-
current = []
|
|
189
|
-
i += 2
|
|
190
|
-
continue
|
|
191
|
-
# ; (but not ;;)
|
|
192
|
-
if ch == ';' and (i + 1 >= len(raw) or raw[i+1] != ';'):
|
|
193
|
-
seg = ''.join(current).strip()
|
|
194
|
-
if seg:
|
|
195
|
-
segments.append(seg)
|
|
196
|
-
current = []
|
|
197
|
-
i += 1
|
|
198
|
-
continue
|
|
199
|
-
# | (but not ||, already handled above)
|
|
200
|
-
if ch == '|':
|
|
201
|
-
seg = ''.join(current).strip()
|
|
202
|
-
if seg:
|
|
203
|
-
segments.append(seg)
|
|
204
|
-
current = []
|
|
205
|
-
i += 1
|
|
206
|
-
continue
|
|
207
|
-
|
|
208
|
-
current.append(ch)
|
|
209
|
-
i += 1
|
|
210
|
-
|
|
211
|
-
seg = ''.join(current).strip()
|
|
212
|
-
if seg:
|
|
213
|
-
segments.append(seg)
|
|
214
|
-
|
|
215
|
-
return segments if segments else [raw.strip()]
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def strip_quotes(s: str) -> str:
|
|
219
|
-
"""Remove surrounding quotes from a shell token."""
|
|
220
|
-
if len(s) >= 2:
|
|
221
|
-
if (s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'"):
|
|
222
|
-
return s[1:-1]
|
|
223
|
-
return s
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def extract_command_word(segment: str) -> str:
|
|
227
|
-
"""Extract the first command word from a segment.
|
|
228
|
-
|
|
229
|
-
Strips leading variable assignments (FOO=bar), env prefixes,
|
|
230
|
-
and common wrappers like 'time'.
|
|
231
|
-
"""
|
|
232
|
-
try:
|
|
233
|
-
tokens = shlex.split(segment)
|
|
234
|
-
except ValueError:
|
|
235
|
-
# Malformed quoting — return raw first word
|
|
236
|
-
return segment.split()[0] if segment.split() else ""
|
|
237
|
-
|
|
238
|
-
for tok in tokens:
|
|
239
|
-
# Skip variable assignments
|
|
240
|
-
if '=' in tok and not tok.startswith('-'):
|
|
241
|
-
continue
|
|
242
|
-
# Skip common prefixes
|
|
243
|
-
if tok in ('env', 'time', 'nice', 'nohup', 'command', 'builtin'):
|
|
244
|
-
continue
|
|
245
|
-
return tok
|
|
246
|
-
return ""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# ---------------------------------------------------------------------------
|
|
250
|
-
# rm classifier
|
|
251
|
-
# ---------------------------------------------------------------------------
|
|
252
|
-
|
|
253
|
-
def is_recursive_rm(segment: str) -> tuple[bool, list[str]]:
|
|
254
|
-
"""Check if a segment is an rm command with recursive flags.
|
|
255
|
-
|
|
256
|
-
Returns (is_recursive, [target_paths]).
|
|
257
|
-
"""
|
|
258
|
-
try:
|
|
259
|
-
tokens = shlex.split(segment)
|
|
260
|
-
except ValueError:
|
|
261
|
-
# Cannot parse — be conservative
|
|
262
|
-
if re.search(r'\brm\b', segment) and re.search(r'-[a-zA-Z]*r', segment):
|
|
263
|
-
return True, []
|
|
264
|
-
return False, []
|
|
265
|
-
|
|
266
|
-
if not tokens:
|
|
267
|
-
return False, []
|
|
268
|
-
|
|
269
|
-
# Find the rm command (skip env/time prefixes)
|
|
270
|
-
rm_idx = -1
|
|
271
|
-
for idx, tok in enumerate(tokens):
|
|
272
|
-
if tok in ('env', 'time', 'nice', 'nohup', 'command', 'builtin'):
|
|
273
|
-
continue
|
|
274
|
-
if '=' in tok and not tok.startswith('-'):
|
|
275
|
-
continue
|
|
276
|
-
if tok == 'rm':
|
|
277
|
-
rm_idx = idx
|
|
278
|
-
break
|
|
279
|
-
|
|
280
|
-
if rm_idx < 0:
|
|
281
|
-
return False, []
|
|
282
|
-
|
|
283
|
-
# Check for recursive flag
|
|
284
|
-
is_recursive = False
|
|
285
|
-
targets: list[str] = []
|
|
286
|
-
i = rm_idx + 1
|
|
287
|
-
while i < len(tokens):
|
|
288
|
-
tok = tokens[i]
|
|
289
|
-
if tok == '--':
|
|
290
|
-
# Everything after -- is targets
|
|
291
|
-
targets.extend(tokens[i+1:])
|
|
292
|
-
break
|
|
293
|
-
if tok.startswith('-') and not tok.startswith('--'):
|
|
294
|
-
if 'r' in tok or 'R' in tok:
|
|
295
|
-
is_recursive = True
|
|
296
|
-
elif tok.startswith('--'):
|
|
297
|
-
if tok == '--recursive':
|
|
298
|
-
is_recursive = True
|
|
299
|
-
# Other long options: skip
|
|
300
|
-
else:
|
|
301
|
-
targets.append(tok)
|
|
302
|
-
i += 1
|
|
303
|
-
|
|
304
|
-
return is_recursive, targets
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def classify_rm_target(
|
|
308
|
-
target: str,
|
|
309
|
-
repo_root: Path,
|
|
310
|
-
home: Path,
|
|
311
|
-
cwd: Path,
|
|
312
|
-
) -> tuple[str, str]:
|
|
313
|
-
"""Classify a single rm target path.
|
|
314
|
-
|
|
315
|
-
Returns ("deny"|"ask"|"allow", reason).
|
|
316
|
-
"""
|
|
317
|
-
# Resolve the target to an absolute path
|
|
318
|
-
raw = target.strip()
|
|
319
|
-
if not raw:
|
|
320
|
-
return "deny", "empty target on recursive delete"
|
|
321
|
-
|
|
322
|
-
# Handle glob-like patterns conservatively
|
|
323
|
-
if any(c in raw for c in ('*', '?', '[', ']')):
|
|
324
|
-
# Check if it is /* or ~/* which are catastrophic
|
|
325
|
-
stripped = raw.rstrip('/')
|
|
326
|
-
if stripped in ('/*', '~/*', './*'):
|
|
327
|
-
return "deny", f"glob expansion at dangerous root: {raw}"
|
|
328
|
-
# Other globs: confirm
|
|
329
|
-
return "ask", f"recursive delete with glob pattern: {raw}"
|
|
330
|
-
|
|
331
|
-
# Resolve path
|
|
332
|
-
try:
|
|
333
|
-
if raw.startswith('~'):
|
|
334
|
-
resolved = (home / raw[2:]).resolve(strict=False) if len(raw) > 1 else home
|
|
335
|
-
elif raw.startswith('/'):
|
|
336
|
-
resolved = Path(raw).resolve(strict=False)
|
|
337
|
-
else:
|
|
338
|
-
resolved = (cwd / raw).resolve(strict=False)
|
|
339
|
-
except (ValueError, OSError):
|
|
340
|
-
return "ask", f"cannot resolve path: {raw}"
|
|
341
|
-
|
|
342
|
-
resolved_str = str(resolved)
|
|
343
|
-
repo_str = str(repo_root)
|
|
344
|
-
home_str = str(home)
|
|
345
|
-
|
|
346
|
-
# Hard-block: root, home, repo root
|
|
347
|
-
if resolved_str == '/':
|
|
348
|
-
return "deny", f"recursive delete targets filesystem root"
|
|
349
|
-
if resolved_str == home_str:
|
|
350
|
-
return "deny", f"recursive delete targets home directory"
|
|
351
|
-
if resolved_str == repo_str:
|
|
352
|
-
return "deny", f"recursive delete targets repository root"
|
|
353
|
-
|
|
354
|
-
# Check if resolved path is a parent of repo or home (even worse)
|
|
355
|
-
if repo_str.startswith(resolved_str + '/'):
|
|
356
|
-
return "deny", f"recursive delete targets ancestor of repository: {raw}"
|
|
357
|
-
if home_str.startswith(resolved_str + '/'):
|
|
358
|
-
return "deny", f"recursive delete targets ancestor of home directory: {raw}"
|
|
359
|
-
|
|
360
|
-
# Allow: known safe prefixes (relative to repo root)
|
|
361
|
-
try:
|
|
362
|
-
rel = resolved.relative_to(repo_root)
|
|
363
|
-
rel_str = str(rel) + '/'
|
|
364
|
-
for prefix in SAFE_DELETE_PREFIXES:
|
|
365
|
-
if rel_str.startswith(prefix):
|
|
366
|
-
return "allow", ""
|
|
367
|
-
except ValueError:
|
|
368
|
-
pass # Not inside repo root
|
|
369
|
-
|
|
370
|
-
# Default: confirm
|
|
371
|
-
return "ask", f"recursive delete: {raw}"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def classify_find_delete(segment: str) -> tuple[str, str] | None:
|
|
375
|
-
"""Check if segment is a find command with -delete or -exec rm.
|
|
376
|
-
|
|
377
|
-
Returns classification tuple or None if not a find-delete.
|
|
378
|
-
"""
|
|
379
|
-
try:
|
|
380
|
-
tokens = shlex.split(segment)
|
|
381
|
-
except ValueError:
|
|
382
|
-
return None
|
|
383
|
-
|
|
384
|
-
cmd = extract_command_word(segment)
|
|
385
|
-
if cmd != 'find':
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
has_delete = '-delete' in tokens
|
|
389
|
-
has_exec_rm = False
|
|
390
|
-
for i, tok in enumerate(tokens):
|
|
391
|
-
if tok == '-exec' and i + 1 < len(tokens) and 'rm' in tokens[i + 1]:
|
|
392
|
-
has_exec_rm = True
|
|
393
|
-
break
|
|
394
|
-
|
|
395
|
-
if not has_delete and not has_exec_rm:
|
|
396
|
-
return None
|
|
397
|
-
|
|
398
|
-
return "ask", f"find with delete action"
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
def strip_quoted_regions(raw: str) -> str:
|
|
402
|
-
"""Remove content inside single/double quotes and heredocs.
|
|
403
|
-
|
|
404
|
-
Returns only the executable shell surface — quoted literals, heredoc
|
|
405
|
-
bodies, and $(...) subshell content embedded in quotes are replaced
|
|
406
|
-
with whitespace so that regex patterns only match actual commands.
|
|
407
|
-
"""
|
|
408
|
-
result: list[str] = []
|
|
409
|
-
i = 0
|
|
410
|
-
in_single = False
|
|
411
|
-
in_double = False
|
|
412
|
-
in_heredoc: str | None = None
|
|
413
|
-
|
|
414
|
-
while i < len(raw):
|
|
415
|
-
ch = raw[i]
|
|
416
|
-
|
|
417
|
-
# Heredoc detection (outside quotes)
|
|
418
|
-
if not in_single and not in_double and in_heredoc is None:
|
|
419
|
-
if raw[i:i+2] == "<<":
|
|
420
|
-
j = i + 2
|
|
421
|
-
while j < len(raw) and raw[j] in (' ', '\t'):
|
|
422
|
-
j += 1
|
|
423
|
-
if j < len(raw) and raw[j] in ("'", '"'):
|
|
424
|
-
j += 1
|
|
425
|
-
k = j
|
|
426
|
-
while k < len(raw) and raw[k] not in (' ', '\t', '\n', "'", '"', ')'):
|
|
427
|
-
k += 1
|
|
428
|
-
if k > j:
|
|
429
|
-
in_heredoc = raw[j:k]
|
|
430
|
-
# Keep the << marker but skip to end of line
|
|
431
|
-
result.append(raw[i:i+2])
|
|
432
|
-
nl = raw.find('\n', i)
|
|
433
|
-
if nl >= 0:
|
|
434
|
-
i = nl + 1
|
|
435
|
-
else:
|
|
436
|
-
i = len(raw)
|
|
437
|
-
continue
|
|
438
|
-
|
|
439
|
-
# Inside heredoc: skip until closing marker
|
|
440
|
-
if in_heredoc is not None:
|
|
441
|
-
nl = raw.find('\n', i)
|
|
442
|
-
if nl < 0:
|
|
443
|
-
i = len(raw)
|
|
444
|
-
continue
|
|
445
|
-
line = raw[i:nl]
|
|
446
|
-
i = nl + 1
|
|
447
|
-
if line.strip() == in_heredoc:
|
|
448
|
-
in_heredoc = None
|
|
449
|
-
else:
|
|
450
|
-
result.append(' ') # placeholder
|
|
451
|
-
continue
|
|
452
|
-
|
|
453
|
-
# Escape handling
|
|
454
|
-
if ch == '\\' and not in_single:
|
|
455
|
-
result.append(' ')
|
|
456
|
-
i += 2
|
|
457
|
-
continue
|
|
458
|
-
|
|
459
|
-
# Quote tracking
|
|
460
|
-
if ch == "'" and not in_double:
|
|
461
|
-
if in_single:
|
|
462
|
-
in_single = False
|
|
463
|
-
else:
|
|
464
|
-
in_single = True
|
|
465
|
-
i += 1
|
|
466
|
-
continue
|
|
467
|
-
if ch == '"' and not in_single:
|
|
468
|
-
if in_double:
|
|
469
|
-
in_double = False
|
|
470
|
-
else:
|
|
471
|
-
in_double = True
|
|
472
|
-
i += 1
|
|
473
|
-
continue
|
|
474
|
-
|
|
475
|
-
# Inside quotes: replace with space
|
|
476
|
-
if in_single or in_double:
|
|
477
|
-
result.append(' ')
|
|
478
|
-
i += 1
|
|
479
|
-
continue
|
|
480
|
-
|
|
481
|
-
result.append(ch)
|
|
482
|
-
i += 1
|
|
483
|
-
|
|
484
|
-
return ''.join(result)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
# ---------------------------------------------------------------------------
|
|
488
|
-
# Main classifier
|
|
489
|
-
# ---------------------------------------------------------------------------
|
|
490
|
-
|
|
491
|
-
def classify_command(
|
|
492
|
-
raw_command: str,
|
|
493
|
-
repo_root: Path,
|
|
494
|
-
home: Path,
|
|
495
|
-
cwd: Path,
|
|
496
|
-
caws_worktree: bool = False,
|
|
497
|
-
) -> tuple[str, str]:
|
|
498
|
-
"""Classify a full command string.
|
|
499
|
-
|
|
500
|
-
Returns the most restrictive (decision, reason) across all segments.
|
|
501
|
-
Priority: deny > ask > allow.
|
|
502
|
-
"""
|
|
503
|
-
worst_decision = "allow"
|
|
504
|
-
worst_reason = ""
|
|
505
|
-
|
|
506
|
-
def escalate(decision: str, reason: str) -> None:
|
|
507
|
-
nonlocal worst_decision, worst_reason
|
|
508
|
-
priority = {"allow": 0, "ask": 1, "deny": 2}
|
|
509
|
-
if priority.get(decision, 0) > priority.get(worst_decision, 0):
|
|
510
|
-
worst_decision = decision
|
|
511
|
-
worst_reason = reason
|
|
512
|
-
|
|
513
|
-
# --- Pipeline-aware deny patterns ---
|
|
514
|
-
# Strip quoted regions so patterns only match executable shell surface.
|
|
515
|
-
# This prevents commit messages, echo arguments, etc. from triggering.
|
|
516
|
-
executable_surface = strip_quoted_regions(raw_command)
|
|
517
|
-
for pattern, desc in DENY_PIPELINE_PATTERNS:
|
|
518
|
-
if re.search(pattern, executable_surface, re.IGNORECASE):
|
|
519
|
-
escalate("deny", desc)
|
|
520
|
-
|
|
521
|
-
segments = segment_command(raw_command)
|
|
522
|
-
|
|
523
|
-
for segment in segments:
|
|
524
|
-
# Strip quoted regions for pattern matching so that e.g.
|
|
525
|
-
# echo "git reset --hard" does not trigger the git pattern.
|
|
526
|
-
# The original segment is still used for rm/find parsing
|
|
527
|
-
# (shlex.split handles quotes correctly for argument extraction).
|
|
528
|
-
segment_surface = strip_quoted_regions(segment)
|
|
529
|
-
|
|
530
|
-
# --- Hard-block patterns (segment-level) ---
|
|
531
|
-
for pattern, desc in DENY_SEGMENT_PATTERNS:
|
|
532
|
-
if re.search(pattern, segment_surface, re.IGNORECASE):
|
|
533
|
-
escalate("deny", desc)
|
|
534
|
-
|
|
535
|
-
# --- Confirm patterns (segment-level) ---
|
|
536
|
-
for pattern, desc in CONFIRM_SEGMENT_PATTERNS:
|
|
537
|
-
if re.search(pattern, segment_surface, re.IGNORECASE):
|
|
538
|
-
# Special case: git init in worktree context is allowed
|
|
539
|
-
if "git init" in desc and caws_worktree:
|
|
540
|
-
continue
|
|
541
|
-
escalate("ask", desc)
|
|
542
|
-
|
|
543
|
-
# --- rm classifier ---
|
|
544
|
-
is_recursive, targets = is_recursive_rm(segment)
|
|
545
|
-
if is_recursive:
|
|
546
|
-
if not targets:
|
|
547
|
-
# Cannot determine targets — be conservative
|
|
548
|
-
escalate("ask", "recursive delete with unparseable targets")
|
|
549
|
-
else:
|
|
550
|
-
for target in targets:
|
|
551
|
-
decision, reason = classify_rm_target(
|
|
552
|
-
target, repo_root, home, cwd,
|
|
553
|
-
)
|
|
554
|
-
escalate(decision, reason)
|
|
555
|
-
|
|
556
|
-
# --- find -delete classifier ---
|
|
557
|
-
find_result = classify_find_delete(segment)
|
|
558
|
-
if find_result:
|
|
559
|
-
escalate(*find_result)
|
|
560
|
-
|
|
561
|
-
return worst_decision, worst_reason
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
# ---------------------------------------------------------------------------
|
|
565
|
-
# Entry point
|
|
566
|
-
# ---------------------------------------------------------------------------
|
|
567
|
-
|
|
568
|
-
def main() -> None:
|
|
569
|
-
import argparse
|
|
570
|
-
|
|
571
|
-
parser = argparse.ArgumentParser(description="Classify shell command safety")
|
|
572
|
-
parser.add_argument("--repo-root", default=os.environ.get("CLAUDE_PROJECT_DIR", "."))
|
|
573
|
-
parser.add_argument("--home", default=str(Path.home()))
|
|
574
|
-
parser.add_argument("--cwd", default=os.getcwd())
|
|
575
|
-
args = parser.parse_args()
|
|
576
|
-
|
|
577
|
-
raw_command = sys.stdin.read()
|
|
578
|
-
|
|
579
|
-
repo_root = Path(args.repo_root).resolve(strict=False)
|
|
580
|
-
home = Path(args.home).resolve(strict=False)
|
|
581
|
-
cwd = Path(args.cwd).resolve(strict=False)
|
|
582
|
-
caws_worktree = os.environ.get("CAWS_WORKTREE_CONTEXT", "0") == "1"
|
|
583
|
-
|
|
584
|
-
decision, reason = classify_command(
|
|
585
|
-
raw_command, repo_root, home, cwd, caws_worktree,
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
json.dump({"decision": decision, "reason": reason}, sys.stdout)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if __name__ == "__main__":
|
|
592
|
-
main()
|