blun-king-cli 9.1.587 → 9.1.588
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 +11 -185
- package/LIESMICH.txt +51 -13
- package/README.md +44 -47
- package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
- package/agent-spine-plugin/CHANGELOG.md +37 -5
- package/agent-spine-plugin/README.md +3 -3
- package/agent-spine-plugin/blun.plugin.json +45 -10
- package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
- package/agent-spine-plugin/docs/host-integration.md +42 -27
- package/agent-spine-plugin/docs/preflight-recall.md +4 -2
- package/agent-spine-plugin/docs/session-timeline.md +97 -236
- package/agent-spine-plugin/docs/world-model.md +25 -0
- package/agent-spine-plugin/hooks/codex.json +1 -1
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/package.json +1 -3
- package/agent-spine-plugin/scripts/check-hosts.js +3 -3
- package/agent-spine-plugin/scripts/release-check.js +10 -5
- package/agent-spine-plugin/scripts/run-checks.js +4 -1
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
- package/agent-spine-plugin/src/cli-learning.js +15 -0
- package/agent-spine-plugin/src/cli.js +2 -0
- package/agent-spine-plugin/src/hook.js +32 -32
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
- package/agent-spine-plugin/src/lib/briefing.js +146 -36
- package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
- package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
- package/agent-spine-plugin/src/lib/hook-context.js +16 -3
- package/agent-spine-plugin/src/lib/hook-output.js +129 -5
- package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
- package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
- package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
- package/agent-spine-plugin/src/lib/learning-context.js +11 -4
- package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
- package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
- package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
- package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
- package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
- package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
- package/agent-spine-plugin/src/lib/preflight.js +4 -4
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
- package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
- package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
- package/agent-spine-plugin/src/lib/source-roots.js +3 -2
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
- package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
- package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
- package/agent-spine-plugin/src/lib/world-model.js +64 -9
- package/agent-spine-plugin/src/worker.js +13 -1
- package/bin/blun.js +43 -28
- package/bin/core-bootstrap.js +5 -4
- package/bin/king.js +43 -28
- package/bin/launcher-mode.js +1 -10
- package/bin/launcher-runtime.js +128 -295
- package/bin/managed-node.js +0 -0
- package/bin/managed-plugin-selection.cjs +0 -1
- package/bin/native-module-repair.js +0 -0
- package/bin/node-runtime.js +0 -0
- package/bin/node-version.js +0 -0
- package/bin/plugin-bootstrap.js +56 -120
- package/bin/private-paths.js +11 -34
- package/bin/standard-tools-bootstrap.js +34 -114
- package/bin/turn-thinking-policy.cjs +3 -11
- package/bin/update-copy.js +200 -0
- package/bin/update-lease.js +0 -0
- package/bin/update-notice.js +136 -289
- package/bin/verify-agent-behavior.cjs +122 -0
- package/bin/verify-agent-components.cjs +104 -0
- package/bin/verify-bundled-agent-sources.cjs +57 -0
- package/blun.mjs +143076 -135288
- package/bundled-agent-sources.json +701 -0
- package/package.json +12 -15
- package/standard-skills/translate-native/README.md +1293 -0
- package/standard-skills/translate-native/SKILL.md +172 -22
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/agents/openai.yaml +18 -0
- package/standard-skills/translate-native/assets/icon.svg +8 -0
- package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
- package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
- package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
- package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
- package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
- package/standard-skills/translate-native/integrations/audit_log.py +327 -0
- package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
- package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
- package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
- package/standard-skills/translate-native/integrations/guard_service.py +435 -0
- package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
- package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
- package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
- package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
- package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
- package/standard-skills/translate-native/integrations/task_router.py +101 -0
- package/standard-skills/translate-native/integrations/website_localization.py +401 -0
- package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
- package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
- package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
- package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
- package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
- package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
- package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
- package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
- package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
- package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
- package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
- package/standard-skills/translate-native/provenance.json +3 -4
- package/standard-skills/translate-native/references/commercial-localization.md +177 -0
- package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/LICENSE +21 -0
- package/standard-tools/language-guard/VERSION +1 -0
- package/standard-tools/language-guard/blun_language_guard.py +7 -1
- package/standard-tools/language-guard/check_commercial_review.py +80 -0
- package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
- package/standard-tools/language-guard/language_gateway.py +62 -0
- package/standard-tools/language-guard/pre_output_guard.py +64 -0
- package/standard-tools/language-guard/provenance.json +4 -11
- package/standard-tools/manifest.json +34 -11
- package/telegram-plugin/commands/access.md +2 -10
- package/telegram-plugin/dist/bridge.mjs +64041 -687
- package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
- package/telegram-plugin/dist/noise.mjs +28 -63511
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
- package/agent-spine-plugin/docs/releasing.md +0 -85
- package/agent-spine-plugin/docs/structured-completion.md +0 -67
- package/bin/abort-listener-policy.cjs +0 -43
- package/bin/active-steer-priority-policy.cjs +0 -24
- package/bin/agent-api-http-adapter.mjs +0 -446
- package/bin/agent-api-private-http-server.mjs +0 -288
- package/bin/agent-api-runtime.mjs +0 -252
- package/bin/agent-api-service-environment.mjs +0 -236
- package/bin/agent-api-service-host.mjs +0 -209
- package/bin/agent-api-service-process.mjs +0 -171
- package/bin/agent-api-session-registry.mjs +0 -428
- package/bin/agent-api-tool-broker.cjs +0 -248
- package/bin/agent-api-turn-controller.mjs +0 -461
- package/bin/agent-api-usage-journal.cjs +0 -259
- package/bin/agent-resume-snapshot.cjs +0 -241
- package/bin/agentspine-king-goal-inbox.mjs +0 -111
- package/bin/agentspine-king-goal-intake.mjs +0 -106
- package/bin/approval-rejection-stop.cjs +0 -15
- package/bin/assistant-message-offload-policy.cjs +0 -284
- package/bin/baseline-skill-performance-policy.cjs +0 -39
- package/bin/bash-search-scope-policy.cjs +0 -49
- package/bin/codebase-search-runtime.cjs +0 -23
- package/bin/cognitive-action-checkpoint.cjs +0 -1104
- package/bin/cognitive-attention-delivery.cjs +0 -76
- package/bin/cognitive-attention-policy.cjs +0 -143
- package/bin/cognitive-attention-runtime.cjs +0 -91
- package/bin/cognitive-context-projection.cjs +0 -73
- package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
- package/bin/cognitive-effective-view.cjs +0 -77
- package/bin/cognitive-focus-projection.cjs +0 -206
- package/bin/cognitive-focus-scope.cjs +0 -37
- package/bin/cognitive-goal-autostart-policy.cjs +0 -72
- package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
- package/bin/cognitive-memory-adapter.cjs +0 -282
- package/bin/cognitive-memory-command.cjs +0 -293
- package/bin/cognitive-memory-provider.cjs +0 -92
- package/bin/cognitive-salience-policy.cjs +0 -159
- package/bin/cognitive-state-store.cjs +0 -508
- package/bin/cognitive-turn-lifecycle.cjs +0 -624
- package/bin/cognitive-work-focus.cjs +0 -180
- package/bin/compaction-history-archive.cjs +0 -166
- package/bin/compaction-history-startup.cjs +0 -50
- package/bin/compaction-model-policy.cjs +0 -31
- package/bin/compaction-stage-policy.cjs +0 -21
- package/bin/compaction-transaction-policy.cjs +0 -122
- package/bin/config-write-dedup-policy.cjs +0 -27
- package/bin/context-budget-ledger.cjs +0 -31
- package/bin/context-doctor-policy.cjs +0 -70
- package/bin/context-insight-policy.cjs +0 -36
- package/bin/context-performance-policy.cjs +0 -19
- package/bin/context-pressure-policy.cjs +0 -20
- package/bin/cron-run-output.cjs +0 -45
- package/bin/cron-run-store.cjs +0 -145
- package/bin/curiosity-scout-policy.cjs +0 -49
- package/bin/default-model-output-budget-policy.cjs +0 -28
- package/bin/durable-task-resume-policy.cjs +0 -130
- package/bin/durable-task-resume-runtime.cjs +0 -117
- package/bin/durable-task-resume-store.cjs +0 -88
- package/bin/editable-tool-approval-policy.cjs +0 -540
- package/bin/editable-tool-approval-runtime.cjs +0 -99
- package/bin/effective-system-prompt-cache-policy.cjs +0 -33
- package/bin/error-memory-performance-policy.cjs +0 -113
- package/bin/file-observation-policy.cjs +0 -133
- package/bin/foreground-output-capture-policy.cjs +0 -41
- package/bin/generated-source-health.cjs +0 -142
- package/bin/glob-pattern-policy.cjs +0 -13
- package/bin/goal-completion-evidence-policy.cjs +0 -120
- package/bin/grep-output-limit-policy.cjs +0 -39
- package/bin/historical-media-projection-policy.cjs +0 -48
- package/bin/history-offload-pressure-policy.cjs +0 -33
- package/bin/html-to-research-markdown.cjs +0 -147
- package/bin/identity-context-policy.cjs +0 -764
- package/bin/identity-journal-policy.cjs +0 -107
- package/bin/input-draft-persistence.cjs +0 -77
- package/bin/king-tui-function-contract.json +0 -33
- package/bin/launcher-restart-policy.cjs +0 -150
- package/bin/live-response-repetition-guard.cjs +0 -196
- package/bin/llm-config-log-dedup-policy.cjs +0 -76
- package/bin/loop-event-record-policy.cjs +0 -174
- package/bin/managed-context-startup-policy.cjs +0 -27
- package/bin/media-activity-layout-policy.cjs +0 -34
- package/bin/media-auto-retrieval-policy.cjs +0 -90
- package/bin/media-result-policy.cjs +0 -59
- package/bin/micro-compaction-policy.cjs +0 -145
- package/bin/mistake-relevance-policy.cjs +0 -319
- package/bin/model-retry-progress-policy.cjs +0 -46
- package/bin/native-large-file-io.cjs +0 -42
- package/bin/native-runtime-cache.cjs +0 -76
- package/bin/natural-presence-policy.cjs +0 -28
- package/bin/noninteractive-shell-env-policy.cjs +0 -19
- package/bin/observer-hooks.cjs +0 -14
- package/bin/outbound-claim-provenance.cjs +0 -150
- package/bin/oversized-context-offload-policy.cjs +0 -86
- package/bin/pending-media-policy.cjs +0 -182
- package/bin/pending-token-estimate-policy.cjs +0 -41
- package/bin/personal-memory-consent-policy.cjs +0 -72
- package/bin/personal-memory-performance-policy.cjs +0 -12
- package/bin/personality-choice-policy.cjs +0 -101
- package/bin/personality-memory-adapter.cjs +0 -379
- package/bin/personality-mode.cjs +0 -46
- package/bin/personality-setup-policy.cjs +0 -197
- package/bin/proactive-compaction-policy.cjs +0 -25
- package/bin/profile-identity-resolution.cjs +0 -136
- package/bin/profile-runtime.cjs +0 -318
- package/bin/profile-tool-exclusion-policy.cjs +0 -37
- package/bin/programmatic-context-isolation.cjs +0 -25
- package/bin/programmatic-tool-runtime.mjs +0 -627
- package/bin/provider-idle-timeout-policy.cjs +0 -14
- package/bin/provider-model-refresh-deadline.cjs +0 -53
- package/bin/provider-model-refresh-policy.cjs +0 -107
- package/bin/rate-limit-recovery-policy.cjs +0 -47
- package/bin/read-batch-policy.cjs +0 -32
- package/bin/read-continuation-policy.cjs +0 -59
- package/bin/recurring-cron-history-policy.cjs +0 -124
- package/bin/relationship-continuity-policy.cjs +0 -143
- package/bin/relationship-curiosity-policy.cjs +0 -107
- package/bin/relationship-learning-policy.cjs +0 -168
- package/bin/release-artifact-freeze-policy.cjs +0 -30
- package/bin/reload-plugin-bootstrap.cjs +0 -18
- package/bin/reload-queue-policy.cjs +0 -38
- package/bin/repeated-assistant-response-policy.cjs +0 -232
- package/bin/repeated-injection-projection.cjs +0 -107
- package/bin/repeated-user-message-projection.cjs +0 -8
- package/bin/research-page-result.cjs +0 -74
- package/bin/retry-checkpoint-policy.cjs +0 -13
- package/bin/runtime-exit-ledger.cjs +0 -144
- package/bin/scoped-cron-run-policy.cjs +0 -358
- package/bin/session-checkpoint-policy.cjs +0 -25
- package/bin/session-compaction-policy.cjs +0 -84
- package/bin/session-replay-policy.cjs +0 -20
- package/bin/session-replay-window-policy.cjs +0 -40
- package/bin/session-resume-checkpoint.cjs +0 -254
- package/bin/session-scrollback-archive.cjs +0 -229
- package/bin/skill-activation-performance-policy.cjs +0 -69
- package/bin/skill-listing-performance-policy.cjs +0 -92
- package/bin/soul-organization-policy.cjs +0 -78
- package/bin/soul-preservation-policy.cjs +0 -20
- package/bin/startup-preferences.cjs +0 -131
- package/bin/streaming-flush-performance-policy.cjs +0 -28
- package/bin/structured-agent-swarm-output.cjs +0 -325
- package/bin/structured-subagent-output.cjs +0 -252
- package/bin/subagent-context-fork-policy.cjs +0 -155
- package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
- package/bin/subagent-parent-responsiveness.cjs +0 -19
- package/bin/subagent-skill-policy.cjs +0 -206
- package/bin/subagent-timeout-policy.cjs +0 -182
- package/bin/subagent-tool-policy.cjs +0 -60
- package/bin/subagent-usage-rollup-policy.cjs +0 -29
- package/bin/system-prompt-context-policy.cjs +0 -124
- package/bin/system-prompt-token-cache-policy.cjs +0 -60
- package/bin/telegram-addressed-focus.cjs +0 -55
- package/bin/telegram-addressed-priority.cjs +0 -12
- package/bin/telegram-approval-relay.cjs +0 -290
- package/bin/telegram-bot-priority.cjs +0 -17
- package/bin/telegram-console-status-policy.cjs +0 -174
- package/bin/telegram-context-projection-policy.cjs +0 -141
- package/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/bin/telegram-direct-focus-policy.cjs +0 -273
- package/bin/telegram-mcp-compatibility.cjs +0 -49
- package/bin/telegram-media-delivery-policy.cjs +0 -42
- package/bin/telegram-private-conversation-policy.cjs +0 -185
- package/bin/telegram-queue-handoff-policy.cjs +0 -73
- package/bin/telegram-remote-status-policy.cjs +0 -120
- package/bin/telegram-session-queue-runtime.mjs +0 -306
- package/bin/telegram-text-chunk-policy.cjs +0 -63
- package/bin/telegram-truncated-reply-policy.cjs +0 -37
- package/bin/telegram-urgent-policy.cjs +0 -45
- package/bin/telemetry-spool-policy.cjs +0 -57
- package/bin/thinking-activity-status-policy.cjs +0 -132
- package/bin/thinking-only-guard.cjs +0 -80
- package/bin/todo-list-turn-policy.cjs +0 -131
- package/bin/tool-call-loop-policy.cjs +0 -51
- package/bin/tool-file-persistence.cjs +0 -141
- package/bin/tool-result-offload-policy.cjs +0 -359
- package/bin/tool-result-offload-telemetry.cjs +0 -12
- package/bin/tool-schema-token-cache-policy.cjs +0 -41
- package/bin/tool-stream-preview-policy.cjs +0 -9
- package/bin/tui-functional-contract.cjs +0 -55
- package/bin/turn-tool-performance-policy.cjs +0 -486
- package/bin/usage-cache-efficiency-policy.cjs +0 -26
- package/bin/user-home-path-policy.cjs +0 -13
- package/bin/user-message-offload-policy.cjs +0 -103
- package/bin/user-prompt-hook-origin-policy.cjs +0 -34
- package/bin/user-tool-record-policy.cjs +0 -7
- package/bin/validated-learning-insight-policy.cjs +0 -58
- package/bin/validated-learning-outcome-trace.cjs +0 -107
- package/bin/validated-learning-performance-policy.cjs +0 -53
- package/bin/validated-learning-signal.cjs +0 -463
- package/bin/windows-bash-dialect-policy.cjs +0 -25
- package/bin/windows-node-crash-dump.cjs +0 -110
- package/bin/write-continuation-policy.cjs +0 -69
- package/codebase-index/README.md +0 -82
- package/codebase-index/codebase_index.py +0 -470
- package/standard-skills/agent-browser/SKILL.md +0 -19
- package/standard-skills/agent-browser/references/runtime.md +0 -8
- package/standard-skills/blun-session-inspector/SKILL.md +0 -41
- package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
- package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
- package/standard-skills/full-output-enforcement/SKILL.md +0 -49
- package/standard-skills/high-end-visual-design/SKILL.md +0 -98
- package/standard-skills/image-to-code/SKILL.md +0 -1228
- package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
- package/standard-skills/minimalist-ui/SKILL.md +0 -85
- package/standard-skills/motion-design-taste/SKILL.md +0 -74
- package/standard-skills/playwright-testing/SKILL.md +0 -19
- package/standard-skills/playwright-testing/references/runtime.md +0 -7
- package/standard-skills/premortem/SKILL.md +0 -148
- package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
- package/standard-skills/research-evidence/SKILL.md +0 -39
- package/standard-skills/research-evidence/references/evidence-format.md +0 -104
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
- package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
- package/standard-skills/screenshot-lesen/SKILL.md +0 -52
- package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
- package/standard-skills/stitch-design-taste/SKILL.md +0 -184
- package/standard-skills/telegram-channel/SKILL.md +0 -18
- package/standard-skills/telegram-channel/references/runtime.md +0 -7
- package/standard-skills/venture-flywheel/SKILL.md +0 -32
- package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
- package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
- package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
- package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
- package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
- package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
- package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
- package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
- package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
- package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
- package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
- package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
- package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
- package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
- package/standard-skills/web-lesen/SKILL.md +0 -73
- package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
- package/standard-skills/windows-mcp/SKILL.md +0 -19
- package/standard-skills/windows-mcp/references/runtime.md +0 -9
- package/telegram-plugin/DELIVERY.md +0 -36
- package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
- package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
- package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
- package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
- package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
- package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
- package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
- package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
- /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Durable, content-free terminal notifications for a source CMS.
|
|
3
|
+
|
|
4
|
+
The localization lifecycle remains authoritative. This outbox copies only the
|
|
5
|
+
verified terminal binding and delivers it through a host-supplied callback.
|
|
6
|
+
Lost responses are replayed with the same notification identity.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
import secrets
|
|
16
|
+
import sqlite3
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SCHEMA_VERSION = 1
|
|
23
|
+
NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
|
|
24
|
+
ACK_SCHEMA = "blun.cms-source-terminal-notification-ack.v1"
|
|
25
|
+
STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
26
|
+
TERMINAL_STATUSES = {
|
|
27
|
+
"cancelled", "deleted", "deletion_failed", "localization_failed",
|
|
28
|
+
"publication_blocked", "publication_failed", "published", "superseded",
|
|
29
|
+
}
|
|
30
|
+
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
31
|
+
_COLUMNS = (
|
|
32
|
+
"event_id", "notification_id", "payload_json", "payload_sha256",
|
|
33
|
+
"status", "attempts", "max_attempts", "next_attempt_at",
|
|
34
|
+
"lease_owner", "lease_token", "lease_expires_at", "last_error_code",
|
|
35
|
+
"ack_sha256", "created_at", "updated_at",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TerminalNotificationBlocked(RuntimeError):
|
|
40
|
+
"""Stable notification failure without customer content."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, code: str):
|
|
43
|
+
super().__init__(code)
|
|
44
|
+
self.code = code
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TerminalNotificationFailure(RuntimeError):
|
|
48
|
+
"""Public callback failure contract used to request a bounded retry."""
|
|
49
|
+
|
|
50
|
+
cms_notification_failure = True
|
|
51
|
+
|
|
52
|
+
def __init__(self, code: str, *, retryable: bool):
|
|
53
|
+
if not _error(code) or not isinstance(retryable, bool):
|
|
54
|
+
raise ValueError("invalid terminal notification failure")
|
|
55
|
+
super().__init__(code)
|
|
56
|
+
self.code = code
|
|
57
|
+
self.retryable = retryable
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class TerminalNotificationClaim:
|
|
62
|
+
event_id: str
|
|
63
|
+
notification_id: str
|
|
64
|
+
payload_sha256: str
|
|
65
|
+
attempt: int
|
|
66
|
+
max_attempts: int
|
|
67
|
+
lease_owner: str
|
|
68
|
+
lease_token: str
|
|
69
|
+
lease_expires_at: float
|
|
70
|
+
payload_json: str = field(repr=False, compare=False)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class TerminalNotificationStatus:
|
|
75
|
+
event_id: str
|
|
76
|
+
notification_id: str
|
|
77
|
+
payload_sha256: str
|
|
78
|
+
status: str
|
|
79
|
+
attempts: int
|
|
80
|
+
max_attempts: int
|
|
81
|
+
next_attempt_at: float
|
|
82
|
+
lease_expires_at: float | None
|
|
83
|
+
lease_expired: bool
|
|
84
|
+
last_error_code: str | None
|
|
85
|
+
ack_sha256: str | None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class TerminalNotificationOutcome:
|
|
90
|
+
event_id: str
|
|
91
|
+
notification_id: str
|
|
92
|
+
status: str
|
|
93
|
+
attempt: int
|
|
94
|
+
error_code: str | None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class TerminalNotificationHealth:
|
|
99
|
+
status: str
|
|
100
|
+
counts: dict[str, int]
|
|
101
|
+
due: int
|
|
102
|
+
expired_leases: int
|
|
103
|
+
failed: int
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _canonical(value: Any) -> str:
|
|
107
|
+
try:
|
|
108
|
+
return json.dumps(
|
|
109
|
+
value, ensure_ascii=False, allow_nan=False, sort_keys=True,
|
|
110
|
+
separators=(",", ":"),
|
|
111
|
+
)
|
|
112
|
+
except (TypeError, ValueError, RecursionError):
|
|
113
|
+
raise TerminalNotificationBlocked("terminal_notification.payload_invalid") from None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _hash(value: str) -> str:
|
|
117
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _token(value: Any, *, limit: int = 256) -> str:
|
|
121
|
+
if (
|
|
122
|
+
not isinstance(value, str) or not 1 <= len(value) <= limit
|
|
123
|
+
or any(
|
|
124
|
+
not (
|
|
125
|
+
"A" <= character <= "Z" or "a" <= character <= "z"
|
|
126
|
+
or "0" <= character <= "9" or character in "_.:-"
|
|
127
|
+
)
|
|
128
|
+
for character in value
|
|
129
|
+
)
|
|
130
|
+
):
|
|
131
|
+
raise TerminalNotificationBlocked("terminal_notification.payload_invalid")
|
|
132
|
+
return value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _sha(value: Any) -> str:
|
|
136
|
+
if (
|
|
137
|
+
not isinstance(value, str) or len(value) != 64
|
|
138
|
+
or any(character not in "0123456789abcdef" for character in value)
|
|
139
|
+
):
|
|
140
|
+
raise TerminalNotificationBlocked("terminal_notification.payload_invalid")
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _error(value: Any) -> bool:
|
|
145
|
+
return isinstance(value, str) and ERROR_CODE.fullmatch(value) is not None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _time(value: Any) -> float:
|
|
149
|
+
if (
|
|
150
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
151
|
+
or not math.isfinite(float(value)) or float(value) < 0
|
|
152
|
+
):
|
|
153
|
+
raise TerminalNotificationBlocked("terminal_notification.time_invalid")
|
|
154
|
+
return float(value)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _duration(value: Any, code: str) -> float:
|
|
158
|
+
result = _time(value)
|
|
159
|
+
if not 0 < result <= 86_400:
|
|
160
|
+
raise TerminalNotificationBlocked(code)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _payload(lifecycle: Any) -> tuple[str, str, str]:
|
|
165
|
+
def value(name: str) -> Any:
|
|
166
|
+
return (
|
|
167
|
+
lifecycle.get(name)
|
|
168
|
+
if isinstance(lifecycle, Mapping)
|
|
169
|
+
else getattr(lifecycle, name, None)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if (
|
|
173
|
+
value("state") != "terminal"
|
|
174
|
+
or value("remote_status") not in TERMINAL_STATUSES
|
|
175
|
+
):
|
|
176
|
+
raise TerminalNotificationBlocked("terminal_notification.lifecycle_invalid")
|
|
177
|
+
event_id = _token(value("event_id"))
|
|
178
|
+
lifecycle_sha256 = value("lifecycle_sha256")
|
|
179
|
+
if lifecycle_sha256 is not None:
|
|
180
|
+
lifecycle_sha256 = _sha(lifecycle_sha256)
|
|
181
|
+
if (value("remote_status") in {"cancelled", "superseded"}) != (
|
|
182
|
+
lifecycle_sha256 is None
|
|
183
|
+
):
|
|
184
|
+
raise TerminalNotificationBlocked("terminal_notification.lifecycle_invalid")
|
|
185
|
+
content = {
|
|
186
|
+
"schema": NOTIFICATION_SCHEMA,
|
|
187
|
+
"event_id": event_id,
|
|
188
|
+
"site_id": _token(value("site_id")),
|
|
189
|
+
"plan_id": _token(value("plan_id")),
|
|
190
|
+
"website_version": _token(value("website_version")),
|
|
191
|
+
"source_sequence": value("source_sequence"),
|
|
192
|
+
"job_count": value("job_count"),
|
|
193
|
+
"change_sha256": _sha(value("change_sha256")),
|
|
194
|
+
"lifecycle_binding_sha256": _sha(value("binding_sha256")),
|
|
195
|
+
"terminal_status": value("remote_status"),
|
|
196
|
+
"lifecycle_sha256": lifecycle_sha256,
|
|
197
|
+
}
|
|
198
|
+
for name in ("source_sequence", "job_count"):
|
|
199
|
+
item = content[name]
|
|
200
|
+
if isinstance(item, bool) or not isinstance(item, int) or item <= 0:
|
|
201
|
+
raise TerminalNotificationBlocked("terminal_notification.lifecycle_invalid")
|
|
202
|
+
identity = _hash(_canonical(content))
|
|
203
|
+
content["notification_id"] = "terminal-" + identity
|
|
204
|
+
rendered = _canonical(content)
|
|
205
|
+
return event_id, rendered, _hash(rendered)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@contextmanager
|
|
209
|
+
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
210
|
+
if connection.in_transaction:
|
|
211
|
+
raise TerminalNotificationBlocked("terminal_notification.transaction_nested")
|
|
212
|
+
try:
|
|
213
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
214
|
+
yield
|
|
215
|
+
connection.commit()
|
|
216
|
+
except Exception:
|
|
217
|
+
if connection.in_transaction:
|
|
218
|
+
connection.rollback()
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class DurableCMSTerminalNotifier:
|
|
223
|
+
"""Register and deliver exact terminal lifecycle evidence."""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
connection: sqlite3.Connection,
|
|
228
|
+
*,
|
|
229
|
+
base_delay_seconds: float | int = 5,
|
|
230
|
+
max_delay_seconds: float | int = 300,
|
|
231
|
+
):
|
|
232
|
+
if not isinstance(connection, sqlite3.Connection):
|
|
233
|
+
raise TypeError("connection must be sqlite3.Connection")
|
|
234
|
+
self.connection = connection
|
|
235
|
+
self.connection.row_factory = sqlite3.Row
|
|
236
|
+
self.base_delay_seconds = _duration(
|
|
237
|
+
base_delay_seconds, "terminal_notification.delay_invalid"
|
|
238
|
+
)
|
|
239
|
+
self.max_delay_seconds = _duration(
|
|
240
|
+
max_delay_seconds, "terminal_notification.delay_invalid"
|
|
241
|
+
)
|
|
242
|
+
if self.base_delay_seconds > self.max_delay_seconds:
|
|
243
|
+
raise TerminalNotificationBlocked("terminal_notification.delay_invalid")
|
|
244
|
+
self._initialize()
|
|
245
|
+
|
|
246
|
+
def _initialize(self) -> None:
|
|
247
|
+
with _transaction(self.connection):
|
|
248
|
+
self.connection.execute("""
|
|
249
|
+
CREATE TABLE IF NOT EXISTS cms_source_terminal_notification_meta (
|
|
250
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
251
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 1)
|
|
252
|
+
)
|
|
253
|
+
""")
|
|
254
|
+
self.connection.execute("""
|
|
255
|
+
INSERT OR IGNORE INTO cms_source_terminal_notification_meta
|
|
256
|
+
VALUES (1, 1)
|
|
257
|
+
""")
|
|
258
|
+
self.connection.execute("""
|
|
259
|
+
CREATE TABLE IF NOT EXISTS cms_source_terminal_notification (
|
|
260
|
+
event_id TEXT PRIMARY KEY, notification_id TEXT NOT NULL UNIQUE,
|
|
261
|
+
payload_json TEXT NOT NULL, payload_sha256 TEXT NOT NULL,
|
|
262
|
+
status TEXT NOT NULL CHECK (
|
|
263
|
+
status IN ('pending', 'leased', 'retry_wait',
|
|
264
|
+
'succeeded', 'failed')
|
|
265
|
+
),
|
|
266
|
+
attempts INTEGER NOT NULL CHECK (attempts >= 0),
|
|
267
|
+
max_attempts INTEGER NOT NULL CHECK (max_attempts BETWEEN 1 AND 20),
|
|
268
|
+
next_attempt_at REAL NOT NULL, lease_owner TEXT, lease_token TEXT,
|
|
269
|
+
lease_expires_at REAL, last_error_code TEXT, ack_sha256 TEXT,
|
|
270
|
+
created_at REAL NOT NULL, updated_at REAL NOT NULL,
|
|
271
|
+
CHECK (
|
|
272
|
+
(status = 'leased' AND lease_owner IS NOT NULL
|
|
273
|
+
AND lease_token IS NOT NULL
|
|
274
|
+
AND lease_expires_at IS NOT NULL)
|
|
275
|
+
OR
|
|
276
|
+
(status <> 'leased' AND lease_owner IS NULL
|
|
277
|
+
AND lease_token IS NULL
|
|
278
|
+
AND lease_expires_at IS NULL)
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
""")
|
|
282
|
+
self.connection.execute("""
|
|
283
|
+
CREATE INDEX IF NOT EXISTS cms_source_terminal_notification_due
|
|
284
|
+
ON cms_source_terminal_notification (status, next_attempt_at, created_at, event_id)
|
|
285
|
+
""")
|
|
286
|
+
self._validate_schema()
|
|
287
|
+
|
|
288
|
+
def _validate_schema(self) -> None:
|
|
289
|
+
meta_columns = tuple(row["name"] for row in self.connection.execute(
|
|
290
|
+
"PRAGMA table_info(cms_source_terminal_notification_meta)"
|
|
291
|
+
).fetchall())
|
|
292
|
+
columns = tuple(row["name"] for row in self.connection.execute(
|
|
293
|
+
"PRAGMA table_info(cms_source_terminal_notification)"
|
|
294
|
+
).fetchall())
|
|
295
|
+
meta = self.connection.execute(
|
|
296
|
+
"SELECT singleton, schema_version FROM cms_source_terminal_notification_meta"
|
|
297
|
+
).fetchall()
|
|
298
|
+
if (
|
|
299
|
+
meta_columns != ("singleton", "schema_version")
|
|
300
|
+
or columns != _COLUMNS or len(meta) != 1
|
|
301
|
+
or tuple(meta[0]) != (1, SCHEMA_VERSION)
|
|
302
|
+
):
|
|
303
|
+
raise TerminalNotificationBlocked("terminal_notification.schema_altered")
|
|
304
|
+
|
|
305
|
+
def register(
|
|
306
|
+
self,
|
|
307
|
+
lifecycle: Any,
|
|
308
|
+
*,
|
|
309
|
+
max_attempts: int = 5,
|
|
310
|
+
now: float | int,
|
|
311
|
+
) -> TerminalNotificationStatus:
|
|
312
|
+
self._validate_schema()
|
|
313
|
+
if (
|
|
314
|
+
isinstance(max_attempts, bool)
|
|
315
|
+
or not isinstance(max_attempts, int)
|
|
316
|
+
or not 1 <= max_attempts <= 20
|
|
317
|
+
):
|
|
318
|
+
raise TerminalNotificationBlocked(
|
|
319
|
+
"terminal_notification.max_attempts_invalid"
|
|
320
|
+
)
|
|
321
|
+
now = _time(now)
|
|
322
|
+
event_id, payload_json, payload_sha256 = _payload(lifecycle)
|
|
323
|
+
notification_id = json.loads(payload_json)["notification_id"]
|
|
324
|
+
with _transaction(self.connection):
|
|
325
|
+
row = self.connection.execute(
|
|
326
|
+
"SELECT payload_sha256, max_attempts "
|
|
327
|
+
"FROM cms_source_terminal_notification WHERE event_id = ?",
|
|
328
|
+
(event_id,),
|
|
329
|
+
).fetchone()
|
|
330
|
+
if row is not None:
|
|
331
|
+
if (
|
|
332
|
+
row["payload_sha256"] != payload_sha256
|
|
333
|
+
or row["max_attempts"] != max_attempts
|
|
334
|
+
):
|
|
335
|
+
raise TerminalNotificationBlocked(
|
|
336
|
+
"terminal_notification.idempotency_collision"
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
self.connection.execute("""
|
|
340
|
+
INSERT INTO cms_source_terminal_notification (
|
|
341
|
+
event_id, notification_id, payload_json, payload_sha256, status,
|
|
342
|
+
attempts, max_attempts, next_attempt_at, created_at, updated_at
|
|
343
|
+
) VALUES (?, ?, ?, ?, 'pending', 0, ?, ?, ?, ?)
|
|
344
|
+
""", (event_id, notification_id, payload_json, payload_sha256,
|
|
345
|
+
max_attempts, now, now, now))
|
|
346
|
+
return self.status(event_id, now=now)
|
|
347
|
+
|
|
348
|
+
def claim(
|
|
349
|
+
self,
|
|
350
|
+
worker_id: str,
|
|
351
|
+
*,
|
|
352
|
+
now: float | int,
|
|
353
|
+
lease_seconds: float | int = 600,
|
|
354
|
+
) -> TerminalNotificationClaim | None:
|
|
355
|
+
self._validate_schema()
|
|
356
|
+
worker_id = _token(worker_id, limit=128)
|
|
357
|
+
now = _time(now)
|
|
358
|
+
lease_seconds = _duration(
|
|
359
|
+
lease_seconds, "terminal_notification.lease_invalid"
|
|
360
|
+
)
|
|
361
|
+
with _transaction(self.connection):
|
|
362
|
+
self.connection.execute("""
|
|
363
|
+
UPDATE cms_source_terminal_notification
|
|
364
|
+
SET status = CASE WHEN attempts >= max_attempts
|
|
365
|
+
THEN 'failed' ELSE 'retry_wait' END, next_attempt_at = ?,
|
|
366
|
+
lease_owner = NULL, lease_token = NULL,
|
|
367
|
+
lease_expires_at = NULL,
|
|
368
|
+
last_error_code = 'lease_expired',
|
|
369
|
+
updated_at = ? WHERE status = 'leased' AND lease_expires_at <= ?
|
|
370
|
+
""", (now, now, now))
|
|
371
|
+
row = self.connection.execute("""
|
|
372
|
+
SELECT * FROM cms_source_terminal_notification
|
|
373
|
+
WHERE status IN ('pending','retry_wait') AND next_attempt_at <= ?
|
|
374
|
+
AND attempts < max_attempts ORDER BY created_at, event_id LIMIT 1
|
|
375
|
+
""", (now,)).fetchone()
|
|
376
|
+
if row is None:
|
|
377
|
+
return None
|
|
378
|
+
self._validated_row(row)
|
|
379
|
+
token = secrets.token_urlsafe(32)
|
|
380
|
+
expires = now + lease_seconds
|
|
381
|
+
updated = self.connection.execute("""
|
|
382
|
+
UPDATE cms_source_terminal_notification
|
|
383
|
+
SET status = 'leased', attempts = attempts + 1,
|
|
384
|
+
lease_owner = ?, lease_token = ?, lease_expires_at = ?,
|
|
385
|
+
last_error_code = NULL, updated_at = ?
|
|
386
|
+
WHERE event_id = ? AND status IN ('pending', 'retry_wait')
|
|
387
|
+
""", (worker_id, token, expires, now, row["event_id"]))
|
|
388
|
+
if updated.rowcount != 1:
|
|
389
|
+
raise TerminalNotificationBlocked(
|
|
390
|
+
"terminal_notification.claim_lost"
|
|
391
|
+
)
|
|
392
|
+
return TerminalNotificationClaim(
|
|
393
|
+
row["event_id"], row["notification_id"], row["payload_sha256"],
|
|
394
|
+
int(row["attempts"]) + 1, int(row["max_attempts"]), worker_id,
|
|
395
|
+
token, expires, row["payload_json"],
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def run_once(
|
|
399
|
+
self,
|
|
400
|
+
callback: Callable[[Mapping[str, Any]], Any],
|
|
401
|
+
worker_id: str,
|
|
402
|
+
*,
|
|
403
|
+
now: float | int,
|
|
404
|
+
lease_seconds: float | int = 600,
|
|
405
|
+
) -> TerminalNotificationOutcome | None:
|
|
406
|
+
if not callable(callback):
|
|
407
|
+
raise TerminalNotificationBlocked("terminal_notification.callback_invalid")
|
|
408
|
+
claim = self.claim(worker_id, now=now, lease_seconds=lease_seconds)
|
|
409
|
+
if claim is None:
|
|
410
|
+
return None
|
|
411
|
+
try:
|
|
412
|
+
response = callback(json.loads(claim.payload_json))
|
|
413
|
+
status = self.complete(claim, response, now=now)
|
|
414
|
+
except TerminalNotificationBlocked as error:
|
|
415
|
+
if error.code != "terminal_notification.response_invalid":
|
|
416
|
+
raise
|
|
417
|
+
status = self.fail(claim, error.code, now=now)
|
|
418
|
+
except Exception as error:
|
|
419
|
+
retryable = (
|
|
420
|
+
getattr(error, "cms_notification_failure", False) is True
|
|
421
|
+
and getattr(error, "retryable", None) is True
|
|
422
|
+
)
|
|
423
|
+
code = getattr(error, "code", None)
|
|
424
|
+
if not _error(code):
|
|
425
|
+
code, retryable = "terminal_notification.callback_failure", False
|
|
426
|
+
status = (
|
|
427
|
+
self.retry(claim, code, now=now)
|
|
428
|
+
if retryable
|
|
429
|
+
else self.fail(claim, code, now=now)
|
|
430
|
+
)
|
|
431
|
+
return TerminalNotificationOutcome(
|
|
432
|
+
status.event_id, status.notification_id, status.status,
|
|
433
|
+
status.attempts, status.last_error_code,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def complete(
|
|
437
|
+
self,
|
|
438
|
+
claim: TerminalNotificationClaim,
|
|
439
|
+
response: Any,
|
|
440
|
+
*,
|
|
441
|
+
now: float | int,
|
|
442
|
+
) -> TerminalNotificationStatus:
|
|
443
|
+
now = _time(now)
|
|
444
|
+
if not isinstance(response, Mapping):
|
|
445
|
+
raise TerminalNotificationBlocked("terminal_notification.response_invalid")
|
|
446
|
+
try:
|
|
447
|
+
copied = json.loads(_canonical(dict(response)))
|
|
448
|
+
except TerminalNotificationBlocked:
|
|
449
|
+
raise TerminalNotificationBlocked(
|
|
450
|
+
"terminal_notification.response_invalid"
|
|
451
|
+
) from None
|
|
452
|
+
expected = {
|
|
453
|
+
"schema": ACK_SCHEMA, "notification_id": claim.notification_id,
|
|
454
|
+
"event_id": claim.event_id,
|
|
455
|
+
"site_id": json.loads(claim.payload_json)["site_id"],
|
|
456
|
+
"status": "accepted", "notification_sha256": claim.payload_sha256,
|
|
457
|
+
}
|
|
458
|
+
if copied != expected:
|
|
459
|
+
raise TerminalNotificationBlocked("terminal_notification.response_invalid")
|
|
460
|
+
ack_json = _canonical(copied)
|
|
461
|
+
with _transaction(self.connection):
|
|
462
|
+
self._require_claim(claim, now)
|
|
463
|
+
updated = self.connection.execute("""
|
|
464
|
+
UPDATE cms_source_terminal_notification
|
|
465
|
+
SET status = 'succeeded', next_attempt_at = ?,
|
|
466
|
+
lease_owner = NULL, lease_token = NULL,
|
|
467
|
+
lease_expires_at = NULL,
|
|
468
|
+
last_error_code = NULL, ack_sha256 = ?, updated_at = ?
|
|
469
|
+
WHERE event_id = ? AND status = 'leased'
|
|
470
|
+
AND lease_owner = ? AND lease_token = ?
|
|
471
|
+
""", (
|
|
472
|
+
now, _hash(ack_json), now, claim.event_id,
|
|
473
|
+
claim.lease_owner, claim.lease_token,
|
|
474
|
+
))
|
|
475
|
+
if updated.rowcount != 1:
|
|
476
|
+
raise TerminalNotificationBlocked("terminal_notification.completion_lost")
|
|
477
|
+
return self.status(claim.event_id, now=now)
|
|
478
|
+
|
|
479
|
+
def retry(
|
|
480
|
+
self, claim: TerminalNotificationClaim, code: str, *, now: float | int,
|
|
481
|
+
) -> TerminalNotificationStatus:
|
|
482
|
+
return self._finish(claim, code, retryable=True, now=now)
|
|
483
|
+
|
|
484
|
+
def fail(
|
|
485
|
+
self, claim: TerminalNotificationClaim, code: str, *, now: float | int,
|
|
486
|
+
) -> TerminalNotificationStatus:
|
|
487
|
+
return self._finish(claim, code, retryable=False, now=now)
|
|
488
|
+
|
|
489
|
+
def _finish(
|
|
490
|
+
self,
|
|
491
|
+
claim: TerminalNotificationClaim,
|
|
492
|
+
code: str,
|
|
493
|
+
*,
|
|
494
|
+
retryable: bool,
|
|
495
|
+
now: float | int,
|
|
496
|
+
) -> TerminalNotificationStatus:
|
|
497
|
+
if not _error(code):
|
|
498
|
+
raise TerminalNotificationBlocked("terminal_notification.error_invalid")
|
|
499
|
+
now = _time(now)
|
|
500
|
+
with _transaction(self.connection):
|
|
501
|
+
row = self._require_claim(claim, now)
|
|
502
|
+
terminal = not retryable or int(row["attempts"]) >= int(
|
|
503
|
+
row["max_attempts"]
|
|
504
|
+
)
|
|
505
|
+
delay = min(
|
|
506
|
+
self.max_delay_seconds,
|
|
507
|
+
self.base_delay_seconds * (2 ** max(0, claim.attempt - 1)),
|
|
508
|
+
)
|
|
509
|
+
updated = self.connection.execute("""
|
|
510
|
+
UPDATE cms_source_terminal_notification SET status = ?, next_attempt_at = ?,
|
|
511
|
+
lease_owner = NULL, lease_token = NULL, lease_expires_at = NULL,
|
|
512
|
+
last_error_code = ?, updated_at = ?
|
|
513
|
+
WHERE event_id = ? AND status = 'leased'
|
|
514
|
+
AND lease_owner = ? AND lease_token = ?
|
|
515
|
+
""", (
|
|
516
|
+
"failed" if terminal else "retry_wait",
|
|
517
|
+
now if terminal else now + delay,
|
|
518
|
+
code, now, claim.event_id, claim.lease_owner,
|
|
519
|
+
claim.lease_token,
|
|
520
|
+
))
|
|
521
|
+
if updated.rowcount != 1:
|
|
522
|
+
raise TerminalNotificationBlocked(
|
|
523
|
+
"terminal_notification.failure_lost"
|
|
524
|
+
)
|
|
525
|
+
return self.status(claim.event_id, now=now)
|
|
526
|
+
|
|
527
|
+
def status(
|
|
528
|
+
self, event_id: str, *, now: float | int,
|
|
529
|
+
) -> TerminalNotificationStatus:
|
|
530
|
+
self._validate_schema()
|
|
531
|
+
event_id = _token(event_id)
|
|
532
|
+
now = _time(now)
|
|
533
|
+
row = self.connection.execute(
|
|
534
|
+
"SELECT * FROM cms_source_terminal_notification WHERE event_id = ?", (event_id,)
|
|
535
|
+
).fetchone()
|
|
536
|
+
if row is None:
|
|
537
|
+
raise TerminalNotificationBlocked("terminal_notification.missing")
|
|
538
|
+
self._validated_row(row)
|
|
539
|
+
lease = row["lease_expires_at"]
|
|
540
|
+
return TerminalNotificationStatus(
|
|
541
|
+
event_id, row["notification_id"], row["payload_sha256"], row["status"],
|
|
542
|
+
int(row["attempts"]), int(row["max_attempts"]), float(row["next_attempt_at"]),
|
|
543
|
+
None if lease is None else float(lease),
|
|
544
|
+
row["status"] == "leased" and float(lease) <= now,
|
|
545
|
+
row["last_error_code"], row["ack_sha256"],
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def health(self, *, now: float | int) -> TerminalNotificationHealth:
|
|
549
|
+
now = _time(now)
|
|
550
|
+
self._validate_schema()
|
|
551
|
+
quick = self.connection.execute("PRAGMA quick_check").fetchall()
|
|
552
|
+
if len(quick) != 1 or tuple(quick[0]) != ("ok",):
|
|
553
|
+
raise TerminalNotificationBlocked(
|
|
554
|
+
"terminal_notification.database_integrity"
|
|
555
|
+
)
|
|
556
|
+
rows = self.connection.execute(
|
|
557
|
+
"SELECT * FROM cms_source_terminal_notification ORDER BY event_id"
|
|
558
|
+
).fetchall()
|
|
559
|
+
for row in rows:
|
|
560
|
+
self._validated_row(row)
|
|
561
|
+
counts = {status: 0 for status in STATUSES}
|
|
562
|
+
for row in rows:
|
|
563
|
+
counts[row["status"]] += 1
|
|
564
|
+
due = sum(
|
|
565
|
+
row["status"] in {"pending", "retry_wait"}
|
|
566
|
+
and row["next_attempt_at"] <= now
|
|
567
|
+
for row in rows
|
|
568
|
+
)
|
|
569
|
+
expired = sum(
|
|
570
|
+
row["status"] == "leased" and row["lease_expires_at"] <= now
|
|
571
|
+
for row in rows
|
|
572
|
+
)
|
|
573
|
+
return TerminalNotificationHealth(
|
|
574
|
+
"blocked" if expired or counts["failed"] else "ok", counts, due,
|
|
575
|
+
expired, counts["failed"],
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def _require_claim(
|
|
579
|
+
self, claim: TerminalNotificationClaim, now: float,
|
|
580
|
+
) -> sqlite3.Row:
|
|
581
|
+
row = self.connection.execute(
|
|
582
|
+
"SELECT * FROM cms_source_terminal_notification WHERE event_id = ?",
|
|
583
|
+
(claim.event_id,),
|
|
584
|
+
).fetchone()
|
|
585
|
+
if (
|
|
586
|
+
row is None or row["status"] != "leased"
|
|
587
|
+
or row["lease_owner"] != claim.lease_owner
|
|
588
|
+
or row["lease_token"] != claim.lease_token
|
|
589
|
+
):
|
|
590
|
+
raise TerminalNotificationBlocked("terminal_notification.claim_lost")
|
|
591
|
+
self._validated_row(row)
|
|
592
|
+
if float(row["lease_expires_at"]) <= now:
|
|
593
|
+
raise TerminalNotificationBlocked("terminal_notification.lease_expired")
|
|
594
|
+
return row
|
|
595
|
+
|
|
596
|
+
def _validated_row(self, row: sqlite3.Row) -> None:
|
|
597
|
+
try:
|
|
598
|
+
payload = json.loads(row["payload_json"])
|
|
599
|
+
lifecycle = dict(payload)
|
|
600
|
+
lifecycle.pop("notification_id")
|
|
601
|
+
expected_id = "terminal-" + _hash(_canonical(lifecycle))
|
|
602
|
+
lifecycle_sha256 = payload.get("lifecycle_sha256")
|
|
603
|
+
terminal_status = payload.get("terminal_status")
|
|
604
|
+
semantic_valid = (
|
|
605
|
+
_token(payload.get("event_id")) == payload.get("event_id")
|
|
606
|
+
and _token(payload.get("site_id")) == payload.get("site_id")
|
|
607
|
+
and _token(payload.get("plan_id")) == payload.get("plan_id")
|
|
608
|
+
and _token(payload.get("website_version"))
|
|
609
|
+
== payload.get("website_version")
|
|
610
|
+
and isinstance(payload.get("source_sequence"), int)
|
|
611
|
+
and not isinstance(payload.get("source_sequence"), bool)
|
|
612
|
+
and payload["source_sequence"] > 0
|
|
613
|
+
and isinstance(payload.get("job_count"), int)
|
|
614
|
+
and not isinstance(payload.get("job_count"), bool)
|
|
615
|
+
and payload["job_count"] > 0
|
|
616
|
+
and _sha(payload.get("change_sha256"))
|
|
617
|
+
== payload.get("change_sha256")
|
|
618
|
+
and _sha(payload.get("lifecycle_binding_sha256"))
|
|
619
|
+
== payload.get("lifecycle_binding_sha256")
|
|
620
|
+
and terminal_status in TERMINAL_STATUSES
|
|
621
|
+
and (
|
|
622
|
+
terminal_status in {"cancelled", "superseded"}
|
|
623
|
+
and lifecycle_sha256 is None
|
|
624
|
+
or terminal_status not in {"cancelled", "superseded"}
|
|
625
|
+
and _sha(lifecycle_sha256) == lifecycle_sha256
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
valid_payload = (
|
|
629
|
+
payload.get("schema") == NOTIFICATION_SCHEMA
|
|
630
|
+
and set(payload) == {
|
|
631
|
+
"schema", "notification_id", "event_id", "site_id", "plan_id",
|
|
632
|
+
"website_version", "source_sequence", "job_count", "change_sha256",
|
|
633
|
+
"lifecycle_binding_sha256", "terminal_status", "lifecycle_sha256",
|
|
634
|
+
}
|
|
635
|
+
and payload["event_id"] == row["event_id"]
|
|
636
|
+
and payload["notification_id"] == row["notification_id"] == expected_id
|
|
637
|
+
and _hash(row["payload_json"]) == row["payload_sha256"]
|
|
638
|
+
and semantic_valid
|
|
639
|
+
)
|
|
640
|
+
except Exception:
|
|
641
|
+
valid_payload = False
|
|
642
|
+
status = row["status"]
|
|
643
|
+
leased = status == "leased"
|
|
644
|
+
lease_values = (row["lease_owner"], row["lease_token"], row["lease_expires_at"])
|
|
645
|
+
succeeded = status == "succeeded"
|
|
646
|
+
error = row["last_error_code"]
|
|
647
|
+
expected_ack_sha256 = None
|
|
648
|
+
if valid_payload:
|
|
649
|
+
expected_ack_sha256 = _hash(_canonical({
|
|
650
|
+
"schema": ACK_SCHEMA,
|
|
651
|
+
"notification_id": payload["notification_id"],
|
|
652
|
+
"event_id": payload["event_id"],
|
|
653
|
+
"site_id": payload["site_id"],
|
|
654
|
+
"status": "accepted",
|
|
655
|
+
"notification_sha256": row["payload_sha256"],
|
|
656
|
+
}))
|
|
657
|
+
if (
|
|
658
|
+
not valid_payload or status not in STATUSES
|
|
659
|
+
or isinstance(row["attempts"], bool) or not isinstance(row["attempts"], int)
|
|
660
|
+
or not 0 <= row["attempts"] <= row["max_attempts"] <= 20
|
|
661
|
+
or row["max_attempts"] < 1
|
|
662
|
+
or leased != all(item is not None for item in lease_values)
|
|
663
|
+
or (not leased and any(item is not None for item in lease_values))
|
|
664
|
+
or (status in {"retry_wait", "failed"}) != (error is not None)
|
|
665
|
+
or (error is not None and not _error(error))
|
|
666
|
+
or succeeded != (row["ack_sha256"] is not None)
|
|
667
|
+
or (
|
|
668
|
+
row["ack_sha256"] is not None
|
|
669
|
+
and row["ack_sha256"] != expected_ack_sha256
|
|
670
|
+
)
|
|
671
|
+
or _time(row["next_attempt_at"]) < _time(row["created_at"])
|
|
672
|
+
or _time(row["updated_at"]) < _time(row["created_at"])
|
|
673
|
+
):
|
|
674
|
+
raise TerminalNotificationBlocked("terminal_notification.state_invalid")
|