@mmerterden/multi-agent-pipeline 20.33.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 +98 -1
- package/README.md +3 -1
- package/README.tr.md +2 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +5 -4
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +24 -2
- package/install/templates/claude-hooks.json +34 -1
- package/manifest.json +393 -339
- package/package.json +3 -1
- package/pipeline/agents/bulk-reader.md +3 -0
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -298
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/office-status/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +7 -131
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/README.md +6 -2
- 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/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +1 -1
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-remote-protocol.md +166 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +2 -2
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +25 -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/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +8 -10
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +18 -20
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +20 -14
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +8 -7
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +6 -3
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/rules.md +3 -2
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- 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 +304 -55
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/agent-state.schema.json +36 -0
- package/pipeline/schemas/phases.json +4 -4
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +43 -2
- package/pipeline/schemas/pr-reach.schema.json +55 -0
- package/pipeline/schemas/prefs.schema.json +37 -8
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/triage-output.schema.json +32 -3
- package/pipeline/schemas/unattended-policy.json +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_guard_rules.py +449 -1
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_test-gaming.mjs +36 -0
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- 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/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +71 -16
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gate-ledger.mjs +144 -21
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- 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/plan-critique-gate.mjs +11 -2
- 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/rework-rulings.mjs +106 -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 +213 -2
- 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/triage-memory.mjs +64 -37
- package/pipeline/scripts/update-check.sh +7 -1
- package/pipeline/scripts/update-issue-progress.sh +73 -8
- package/pipeline/scripts/validate-planning.mjs +172 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +149 -4
- 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 +9 -9
- 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-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +2 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- 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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-remote-client.mjs - the machine end of the office remote
|
|
4
|
+
* layer (features/office-remote-protocol.md).
|
|
5
|
+
*
|
|
6
|
+
* It polls the relay for frames, opens each with the paired device's key,
|
|
7
|
+
* checks the passkey tier of the request inside, forwards an accepted request
|
|
8
|
+
* to the local server's phone routes and seals the response back. The local
|
|
9
|
+
* server still verifies the device signature, window, nonce and scopes; this
|
|
10
|
+
* file adds what the relay path needs on top:
|
|
11
|
+
*
|
|
12
|
+
* - a frame opens only with its device's key, in its direction, with a
|
|
13
|
+
* counter above the last one accepted from that device;
|
|
14
|
+
* - the target is a phone route (`/v1/phone/`), the method GET or POST, the
|
|
15
|
+
* device header names the device the frame came from;
|
|
16
|
+
* - the tier: `read` (GET) needs a passkey session that has not been idle for
|
|
17
|
+
* more than READ_IDLE_MS; `low` (an answer) needs a passkey within
|
|
18
|
+
* LOW_WINDOW_MS; `high` (launch and every other POST) needs a passkey
|
|
19
|
+
* assertion bound to this very request (its challenge is SHA-256 of the
|
|
20
|
+
* request's canonical signed text);
|
|
21
|
+
* - no `Authorization` header is ever added, and `Host` is the loopback.
|
|
22
|
+
*
|
|
23
|
+
* A refused frame still gets a sealed reply with the reason, so the phone can
|
|
24
|
+
* show it. One audit line per frame: time, device, counter, path without
|
|
25
|
+
* query, tier, verdict and reason; never a body, header value or signature.
|
|
26
|
+
*
|
|
27
|
+
* Config: <phone dir>/remote.json (0600), written by office-pair. Remote is off
|
|
28
|
+
* unless that file exists and the office server is running.
|
|
29
|
+
*
|
|
30
|
+
* Usage: office-remote-client.mjs once | loop
|
|
31
|
+
*
|
|
32
|
+
* @module pipeline/scripts/office-remote-client
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import {
|
|
37
|
+
appendFileSync,
|
|
38
|
+
chmodSync,
|
|
39
|
+
closeSync,
|
|
40
|
+
existsSync,
|
|
41
|
+
mkdirSync,
|
|
42
|
+
openSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
renameSync,
|
|
45
|
+
rmSync,
|
|
46
|
+
statSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
} from "node:fs";
|
|
49
|
+
import { request as httpRequest } from "node:http";
|
|
50
|
+
import { dirname, join } from "node:path";
|
|
51
|
+
|
|
52
|
+
import { directionKeys, openFrame, sealFrame, verifyPasskey } from "../lib/office-remote.mjs";
|
|
53
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
54
|
+
import {
|
|
55
|
+
bodyDigest,
|
|
56
|
+
canonicalPayload,
|
|
57
|
+
DEVICE_ID,
|
|
58
|
+
HEADERS,
|
|
59
|
+
PHONE_PREFIX,
|
|
60
|
+
phoneDir,
|
|
61
|
+
} from "./_phone-auth.mjs";
|
|
62
|
+
|
|
63
|
+
export const READ_IDLE_MS = 5 * 60_000;
|
|
64
|
+
export const LOW_WINDOW_MS = 2 * 60_000;
|
|
65
|
+
const POLL_TIMEOUT_MS = 30_000;
|
|
66
|
+
const MAX_BODY = 256 * 1024;
|
|
67
|
+
const PASSKEY_HEADER = "x-ma-passkey";
|
|
68
|
+
const FORWARDED = new Set([...Object.values(HEADERS), "content-type", "accept"]);
|
|
69
|
+
const AUDIT_MAX_BYTES = 1024 * 1024;
|
|
70
|
+
|
|
71
|
+
export const configPath = (env = process.env) => join(phoneDir(env), "remote.json");
|
|
72
|
+
export const framesLogPath = (env = process.env) => join(phoneDir(env), "remote-frames.jsonl");
|
|
73
|
+
export const lockPath = (env = process.env) => join(phoneDir(env), "remote-client.lock");
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The target's path when it is already in the canonical form the local
|
|
77
|
+
* server routes on, else null. The server parses the target as a URL, which
|
|
78
|
+
* decodes dot segments (`%2e%2e`), turns `\` into `/` and drops a fragment;
|
|
79
|
+
* a target those steps would change is refused here, so the tier and the
|
|
80
|
+
* prefix are judged on the same path the server executes.
|
|
81
|
+
*/
|
|
82
|
+
export function canonicalPath(target) {
|
|
83
|
+
if (typeof target !== "string" || !target.startsWith("/") || /%2e|\\|#/i.test(target))
|
|
84
|
+
return null;
|
|
85
|
+
let u;
|
|
86
|
+
try {
|
|
87
|
+
u = new URL(target, "http://127.0.0.1");
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return u.pathname + u.search === target ? u.pathname : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The passkey tier of a phone route. Anything not known to be milder is high. */
|
|
95
|
+
export function tierOf(method, pathname) {
|
|
96
|
+
if (method === "GET" && /^\/v1\/phone\/runs(\/[^/]+)?$/.test(pathname)) return "read";
|
|
97
|
+
if (method === "POST" && /^\/v1\/phone\/runs\/[^/]+\/answer$/.test(pathname)) return "low";
|
|
98
|
+
return "high";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lower = (headers) =>
|
|
102
|
+
Object.fromEntries(
|
|
103
|
+
Object.entries(headers && typeof headers === "object" ? headers : {}).map(([k, v]) => [
|
|
104
|
+
String(k).toLowerCase(),
|
|
105
|
+
String(v),
|
|
106
|
+
]),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/** The challenge a passkey must sign for this request. */
|
|
110
|
+
export function requestChallenge(req, headers, body) {
|
|
111
|
+
const text = canonicalPayload({
|
|
112
|
+
method: req.method,
|
|
113
|
+
target: req.target,
|
|
114
|
+
bodyDigest: bodyDigest(body),
|
|
115
|
+
deviceId: headers[HEADERS.device],
|
|
116
|
+
timestamp: headers[HEADERS.timestamp],
|
|
117
|
+
nonce: headers[HEADERS.nonce],
|
|
118
|
+
});
|
|
119
|
+
return createHash("sha256").update(text, "utf8").digest();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether the request's tier is satisfied, updating the device's passkey
|
|
124
|
+
* record when an assertion verifies.
|
|
125
|
+
*
|
|
126
|
+
* @returns {{ok: true} | {ok: false, reason: string}}
|
|
127
|
+
*/
|
|
128
|
+
export function passkeyDecision(device, tier, req, headers, body, now) {
|
|
129
|
+
const raw = headers[PASSKEY_HEADER];
|
|
130
|
+
if (raw) {
|
|
131
|
+
let assertion;
|
|
132
|
+
try {
|
|
133
|
+
assertion = JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
return { ok: false, reason: "the passkey header is not JSON" };
|
|
136
|
+
}
|
|
137
|
+
let challenge;
|
|
138
|
+
try {
|
|
139
|
+
challenge = requestChallenge(req, headers, body);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return { ok: false, reason: err.message };
|
|
142
|
+
}
|
|
143
|
+
const pk = device.passkey || {};
|
|
144
|
+
const v = verifyPasskey({
|
|
145
|
+
assertion,
|
|
146
|
+
credential: pk.credential,
|
|
147
|
+
rpId: pk.rpId,
|
|
148
|
+
origin: pk.origin,
|
|
149
|
+
challenge,
|
|
150
|
+
prevSignCount: Number(pk.signCount) || 0,
|
|
151
|
+
});
|
|
152
|
+
if (!v.ok) return v;
|
|
153
|
+
device.passkey = { ...pk, signCount: v.signCount };
|
|
154
|
+
device.lastPasskeyAt = now;
|
|
155
|
+
device.lastActivityAt = now;
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
if (tier === "high") return { ok: false, reason: "this action needs a passkey for this request" };
|
|
159
|
+
const last = Number(device.lastPasskeyAt) || 0;
|
|
160
|
+
if (tier === "low" && now - last > LOW_WINDOW_MS) {
|
|
161
|
+
return { ok: false, reason: "confirm with the passkey again (the low-risk window has passed)" };
|
|
162
|
+
}
|
|
163
|
+
if (tier === "read" && (!last || now - (Number(device.lastActivityAt) || 0) > READ_IDLE_MS)) {
|
|
164
|
+
return { ok: false, reason: "the session is locked; open it with the passkey" };
|
|
165
|
+
}
|
|
166
|
+
device.lastActivityAt = now;
|
|
167
|
+
return { ok: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function keysFor(config, device) {
|
|
171
|
+
return directionKeys({
|
|
172
|
+
privatePem: config.machinePrivatePem,
|
|
173
|
+
peerPublic: device.sessionPublic,
|
|
174
|
+
pairingId: config.pairingId,
|
|
175
|
+
role: "machine",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function reply(config, device, inReplyTo, response) {
|
|
180
|
+
device.sendCounter = (Number(device.sendCounter) || 0) + 1;
|
|
181
|
+
const sealed = sealFrame(keysFor(config, device).send, device.sendCounter, "machine-to-phone", {
|
|
182
|
+
inReplyTo,
|
|
183
|
+
...response,
|
|
184
|
+
});
|
|
185
|
+
return { deviceId: device.id, ...sealed };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const refuse = (status, reason) => ({
|
|
189
|
+
status,
|
|
190
|
+
headers: { "content-type": "application/json" },
|
|
191
|
+
body: Buffer.from(JSON.stringify({ error: "remote-refused", reason })).toString("base64url"),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Open, judge and forward one frame. Mutates the device's counters and passkey
|
|
196
|
+
* record in `config`; returns the sealed reply (or null for a frame that could
|
|
197
|
+
* not be opened at all) and the audit row.
|
|
198
|
+
*/
|
|
199
|
+
export async function handleFrame(config, frame, { forward, now = Date.now() }) {
|
|
200
|
+
const id =
|
|
201
|
+
typeof frame?.deviceId === "string" && DEVICE_ID.test(frame.deviceId) ? frame.deviceId : null;
|
|
202
|
+
const device =
|
|
203
|
+
id && config.devices && Object.hasOwn(config.devices, id) ? config.devices[id] : null;
|
|
204
|
+
const row = {
|
|
205
|
+
at: new Date(now).toISOString(),
|
|
206
|
+
device: id,
|
|
207
|
+
counter: Number.isSafeInteger(frame?.counter) ? frame.counter : null,
|
|
208
|
+
};
|
|
209
|
+
if (!device || device.revoked)
|
|
210
|
+
return {
|
|
211
|
+
reply: null,
|
|
212
|
+
row: { ...row, verdict: "dropped", reason: "unknown or revoked device" },
|
|
213
|
+
};
|
|
214
|
+
const opened = openFrame(
|
|
215
|
+
keysFor(config, device).recv,
|
|
216
|
+
frame,
|
|
217
|
+
"phone-to-machine",
|
|
218
|
+
device.recvCounter,
|
|
219
|
+
);
|
|
220
|
+
if (!opened.ok)
|
|
221
|
+
return { reply: null, row: { ...row, verdict: "dropped", reason: opened.reason } };
|
|
222
|
+
device.recvCounter = opened.counter;
|
|
223
|
+
const req = opened.value || {};
|
|
224
|
+
const target = typeof req.target === "string" ? req.target : "";
|
|
225
|
+
const canonical = canonicalPath(target);
|
|
226
|
+
const path = canonical ?? "(not canonical)";
|
|
227
|
+
const out = (status, reason, extra = {}) => ({
|
|
228
|
+
reply: reply(config, device, opened.counter, refuse(status, reason)),
|
|
229
|
+
row: { ...row, path, verdict: "refused", reason, ...extra },
|
|
230
|
+
});
|
|
231
|
+
if (!["GET", "POST"].includes(req.method)) return out(405, "only GET and POST are carried");
|
|
232
|
+
if (canonical === null) return out(400, "the target is not in canonical form");
|
|
233
|
+
if (!canonical.startsWith(PHONE_PREFIX)) return out(403, "only phone routes are carried");
|
|
234
|
+
const headers = lower(req.headers);
|
|
235
|
+
if (headers[HEADERS.device] !== device.id)
|
|
236
|
+
return out(403, "the signed device is not the paired device");
|
|
237
|
+
const body = Buffer.from(typeof req.body === "string" ? req.body : "", "base64url");
|
|
238
|
+
if (body.length > MAX_BODY) return out(413, "the body is too large");
|
|
239
|
+
const tier = tierOf(req.method, path);
|
|
240
|
+
const pass = passkeyDecision(device, tier, req, headers, body, now);
|
|
241
|
+
if (!pass.ok) return out(401, pass.reason, { tier });
|
|
242
|
+
const forwarded = Object.fromEntries(Object.entries(headers).filter(([k]) => FORWARDED.has(k)));
|
|
243
|
+
const response = await forward({ method: req.method, target, headers: forwarded, body });
|
|
244
|
+
return {
|
|
245
|
+
reply: reply(config, device, opened.counter, response),
|
|
246
|
+
row: { ...row, path, tier, verdict: "forwarded", status: response.status },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Forward to the local server: loopback Host, no Authorization, body bytes untouched. */
|
|
251
|
+
export function localForwarder(baseUrl) {
|
|
252
|
+
const base = new URL(baseUrl);
|
|
253
|
+
return ({ method, target, headers, body }) =>
|
|
254
|
+
new Promise((resolve) => {
|
|
255
|
+
const req = httpRequest(
|
|
256
|
+
{
|
|
257
|
+
host: "127.0.0.1",
|
|
258
|
+
port: base.port,
|
|
259
|
+
method,
|
|
260
|
+
path: target,
|
|
261
|
+
headers: { ...headers, host: `127.0.0.1:${base.port}`, "content-length": body.length },
|
|
262
|
+
timeout: POLL_TIMEOUT_MS,
|
|
263
|
+
},
|
|
264
|
+
(res) => {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
let size = 0;
|
|
267
|
+
res.on("data", (c) => {
|
|
268
|
+
size += c.length;
|
|
269
|
+
if (size <= MAX_BODY) chunks.push(c);
|
|
270
|
+
});
|
|
271
|
+
res.on("end", () =>
|
|
272
|
+
resolve({
|
|
273
|
+
status: res.statusCode || 502,
|
|
274
|
+
headers: { "content-type": res.headers["content-type"] || "application/json" },
|
|
275
|
+
body: Buffer.concat(chunks).toString("base64url"),
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
req.on("error", () => resolve(refuse(502, "the local server did not answer")));
|
|
281
|
+
req.on("timeout", () => req.destroy());
|
|
282
|
+
req.end(body);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function readConfig(env = process.env) {
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(readFileSync(configPath(env), "utf8"));
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function writeConfig(config, env = process.env) {
|
|
295
|
+
const file = configPath(env);
|
|
296
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
297
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
298
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
299
|
+
chmodSync(tmp, 0o600);
|
|
300
|
+
renameSync(tmp, file);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function audit(row, env) {
|
|
304
|
+
try {
|
|
305
|
+
const file = framesLogPath(env);
|
|
306
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
307
|
+
if (existsSync(file) && statSync(file).size > AUDIT_MAX_BYTES) renameSync(file, `${file}.1`);
|
|
308
|
+
appendFileSync(file, JSON.stringify(row) + "\n", { mode: 0o600 });
|
|
309
|
+
} catch {
|
|
310
|
+
/* an audit line that cannot be written never changes a verdict */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Write this poll's counters and passkey records onto the config as it is on
|
|
316
|
+
* disk now, so a revoke or a new pairing made while the poll ran is kept: a
|
|
317
|
+
* device that is gone from the file stays gone, and a different pairing id
|
|
318
|
+
* means the file was re-paired and this poll's state is dropped.
|
|
319
|
+
*/
|
|
320
|
+
function persist(config, env) {
|
|
321
|
+
const fresh = readConfig(env);
|
|
322
|
+
if (!fresh || fresh.pairingId !== config.pairingId) return;
|
|
323
|
+
for (const [id, d] of Object.entries(config.devices ?? {})) {
|
|
324
|
+
if (!fresh.devices || !Object.hasOwn(fresh.devices, id)) continue;
|
|
325
|
+
const { recvCounter, sendCounter, passkey, lastPasskeyAt, lastActivityAt } = d;
|
|
326
|
+
fresh.devices[id] = {
|
|
327
|
+
...fresh.devices[id],
|
|
328
|
+
recvCounter,
|
|
329
|
+
sendCounter,
|
|
330
|
+
passkey: { ...fresh.devices[id].passkey, signCount: passkey?.signCount ?? 0 },
|
|
331
|
+
lastPasskeyAt,
|
|
332
|
+
lastActivityAt,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
writeConfig(fresh, env);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function holderAlive(pid) {
|
|
339
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
340
|
+
try {
|
|
341
|
+
process.kill(pid, 0);
|
|
342
|
+
return true;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return e.code === "EPERM";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Hold the single-instance lock: two clients would seal replies from the same
|
|
350
|
+
* counter, reusing an AES-GCM nonce. A lock whose process is gone is taken over.
|
|
351
|
+
*
|
|
352
|
+
* @returns {(() => void) | null} the release function, or null when another client holds it
|
|
353
|
+
*/
|
|
354
|
+
export function acquireLock(env = process.env) {
|
|
355
|
+
const file = lockPath(env);
|
|
356
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
357
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
358
|
+
try {
|
|
359
|
+
const fd = openSync(file, "wx", 0o600);
|
|
360
|
+
writeFileSync(fd, String(process.pid));
|
|
361
|
+
closeSync(fd);
|
|
362
|
+
return () => rmSync(file, { force: true });
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err.code !== "EEXIST") throw err;
|
|
365
|
+
if (holderAlive(Number(readFileSync(file, "utf8")))) return null;
|
|
366
|
+
rmSync(file, { force: true });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* One poll: fetch frames, handle each, post the replies, persist counters.
|
|
374
|
+
*
|
|
375
|
+
* @returns {Promise<{handled: number}>}
|
|
376
|
+
*/
|
|
377
|
+
export async function runOnce({
|
|
378
|
+
config,
|
|
379
|
+
fetchImpl = fetch,
|
|
380
|
+
forward,
|
|
381
|
+
env = process.env,
|
|
382
|
+
now = Date.now,
|
|
383
|
+
}) {
|
|
384
|
+
const base = String(config.relayUrl).replace(/\/+$/, "");
|
|
385
|
+
const auth = { Authorization: `Bearer ${config.relayToken}` };
|
|
386
|
+
const res = await fetchImpl(
|
|
387
|
+
`${base}/api/office/relay/machine?pairing=${encodeURIComponent(config.pairingId)}`,
|
|
388
|
+
{
|
|
389
|
+
headers: auth,
|
|
390
|
+
signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
if (!res.ok) throw new Error(`the relay answered ${res.status}`);
|
|
394
|
+
const { frames = [] } = await res.json();
|
|
395
|
+
const replies = [];
|
|
396
|
+
for (const frame of Array.isArray(frames) ? frames : []) {
|
|
397
|
+
const { reply: r, row } = await handleFrame(config, frame, { forward, now: now() });
|
|
398
|
+
audit(row, env);
|
|
399
|
+
if (r) replies.push(r);
|
|
400
|
+
}
|
|
401
|
+
persist(config, env);
|
|
402
|
+
if (replies.length) {
|
|
403
|
+
const post = await fetchImpl(
|
|
404
|
+
`${base}/api/office/relay/machine?pairing=${encodeURIComponent(config.pairingId)}`,
|
|
405
|
+
{
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
408
|
+
body: JSON.stringify({ frames: replies }),
|
|
409
|
+
signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
if (!post.ok) throw new Error(`the relay refused the replies (${post.status})`);
|
|
413
|
+
}
|
|
414
|
+
return { handled: Array.isArray(frames) ? frames.length : 0 };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function main(argv) {
|
|
418
|
+
const mode = argv[0] || "once";
|
|
419
|
+
if (!readConfig() || !existsSync(configPath())) {
|
|
420
|
+
process.stderr.write("office-remote-client: not paired (run office-pair first)\n");
|
|
421
|
+
return 1;
|
|
422
|
+
}
|
|
423
|
+
const { current } = await import("./office-control.mjs");
|
|
424
|
+
const server = current();
|
|
425
|
+
if (!server) {
|
|
426
|
+
process.stderr.write("office-remote-client: the office is not running (office-on)\n");
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
const release = acquireLock();
|
|
430
|
+
if (!release) {
|
|
431
|
+
process.stderr.write("office-remote-client: another client is already running\n");
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
const forward = localForwarder(server.url);
|
|
435
|
+
try {
|
|
436
|
+
do {
|
|
437
|
+
const config = readConfig();
|
|
438
|
+
if (!config?.pairingId) return 1;
|
|
439
|
+
try {
|
|
440
|
+
await runOnce({ config, forward });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
process.stderr.write(`office-remote-client: ${err.message}\n`);
|
|
443
|
+
if (mode !== "loop") return 1;
|
|
444
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
445
|
+
}
|
|
446
|
+
} while (mode === "loop" && current());
|
|
447
|
+
return 0;
|
|
448
|
+
} finally {
|
|
449
|
+
release();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (invokedDirectly(import.meta.url)) {
|
|
454
|
+
main(process.argv.slice(2)).then(
|
|
455
|
+
(code) => {
|
|
456
|
+
process.exitCode = code;
|
|
457
|
+
},
|
|
458
|
+
(err) => {
|
|
459
|
+
process.stderr.write(`office-remote-client: ${err.message}\n`);
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
}
|
|
@@ -1123,6 +1123,14 @@ case "$ACTION" in
|
|
|
1123
1123
|
mkdir -p "$TRACKER_DIR" 2>/dev/null
|
|
1124
1124
|
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1125
1125
|
acquire_state_lock
|
|
1126
|
+
# A tracker that already holds phases is a run's history: a resume
|
|
1127
|
+
# continues it, and only an explicit --force starts it over.
|
|
1128
|
+
if [ "${2:-}" != "--force" ] && [ -f "$TRACKER_FILE" ] &&
|
|
1129
|
+
[ "$(jq -r '(.phases // []) | length' "$TRACKER_FILE" 2>/dev/null || echo 0)" != "0" ]; then
|
|
1130
|
+
release_state_lock
|
|
1131
|
+
echo "init: $TRACKER_FILE already holds phases; resume the run, or pass --force to start the tracker over" >&2
|
|
1132
|
+
exit 3
|
|
1133
|
+
fi
|
|
1126
1134
|
save_state "$(jq -nc --arg id "$TASK_ID" --arg ts "$NOW" \
|
|
1127
1135
|
'{task_id:$id,started_at:$ts,phases:[]}')"
|
|
1128
1136
|
release_state_lock
|
|
@@ -1,43 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* phase0-exit-gate.mjs - Phase 0 may not be marked completed until it has
|
|
4
|
-
*
|
|
4
|
+
* produced its own output.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* token-compliance check, no Code Connect publish, no 14-item component review,
|
|
14
|
-
* and spacing guessed at 16 where the frame said `Spacing/12`. Half the commits on
|
|
15
|
-
* that branch were rework.
|
|
6
|
+
* Phase 0 owns decisions every later phase reads back out of agent-state.json:
|
|
7
|
+
* the task type (Phase 3 routes a Figma-driven task to the component skills on
|
|
8
|
+
* it), the repository and base branch with the evidence for how the branch was
|
|
9
|
+
* chosen (`baseBranchSource`, `baseBranchEvidence.refProvenance` agreeing with
|
|
10
|
+
* `baseFetchStatus`), the dev-context siblings, the Figma access tier when the
|
|
11
|
+
* input carries a Figma URL, and, through require-supported-version.sh, that
|
|
12
|
+
* the installed pipeline is not below the published floor.
|
|
16
13
|
*
|
|
17
|
-
* A
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `agent-state.json` is the only durable evidence they happened - so the gate asserts
|
|
21
|
-
* their output too, not just `taskType`.
|
|
22
|
-
*
|
|
23
|
-
* A third run got past that, because `baseBranch` and `baseFetchStatus` can be filled
|
|
24
|
-
* in by a branch that was never chosen. The remote had one PR-targetable branch, the
|
|
25
|
-
* one-option `AskUserQuestion` was refused by the host (its schema needs two), and the
|
|
26
|
-
* run announced "only candidate, continuing with it" and carried on. The branch was
|
|
27
|
-
* right; nothing was asked. `baseBranchSource` is the field that separates those two,
|
|
28
|
-
* and the dev-context picker never ran at all - no `siblings`, so Phase 4's parity
|
|
29
|
-
* cross-check had nothing to read and could not tell an empty answer from no answer.
|
|
30
|
-
*
|
|
31
|
-
* A fourth showed the same shape one layer down: `git fetch origin` failed on a
|
|
32
|
-
* restricted network, `git branch -r` printed the remote-tracking cache anyway, and a
|
|
33
|
-
* weeks-old local list was presented as the remote's answer. Degrading to local refs is
|
|
34
|
-
* correct; reporting them as remote is not, so `baseBranchEvidence.refProvenance` has to
|
|
35
|
-
* agree with `baseFetchStatus`.
|
|
36
|
-
*
|
|
37
|
-
* A phase that reports success without its output is worse than one that fails:
|
|
38
|
-
* every later phase then reasons from a field that is not there. So this is a
|
|
39
|
-
* gate, not a lint - the spec already said what to write, and prose alone did
|
|
40
|
-
* not make it happen.
|
|
14
|
+
* A phase that reports success without its output is worse than one that
|
|
15
|
+
* fails: later phases then reason from fields that are not there. So this is a
|
|
16
|
+
* gate on the output, not on the phase's status.
|
|
41
17
|
*
|
|
42
18
|
* Usage:
|
|
43
19
|
* node phase0-exit-gate.mjs <task_id> [--input "<original user input>"] [--json]
|
|
@@ -48,9 +24,10 @@
|
|
|
48
24
|
*/
|
|
49
25
|
|
|
50
26
|
import { existsSync, readFileSync } from "fs";
|
|
51
|
-
import { join } from "path";
|
|
27
|
+
import { dirname, join } from "path";
|
|
52
28
|
import { homedir } from "os";
|
|
53
|
-
import { pathToFileURL } from "url";
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
30
|
+
import { spawnSync } from "child_process";
|
|
54
31
|
|
|
55
32
|
import { gatesActive } from "../lib/unattended.mjs";
|
|
56
33
|
|
|
@@ -337,6 +314,27 @@ export function evaluate(state, extraInput = "", { env = process.env } = {}) {
|
|
|
337
314
|
return { ok: failures.length === 0, failures, taskType: taskType || undefined, figmaSeen };
|
|
338
315
|
}
|
|
339
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The failure line when the installed pipeline is below the published floor,
|
|
319
|
+
* or null. Asks require-supported-version.sh with --cache-only: the answer
|
|
320
|
+
* Step 0.6 cached, at any age, and never the network, so the gate costs no
|
|
321
|
+
* wait and passes when nothing is cached.
|
|
322
|
+
*
|
|
323
|
+
* @param {{env?: NodeJS.ProcessEnv}} [opts]
|
|
324
|
+
* @returns {string|null}
|
|
325
|
+
*/
|
|
326
|
+
export function versionFloorFailure({ env = process.env } = {}) {
|
|
327
|
+
const script = join(dirname(fileURLToPath(import.meta.url)), "require-supported-version.sh");
|
|
328
|
+
if (!existsSync(script)) return null;
|
|
329
|
+
const r = spawnSync("bash", [script, "--cache-only"], { env, encoding: "utf8", timeout: 10_000 });
|
|
330
|
+
if (r.status !== 3) return null;
|
|
331
|
+
const [, local = "?", latest = "?", required = "?"] = String(r.stdout).trim().split("|");
|
|
332
|
+
return (
|
|
333
|
+
`the installed pipeline v${local} is below the required v${required} (latest v${latest}). ` +
|
|
334
|
+
"Run /multi-agent:update, then start the command again."
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
340
338
|
/** @param {string} taskId */
|
|
341
339
|
function statePathFor(taskId) {
|
|
342
340
|
return join(homedir(), ".claude", "logs", "multi-agent", taskId, "agent-state.json");
|
|
@@ -365,6 +363,11 @@ function main(argv) {
|
|
|
365
363
|
}
|
|
366
364
|
|
|
367
365
|
const result = evaluate(state, extraInput);
|
|
366
|
+
const floor = versionFloorFailure();
|
|
367
|
+
if (floor) {
|
|
368
|
+
result.failures.push(floor);
|
|
369
|
+
result.ok = false;
|
|
370
|
+
}
|
|
368
371
|
|
|
369
372
|
if (asJson) {
|
|
370
373
|
console.log(JSON.stringify({ taskId, statePath: path, ...result }, null, 2));
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* launch POST /v1/phone/launch, and only while `launch on` is set
|
|
15
15
|
*
|
|
16
16
|
* The registry is <autopilot root>/phone/devices.json (0600, directory 0700;
|
|
17
|
-
* phone-devices.schema.json).
|
|
17
|
+
* phone-devices.schema.json). `revoke` also drops the device's office remote
|
|
18
|
+
* keys (office-remote-client.mjs), so neither path reaches the machine any
|
|
19
|
+
* more. An unattended run may not change it: every
|
|
18
20
|
* subcommand but `list` refuses under MULTI_AGENT_UNATTENDED=1, and the guard
|
|
19
21
|
* blocks the same subcommands by name (unattended-policy.json).
|
|
20
22
|
*
|
|
@@ -37,6 +39,7 @@ import { readFileSync } from "node:fs";
|
|
|
37
39
|
import { runMain } from "../lib/fatal.mjs";
|
|
38
40
|
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
39
41
|
import { isUnattended } from "../lib/unattended.mjs";
|
|
42
|
+
import { readConfig, writeConfig } from "./office-remote-client.mjs";
|
|
40
43
|
import {
|
|
41
44
|
DEVICE_ID,
|
|
42
45
|
SCOPES,
|
|
@@ -197,6 +200,11 @@ function main(argv) {
|
|
|
197
200
|
}
|
|
198
201
|
d.revokedAt = new Date().toISOString();
|
|
199
202
|
writeRegistry(reg);
|
|
203
|
+
const remote = readConfig();
|
|
204
|
+
if (remote?.devices?.[id]) {
|
|
205
|
+
delete remote.devices[id];
|
|
206
|
+
writeConfig(remote);
|
|
207
|
+
}
|
|
200
208
|
process.stdout.write(JSON.stringify({ id, revokedAt: d.revokedAt }) + "\n");
|
|
201
209
|
return 0;
|
|
202
210
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* answer per objection, and a verdict decided by constitution rule id.
|
|
5
5
|
*
|
|
6
6
|
* At the end of Phase 1 the plan-critic persona writes objections to the plan
|
|
7
|
-
* under
|
|
7
|
+
* under six lenses (scope, feasibility, security, a cheaper alternative, an
|
|
8
|
+
* input the spec implies but no test exercises, a decision left to the
|
|
9
|
+
* implementer), and
|
|
8
10
|
* the planner answers each one exactly once: accept and revise the plan, or
|
|
9
11
|
* rebut with evidence. Both halves live in one file,
|
|
10
12
|
* `<worktree>/.pipeline/plan-critique.json` (schemas/plan-critique.schema.json).
|
|
@@ -59,7 +61,14 @@ import { requirementIds } from "./spec-consistency-gate.mjs";
|
|
|
59
61
|
import { resolveOne } from "./verify-citations.mjs";
|
|
60
62
|
|
|
61
63
|
export const GATE = "plan-critique";
|
|
62
|
-
export const LENSES = [
|
|
64
|
+
export const LENSES = [
|
|
65
|
+
"scope",
|
|
66
|
+
"feasibility",
|
|
67
|
+
"security",
|
|
68
|
+
"alternative",
|
|
69
|
+
"unstated-input",
|
|
70
|
+
"underspecified",
|
|
71
|
+
];
|
|
63
72
|
export const ANCHOR_KINDS = ["file", "plan", "analysis", "inference"];
|
|
64
73
|
export const ACTIONS = ["accept", "rebut"];
|
|
65
74
|
const OBJ_ID = /^OBJ-\d{2,3}$/;
|