@mmerterden/multi-agent-pipeline 20.34.0 → 20.35.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/CHANGELOG.md +55 -1
- package/README.md +1 -0
- package/docs/facts.json +2 -2
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +16 -1
- package/install/templates/claude-hooks.json +23 -1
- package/manifest.json +324 -293
- package/package.json +1 -1
- package/pipeline/agents/bulk-reader.md +3 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/office-on/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/office-status/SKILL.md +3 -2
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/README.md +1 -0
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/manifest.json +14 -1
- package/pipeline/contract/types/index.d.ts +24 -2
- package/pipeline/lib/json-file-lock.mjs +61 -15
- package/pipeline/lib/office-remote.mjs +228 -0
- package/pipeline/lib/qr-code.mjs +360 -0
- package/pipeline/lib/repo-hygiene.sh +1 -0
- package/pipeline/lib/run-completion.mjs +17 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
- package/pipeline/multi-agent-refs/features/office-remote-protocol.md +166 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +1 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +16 -1
- package/pipeline/multi-agent-refs/features/verify-by-test.md +10 -0
- package/pipeline/multi-agent-refs/keychain.md +7 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -6
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -12
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +9 -8
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +1 -4
- package/pipeline/multi-agent-refs/rules.md +3 -2
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/char_12.png +0 -0
- package/pipeline/office/assets/char_13.png +0 -0
- package/pipeline/office/assets/char_14.png +0 -0
- package/pipeline/office/assets/char_15.png +0 -0
- package/pipeline/office/assets/char_16.png +0 -0
- package/pipeline/office/assets/char_17.png +0 -0
- package/pipeline/office/assets/char_18.png +0 -0
- package/pipeline/office/assets/char_19.png +0 -0
- package/pipeline/office/assets/char_20.png +0 -0
- package/pipeline/office/assets/char_21.png +0 -0
- package/pipeline/office/assets/char_22.png +0 -0
- package/pipeline/office/assets/char_23.png +0 -0
- package/pipeline/office/assets/char_24.png +0 -0
- package/pipeline/office/assets/char_25.png +0 -0
- package/pipeline/office/assets/char_26.png +0 -0
- package/pipeline/office/assets/char_27.png +0 -0
- package/pipeline/office/assets/char_28.png +0 -0
- package/pipeline/office/assets/char_29.png +0 -0
- package/pipeline/office/assets/char_30.png +0 -0
- package/pipeline/office/index.html +293 -49
- package/pipeline/schemas/agent-state.schema.json +18 -0
- package/pipeline/schemas/planning-output.schema.json +18 -0
- package/pipeline/schemas/pr-reach.schema.json +55 -0
- package/pipeline/schemas/prefs.schema.json +13 -7
- package/pipeline/schemas/triage-output.schema.json +12 -1
- package/pipeline/schemas/unattended-policy.json +2 -1
- package/pipeline/scripts/_guard_rules.py +449 -1
- package/pipeline/scripts/_test-gaming.mjs +36 -0
- package/pipeline/scripts/agent-guard.py +79 -19
- package/pipeline/scripts/anonymize-findings.mjs +99 -0
- package/pipeline/scripts/answer-check.mjs +113 -0
- package/pipeline/scripts/answer-check.sh +20 -0
- package/pipeline/scripts/autopilot-publish.mjs +9 -2
- package/pipeline/scripts/contract-server.mjs +35 -1
- package/pipeline/scripts/gate-ledger.mjs +144 -21
- package/pipeline/scripts/office-build.mjs +276 -5
- package/pipeline/scripts/office-control.mjs +70 -10
- package/pipeline/scripts/office-pair.mjs +407 -0
- package/pipeline/scripts/office-remote-client.mjs +463 -0
- package/pipeline/scripts/phase-tracker.sh +8 -0
- package/pipeline/scripts/phase0-exit-gate.mjs +40 -37
- package/pipeline/scripts/phone-devices.mjs +9 -1
- package/pipeline/scripts/pre-commit-check.sh +28 -2
- package/pipeline/scripts/pre-push-check.sh +6 -12
- package/pipeline/scripts/rework-base.mjs +87 -0
- package/pipeline/scripts/run-commit-check.mjs +104 -0
- package/pipeline/scripts/run-scope-guard.mjs +58 -3
- package/pipeline/scripts/run-scope-guard.sh +13 -1
- package/pipeline/scripts/scope-check-gate.mjs +70 -6
- package/pipeline/scripts/task-mirror.mjs +106 -15
- package/pipeline/scripts/task-mirror.sh +3 -2
- package/pipeline/scripts/test-stability-gate.mjs +112 -0
- package/pipeline/scripts/update-check.sh +7 -1
- package/pipeline/scripts/update-issue-progress.sh +73 -8
- package/pipeline/scripts/validate-planning.mjs +105 -1
- package/pipeline/scripts/validate-triage.mjs +42 -2
- package/pipeline/scripts/work-items.mjs +119 -1
- package/pipeline/scripts/worktree-prepare-all.mjs +200 -0
- package/pipeline/scripts/worktree-prepare.sh +40 -4
- package/pipeline/scripts/write-state.mjs +67 -24
- package/pipeline/skills/.skill-manifest.json +5 -5
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +5 -5
- package/pipeline/skills/shared/core/google-play-compliance/references/rules.yml +1 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +2 -2
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +14 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +6 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +10 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +11 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +8 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/references/patterns.md +6 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +10 -0
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +13 -0
- package/pipeline/skills/shared/external/android-security/references/patterns.md +16 -0
- package/pipeline/skills/shared/external/api-patterns/references/contract-details.md +9 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/abuse-controls.md +12 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/identity-and-access.md +13 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/request-handling.md +12 -0
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +9 -0
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +8 -0
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +10 -0
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +21 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +9 -0
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +7 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +2 -2
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +10 -0
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +10 -1
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +22 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +17 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +22 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +17 -0
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +9 -0
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +11 -0
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +13 -0
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +9 -0
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +10 -0
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +15 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +7 -0
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +7 -0
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +15 -0
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +9 -0
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +11 -0
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +10 -0
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +13 -0
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +20 -0
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +16 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +16 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +14 -0
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +13 -0
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +10 -0
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +10 -0
- package/pipeline/skills/shared/external/fastapi-pro/references/app-structure.md +10 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +17 -0
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +12 -0
- package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +10 -0
- package/pipeline/skills/shared/external/hig-components-content/references/content-views.md +10 -0
- package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +11 -0
- package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +13 -0
- package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +13 -0
- package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +11 -0
- package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +12 -0
- package/pipeline/skills/shared/external/hig-technologies/references/technologies.md +16 -0
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +18 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +9 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +7 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +13 -0
- package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +9 -0
- package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +15 -0
- package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +11 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +11 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +15 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +11 -0
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +18 -0
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +19 -0
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +15 -0
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +7 -0
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +8 -0
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +12 -0
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +17 -0
- package/pipeline/skills/shared/external/ios-security/references/app-review-guidelines.md +8 -0
- package/pipeline/skills/shared/external/ios-security/references/cryptokit-advanced.md +14 -0
- package/pipeline/skills/shared/external/ios-security/references/file-storage-patterns.md +8 -0
- package/pipeline/skills/shared/external/ios-security/references/privacy-manifest.md +9 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +9 -0
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +13 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/references/patterns.md +8 -0
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +21 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +9 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +12 -0
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +16 -0
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +21 -0
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +10 -0
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +9 -0
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +12 -0
- package/pipeline/skills/shared/external/nodejs-backend-patterns/references/runtime-patterns.md +11 -0
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +11 -0
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +18 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +15 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +14 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +14 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +14 -0
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +14 -0
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +10 -0
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +12 -0
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +14 -0
- package/pipeline/skills/shared/external/room-database/references/patterns.md +16 -0
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +10 -0
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +10 -0
- package/pipeline/skills/shared/external/spm-build-analysis/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/spm-build-analysis/references/spm-analysis-checks.md +19 -0
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +13 -0
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +31 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +8 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +9 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +10 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +8 -0
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +24 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +8 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +14 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +12 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-expert/references/swift-6-2-concurrency.md +8 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/actors.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/cancellation.md +7 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/interop.md +10 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/new-features.md +11 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/structured.md +9 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/testing.md +13 -0
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +7 -0
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +14 -0
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +19 -0
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +19 -0
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +18 -0
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +17 -0
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +18 -0
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +21 -0
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +16 -0
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +16 -0
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +9 -0
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +24 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/async-tests.md +9 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +11 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/writing-better-tests.md +13 -0
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +8 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +17 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +12 -0
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +11 -0
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +7 -0
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +13 -0
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +24 -0
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +12 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +15 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +15 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/accessibility-patterns.md +11 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +7 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +13 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts-accessibility.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts.md +17 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/image-optimization.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/latest-apis.md +10 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/layout-best-practices.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/list-patterns.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-scenes.md +13 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-views.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-window-styling.md +12 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/performance-patterns.md +7 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/scroll-patterns.md +9 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/sheet-navigation-patterns.md +10 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/state-management.md +18 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/view-structure.md +16 -0
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +11 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +8 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +14 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +9 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +22 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +14 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +10 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +10 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +13 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +10 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +14 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +30 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +16 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +11 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +11 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +10 -0
- package/pipeline/skills/shared/external/swiftui-performance-audit/references/code-smells.md +8 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/app-wiring.md +12 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/menu-bar.md +10 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/navigationstack.md +11 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/scroll-reveal.md +12 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/sheets.md +14 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/tabview.md +10 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +10 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +13 -0
- package/pipeline/skills/shared/external/swiftui-view-refactor/references/mv-patterns.md +12 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +8 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +8 -0
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +14 -0
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +17 -0
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +11 -0
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +11 -0
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +27 -0
- package/pipeline/skills/shared/external/xcode-build-fixer/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-build-fixer/references/fix-patterns.md +7 -0
- package/pipeline/skills/shared/external/xcode-build-orchestrator/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/xcode-compilation-analyzer/references/code-compilation-checks.md +8 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-optimization-sources.md +14 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/build-settings-best-practices.md +9 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/references/project-audit-checks.md +13 -0
|
@@ -32,6 +32,8 @@ import re
|
|
|
32
32
|
import shlex
|
|
33
33
|
import time
|
|
34
34
|
|
|
35
|
+
RUN_STATE = "agent-state.json"
|
|
36
|
+
|
|
35
37
|
MAX_FILE_BYTES = 256 * 1024
|
|
36
38
|
CONFIRM_TTL_SECONDS = 600
|
|
37
39
|
|
|
@@ -47,6 +49,16 @@ AI_GENERATED = re.compile(
|
|
|
47
49
|
)
|
|
48
50
|
JIRA_TRANSITION = re.compile(r"/rest/api/(?:\d+|latest)/issue/[^/\s'\"]+/transitions\b", re.IGNORECASE)
|
|
49
51
|
POST_METHOD = re.compile(r"(?:^|\s)(?:-X|--request)\s*=?\s*['\"]?post\b", re.IGNORECASE)
|
|
52
|
+
WRITE_METHOD = re.compile(r"(?:^|\s)(?:-X|--request)\s*=?\s*['\"]?(?:post|put|patch)\b", re.IGNORECASE)
|
|
53
|
+
PUT_METHOD = re.compile(r"(?:^|\s)(?:-X|--request)\s*=?\s*['\"]?put\b", re.IGNORECASE)
|
|
54
|
+
SHARED_WRITE = re.compile(
|
|
55
|
+
r"/rest/api/(?:\d+|latest)/issue/[^/\s'\"]+/comment\b|/rest/api/content\b|hooks\.slack\.com/services/",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
PR_UPDATE = re.compile(
|
|
59
|
+
r"/rest/api/(?:[\d.]+|latest)/projects/[^/\s'\"]+/repos/[^/\s'\"]+/pull-requests/\d+(?=[\s'\"?]|$)"
|
|
60
|
+
)
|
|
61
|
+
DATA_FLAGS = {"-d", "--data", "--data-binary", "--data-raw", "--data-ascii"}
|
|
50
62
|
BODY_FLAG = re.compile(r"(?:^|\s)(?:-d|--data[\w-]*|--json|-F|--form)(?:\s|=|$)")
|
|
51
63
|
HEREDOC_OPEN = re.compile(r"<<(-?)\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\2")
|
|
52
64
|
ASSIGN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
@@ -446,6 +458,7 @@ def _gh_ask(argv, cwd):
|
|
|
446
458
|
if group == "pr" and sub in ("create", "edit"):
|
|
447
459
|
bodies = _flag_values(cleaned, {"--body"}, "-b")
|
|
448
460
|
bodies += [read_bounded(p, cwd) for p in _flag_values(cleaned, {"--body-file"}, "-F")]
|
|
461
|
+
bodies += _flag_values(cleaned, {"--title"}, "-t")
|
|
449
462
|
if any(CLOSING.search(b or "") for b in bodies):
|
|
450
463
|
return (
|
|
451
464
|
f"gh pr {sub} with a closing keyword (Closes/Fixes/Resolves #N) closes the issue on merge; "
|
|
@@ -468,10 +481,445 @@ def _jira_ask(argv, seg):
|
|
|
468
481
|
return None
|
|
469
482
|
|
|
470
483
|
|
|
484
|
+
def _shared_ask(argv, seg):
|
|
485
|
+
"""A comment, page or message posted to a system other people read."""
|
|
486
|
+
if not argv or os.path.basename(argv[0]) not in HTTP_CLIENTS:
|
|
487
|
+
return None
|
|
488
|
+
if not SHARED_WRITE.search(seg):
|
|
489
|
+
return None
|
|
490
|
+
if WRITE_METHOD.search(seg) or BODY_FLAG.search(seg):
|
|
491
|
+
return "this posts to a shared system (Jira comment, Confluence page or Slack); the user confirms each post"
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _payload(argv, cwd):
|
|
496
|
+
"""The request body a curl-like command sends, read from @file when given."""
|
|
497
|
+
for i, a in enumerate(argv):
|
|
498
|
+
val = None
|
|
499
|
+
if a in DATA_FLAGS and i + 1 < len(argv):
|
|
500
|
+
val = argv[i + 1]
|
|
501
|
+
else:
|
|
502
|
+
for f in DATA_FLAGS:
|
|
503
|
+
if a.startswith(f + "="):
|
|
504
|
+
val = a.split("=", 1)[1]
|
|
505
|
+
if val is not None:
|
|
506
|
+
return read_bounded(val[1:], cwd) if val.startswith("@") else val
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def pr_update_reason(toks, seg, cwd=None):
|
|
511
|
+
"""A Bitbucket pull request PUT replaces the whole record: a body without
|
|
512
|
+
reviewers, fromRef or toRef wipes them."""
|
|
513
|
+
_, argv = command_word(toks)
|
|
514
|
+
if not argv or os.path.basename(argv[0]) not in HTTP_CLIENTS:
|
|
515
|
+
return None
|
|
516
|
+
if not PR_UPDATE.search(seg) or not PUT_METHOD.search(seg):
|
|
517
|
+
return None
|
|
518
|
+
body = _payload(argv, cwd)
|
|
519
|
+
try:
|
|
520
|
+
doc = json.loads(body) if body else None
|
|
521
|
+
except ValueError:
|
|
522
|
+
doc = None
|
|
523
|
+
if not isinstance(doc, dict):
|
|
524
|
+
return "this pull request update sends no readable JSON body; send the full record with reviewers, fromRef and toRef"
|
|
525
|
+
missing = [k for k in ("fromRef", "toRef") if not doc.get(k)]
|
|
526
|
+
if not doc.get("reviewers"):
|
|
527
|
+
missing.insert(0, "reviewers")
|
|
528
|
+
if missing:
|
|
529
|
+
return (
|
|
530
|
+
"a pull request PUT replaces the record, and this body has no "
|
|
531
|
+
+ ", ".join(missing)
|
|
532
|
+
+ "; read the current pull request and send its reviewers, fromRef, toRef and draft back"
|
|
533
|
+
)
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
AGENT_NAME = re.compile(r"^[A-Za-z0-9._:-]{1,80}$")
|
|
538
|
+
GH_WRITE_SUBS = {
|
|
539
|
+
("pr", "create"), ("pr", "edit"), ("pr", "comment"), ("pr", "review"), ("pr", "merge"),
|
|
540
|
+
("pr", "close"), ("pr", "ready"), ("issue", "create"), ("issue", "edit"),
|
|
541
|
+
("issue", "comment"), ("issue", "close"), ("issue", "reopen"), ("release", "create"),
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def read_only_agent(agent_type, env=None):
|
|
546
|
+
"""True when the subagent's definition denies it the Write tool: it reviews,
|
|
547
|
+
reads or reports, and anything it does outside its answer is a side effect."""
|
|
548
|
+
if not isinstance(agent_type, str) or not AGENT_NAME.match(agent_type):
|
|
549
|
+
return False
|
|
550
|
+
env = os.environ if env is None else env
|
|
551
|
+
home = env.get("HOME") or os.path.expanduser("~")
|
|
552
|
+
name = agent_type.split(":")[-1]
|
|
553
|
+
for root in (os.path.join(home, ".claude", "agents"), os.path.join(os.getcwd(), ".claude", "agents")):
|
|
554
|
+
text = read_bounded(os.path.join(root, name + ".md"), None)
|
|
555
|
+
if not text.startswith("---"):
|
|
556
|
+
continue
|
|
557
|
+
head = text.split("---", 2)[1] if text.count("---") >= 2 else ""
|
|
558
|
+
m = re.search(r"^disallowedTools:\s*(.+)$", head, re.MULTILINE)
|
|
559
|
+
return bool(m and re.search(r"\bWrite\b", m.group(1)))
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def subagent_side_effect(cmd):
|
|
564
|
+
"""What in this command reaches past a read-only subagent's answer, or None."""
|
|
565
|
+
stripped, _ = strip_heredoc_bodies(cmd)
|
|
566
|
+
if re.search(r"write-state\.mjs", stripped):
|
|
567
|
+
return "it writes run state (write-state.mjs)"
|
|
568
|
+
for seg in split_simple(stripped):
|
|
569
|
+
toks = tokens(seg)
|
|
570
|
+
split = _git_split(toks)
|
|
571
|
+
if split and split[2] in WRITE_SUBS:
|
|
572
|
+
return f"it runs git {split[2]}"
|
|
573
|
+
_, argv = command_word(toks)
|
|
574
|
+
if not argv:
|
|
575
|
+
continue
|
|
576
|
+
base = os.path.basename(argv[0])
|
|
577
|
+
if base in HTTP_CLIENTS and (WRITE_METHOD.search(seg) or BODY_FLAG.search(seg)):
|
|
578
|
+
return f"it sends a write request with {base}"
|
|
579
|
+
if base == "gh":
|
|
580
|
+
plains = [a for a in argv[1:] if not a.startswith("-")]
|
|
581
|
+
if len(plains) >= 2 and (GH_ALIASES.get(plains[0], plains[0]), plains[1]) in GH_WRITE_SUBS:
|
|
582
|
+
return f"it runs gh {plains[0]} {plains[1]}"
|
|
583
|
+
if plains[:1] == ["api"] and (
|
|
584
|
+
re.search(r"(?:^|\s)(?:-X|--method)\s*=?\s*(?:post|put|patch|delete)\b", seg, re.I)
|
|
585
|
+
or re.search(r"(?:^|\s)(?:-f|-F|--field|--raw-field|--input)(?:\s|=)", seg)
|
|
586
|
+
):
|
|
587
|
+
return "it writes through gh api"
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
CLEANUP_SCRIPTS = {"purge.sh": None, "prune-logs.sh": "--yes", "run-kill.mjs": "apply"}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _cleanup_ask(argv):
|
|
595
|
+
"""purge, prune-logs --yes and run-kill apply delete run history for good."""
|
|
596
|
+
if not argv:
|
|
597
|
+
return None
|
|
598
|
+
rest = argv
|
|
599
|
+
if os.path.basename(argv[0]) in ("bash", "sh", "zsh", "node"):
|
|
600
|
+
rest = argv[1:]
|
|
601
|
+
if not rest:
|
|
602
|
+
return None
|
|
603
|
+
name = os.path.basename(rest[0])
|
|
604
|
+
if name not in CLEANUP_SCRIPTS:
|
|
605
|
+
return None
|
|
606
|
+
needs = CLEANUP_SCRIPTS[name]
|
|
607
|
+
if needs and needs not in rest[1:]:
|
|
608
|
+
return None
|
|
609
|
+
return f"{name} deletes run logs or worktrees for good; the user confirms each clean-up"
|
|
610
|
+
|
|
611
|
+
|
|
471
612
|
def ask_reason(toks, seg, cwd=None):
|
|
472
613
|
"""Why a person should confirm this segment, or None."""
|
|
473
614
|
_, argv = command_word(toks)
|
|
474
|
-
return _gh_ask(argv, cwd) or _jira_ask(argv, seg)
|
|
615
|
+
return _gh_ask(argv, cwd) or _jira_ask(argv, seg) or _shared_ask(argv, seg) or _cleanup_ask(argv)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
GH_TEXT_SUBS = {
|
|
619
|
+
("pr", "create"), ("pr", "edit"), ("pr", "comment"), ("pr", "review"),
|
|
620
|
+
("issue", "create"), ("issue", "edit"), ("issue", "comment"),
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def gh_text(toks, cwd=None):
|
|
625
|
+
"""The title and body text a gh pull request or issue write publishes."""
|
|
626
|
+
_, argv = command_word(toks)
|
|
627
|
+
if not argv or os.path.basename(argv[0]) != "gh":
|
|
628
|
+
return ""
|
|
629
|
+
args = [a for a in argv[1:]]
|
|
630
|
+
plains = [a for a in args if not a.startswith("-")]
|
|
631
|
+
if len(plains) < 2:
|
|
632
|
+
return ""
|
|
633
|
+
if (GH_ALIASES.get(plains[0], plains[0]), plains[1]) not in GH_TEXT_SUBS:
|
|
634
|
+
return ""
|
|
635
|
+
parts = _flag_values(args, {"--body", "--title"}, "-b") + _flag_values(args, {"--title"}, "-t")
|
|
636
|
+
parts += [read_bounded(p, cwd) for p in _flag_values(args, {"--body-file"}, "-F")]
|
|
637
|
+
return "\n".join(p for p in parts if p)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def vercel_token_reason(toks):
|
|
641
|
+
"""A --token on the vercel command line lands in the transcript."""
|
|
642
|
+
_, argv = command_word(toks)
|
|
643
|
+
if not argv:
|
|
644
|
+
return None
|
|
645
|
+
words = [os.path.basename(argv[0])] + argv[1:3]
|
|
646
|
+
if "vercel" not in words:
|
|
647
|
+
return None
|
|
648
|
+
if any(a in ("--token", "-t") or a.startswith("--token=") for a in argv[1:]):
|
|
649
|
+
return (
|
|
650
|
+
"vercel with --token puts the token on the command line; run it through the "
|
|
651
|
+
"vercel wrapper, which reads the token from the credential store"
|
|
652
|
+
)
|
|
653
|
+
return None
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
SECRET_READ = re.compile(
|
|
657
|
+
r"credential-store\.sh['\"]?\s+get\b|\bsecurity\s+find-(?:generic|internet)-password\b[^;&|\n]*\s-w\b"
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def secret_echo_reason(cmd):
|
|
662
|
+
"""A stored secret read with nothing capturing or consuming its output prints
|
|
663
|
+
it into the transcript."""
|
|
664
|
+
stripped, _ = strip_heredoc_bodies(cmd)
|
|
665
|
+
for m in SECRET_READ.finditer(stripped):
|
|
666
|
+
before = stripped[: m.start()]
|
|
667
|
+
if before.count("$(") > before.count(")") or before.count("`") % 2:
|
|
668
|
+
continue
|
|
669
|
+
tail = re.split(r";|&&|\|\||\n", stripped[m.end():], maxsplit=1)[0]
|
|
670
|
+
if re.search(r"(?<!\|)\|(?!\|)|>", tail):
|
|
671
|
+
continue
|
|
672
|
+
return (
|
|
673
|
+
"this prints a stored secret into the transcript; capture it in a variable "
|
|
674
|
+
"(VAR=\"$(...)\"), pipe it into the consumer, or send it to /dev/null"
|
|
675
|
+
)
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
# --- the run this session drives ----------------------------------------------
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _logs_root(env=None):
|
|
683
|
+
env = os.environ if env is None else env
|
|
684
|
+
return env.get("LOGS_ROOT") or os.path.join(os.path.expanduser("~"), ".claude", "logs", "multi-agent")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
LIVE_STATUSES = ("in_progress", "awaiting_input")
|
|
688
|
+
_DRIVING = {}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def driving_run(session_id, env=None):
|
|
692
|
+
"""The live run whose hostSessionId is this session, or None: status
|
|
693
|
+
in_progress or awaiting_input (the statuses _active-run.mjs treats as
|
|
694
|
+
live), the most recently written one when several match. Read once per
|
|
695
|
+
hook process.
|
|
696
|
+
|
|
697
|
+
Runs live at <root>/<project>/<task>/agent-state.json or <root>/<task>/; both
|
|
698
|
+
depths are read. A missing session id matches nothing."""
|
|
699
|
+
if not isinstance(session_id, str) or not session_id:
|
|
700
|
+
return None
|
|
701
|
+
root = _logs_root(env)
|
|
702
|
+
key = (root, session_id)
|
|
703
|
+
if key not in _DRIVING:
|
|
704
|
+
_DRIVING[key] = _scan_driving(root, session_id)
|
|
705
|
+
return _DRIVING[key]
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _scan_driving(root, session_id):
|
|
709
|
+
candidates = []
|
|
710
|
+
try:
|
|
711
|
+
for a in os.listdir(root):
|
|
712
|
+
pa = os.path.join(root, a)
|
|
713
|
+
if not os.path.isdir(pa):
|
|
714
|
+
continue
|
|
715
|
+
candidates.append(os.path.join(pa, RUN_STATE))
|
|
716
|
+
try:
|
|
717
|
+
for b in os.listdir(pa):
|
|
718
|
+
candidates.append(os.path.join(pa, b, RUN_STATE))
|
|
719
|
+
except OSError:
|
|
720
|
+
continue
|
|
721
|
+
except OSError:
|
|
722
|
+
return None
|
|
723
|
+
best, best_t = None, -1.0
|
|
724
|
+
for path in candidates:
|
|
725
|
+
try:
|
|
726
|
+
st = os.stat(path)
|
|
727
|
+
if st.st_size > 4 * 1024 * 1024:
|
|
728
|
+
continue
|
|
729
|
+
with open(path, encoding="utf-8") as fh:
|
|
730
|
+
doc = json.load(fh)
|
|
731
|
+
except (OSError, ValueError):
|
|
732
|
+
continue
|
|
733
|
+
if not isinstance(doc, dict) or doc.get("hostSessionId") != session_id:
|
|
734
|
+
continue
|
|
735
|
+
if doc.get("status") in LIVE_STATUSES and st.st_mtime > best_t:
|
|
736
|
+
best, best_t = doc, st.st_mtime
|
|
737
|
+
return best
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def run_commit_reason(doc):
|
|
741
|
+
"""Why a commit in the attended run this session drives needs the user's
|
|
742
|
+
yes (rules.md, "No auto-commit"), or None for no run or an unattended one."""
|
|
743
|
+
if not isinstance(doc, dict):
|
|
744
|
+
return None
|
|
745
|
+
launch = doc.get("launchRequest") if isinstance(doc.get("launchRequest"), dict) else {}
|
|
746
|
+
if doc.get("autopilot") is True or launch.get("mode") in ("autopilot", "background"):
|
|
747
|
+
return None
|
|
748
|
+
return (
|
|
749
|
+
"this commits inside the pipeline run this session is driving (" + str(doc.get("taskId") or "?")
|
|
750
|
+
+ "); a commit waits for the user's explicit yes"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# --- output format inside a run --------------------------------------------------
|
|
755
|
+
|
|
756
|
+
# A repo profile may put the ticket first in any of its styles: [KEY-1], (KEY-1),
|
|
757
|
+
# KEY-1:, #1 (repo-profile-derive.mjs PREFIX_RE). The type and scope follow the
|
|
758
|
+
# same reader's CONV_RE: a lowercase word, any parenthesised scope.
|
|
759
|
+
TICKET_PREFIX = r"(\s*[\[(]?\s*(?:[A-Z][A-Z0-9]+-\d+|#\d+)\s*[\])]?:?\s+)?"
|
|
760
|
+
COMMIT_SUBJECT = re.compile(r"^" + TICKET_PREFIX + r"[a-z]+(\([^)]+\))?!?:\s+\S")
|
|
761
|
+
SUBJECT_MAX = 72
|
|
762
|
+
SKIPPED_SUBJECTS = re.compile(r"^(Merge |Revert \"|fixup! |squash! |amend! )|^\s*wip\b", re.I)
|
|
763
|
+
ENTITY = re.compile(r"&(amp|lt|gt|quot|#39|#x27);")
|
|
764
|
+
FENCE = re.compile(r"```.*?(```|$)", re.S)
|
|
765
|
+
INLINE = re.compile(r"`[^`\n]*`")
|
|
766
|
+
JIRA_CODE = re.compile(r"\{(code|noformat)[^}]*\}.*?(\{\1\}|$)", re.S)
|
|
767
|
+
# A literal backslash-n meant as a line break sits between two pieces of
|
|
768
|
+
# text: glued to the word before it and followed by more text
|
|
769
|
+
# ("Summary\nThe change", "Changes:\n- a"), or after a space before a
|
|
770
|
+
# capital ("Summary \nNext"). One ending a token (printf hello\n), quoted or
|
|
771
|
+
# bracketed is a mention of the escape; \r\n and drive paths (C:\Users\name)
|
|
772
|
+
# are removed before the search.
|
|
773
|
+
LOST_BREAK = re.compile(r"(?<=[^\s\\'\"`(\[])\\n(?=[^\s'\"`)\]])|(?<=\s)\\n(?=[A-Z])")
|
|
774
|
+
CRLF = re.compile(r"\\r\\n")
|
|
775
|
+
DRIVE_PATH = re.compile(r"\b[A-Za-z]:\\\S*")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _subject(message):
|
|
779
|
+
"""The first line of a commit message that carries text. A heredoc message
|
|
780
|
+
(`-m "$(cat <<EOF ...)"`) reaches the hook with the opening quote on its
|
|
781
|
+
own line, ahead of the body."""
|
|
782
|
+
for line in (message or "").split("\n"):
|
|
783
|
+
if re.search(r"[A-Za-z0-9]", line):
|
|
784
|
+
return line.strip()
|
|
785
|
+
return ""
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def profile_conventional(cwd):
|
|
789
|
+
"""False when the repo's stored profile says its subjects are not
|
|
790
|
+
conventional, else True (no profile, unreadable, or conventional)."""
|
|
791
|
+
script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "repo-profile.mjs")
|
|
792
|
+
if not os.path.isfile(script):
|
|
793
|
+
return True
|
|
794
|
+
try:
|
|
795
|
+
import subprocess
|
|
796
|
+
|
|
797
|
+
r = subprocess.run(
|
|
798
|
+
["node", script, "resolve", "commit.format", "--repo", cwd or os.getcwd()],
|
|
799
|
+
capture_output=True, text=True, timeout=3,
|
|
800
|
+
)
|
|
801
|
+
doc = json.loads(r.stdout) if r.returncode == 0 and r.stdout.strip() else {}
|
|
802
|
+
except (OSError, ValueError, Exception):
|
|
803
|
+
return True
|
|
804
|
+
value = doc.get("value") if isinstance(doc, dict) and doc.get("use") else None
|
|
805
|
+
return not (isinstance(value, dict) and value.get("conventional") is False)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def commit_subject_reason(message, cwd=None):
|
|
809
|
+
"""Why a commit subject breaks the commit convention (rules.md), or None.
|
|
810
|
+
|
|
811
|
+
Not judged: a subject the hook cannot read for certain (a shell expansion
|
|
812
|
+
in it), git's merge, revert, fixup and squash subjects, checkpoint commits
|
|
813
|
+
(wip ...), and the type check in a repo whose profile is not conventional."""
|
|
814
|
+
subject = _subject(message)
|
|
815
|
+
if not subject or "$" in subject or "`" in subject or SKIPPED_SUBJECTS.search(subject):
|
|
816
|
+
return None
|
|
817
|
+
if len(subject) > SUBJECT_MAX:
|
|
818
|
+
return "the commit subject is " + str(len(subject)) + " characters; the convention caps it at " + str(SUBJECT_MAX)
|
|
819
|
+
if not COMMIT_SUBJECT.match(subject) and profile_conventional(cwd):
|
|
820
|
+
return (
|
|
821
|
+
"the commit subject does not start with a conventional type, as in type(scope): description "
|
|
822
|
+
"(after a repo profile's ticket prefix if any)"
|
|
823
|
+
)
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def body_text_reason(text, entities=False):
|
|
828
|
+
"""Why text published as a PR, issue or Jira body will render wrong, or
|
|
829
|
+
None: a literal backslash-n where a line break was meant, outside Markdown
|
|
830
|
+
and Jira code. With entities, an HTML entity too: a Jira payload renders
|
|
831
|
+
it verbatim, while Markdown on GitHub decodes it (and needs < for a
|
|
832
|
+
literal angle bracket)."""
|
|
833
|
+
prose = DRIVE_PATH.sub(" ", CRLF.sub(" ", INLINE.sub("", FENCE.sub("", JIRA_CODE.sub("", text or "")))))
|
|
834
|
+
if LOST_BREAK.search(prose):
|
|
835
|
+
return "the body carries a literal \\n; write real line breaks (a heredoc or a body file)"
|
|
836
|
+
m = ENTITY.search(prose) if entities else None
|
|
837
|
+
if m:
|
|
838
|
+
return "the body carries the HTML entity " + m.group(0) + "; write the character itself"
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _json_strings(value, out):
|
|
843
|
+
"""The prose strings of a JSON body, leaving out ADF code blocks and text
|
|
844
|
+
marked as code."""
|
|
845
|
+
if isinstance(value, str):
|
|
846
|
+
out.append(value)
|
|
847
|
+
elif isinstance(value, dict):
|
|
848
|
+
if value.get("type") == "codeBlock":
|
|
849
|
+
return out
|
|
850
|
+
marks = value.get("marks")
|
|
851
|
+
if isinstance(marks, list) and any(isinstance(m, dict) and m.get("type") == "code" for m in marks):
|
|
852
|
+
return out
|
|
853
|
+
for v in value.values():
|
|
854
|
+
_json_strings(v, out)
|
|
855
|
+
elif isinstance(value, list):
|
|
856
|
+
for v in value:
|
|
857
|
+
_json_strings(v, out)
|
|
858
|
+
return out
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def payload_files(argv):
|
|
862
|
+
"""Files a request sends as its body: curl -d/--data/--data-binary/--json
|
|
863
|
+
@file and --data-urlencode [name]@file, and gh api --input file and
|
|
864
|
+
-F/--field name=@file. "@-" (stdin) is the caller's to resolve."""
|
|
865
|
+
if argv and os.path.basename(argv[0]) == "gh" and len(argv) > 1 and argv[1] == "api":
|
|
866
|
+
out, i = [], 2
|
|
867
|
+
while i < len(argv):
|
|
868
|
+
a = argv[i]
|
|
869
|
+
if a == "--input" and i + 1 < len(argv):
|
|
870
|
+
out.append(argv[i + 1])
|
|
871
|
+
i += 1
|
|
872
|
+
elif a in ("-F", "--field") and i + 1 < len(argv):
|
|
873
|
+
v = argv[i + 1]
|
|
874
|
+
if "=@" in v and not v.endswith("=@-"):
|
|
875
|
+
out.append(v.split("=@", 1)[1])
|
|
876
|
+
i += 1
|
|
877
|
+
i += 1
|
|
878
|
+
return out
|
|
879
|
+
if not argv or os.path.basename(argv[0]) not in HTTP_CLIENTS:
|
|
880
|
+
return []
|
|
881
|
+
out, i = [], 1
|
|
882
|
+
while i < len(argv):
|
|
883
|
+
a = argv[i]
|
|
884
|
+
val = None
|
|
885
|
+
if a in ("-d", "--data", "--data-binary", "--data-ascii", "--json") and i + 1 < len(argv):
|
|
886
|
+
val = argv[i + 1]
|
|
887
|
+
i += 1
|
|
888
|
+
elif a.startswith(("--data=", "--data-binary=", "--data-ascii=", "--json=")):
|
|
889
|
+
val = a.split("=", 1)[1]
|
|
890
|
+
elif a == "--data-urlencode" and i + 1 < len(argv):
|
|
891
|
+
m = re.match(r"^[A-Za-z0-9_.-]*@(.+)$", argv[i + 1])
|
|
892
|
+
i += 1
|
|
893
|
+
val = "@" + m.group(1) if m else None
|
|
894
|
+
if val and val.startswith("@") and len(val) > 1 and val != "@-":
|
|
895
|
+
out.append(val[1:])
|
|
896
|
+
i += 1
|
|
897
|
+
return out
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def stdin_payload(argv):
|
|
901
|
+
"""True when the request reads its body from stdin (-d @-, --data-binary @-, --input -)."""
|
|
902
|
+
return "@-" in argv or (len(argv) > 2 and argv[1] == "api" and "--input" in argv and "-" in argv)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
# Jira issue and comment endpoints only: Bitbucket Server (/rest/api/1.0/)
|
|
906
|
+
# takes Markdown and Confluence storage format needs entities.
|
|
907
|
+
JIRA_API = re.compile(r"/rest/api/(?:2|3|latest)/(?:issue|comment)\b", re.I)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def payload_reason(path, cwd, entities=True, raw=None):
|
|
911
|
+
"""body_text_reason over a request body file (or raw text). A JSON body is
|
|
912
|
+
judged on its decoded string values, where an escaped newline is a real
|
|
913
|
+
line break. Entities are judged only for a Jira target, which renders
|
|
914
|
+
them verbatim; GitHub and Bitbucket Markdown decode them."""
|
|
915
|
+
raw = read_bounded(path, cwd) if raw is None else raw
|
|
916
|
+
if not raw:
|
|
917
|
+
return None
|
|
918
|
+
try:
|
|
919
|
+
text = "\n".join(_json_strings(json.loads(raw), []))
|
|
920
|
+
except ValueError:
|
|
921
|
+
text = raw
|
|
922
|
+
return body_text_reason(text, entities=entities)
|
|
475
923
|
|
|
476
924
|
|
|
477
925
|
# --- confirmations ---------------------------------------------------------------
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
* timeouts are milliseconds, Swift and Python seconds,
|
|
23
23
|
* Kotlin and Java milliseconds; a Ms / Millis / Seconds
|
|
24
24
|
* suffix overrides.
|
|
25
|
+
* disabled-test a test switched off in a test file: an unconditional
|
|
26
|
+
* skip (`XCTSkip(`, `XCTSkipIf(true`, `.skip(`, `xit(`,
|
|
27
|
+
* `@Disabled`, `@Ignore`, `@pytest.mark.skip`,
|
|
28
|
+
* `t.Skip(`), a Swift Testing `.disabled` trait, or a
|
|
29
|
+
* renamed `disabled_test`. Blocking: the suite goes green
|
|
30
|
+
* by not running the test. A skip with a real condition
|
|
31
|
+
* (`skipif`, `XCTSkipUnless(<expr>)`) is a platform
|
|
32
|
+
* choice and is left alone.
|
|
25
33
|
*
|
|
26
34
|
* Only source files are read, and comment lines are skipped. Configuration
|
|
27
35
|
* (`*.config.*`, CI workflows, YAML) and documentation are out of scope:
|
|
@@ -66,6 +74,19 @@ const KOTLIN_RUN_CATCHING = /\brunCatching\s*\{/;
|
|
|
66
74
|
const RESULT_READ =
|
|
67
75
|
/\.(onFailure|getOrThrow|getOrElse|getOrDefault|exceptionOrNull|fold|isFailure|isSuccess|recover)\b|=\s*runCatching|return\s+runCatching/;
|
|
68
76
|
|
|
77
|
+
const DISABLED_TEST = [
|
|
78
|
+
/\bXCTSkip\s*\(/,
|
|
79
|
+
/\bXCTSkip(?:If|Unless)\s*\(\s*(?:true|false)\s*[,)]/,
|
|
80
|
+
/@Test\s*\([^)]*\.disabled\b/,
|
|
81
|
+
/\bfunc\s+(?:disabled_|x)test\w*\s*\(/,
|
|
82
|
+
/^\s*@(?:Disabled|Ignore)\b/,
|
|
83
|
+
/\b(?:it|test|describe|context|suite)\.skip\s*\(/,
|
|
84
|
+
/(?:^|[^\w.$])(?:xit|xtest|xdescribe|xcontext)\s*\(/,
|
|
85
|
+
/@pytest\.mark\.skip\b(?!if)/,
|
|
86
|
+
/@unittest\.skip\b(?!If|Unless)/,
|
|
87
|
+
/\bt\.Skip(?:Now|f)?\s*\(/,
|
|
88
|
+
];
|
|
89
|
+
|
|
69
90
|
const LONG_TEST_WAIT_SECONDS = 30;
|
|
70
91
|
const RAISE_FACTOR = 2;
|
|
71
92
|
|
|
@@ -220,6 +241,21 @@ export function scanTestGaming(diffText) {
|
|
|
220
241
|
);
|
|
221
242
|
}
|
|
222
243
|
}
|
|
244
|
+
if (testFile) {
|
|
245
|
+
for (const { line, text } of f.addedAt) {
|
|
246
|
+
if (COMMENT.test(text) || !DISABLED_TEST.some((re) => re.test(text))) continue;
|
|
247
|
+
findings.push(
|
|
248
|
+
finding(
|
|
249
|
+
file,
|
|
250
|
+
line,
|
|
251
|
+
"disabled-test",
|
|
252
|
+
"blocking",
|
|
253
|
+
text,
|
|
254
|
+
`A test is switched off at ${file}:${line}. The suite then passes by not running it; fix what it catches, or remove it with the reason in the commit body.`,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
223
259
|
for (const run of segments(f.addedAt)) findings.push(...swallowedIn(file, run));
|
|
224
260
|
for (const hunk of f.hunks) findings.push(...timeoutsIn(file, hunk, testFile));
|
|
225
261
|
}
|
|
@@ -98,24 +98,11 @@ def normalize_for_match(text: str) -> str:
|
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
# Force flags, and
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# git pull --ff-only && git push origin main -> BLOCKED
|
|
108
|
-
# git push --follow-tags origin main -> BLOCKED
|
|
109
|
-
#
|
|
110
|
-
# Neither rewrites history. `--follow-tags` is the normal way to push a release
|
|
111
|
-
# tag with its commit, so the guard blocked a routine release step. Being
|
|
112
|
-
# fail-closed is right for a data-loss guard, but only against the thing it
|
|
113
|
-
# guards; blocking safe commands trains people to work around the hook, which
|
|
114
|
-
# costs more safety than it buys.
|
|
115
|
-
#
|
|
116
|
-
# The short-flag arm is now `-[A-Za-z]*f[A-Za-z]*`: a SINGLE dash followed only by
|
|
117
|
-
# letters, so it still catches `-f`, `-fq`, `-qf`, and never a `--long-flag`
|
|
118
|
-
# (whose second character is `-`, not a letter).
|
|
101
|
+
# Force flags, and only force flags: `--force`, `--force-with-lease[=ref]`,
|
|
102
|
+
# `--force-if-includes`, and a single-dash cluster that contains `f` (`-f`,
|
|
103
|
+
# `-fq`, `-qf`). A long flag that merely contains an f (`--ff-only`,
|
|
104
|
+
# `--follow-tags`, `--file=`) rewrites nothing and passes: blocking safe
|
|
105
|
+
# commands trains people to work around the hook.
|
|
119
106
|
FORCE = re.compile(
|
|
120
107
|
r"(^|\s)("
|
|
121
108
|
r"--force(-with-lease|-if-includes)?(=\S+)?" # --force, --force-with-lease[=ref], --force-if-includes
|
|
@@ -290,6 +277,9 @@ def _attributed(text: str) -> bool:
|
|
|
290
277
|
# The directory the hook payload names as the session's cwd; message files a
|
|
291
278
|
# command names relative to it are read from there.
|
|
292
279
|
PAYLOAD_CWD = {"dir": None}
|
|
280
|
+
# The hook payload's session id: a commit inside the run this session drives
|
|
281
|
+
# is confirmed by the user.
|
|
282
|
+
PAYLOAD_SESSION = {"id": None}
|
|
293
283
|
|
|
294
284
|
|
|
295
285
|
def _command_rules(cmd: str) -> str:
|
|
@@ -299,6 +289,7 @@ def _command_rules(cmd: str) -> str:
|
|
|
299
289
|
segments into another directory."""
|
|
300
290
|
pending_ask = None
|
|
301
291
|
cwd = None
|
|
292
|
+
in_run = None
|
|
302
293
|
for seg, toks in _guard_rules.command_segments(cmd):
|
|
303
294
|
if toks and toks[0] in ("{", "}"):
|
|
304
295
|
toks = toks[1:]
|
|
@@ -318,7 +309,39 @@ def _command_rules(cmd: str) -> str:
|
|
|
318
309
|
reason = _guard_rules.no_verify(toks)
|
|
319
310
|
if reason:
|
|
320
311
|
return "BLOCK_NOVERIFY\t" + reason
|
|
321
|
-
# Rule
|
|
312
|
+
# Rule 1c: attribution in a pull request or issue the gh CLI publishes.
|
|
313
|
+
gh_body = _guard_rules.gh_text(toks, here)
|
|
314
|
+
if gh_body and "$(" in gh_body:
|
|
315
|
+
gh_body = "\n".join([gh_body] + _guard_rules.strip_heredoc_bodies(cmd)[1])
|
|
316
|
+
if _attributed(gh_body):
|
|
317
|
+
return "BLOCK_ATTRIB"
|
|
318
|
+
# Rule 9: inside the run this session drives, a published body keeps
|
|
319
|
+
# real line breaks and no HTML entities.
|
|
320
|
+
_, argv = _guard_rules.command_word(toks)
|
|
321
|
+
bodies = _guard_rules.payload_files(argv)
|
|
322
|
+
piped = _guard_rules.stdin_payload(argv) and bool(_guard_rules.strip_heredoc_bodies(cmd)[1])
|
|
323
|
+
if gh_body or bodies or piped:
|
|
324
|
+
if in_run is None:
|
|
325
|
+
in_run = bool(_guard_rules.driving_run(PAYLOAD_SESSION["id"]))
|
|
326
|
+
if in_run:
|
|
327
|
+
jira = bool(_guard_rules.JIRA_API.search(seg))
|
|
328
|
+
reason = _guard_rules.body_text_reason(gh_body) if gh_body else None
|
|
329
|
+
for path in bodies:
|
|
330
|
+
reason = reason or _guard_rules.payload_reason(path, here, entities=jira)
|
|
331
|
+
if piped:
|
|
332
|
+
for body in _guard_rules.strip_heredoc_bodies(cmd)[1]:
|
|
333
|
+
reason = reason or _guard_rules.payload_reason(None, here, entities=jira, raw=body)
|
|
334
|
+
if reason:
|
|
335
|
+
return "DENY\t" + reason
|
|
336
|
+
# Rule 8: a pull request PUT that would wipe reviewers or refs.
|
|
337
|
+
reason = _guard_rules.pr_update_reason(toks, seg, here)
|
|
338
|
+
if reason:
|
|
339
|
+
return "DENY\t" + reason
|
|
340
|
+
# Rule 6: a token on the vercel command line.
|
|
341
|
+
reason = _guard_rules.vercel_token_reason(toks)
|
|
342
|
+
if reason:
|
|
343
|
+
return "DENY\t" + reason
|
|
344
|
+
# Rule 4: writes a person confirms (issue closure, Jira transition, clean-up).
|
|
322
345
|
ask = _guard_rules.ask_reason(toks, seg, here)
|
|
323
346
|
if ask and not pending_ask:
|
|
324
347
|
pending_ask = "ASK\t" + ask
|
|
@@ -338,7 +361,30 @@ def decide(cmd: str, depth: int = 0) -> str:
|
|
|
338
361
|
return "BLOCK_ATTRIB"
|
|
339
362
|
if commits and _guard_rules.ai_generated(cmd, PAYLOAD_CWD["dir"]):
|
|
340
363
|
return "BLOCK_ATTRIB"
|
|
364
|
+
# Rule 7: a stored secret printed with nothing capturing it.
|
|
365
|
+
reason = _guard_rules.secret_echo_reason(cmd)
|
|
366
|
+
if reason:
|
|
367
|
+
return "DENY\t" + reason
|
|
341
368
|
pending_ask = None
|
|
369
|
+
if commits and _guard_rules.CLOSING.search(_guard_rules.message_text(cmd, PAYLOAD_CWD["dir"])):
|
|
370
|
+
pending_ask = (
|
|
371
|
+
"ASK\tthe commit message carries a closing keyword (Closes/Fixes/Resolves #N), which closes "
|
|
372
|
+
"the issue when the commit lands on the default branch; use Ref: #N, or confirm the closure"
|
|
373
|
+
)
|
|
374
|
+
really_commits = any(
|
|
375
|
+
git_subcommand(toks) == "commit" for _, toks in _guard_rules.command_segments(cmd)
|
|
376
|
+
)
|
|
377
|
+
driving = _guard_rules.driving_run(PAYLOAD_SESSION["id"]) if really_commits else None
|
|
378
|
+
if driving:
|
|
379
|
+
reason = _guard_rules.commit_subject_reason(
|
|
380
|
+
_guard_rules.message_text(cmd, PAYLOAD_CWD["dir"]), PAYLOAD_CWD["dir"]
|
|
381
|
+
)
|
|
382
|
+
if reason:
|
|
383
|
+
return "DENY\t" + reason
|
|
384
|
+
if driving and not pending_ask:
|
|
385
|
+
reason = _guard_rules.run_commit_reason(driving)
|
|
386
|
+
if reason:
|
|
387
|
+
pending_ask = "ASK\t" + reason
|
|
342
388
|
|
|
343
389
|
# Rule 2: force-push to a protected branch. Evaluate each push segment on its
|
|
344
390
|
# own so a neighbouring command's flags cannot implicate it. A `cd` earlier
|
|
@@ -789,6 +835,8 @@ def main() -> None:
|
|
|
789
835
|
cmd = (data.get("tool_input") or {}).get("command", "")
|
|
790
836
|
if isinstance(data.get("cwd"), str) and data.get("cwd"):
|
|
791
837
|
PAYLOAD_CWD["dir"] = data["cwd"]
|
|
838
|
+
if isinstance(data.get("session_id"), str) and data.get("session_id"):
|
|
839
|
+
PAYLOAD_SESSION["id"] = data["session_id"]
|
|
792
840
|
if isinstance(cmd, str):
|
|
793
841
|
seen["text"], seen["raw"] = cmd, False
|
|
794
842
|
verdict = decide(cmd) if isinstance(cmd, str) and cmd else "OK"
|
|
@@ -799,6 +847,18 @@ def main() -> None:
|
|
|
799
847
|
)
|
|
800
848
|
if reason:
|
|
801
849
|
verdict = "BLOCK_READONLY\t" + reason
|
|
850
|
+
if (
|
|
851
|
+
verdict == "OK"
|
|
852
|
+
and isinstance(cmd, str)
|
|
853
|
+
and cmd
|
|
854
|
+
and _guard_rules.read_only_agent(data.get("agent_type"))
|
|
855
|
+
):
|
|
856
|
+
effect = _guard_rules.subagent_side_effect(cmd)
|
|
857
|
+
if effect:
|
|
858
|
+
verdict = (
|
|
859
|
+
"DENY\tthe " + str(data.get("agent_type")) + " subagent is read-only and " + effect
|
|
860
|
+
+ "; it returns findings, and the orchestrator acts on them"
|
|
861
|
+
)
|
|
802
862
|
if verdict.startswith("ASK"):
|
|
803
863
|
verdict = ask_verdict(verdict.split("\t", 1)[1], data, strict)
|
|
804
864
|
if verdict == "OK" and strict:
|