@event4u/agent-config 14.12.0 → 14.14.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +118 -155
- package/CONTRIBUTING.md +38 -0
- package/CREDITS.md +135 -0
- package/NOTICE +13 -0
- package/README.md +8 -6
- package/dist/agent-src/commands/analyze/inbox.md +145 -2
- package/dist/agent-src/commands/feature/dev.md +7 -2
- package/dist/agent-src/commands/ghostwriter/write.md +5 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/mission/upgrade.md +1 -1
- package/dist/agent-src/commands/orchestrate.md +2 -2
- package/dist/agent-src/commands/post-as/me.md +5 -1
- package/dist/agent-src/commands/pr/merge.md +19 -6
- package/dist/agent-src/commands/roadmap/process-full.md +19 -5
- package/dist/agent-src/commands/tdd/red.md +12 -2
- package/dist/agent-src/commands/work.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +40 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -2
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +21 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +40 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +13 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +96 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/code-provenance.md +4 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/domain-safety-pii.md +4 -0
- package/dist/agent-src/rules/domain-safety-retention.md +4 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +14 -12
- package/dist/agent-src/rules/improve-before-implement.md +6 -8
- package/dist/agent-src/rules/laravel-routing.md +1 -1
- package/dist/agent-src/rules/lethal-trifecta-guard.md +4 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +12 -34
- package/dist/agent-src/rules/symfony-routing.md +1 -1
- package/dist/agent-src/rules/token-efficiency.md +14 -0
- package/dist/agent-src/rules/tool-safety.md +4 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +4 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +252 -8
- package/dist/agent-src/scripts/guarded_baseline.ts +343 -0
- package/dist/agent-src/scripts/roadmap_set_step.ts +253 -0
- package/dist/agent-src/scripts/stub_queue.ts +82 -0
- package/dist/agent-src/scripts/stubs_due.ts +516 -0
- package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +16 -12
- package/dist/agent-src/skills/brand-asset-generation/SKILL.md +17 -4
- package/dist/agent-src/skills/brand-audit/SKILL.md +33 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -0
- package/dist/agent-src/skills/database/SKILL.md +8 -0
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +27 -0
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +17 -1
- package/dist/agent-src/skills/feature-planning/SKILL.md +9 -2
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +44 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/scripts/check_package_surface.ts +38 -4
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +2 -2
- package/dist/agent-src/skills/laravel/SKILL.md +35 -0
- package/dist/agent-src/skills/laravel-migration/SKILL.md +57 -3
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +26 -0
- package/dist/agent-src/skills/playbook-authoring/SKILL.md +28 -1
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/references/procedure.md +5 -5
- package/dist/agent-src/skills/sql-writing/SKILL.md +7 -1
- package/dist/agent-src/skills/standards-from-config/SKILL.md +27 -4
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +11 -4
- package/dist/agent-src/skills/symfony-workflow/SKILL.md +35 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +85 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +33 -9
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +37 -0
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +45 -2
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +38 -1
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +9 -4
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +97 -3
- package/dist/agent-src/templates/scripts/check_memory.ts +258 -17
- package/dist/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
- package/dist/agent-src/templates/scripts/memory_lookup.ts +86 -0
- package/dist/agent-src/templates/scripts/memory_signal.ts +44 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
- package/dist/cli/commands/uiAudit.js +124 -5
- package/dist/cli/registry.js +2 -0
- package/dist/cli-delegate/{chunk-HYWSRNUX.js → chunk-2DK2VJWG.js} +3 -3
- package/dist/cli-delegate/{chunk-JCW7ZLHK.js → chunk-4FS4WRXP.js} +5 -0
- package/dist/cli-delegate/{chunk-OMIVVCG2.js → chunk-A4QJ6ENT.js} +24 -0
- package/dist/cli-delegate/{chunk-JJA2QEWA.js → chunk-AKJGUQNO.js} +466 -285
- package/dist/cli-delegate/{chunk-E46KPN6K.js → chunk-BTWUYDGN.js} +424 -363
- package/dist/cli-delegate/{chunk-K4FFODTS.js → chunk-ILMUPLGW.js} +1227 -316
- package/dist/cli-delegate/{chunk-FF7WAFMR.js → chunk-KPBWFOPZ.js} +5 -1
- package/dist/cli-delegate/cmd_adr_effective.js +102 -0
- package/dist/cli-delegate/cmd_conformance.js +80 -11
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +3 -2
- package/dist/cli-delegate/cmd_fleet.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +3 -2
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +3 -2
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +3 -2
- package/dist/cli-delegate/cmd_uninstall.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +3 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +58 -58
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2900 -1049
- package/dist/install/claudeRuleRewrite.js +254 -0
- package/dist/install/detect.js +29 -16
- package/dist/install/detect_php_shape.js +159 -0
- package/dist/install/install.mjs +634 -428
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +3 -3
- package/dist/mcp/server.mjs +4895 -2627
- package/dist/server/schemas/settings.js +3 -0
- package/dist/ui/assets/index-D0jaCRPM.js +29 -0
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/THIRD-PARTY-NOTICES.md +25 -0
- package/docs/contracts/provider-lifecycle.md +1 -1
- package/docs/contracts/settings-classes.md +10 -4
- package/docs/guidelines/agent-infra/5w2h-analysis.md +3 -3
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -0
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +58 -3
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +2 -2
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +3 -3
- package/docs/guidelines/agent-infra/break-glass-usage.md +7 -7
- package/docs/guidelines/agent-infra/carve-out-predicates.md +3 -3
- package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
- package/docs/guidelines/agent-infra/critical-thinking.md +4 -4
- package/docs/guidelines/agent-infra/direct-answers-demos.md +2 -2
- package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +1 -1
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +3 -3
- package/docs/guidelines/agent-infra/existence-question-verdicts.md +80 -0
- package/docs/guidelines/agent-infra/first-principles.md +2 -2
- package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +2 -2
- package/docs/guidelines/agent-infra/guarded-baseline.md +39 -0
- package/docs/guidelines/agent-infra/inversion-thinking.md +5 -5
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +10 -10
- package/docs/guidelines/agent-infra/memory-access.md +1 -1
- package/docs/guidelines/agent-infra/mental-models.md +3 -3
- package/docs/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
- package/docs/guidelines/agent-infra/review-routing-data-format.md +7 -7
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +13 -7
- package/docs/guidelines/agent-infra/role-contracts.md +2 -2
- package/docs/guidelines/agent-infra/role-mode-router.md +3 -3
- package/docs/guidelines/agent-infra/rule-type-governance.md +2 -2
- package/docs/guidelines/agent-infra/scqa-framework.md +5 -5
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +6 -6
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +2 -2
- package/docs/guidelines/agent-infra/six-hats.md +3 -3
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +6 -6
- package/docs/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
- package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +2 -2
- package/docs/guidelines/augment-portability-patterns.md +6 -4
- package/docs/guidelines/code-clarity.md +18 -0
- package/docs/guidelines/component-oriented-and-oop-development.md +3 -2
- package/docs/guidelines/cross-role-handoff.md +2 -2
- package/docs/guidelines/design-fidelity-mechanics.md +1 -1
- package/docs/guidelines/docs/readme-size-and-splitting.md +3 -3
- package/docs/guidelines/php/database.md +90 -7
- package/docs/guidelines/prompt-templates.md +7 -7
- package/llms.txt +2 -2
- package/package.json +5 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +252 -8
- package/src/agent-src/scripts/guarded_baseline.ts +343 -0
- package/src/agent-src/scripts/roadmap_set_step.ts +253 -0
- package/src/agent-src/scripts/stub_queue.ts +82 -0
- package/src/agent-src/scripts/stubs_due.ts +516 -0
- package/src/agent-src/scripts/ticket_bundles.ts +84 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +16 -12
- package/src/agent-src/templates/scripts/check_memory.ts +258 -17
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
- package/src/agent-src/templates/scripts/memory_lookup.ts +86 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +44 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
- package/src/config/agent-settings.template.yml +32 -3
- package/src/config/assurance-capability-registry.json +21 -9
- package/src/config/canonical-terms.yml +119 -0
- package/src/config/capability-native-outcome-bars.json +81 -0
- package/src/config/ci-local-parity.yml +80 -0
- package/src/config/drafting-channels.yml +43 -0
- package/src/config/estate-count-budget.json +57 -36
- package/src/config/evaluator-budgets.json +14 -14
- package/src/config/gate-coverage.yml +729 -63
- package/src/config/gate-reachability-exemptions.json +21 -0
- package/src/config/gate-violation-baselines.json +42 -19
- package/src/config/harness-evolution-budget.json +25 -0
- package/src/config/lapsed-beta-baseline.json +96 -0
- package/src/config/memory-twin-verdicts.yml +129 -0
- package/src/config/metric-registry.yml +124 -0
- package/src/config/pack-size-budget.json +16 -15
- package/src/config/preamble-payload-budget.json +12 -4
- package/src/config/profiles/balanced.ini +27 -0
- package/src/config/profiles/full.ini +27 -0
- package/src/config/profiles/minimal.ini +27 -0
- package/src/config/publish-surface.json +9 -2
- package/src/config/routing-coverage-seed.json +16 -2
- package/src/config/rule-stub-ceilings.json +133 -112
- package/src/config/turnaround-budget.json +66 -0
- package/src/install/claudeRuleRewrite.ts +288 -0
- package/src/install/detect.ts +60 -17
- package/src/install/detect_php_shape.ts +191 -0
- package/src/scripts/_cli/cmd_doctor.ts +27 -112
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/activation_ladder.ts +255 -0
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/activation_states.ts +88 -0
- package/src/scripts/_lib/adherence_detectors.ts +143 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/arith_claims.ts +181 -0
- package/src/scripts/_lib/arm_ranking.ts +62 -0
- package/src/scripts/_lib/asset_delivery_ledger.ts +303 -0
- package/src/scripts/_lib/audit_field_provenance.ts +54 -0
- package/src/scripts/_lib/auto_dispatch.ts +32 -3
- package/src/scripts/_lib/bench_ab_size_claim.ts +98 -4
- package/src/scripts/_lib/benchmark_cache_fields.ts +158 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/candidate_proposer.ts +402 -0
- package/src/scripts/_lib/candidate_record.ts +601 -0
- package/src/scripts/_lib/capture_rate.ts +319 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/collector_denominator.ts +771 -0
- package/src/scripts/_lib/collector_record.ts +340 -0
- package/src/scripts/_lib/collector_store.ts +664 -0
- package/src/scripts/_lib/collector_supervision.ts +827 -0
- package/src/scripts/_lib/concern_estate.ts +77 -0
- package/src/scripts/_lib/config_chain.ts +477 -0
- package/src/scripts/_lib/config_cost.ts +209 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/council_settings_block.ts +6 -0
- package/src/scripts/_lib/curator_ops.ts +172 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +282 -0
- package/src/scripts/_lib/delivery_criticality.ts +126 -0
- package/src/scripts/_lib/doctor_runtime_checks.ts +70 -0
- package/src/scripts/_lib/empty_cycles.ts +88 -0
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/eval_publication.ts +354 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evaluation_vector.ts +283 -0
- package/src/scripts/_lib/evaluator_promotion.ts +225 -0
- package/src/scripts/_lib/evidence_basis.ts +118 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/exec_evidence.ts +8 -0
- package/src/scripts/_lib/experience_card.ts +272 -0
- package/src/scripts/_lib/experience_report.ts +151 -0
- package/src/scripts/_lib/experiment_freeze.ts +112 -0
- package/src/scripts/_lib/generated_by.ts +66 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +239 -0
- package/src/scripts/_lib/hook_effect_probe.ts +141 -0
- package/src/scripts/_lib/host_denominator.ts +687 -0
- package/src/scripts/_lib/host_projection_reach.ts +157 -0
- package/src/scripts/_lib/ignored_blocker.ts +148 -0
- package/src/scripts/_lib/install_reach_checks.ts +495 -0
- package/src/scripts/_lib/json_duplicate_keys.ts +150 -0
- package/src/scripts/_lib/judge_hygiene.ts +228 -0
- package/src/scripts/_lib/legacy_boundary_map.ts +228 -0
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/loop_guards.ts +299 -0
- package/src/scripts/_lib/loss_class.ts +129 -0
- package/src/scripts/_lib/machine_wake.ts +51 -0
- package/src/scripts/_lib/md_prose_lines.ts +177 -0
- package/src/scripts/_lib/minimality_tiebreak.ts +168 -0
- package/src/scripts/_lib/one_resolver_invariant.ts +449 -0
- package/src/scripts/_lib/orchestration_record.ts +253 -5
- package/src/scripts/_lib/outcome_envelope.ts +227 -0
- package/src/scripts/_lib/outcome_vocabularies.ts +125 -0
- package/src/scripts/_lib/paired_stats.ts +245 -0
- package/src/scripts/_lib/paired_verdict.ts +226 -0
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/prefix_stable_surfaces.ts +123 -0
- package/src/scripts/_lib/privacy_class.ts +47 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
- package/src/scripts/_lib/release_highlights.ts +70 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +105 -0
- package/src/scripts/_lib/repo_root.ts +111 -0
- package/src/scripts/_lib/review_skipped_record.ts +37 -0
- package/src/scripts/_lib/role_split.ts +338 -0
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +1499 -0
- package/src/scripts/_lib/runtime_wiring_checks.ts +430 -0
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/_lib/skill_catalogue.ts +11 -1
- package/src/scripts/_lib/source_digest.ts +200 -0
- package/src/scripts/_lib/source_redact.ts +260 -0
- package/src/scripts/_lib/source_shape.ts +502 -0
- package/src/scripts/_lib/source_snapshot_dedup.ts +199 -0
- package/src/scripts/_lib/strip_comments.d.mts +8 -0
- package/src/scripts/_lib/strip_comments.mjs +65 -0
- package/src/scripts/_lib/test_red_state.ts +154 -0
- package/src/scripts/_lib/trigger_shift.ts +144 -0
- package/src/scripts/_lib/untrusted_content.ts +82 -7
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/adoption_report.ts +1 -1
- package/src/scripts/adr/evidence_census.ts +1 -1
- package/src/scripts/adr/regenerate_index.ts +26 -2
- package/src/scripts/adr_cite_check.ts +164 -1
- package/src/scripts/ai_council/blind_review.ts +11 -1
- package/src/scripts/ai_council/clients.ts +4 -0
- package/src/scripts/ai_council/config.ts +3 -0
- package/src/scripts/ai_council/consensus.ts +28 -8
- package/src/scripts/ai_council/consensus_round.ts +226 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/disagreement_signal.ts +338 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/inline_findings.ts +340 -0
- package/src/scripts/ai_council/judge_position_bias.ts +197 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/orchestrator.ts +16 -203
- package/src/scripts/ai_council/orchestrator_results.ts +23 -3
- package/src/scripts/ai_council/pricing.ts +106 -0
- package/src/scripts/ai_council/prompts.ts +74 -7
- package/src/scripts/ai_council/provider_leakage_bench.ts +327 -0
- package/src/scripts/ai_council/py_format.ts +14 -3
- package/src/scripts/ai_council/recouncil_guard.ts +293 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/response_record.ts +50 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/ai_council/topology_bench_manifest.ts +860 -0
- package/src/scripts/ai_council/topology_vocabulary.ts +175 -0
- package/src/scripts/asset_delivery_ledger.ts +157 -0
- package/src/scripts/attest_artifact.ts +193 -0
- package/src/scripts/audit_auto_rules.ts +2 -1
- package/src/scripts/audit_command_surface.ts +1 -1
- package/src/scripts/audit_mcp_tools.ts +2 -2
- package/src/scripts/audit_skill_overlap_allowlist.json +6 -1
- package/src/scripts/audit_user_type_axis.ts +2 -1
- package/src/scripts/bench_ab_clone.ts +215 -10
- package/src/scripts/bench_ab_integrity.ts +119 -5
- package/src/scripts/bench_ab_v2_stats.ts +40 -213
- package/src/scripts/build_archive_index.ts +1 -1
- package/src/scripts/build_linear_digest.ts +1 -1
- package/src/scripts/build_proof.ts +11 -4
- package/src/scripts/build_rule_trigger_matrix.ts +1 -1
- package/src/scripts/build_source_digests.ts +157 -0
- package/src/scripts/cache_realization_report.ts +103 -0
- package/src/scripts/check_adr_frontmatter.ts +131 -10
- package/src/scripts/check_benchmark_report_fields.ts +207 -0
- package/src/scripts/check_beta_review_markers.ts +235 -6
- package/src/scripts/check_bundle_path_leakage.ts +34 -1
- package/src/scripts/check_claims.ts +576 -6
- package/src/scripts/check_composite_arming.ts +35 -0
- package/src/scripts/check_concern_admissions.ts +365 -0
- package/src/scripts/check_enforcement_coverage.ts +28 -0
- package/src/scripts/check_estate_count.ts +347 -25
- package/src/scripts/check_gate_reachability.ts +383 -0
- package/src/scripts/check_host_loadability.ts +104 -0
- package/src/scripts/check_loss_class_declared.ts +308 -0
- package/src/scripts/check_md_language.ts +15 -98
- package/src/scripts/check_memory_proposal.ts +18 -11
- package/src/scripts/check_no_currency_in_cost_surfaces.ts +225 -0
- package/src/scripts/check_no_external_sources.ts +281 -6
- package/src/scripts/check_pack_conformance_fixture.ts +11 -1
- package/src/scripts/check_pack_size.ts +59 -2
- package/src/scripts/check_preamble_payload_budget.ts +114 -3
- package/src/scripts/check_prefix_stable_mutation.ts +372 -0
- package/src/scripts/check_publish_surface.ts +10 -1
- package/src/scripts/check_references.ts +24 -1
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_rule_evidence_declaration.ts +316 -0
- package/src/scripts/check_rule_projection_integrity.ts +14 -4
- package/src/scripts/check_score_contract.ts +26 -3
- package/src/scripts/check_static_parity.ts +285 -0
- package/src/scripts/check_supervision_claim_atomicity.ts +498 -0
- package/src/scripts/check_suppression_hygiene.ts +15 -0
- package/src/scripts/ci_settle.ts +39 -2
- package/src/scripts/code_graph/build.ts +60 -3
- package/src/scripts/code_graph/extract.ts +44 -0
- package/src/scripts/code_graph/types.ts +24 -0
- package/src/scripts/collector_daemon.ts +714 -0
- package/src/scripts/config_cost_report.ts +302 -0
- package/src/scripts/consumer_matrix.ts +74 -1
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/cost/budget.mjs +10 -3
- package/src/scripts/cost/track.mjs +10 -3
- package/src/scripts/council_cli.ts +37 -54
- package/src/scripts/dispatch_r2_reviewer.ts +46 -5
- package/src/scripts/evolution_lab.ts +1153 -0
- package/src/scripts/external_sources_denylist.json +5 -18
- package/src/scripts/extract_audit_patterns.ts +21 -0
- package/src/scripts/fold_intake.ts +12 -1
- package/src/scripts/gen_discovery_baseline.ts +1 -1
- package/src/scripts/generate_capabilities_index.ts +12 -5
- package/src/scripts/generate_capability_matrix.ts +3 -3
- package/src/scripts/generate_command_flows.ts +2 -2
- package/src/scripts/generate_cookbook.ts +2 -2
- package/src/scripts/generate_index.ts +8 -1
- package/src/scripts/generate_knowledge_index.ts +2 -1
- package/src/scripts/generate_ownership_matrix.ts +2 -1
- package/src/scripts/generate_pack_manifests.ts +10 -2
- package/src/scripts/generate_role_experiences_catalog.ts +2 -2
- package/src/scripts/generate_settings_reference.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +22 -1
- package/src/scripts/grade_target_readiness.ts +48 -5
- package/src/scripts/hook_effect_doctor.ts +261 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +125 -16
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +271 -0
- package/src/scripts/hooks/block_unauthorized_git.ts +18 -0
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +32 -0
- package/src/scripts/hooks/journal_record_hook.ts +345 -0
- package/src/scripts/hooks/prepush_metadata_sources.sh +110 -0
- package/src/scripts/hooks/run_continuation_hook.ts +44 -9
- package/src/scripts/hooks/shims/install.sh +61 -0
- package/src/scripts/hooks/shims/php +101 -0
- package/src/scripts/hooks/suggestion_capture_hook.ts +10 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/hot_context_hook.ts +16 -0
- package/src/scripts/install-hooks.sh +20 -5
- package/src/scripts/install.ts +17 -35
- package/src/scripts/inventory_frontmatter.ts +2 -1
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_adr_sweep_routing.ts +12 -0
- package/src/scripts/lint_canonical_terms.ts +604 -0
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +643 -0
- package/src/scripts/lint_composition_review.ts +619 -0
- package/src/scripts/lint_consolidation_lineage.ts +666 -0
- package/src/scripts/lint_eval_specs.ts +593 -0
- package/src/scripts/lint_hook_manifest.ts +19 -0
- package/src/scripts/lint_host_portability.ts +373 -0
- package/src/scripts/lint_judge_prompt_expectation.ts +27 -1
- package/src/scripts/lint_memory_twin_parity.ts +335 -0
- package/src/scripts/lint_metric_consumers.ts +355 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/lint_skill_descriptions.ts +18 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +58 -0
- package/src/scripts/lint_skill_link_reach.ts +252 -0
- package/src/scripts/lint_skill_trigger_corpus.ts +426 -23
- package/src/scripts/lint_ticket_buildable.ts +17 -1
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -1
- package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_docs_dead_links.ts +348 -0
- package/src/scripts/measure_host_capture.ts +461 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/migration_status.ts +3 -3
- package/src/scripts/model_rule_injection.ts +76 -0
- package/src/scripts/orchestration_record.ts +14 -0
- package/src/scripts/preamble_byte_census.ts +139 -10
- package/src/scripts/prepack-check.mjs +4 -1
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/probe_turnaround.ts +384 -0
- package/src/scripts/probe_unblocked_ask.ts +279 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
- package/src/scripts/render_benchmark_md.ts +1 -1
- package/src/scripts/rule_activation_census.ts +70 -9
- package/src/scripts/rule_backlinks.ts +1 -1
- package/src/scripts/run_lifecycle_suite.ts +208 -0
- package/src/scripts/run_skill_evals.ts +40 -0
- package/src/scripts/schemas/delivery-manifest.schema.json +43 -0
- package/src/scripts/schemas/drafting-channels.schema.json +43 -0
- package/src/scripts/schemas/evals.schema.json +24 -1
- package/src/scripts/schemas/rule.schema.json +52 -1
- package/src/scripts/schemas/skill.schema.json +17 -2
- package/src/scripts/skill_overlap.ts +7 -1
- package/src/scripts/skill_tools/run_block_d_eval.ts +8 -1
- package/src/scripts/skill_tools/score_skill_relevance.ts +84 -3
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +8 -1
- package/src/scripts/skill_usage_report.ts +2 -1
- package/src/scripts/stubs_due.ts +211 -0
- package/src/scripts/sweep_source_surfaces.ts +678 -0
- package/src/scripts/sync_pr_branch.ts +3 -3
- package/src/scripts/trigger_eval_grandfather.json +202 -222
- package/src/scripts/utilization_report.ts +22 -2
- package/src/server/schemas/settings.ts +5 -0
- package/src/templates/marketing-copy.yml +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +0 -26
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collector_supervision — the supervised telemetry collector's OPERATIONAL
|
|
3
|
+
* contract (`road-to-supervised-telemetry-collector` Phase 3, steps 3.2–3.4).
|
|
4
|
+
*
|
|
5
|
+
* Phase 2 fixed what a record may contain. This module fixes what the *process*
|
|
6
|
+
* may cost, how it is stopped when it will not stop itself, and what happens
|
|
7
|
+
* when a static-mode tree and a daemon-mode tree exist at the same time. All
|
|
8
|
+
* three were named by the council as things that must be numbers and mechanisms
|
|
9
|
+
* before Phase 4 writes any code, because each one is unfalsifiable once an
|
|
10
|
+
* implementation has already fixed it by accident.
|
|
11
|
+
*
|
|
12
|
+
* ## What this module is NOT
|
|
13
|
+
*
|
|
14
|
+
* It is not the collector. Nothing here spawns a process, opens the store, or
|
|
15
|
+
* writes a record. It is the contract the Phase-4 collector is built against,
|
|
16
|
+
* plus the pure predicates a test can drive without a daemon existing. The one
|
|
17
|
+
* function that touches a live process is {@link terminateCollector}, and it is
|
|
18
|
+
* here rather than in Phase 4 deliberately: **the kill switch must not live
|
|
19
|
+
* inside the thing it kills.**
|
|
20
|
+
*
|
|
21
|
+
* ## The three questions this answers, and the two it does not
|
|
22
|
+
*
|
|
23
|
+
* `docs/contracts/resident-process-floors.md` § 2 asks five questions of every
|
|
24
|
+
* resident-process design. Three are answered here:
|
|
25
|
+
*
|
|
26
|
+
* - **Q3 — state on an unclean stop.** {@link HEARTBEAT_FILE_NAME} is the only
|
|
27
|
+
* process-scoped state, it is rewritten whole on every beat, and a stale or
|
|
28
|
+
* absent beat is a *readable* answer rather than a missing one. The store's
|
|
29
|
+
* own crash state is `MIGRATION-IN-FLIGHT`, owned by `collector_store`.
|
|
30
|
+
* - **Q4 — who supervises it, with what privileges.** A per-user service
|
|
31
|
+
* manager, positively probed, never assumed —
|
|
32
|
+
* {@link detectSupervisor}. No administrator privilege is required or
|
|
33
|
+
* requested on either supported row.
|
|
34
|
+
* - **Q5 — the uniqueness namespace.** One live collector per OS user, from the
|
|
35
|
+
* `uniqueness-namespace` (b) verdict. {@link runtimeLockPath} is that
|
|
36
|
+
* namespace made concrete, and § 3.4 below is what it means for two
|
|
37
|
+
* checkouts.
|
|
38
|
+
*
|
|
39
|
+
* Q1 (failure mode when absent) and Q2 (what it does to a dispatch it cannot
|
|
40
|
+
* serve) are answered by the roadmap's Goal and by row 4 of the 3.1 matrix —
|
|
41
|
+
* static fallback, a degradation and never a block — and are restated in
|
|
42
|
+
* {@link DISPATCH_MODE_CONTRACT} rather than re-decided here.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import fs from 'node:fs';
|
|
46
|
+
import os from 'node:os';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
|
|
49
|
+
import { resolveCollectorStore } from './collector_store.js';
|
|
50
|
+
|
|
51
|
+
/* -------------------------------------------------------------------------- */
|
|
52
|
+
/* 3.2 — Resource budgets as numbers */
|
|
53
|
+
/* -------------------------------------------------------------------------- */
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A single resource ceiling.
|
|
57
|
+
*
|
|
58
|
+
* `headroomAtPeak` is DERIVED (`ceiling - expectedPeak`) rather than stated
|
|
59
|
+
* independently, so the two numbers cannot drift into a headroom that is not
|
|
60
|
+
* the difference between them. `scale-discipline` R-A12 is the rule this
|
|
61
|
+
* satisfies: *an unquantified ceiling is not headroom*, and a headroom that is
|
|
62
|
+
* not arithmetic on the other two fields is a third unquantified number.
|
|
63
|
+
*/
|
|
64
|
+
export interface ResourceBudget {
|
|
65
|
+
/** The hard stop. Crossing it stops the collector — never throttles it. */
|
|
66
|
+
readonly ceiling: number;
|
|
67
|
+
/** Unit, for the operator reading a breach message. */
|
|
68
|
+
readonly unit: string;
|
|
69
|
+
/** What the collector is expected to use at its busiest observed moment. */
|
|
70
|
+
readonly expectedPeak: number;
|
|
71
|
+
/** Where `expectedPeak` comes from. A number with no basis is a guess. */
|
|
72
|
+
readonly basis: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type BudgetName = 'cpu_percent' | 'resident_bytes' | 'disk_bytes' | 'file_descriptors';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The four budgets, committed 2026-08-30.
|
|
79
|
+
*
|
|
80
|
+
* **REVISED 2026-08-30, and the revision is the point.** The first draft's
|
|
81
|
+
* peaks were derivations, with a `revisit-if` saying that a measured peak above
|
|
82
|
+
* a derived one falsifies the derivation. Phase 4 landed, a real daemon ran,
|
|
83
|
+
* and it falsified two of the four on its first start: resident memory read
|
|
84
|
+
* 116.2 MiB against a 96 MiB ceiling, and file descriptors read 28 against a
|
|
85
|
+
* derived peak of 12. The daemon budget-stopped itself within seconds.
|
|
86
|
+
*
|
|
87
|
+
* So two rows are now MEASURED and two are still derived, and each `basis` says
|
|
88
|
+
* which it is. That distinction is the whole value of the field — a table where
|
|
89
|
+
* every row reads with the same confidence tells a reader nothing about which
|
|
90
|
+
* numbers have met reality.
|
|
91
|
+
*
|
|
92
|
+
* **The measurement is one platform and one execution mode**: macOS, running
|
|
93
|
+
* under `tsx`, which loads a TypeScript transpiler into the same process. A
|
|
94
|
+
* built-JS daemon would very likely read far lower. The budget is set against
|
|
95
|
+
* the mode this repository ACTUALLY runs (`./scripts-run` is tsx), because a
|
|
96
|
+
* ceiling calibrated to a build that does not exist would stop the daemon that
|
|
97
|
+
* does.
|
|
98
|
+
*
|
|
99
|
+
* *Revisit-if:* the daemon ships as built JS rather than through `tsx` (both
|
|
100
|
+
* memory rows should then be re-measured and will likely fall by half); or a
|
|
101
|
+
* measured peak on the Linux row exceeds these; or a descriptor count climbs
|
|
102
|
+
* across a long run, which is a leak rather than a load and is the one thing
|
|
103
|
+
* the descriptor ceiling can usefully detect.
|
|
104
|
+
*/
|
|
105
|
+
export const RESOURCE_BUDGETS: Readonly<Record<BudgetName, ResourceBudget>> = Object.freeze({
|
|
106
|
+
cpu_percent: Object.freeze({
|
|
107
|
+
ceiling: 2,
|
|
108
|
+
unit: '% of one core, averaged over 60s',
|
|
109
|
+
expectedPeak: 0.2,
|
|
110
|
+
basis:
|
|
111
|
+
'DERIVED. The collector wakes on a hook write and on its heartbeat interval. ' +
|
|
112
|
+
'At the metric definition\'s observed dispatch volume the wake rate is under ' +
|
|
113
|
+
'one per second, and a wake is a single-row INSERT plus a stat(2). 0.2% of one ' +
|
|
114
|
+
'core is two orders of magnitude above the arithmetic and one below the ' +
|
|
115
|
+
'ceiling. Not yet measured over a full 60s window on a real run — the sampler ' +
|
|
116
|
+
'refuses to compute a percentage over a window shorter than a second, which is ' +
|
|
117
|
+
'itself a lesson from a measured 36800% reading.',
|
|
118
|
+
}),
|
|
119
|
+
resident_bytes: Object.freeze({
|
|
120
|
+
ceiling: 192 * 1024 * 1024,
|
|
121
|
+
unit: 'bytes RSS',
|
|
122
|
+
expectedPeak: 128 * 1024 * 1024,
|
|
123
|
+
basis:
|
|
124
|
+
'MEASURED 2026-08-30: a real daemon read 116.2 MiB RSS on macOS under tsx, ' +
|
|
125
|
+
'steady across three samples. The previous DERIVED pair (60 MiB peak, 96 MiB ' +
|
|
126
|
+
'ceiling) was built from a bare-Node floor of ~40 MiB and did not account for ' +
|
|
127
|
+
'the TypeScript transpiler tsx loads into the same process; the daemon ' +
|
|
128
|
+
'budget-stopped itself within seconds of its first start. 128 MiB is the ' +
|
|
129
|
+
'measurement plus ~10% for heap growth; the 192 MiB ceiling leaves 64 MiB, ' +
|
|
130
|
+
'which is what absorbs a GC high-water mark a three-sample reading cannot see.',
|
|
131
|
+
}),
|
|
132
|
+
disk_bytes: Object.freeze({
|
|
133
|
+
ceiling: 64 * 1024 * 1024,
|
|
134
|
+
unit: 'bytes, the collector directory including quarantine',
|
|
135
|
+
expectedPeak: 12 * 1024 * 1024,
|
|
136
|
+
basis:
|
|
137
|
+
'DERIVED. RETENTION_DAYS is 63 and a record is nine small columns. At the metric ' +
|
|
138
|
+
'definition\'s upper dispatch estimate a full retention window is single-digit ' +
|
|
139
|
+
'MiB; 12 MiB is that plus SQLite page overhead and one quarantined store. The ' +
|
|
140
|
+
'ceiling is deliberately ~5x: quarantine is never deleted by design, so the ' +
|
|
141
|
+
'directory has one growth term this budget cannot bound by retention alone. '
|
|
142
|
+
+ 'CORRECTED after R2 round-2 finding 5: this derivation counted only the store, '
|
|
143
|
+
+ 'while `opportunities.log` sits in the same directory and was UNBOUNDED — an '
|
|
144
|
+
+ 'unbounded file inside a budget whose breach stops the collector is a slow '
|
|
145
|
+
+ 'self-shutdown. It is now pruned on the same 63-day clock, so the derivation '
|
|
146
|
+
+ 'covers it; the 12 MiB peak is unchanged because a denominator line is ~30 bytes '
|
|
147
|
+
+ 'and 63 days of them is well inside the SQLite page overhead already allowed.',
|
|
148
|
+
}),
|
|
149
|
+
file_descriptors: Object.freeze({
|
|
150
|
+
ceiling: 128,
|
|
151
|
+
unit: 'open descriptors held by the collector process',
|
|
152
|
+
expectedPeak: 48,
|
|
153
|
+
basis:
|
|
154
|
+
'MEASURED 2026-08-30: 28 open descriptors on macOS under tsx with the store ' +
|
|
155
|
+
'open, steady across three samples. The previous DERIVED peak of 12 counted ' +
|
|
156
|
+
'only the files this module opens (database, WAL, shm, heartbeat, lock, stdio) ' +
|
|
157
|
+
'and ignored everything the runtime holds. 48 is the measurement plus room for ' +
|
|
158
|
+
'an in-flight quarantine rename; the 128 ceiling is set where a LEAK becomes ' +
|
|
159
|
+
'distinguishable from load, which is the only thing a descriptor ceiling can ' +
|
|
160
|
+
'usefully detect — a ceiling near the steady state would fire on noise instead.',
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** `ceiling - expectedPeak`, per budget. Never stored — always derived. */
|
|
165
|
+
export function headroomAtPeak(name: BudgetName): number {
|
|
166
|
+
const b = RESOURCE_BUDGETS[name];
|
|
167
|
+
return b.ceiling - b.expectedPeak;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A live reading of all four budgets. Every field is required: a missing reading is not a pass. */
|
|
171
|
+
export type ResourceReading = Readonly<Record<BudgetName, number>>;
|
|
172
|
+
|
|
173
|
+
export interface BudgetVerdict {
|
|
174
|
+
/** Budgets whose ceiling the reading crossed, in declaration order. */
|
|
175
|
+
readonly exceeded: readonly BudgetName[];
|
|
176
|
+
/**
|
|
177
|
+
* What the collector must do. `stop` is the only non-continue action:
|
|
178
|
+
* throttling is refused by row 4 of the 3.1 matrix, because an observer
|
|
179
|
+
* that has become a load is no longer an observer.
|
|
180
|
+
*/
|
|
181
|
+
readonly action: 'continue' | 'stop';
|
|
182
|
+
/** One human line per exceeded budget. Empty when `action` is `continue`. */
|
|
183
|
+
readonly reasons: readonly string[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Decide whether a reading is inside every budget.
|
|
188
|
+
*
|
|
189
|
+
* Strictly greater-than is the breach test: a reading exactly AT the ceiling is
|
|
190
|
+
* inside it. The ceiling is the last permitted value, not the first forbidden
|
|
191
|
+
* one, and picking the other convention would make every budget one unit
|
|
192
|
+
* tighter than its documented number.
|
|
193
|
+
*/
|
|
194
|
+
export function budgetVerdict(reading: ResourceReading): BudgetVerdict {
|
|
195
|
+
const exceeded: BudgetName[] = [];
|
|
196
|
+
const reasons: string[] = [];
|
|
197
|
+
for (const name of Object.keys(RESOURCE_BUDGETS) as BudgetName[]) {
|
|
198
|
+
const budget = RESOURCE_BUDGETS[name];
|
|
199
|
+
const value = reading[name];
|
|
200
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
201
|
+
exceeded.push(name);
|
|
202
|
+
reasons.push(`${name}: no finite reading — a missing reading is a breach, not a pass`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (value > budget.ceiling) {
|
|
206
|
+
exceeded.push(name);
|
|
207
|
+
reasons.push(
|
|
208
|
+
`${name}: ${value} ${budget.unit} exceeds the ceiling of ${budget.ceiling}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
exceeded: Object.freeze(exceeded),
|
|
214
|
+
action: exceeded.length > 0 ? 'stop' : 'continue',
|
|
215
|
+
reasons: Object.freeze(reasons),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* -------------------------------------------------------------------------- */
|
|
220
|
+
/* Heartbeat — the observable-death mechanism the supervisor blocker required */
|
|
221
|
+
/* -------------------------------------------------------------------------- */
|
|
222
|
+
|
|
223
|
+
/** The heartbeat file, inside the collector directory. Rewritten whole on every beat. */
|
|
224
|
+
export const HEARTBEAT_FILE_NAME = 'heartbeat.json';
|
|
225
|
+
|
|
226
|
+
/** Beats older than this are `stale`: likely dead, and the operator is told so. */
|
|
227
|
+
export const HEARTBEAT_STALE_AFTER_MS = 90_000;
|
|
228
|
+
|
|
229
|
+
export interface Heartbeat {
|
|
230
|
+
readonly pid: number;
|
|
231
|
+
/** Epoch milliseconds. */
|
|
232
|
+
readonly started_at: number;
|
|
233
|
+
/** Epoch milliseconds, rewritten on each beat. */
|
|
234
|
+
readonly last_heartbeat: number;
|
|
235
|
+
readonly mode: 'supervised' | 'degraded';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function heartbeatPath(userRoot?: string): string {
|
|
239
|
+
return path.join(resolveCollectorStore(userRoot).root, HEARTBEAT_FILE_NAME);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Write the beat. Whole-file, never appended: a partial beat must not be readable as a fresh one. */
|
|
243
|
+
export function writeHeartbeat(beat: Heartbeat, userRoot?: string): void {
|
|
244
|
+
const target = heartbeatPath(userRoot);
|
|
245
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
246
|
+
const tmp = `${target}.tmp`;
|
|
247
|
+
fs.writeFileSync(tmp, `${JSON.stringify(beat)}\n`);
|
|
248
|
+
fs.renameSync(tmp, target);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Read the beat, or null when absent or unparseable. Unparseable and absent are the same answer: not running. */
|
|
252
|
+
export function readHeartbeat(userRoot?: string): Heartbeat | null {
|
|
253
|
+
try {
|
|
254
|
+
const raw = fs.readFileSync(heartbeatPath(userRoot), 'utf8');
|
|
255
|
+
const parsed: unknown = JSON.parse(raw);
|
|
256
|
+
if (parsed === null || typeof parsed !== 'object') return null;
|
|
257
|
+
const beat = parsed as Partial<Heartbeat>;
|
|
258
|
+
if (typeof beat.pid !== 'number' || typeof beat.last_heartbeat !== 'number') return null;
|
|
259
|
+
if (typeof beat.started_at !== 'number') return null;
|
|
260
|
+
if (beat.mode !== 'supervised' && beat.mode !== 'degraded') return null;
|
|
261
|
+
return beat as Heartbeat;
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type LivenessState = 'absent' | 'stale' | 'running';
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Three-valued on purpose. `absent` means nothing claims to be running;
|
|
271
|
+
* `stale` means something claimed to and has not beaten since — which is the
|
|
272
|
+
* state a boolean would report as healthy or as absent, and both readings are
|
|
273
|
+
* wrong. A silently dead collector making incomplete telemetry look healthy is
|
|
274
|
+
* the failure the supervisor blocker named as decisive.
|
|
275
|
+
*/
|
|
276
|
+
export function livenessFromBeat(
|
|
277
|
+
beat: Heartbeat | null,
|
|
278
|
+
now: number = Date.now(),
|
|
279
|
+
alive: (pid: number) => boolean = pidIsAlive,
|
|
280
|
+
): LivenessState {
|
|
281
|
+
if (beat === null) return 'absent';
|
|
282
|
+
// The PID first, then the clock (R2 round-5 finding 5). A purely time-based
|
|
283
|
+
// reading reported `running` for up to 90 s after any unclean death —
|
|
284
|
+
// including the `SIGKILL` escalation, which by construction cannot run the
|
|
285
|
+
// `finally` that removes the beat — so `collector status` told an operator a
|
|
286
|
+
// dead collector was alive during exactly the window they would be looking.
|
|
287
|
+
// A pid that is gone is decisive; a pid that exists is not (it may be
|
|
288
|
+
// recycled), so the staleness test still runs after it.
|
|
289
|
+
if (!alive(beat.pid)) return 'stale';
|
|
290
|
+
return now - beat.last_heartbeat > HEARTBEAT_STALE_AFTER_MS ? 'stale' : 'running';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* -------------------------------------------------------------------------- */
|
|
294
|
+
/* Uniqueness namespace — one live collector per OS user */
|
|
295
|
+
/* -------------------------------------------------------------------------- */
|
|
296
|
+
|
|
297
|
+
export const RUNTIME_LOCK_NAME = 'collector.lock';
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The lock path IS the uniqueness namespace made concrete.
|
|
301
|
+
*
|
|
302
|
+
* It hangs off the user root, so two checkouts — two worktrees of this
|
|
303
|
+
* repository, the common case per `resident-process-floors` § 2 Q5 — resolve to
|
|
304
|
+
* the SAME path and therefore contend for the same lock. That is the
|
|
305
|
+
* `uniqueness-namespace` (b) verdict working: the second checkout does not get
|
|
306
|
+
* a second collector, it gets static mode.
|
|
307
|
+
*/
|
|
308
|
+
export function runtimeLockPath(userRoot?: string): string {
|
|
309
|
+
return path.join(resolveCollectorStore(userRoot).root, RUNTIME_LOCK_NAME);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface LockOutcome {
|
|
313
|
+
readonly acquired: boolean;
|
|
314
|
+
/** The pid recorded in the lock — ours when acquired, the incumbent's when not. */
|
|
315
|
+
readonly holder: number | null;
|
|
316
|
+
/**
|
|
317
|
+
* Set when `acquired` is false.
|
|
318
|
+
*
|
|
319
|
+
* `unwritable` and `contested-out` exist because reporting everything as
|
|
320
|
+
* `held-by-live-process` sent an operator to the two-collectors row of the
|
|
321
|
+
* ops page for an `EACCES` or a full disk (R2 round-2 finding 11). A create
|
|
322
|
+
* that failed for a reason other than "the file is already there" is not
|
|
323
|
+
* contention, and a retry loop that ran out of attempts is not the same as
|
|
324
|
+
* losing to a live incumbent.
|
|
325
|
+
*/
|
|
326
|
+
readonly reason:
|
|
327
|
+
| 'held-by-live-process'
|
|
328
|
+
| 'kill-switch-engaged'
|
|
329
|
+
| 'unwritable'
|
|
330
|
+
| 'contested-out'
|
|
331
|
+
| null;
|
|
332
|
+
/** The errno when `reason` is `unwritable`. Carried so the operator sees the real cause. */
|
|
333
|
+
readonly errno: string | null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function pidIsAlive(pid: number): boolean {
|
|
337
|
+
try {
|
|
338
|
+
process.kill(pid, 0);
|
|
339
|
+
return true;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
// EPERM means it exists and is not ours. Alive for our purposes.
|
|
342
|
+
return (err as NodeJS.ErrnoException).code === 'EPERM';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** How many times {@link acquireRuntimeLock} retries after unlinking a stale lock. */
|
|
347
|
+
const FENCE_ATTEMPTS = 4;
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Take the per-user lock, or report who holds it.
|
|
351
|
+
*
|
|
352
|
+
* `wx` is the atomicity, and it is the ONLY way this function ever ends up
|
|
353
|
+
* holding the lock — including on the fencing path.
|
|
354
|
+
*
|
|
355
|
+
* **The fencing path used to be a compare-then-write race, and the docstring
|
|
356
|
+
* claimed the opposite** (R2 finding 2). It read the pid, probed liveness, then
|
|
357
|
+
* did a plain `writeFileSync`: two starters that both observed the same dead
|
|
358
|
+
* incumbent both wrote and both returned `acquired: true`, which is exactly the
|
|
359
|
+
* one-collector-per-OS-user invariant this file exists to hold. The same branch
|
|
360
|
+
* was taken when `readLockPid` returned `null` because a releasing process
|
|
361
|
+
* removed the file in between, so a normal release could also hand the lock to
|
|
362
|
+
* two starters.
|
|
363
|
+
*
|
|
364
|
+
* The fix keeps `wx` as the only winner-picker: a stale lock is UNLINKED and the
|
|
365
|
+
* exclusive create is retried. Two processes racing to fence both unlink — one
|
|
366
|
+
* succeeds, one gets `ENOENT` — and then both attempt `wx`, where exactly one
|
|
367
|
+
* wins. The loop is bounded because an unbounded retry against a live incumbent
|
|
368
|
+
* that keeps dying and restarting is a spin, not a lock.
|
|
369
|
+
*/
|
|
370
|
+
export function acquireRuntimeLock(userRoot?: string, pid: number = process.pid): LockOutcome {
|
|
371
|
+
if (killSwitchEngaged(userRoot)) {
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
acquired: false,
|
|
374
|
+
holder: null,
|
|
375
|
+
reason: 'kill-switch-engaged' as const,
|
|
376
|
+
errno: null,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const target = runtimeLockPath(userRoot);
|
|
380
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
381
|
+
|
|
382
|
+
for (let attempt = 0; attempt < FENCE_ATTEMPTS; attempt += 1) {
|
|
383
|
+
try {
|
|
384
|
+
fs.writeFileSync(target, `${pid}\n`, { flag: 'wx' });
|
|
385
|
+
return Object.freeze({ acquired: true, holder: pid, reason: null, errno: null });
|
|
386
|
+
} catch (err) {
|
|
387
|
+
// EEXIST is contention; everything else is an environment problem,
|
|
388
|
+
// and calling a full disk "held by a live process" is worse than
|
|
389
|
+
// saying nothing.
|
|
390
|
+
const code = (err as NodeJS.ErrnoException).code ?? '';
|
|
391
|
+
if (code !== 'EEXIST') {
|
|
392
|
+
return Object.freeze({
|
|
393
|
+
acquired: false,
|
|
394
|
+
holder: null,
|
|
395
|
+
reason: 'unwritable' as const,
|
|
396
|
+
errno: code === '' ? 'unknown' : code,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const raw = readLockPid(target);
|
|
400
|
+
if (raw === null) {
|
|
401
|
+
// The file vanished between our failed `wx` and this read — a
|
|
402
|
+
// concurrent release. Retry the exclusive create; do NOT treat
|
|
403
|
+
// an unreadable lock as a stale one to be overwritten.
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const incumbent = Number.parseInt(raw, 10);
|
|
407
|
+
if (Number.isFinite(incumbent) && pidIsAlive(incumbent)) {
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
acquired: false,
|
|
410
|
+
holder: incumbent,
|
|
411
|
+
reason: 'held-by-live-process' as const,
|
|
412
|
+
errno: null,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// Stale owner: unlink and let the next `wx` pick the winner.
|
|
416
|
+
try {
|
|
417
|
+
fs.rmSync(target, { force: true });
|
|
418
|
+
} catch {
|
|
419
|
+
/* another fencer removed it first — fine, retry */
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Bounded out: somebody is winning the race repeatedly. Its own reason —
|
|
424
|
+
// this is not "a live process holds it", it is "the fence loop kept losing",
|
|
425
|
+
// and the two send an operator to different places.
|
|
426
|
+
const raw = readLockPid(target);
|
|
427
|
+
const incumbent = Number.parseInt(raw ?? '', 10);
|
|
428
|
+
return Object.freeze({
|
|
429
|
+
acquired: false,
|
|
430
|
+
holder: Number.isFinite(incumbent) ? incumbent : null,
|
|
431
|
+
reason: 'contested-out' as const,
|
|
432
|
+
errno: null,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readLockPid(target: string): string | null {
|
|
437
|
+
try {
|
|
438
|
+
return fs.readFileSync(target, 'utf8').trim();
|
|
439
|
+
} catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Release the lock, but ONLY when it is still ours.
|
|
446
|
+
*
|
|
447
|
+
* The unconditional `rmSync` this replaces was not "safe to call when someone
|
|
448
|
+
* else fenced us", which is what its docstring claimed (R2 finding 10) — a
|
|
449
|
+
* fenced predecessor tearing down through `runLoop`'s `finally` deleted the
|
|
450
|
+
* SUCCESSOR's lock on the way out, leaving a live collector with no lock so the
|
|
451
|
+
* next starter also acquired.
|
|
452
|
+
*
|
|
453
|
+
* Returns whether anything was removed, so a caller can tell "released" from
|
|
454
|
+
* "somebody else owns this now" instead of inferring it.
|
|
455
|
+
*/
|
|
456
|
+
export function releaseRuntimeLock(userRoot?: string, pid: number = process.pid): boolean {
|
|
457
|
+
const target = runtimeLockPath(userRoot);
|
|
458
|
+
const raw = readLockPid(target);
|
|
459
|
+
if (raw === null) return false;
|
|
460
|
+
if (Number.parseInt(raw, 10) !== pid) return false;
|
|
461
|
+
fs.rmSync(target, { force: true });
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Remove the heartbeat, but ONLY when it is ours.
|
|
467
|
+
*
|
|
468
|
+
* Same ownership argument as {@link releaseRuntimeLock}, and the consequence of
|
|
469
|
+
* getting it wrong is worse: a fenced predecessor deleting the successor's beat
|
|
470
|
+
* makes `livenessFromBeat` report `absent` for a RUNNING collector — the
|
|
471
|
+
* silently-dead-collector reading the whole three-valued design exists to
|
|
472
|
+
* prevent, inverted.
|
|
473
|
+
*/
|
|
474
|
+
export function releaseHeartbeat(userRoot?: string, pid: number = process.pid): boolean {
|
|
475
|
+
const beat = readHeartbeat(userRoot);
|
|
476
|
+
if (beat === null || beat.pid !== pid) return false;
|
|
477
|
+
fs.rmSync(heartbeatPath(userRoot), { force: true });
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* -------------------------------------------------------------------------- */
|
|
482
|
+
/* 3.3 — The kill switch */
|
|
483
|
+
/* -------------------------------------------------------------------------- */
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The kill-switch marker. Its presence is the whole signal; content is ignored
|
|
487
|
+
* — the same shape as `OPT-OUT`, for the same reason: a switch whose meaning
|
|
488
|
+
* depends on parsing its contents can be half-pulled.
|
|
489
|
+
*/
|
|
490
|
+
export const KILL_SWITCH_NAME = 'STOP';
|
|
491
|
+
|
|
492
|
+
export function killSwitchPath(userRoot?: string): string {
|
|
493
|
+
return path.join(resolveCollectorStore(userRoot).root, KILL_SWITCH_NAME);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Pull the switch: refuse activation, then terminate anything already running.
|
|
498
|
+
*
|
|
499
|
+
* **Reachable without the collector's cooperation**, which is 3.3's binding
|
|
500
|
+
* requirement. The marker is a file the operator can create with `touch`; the
|
|
501
|
+
* termination is a signal from outside the process. Neither asks the collector
|
|
502
|
+
* to agree, so a wedged collector cannot decline to be stopped.
|
|
503
|
+
*/
|
|
504
|
+
export function pullKillSwitch(userRoot?: string): void {
|
|
505
|
+
const target = killSwitchPath(userRoot);
|
|
506
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
507
|
+
fs.writeFileSync(
|
|
508
|
+
target,
|
|
509
|
+
'The collector is stopped. Delete this file to allow it to start again.\n' +
|
|
510
|
+
'See docs/contracts/collector-operations.md.\n',
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function killSwitchEngaged(userRoot?: string): boolean {
|
|
515
|
+
return fs.existsSync(killSwitchPath(userRoot));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function clearKillSwitch(userRoot?: string): void {
|
|
519
|
+
fs.rmSync(killSwitchPath(userRoot), { force: true });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export interface TerminationOutcome {
|
|
523
|
+
/** True when nothing was running, or when the process is gone at return. */
|
|
524
|
+
readonly stopped: boolean;
|
|
525
|
+
/**
|
|
526
|
+
* How it ended. `already-absent` is a success, not a failure.
|
|
527
|
+
*
|
|
528
|
+
* `stale-refused` is NOT a success: a beat older than the staleness
|
|
529
|
+
* threshold names a pid this module can no longer vouch for, and signalling
|
|
530
|
+
* it would aim a `SIGKILL` at whatever the OS has since recycled that
|
|
531
|
+
* number onto (R2 finding 9). The caller is told rather than the risk being
|
|
532
|
+
* taken silently.
|
|
533
|
+
*/
|
|
534
|
+
readonly via: 'already-absent' | 'graceful' | 'forced' | 'unreachable' | 'stale-refused';
|
|
535
|
+
readonly pid: number | null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export interface TerminateOptions {
|
|
539
|
+
/** How long to wait for a graceful exit before escalating. */
|
|
540
|
+
readonly graceMs?: number;
|
|
541
|
+
/** Poll interval while waiting. */
|
|
542
|
+
readonly pollMs?: number;
|
|
543
|
+
/** Injected for tests; defaults to the real signal. */
|
|
544
|
+
readonly kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
545
|
+
/** Injected for tests; defaults to a real liveness probe. */
|
|
546
|
+
readonly alive?: (pid: number) => boolean;
|
|
547
|
+
/** Injected for tests; defaults to a real sleep. */
|
|
548
|
+
readonly sleep?: (ms: number) => void;
|
|
549
|
+
/** Injected for tests; defaults to `Date.now`. Used for the staleness check. */
|
|
550
|
+
readonly now?: () => number;
|
|
551
|
+
/**
|
|
552
|
+
* Signal a stale heartbeat's pid anyway.
|
|
553
|
+
*
|
|
554
|
+
* Off by default and deliberately explicit: it is the operator saying "I
|
|
555
|
+
* know this beat is old and I still want that pid killed". Nothing in this
|
|
556
|
+
* package sets it.
|
|
557
|
+
*/
|
|
558
|
+
readonly signalStale?: boolean;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Stop the collector, escalating to `SIGKILL` when it will not stop itself.
|
|
563
|
+
*
|
|
564
|
+
* The escalation is the point. 3.3's `verify:` requires a wedged collector —
|
|
565
|
+
* one that has installed a `SIGTERM` handler and does not honour it — to be
|
|
566
|
+
* killed through this path, so a graceful-only implementation would satisfy the
|
|
567
|
+
* step's prose and fail its test. `SIGKILL` is Unix-specific, which is why the
|
|
568
|
+
* platform table lists exactly two supported rows.
|
|
569
|
+
*
|
|
570
|
+
* Synchronous by construction: this runs from a CLI invocation and from a
|
|
571
|
+
* signal handler, and both are places where an unawaited promise is a bug.
|
|
572
|
+
*/
|
|
573
|
+
export function terminateCollector(
|
|
574
|
+
userRoot?: string,
|
|
575
|
+
options: TerminateOptions = {},
|
|
576
|
+
): TerminationOutcome {
|
|
577
|
+
const beat = readHeartbeat(userRoot);
|
|
578
|
+
if (beat === null) {
|
|
579
|
+
return Object.freeze({ stopped: true, via: 'already-absent' as const, pid: null });
|
|
580
|
+
}
|
|
581
|
+
const now = options.now ?? Date.now;
|
|
582
|
+
const aliveProbe = options.alive ?? pidIsAlive;
|
|
583
|
+
// GONE beats STALE. A pid that is not alive needs no signal and carries no
|
|
584
|
+
// recycle risk, so it is `already-absent` — the staleness refusal is for a
|
|
585
|
+
// pid that EXISTS and may no longer be ours. Ordering matters now that
|
|
586
|
+
// `livenessFromBeat` consults the pid: without this, a dead collector's
|
|
587
|
+
// surviving heartbeat reported `stale-refused` instead of success.
|
|
588
|
+
if (!aliveProbe(beat.pid)) {
|
|
589
|
+
return Object.freeze({ stopped: true, via: 'already-absent' as const, pid: beat.pid });
|
|
590
|
+
}
|
|
591
|
+
if (options.signalStale !== true && livenessFromBeat(beat, now(), aliveProbe) === 'stale') {
|
|
592
|
+
// A SIGKILL aimed at a recycled pid is the worst thing this module can
|
|
593
|
+
// do, and `pidIsAlive` cannot tell the original owner from its
|
|
594
|
+
// successor. The module already owns the vocabulary for this decision;
|
|
595
|
+
// the first version simply did not use it here.
|
|
596
|
+
return Object.freeze({ stopped: false, via: 'stale-refused' as const, pid: beat.pid });
|
|
597
|
+
}
|
|
598
|
+
const graceMs = options.graceMs ?? 5_000;
|
|
599
|
+
const pollMs = options.pollMs ?? 100;
|
|
600
|
+
const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
601
|
+
const alive = aliveProbe;
|
|
602
|
+
const sleep =
|
|
603
|
+
options.sleep ??
|
|
604
|
+
((ms: number) => {
|
|
605
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
kill(beat.pid, 'SIGTERM');
|
|
610
|
+
} catch {
|
|
611
|
+
return Object.freeze({ stopped: false, via: 'unreachable' as const, pid: beat.pid });
|
|
612
|
+
}
|
|
613
|
+
let waited = 0;
|
|
614
|
+
while (waited < graceMs) {
|
|
615
|
+
if (!alive(beat.pid)) {
|
|
616
|
+
return Object.freeze({ stopped: true, via: 'graceful' as const, pid: beat.pid });
|
|
617
|
+
}
|
|
618
|
+
sleep(pollMs);
|
|
619
|
+
waited += pollMs;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
kill(beat.pid, 'SIGKILL');
|
|
623
|
+
} catch {
|
|
624
|
+
return Object.freeze({ stopped: false, via: 'unreachable' as const, pid: beat.pid });
|
|
625
|
+
}
|
|
626
|
+
waited = 0;
|
|
627
|
+
while (waited < graceMs) {
|
|
628
|
+
if (!alive(beat.pid)) {
|
|
629
|
+
return Object.freeze({ stopped: true, via: 'forced' as const, pid: beat.pid });
|
|
630
|
+
}
|
|
631
|
+
sleep(pollMs);
|
|
632
|
+
waited += pollMs;
|
|
633
|
+
}
|
|
634
|
+
return Object.freeze({ stopped: false, via: 'unreachable' as const, pid: beat.pid });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* -------------------------------------------------------------------------- */
|
|
638
|
+
/* 3.4 — Static mode and daemon mode against the same tree */
|
|
639
|
+
/* -------------------------------------------------------------------------- */
|
|
640
|
+
|
|
641
|
+
export type DispatchMode = 'static' | 'daemon';
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The answer to 3.4, stated once so no implementation can decide it by
|
|
645
|
+
* accident: **the two modes are mutually exclusive per OS user, and the
|
|
646
|
+
* exclusion is enforced by the runtime lock rather than by convention.**
|
|
647
|
+
*
|
|
648
|
+
* Concurrent operation is REFUSED rather than made correct. Two writers against
|
|
649
|
+
* one store is duplicate capture — the numerator of the capture rate counting
|
|
650
|
+
* one dispatch twice — and version skew, where two checkouts at different
|
|
651
|
+
* revisions disagree about the schema. Neither is worth solving for an
|
|
652
|
+
* instrument whose entire purpose is an accurate ratio.
|
|
653
|
+
*
|
|
654
|
+
* NO PRODUCTION CALLER, named rather than left for a reviewer to find (R2
|
|
655
|
+
* round-5 finding 9): this constant and {@link resolveDispatchMode} are read by
|
|
656
|
+
* tests and by nothing else. `collector_daemon status` prints enablement,
|
|
657
|
+
* liveness and the supervisor mode, never the dispatch mode. The exclusion
|
|
658
|
+
* itself is enforced — `acquireRuntimeLock` is what makes it true — so what is
|
|
659
|
+
* missing is the REPORTING view, not the mechanism. Same class as
|
|
660
|
+
* `capture_rate`'s note, and it is stated here for the same reason.
|
|
661
|
+
*
|
|
662
|
+
* What "static mode" means concretely: **there is no collector process and
|
|
663
|
+
* nothing is captured.** It is not a second, quieter writer. That is what makes
|
|
664
|
+
* the exclusion cheap — the losing side of the race has nothing to flush and no
|
|
665
|
+
* partial state, and § 1's falsifiable form (*if the module were killed, every
|
|
666
|
+
* dispatch would resolve identically*) holds unchanged.
|
|
667
|
+
*/
|
|
668
|
+
export const DISPATCH_MODE_CONTRACT = Object.freeze({
|
|
669
|
+
concurrentOperationPermitted: false,
|
|
670
|
+
preventedBy: 'per-user runtime lock (RUNTIME_LOCK_NAME) in the collector directory',
|
|
671
|
+
loserBehaviour: 'static' as DispatchMode,
|
|
672
|
+
whenAbsent: 'every dispatch resolves identically; nothing is captured and nothing blocks',
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
export interface ModeResolution {
|
|
676
|
+
readonly mode: DispatchMode;
|
|
677
|
+
/** Why. Always populated — a mode with no stated reason is unauditable. */
|
|
678
|
+
readonly reason: string;
|
|
679
|
+
/** The live pid when another checkout already holds the lock. */
|
|
680
|
+
readonly incumbent: number | null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Decide which mode this invocation runs in, WITHOUT taking the lock.
|
|
685
|
+
*
|
|
686
|
+
* Pure with respect to the lock — it reads, never writes — so a CLI can report
|
|
687
|
+
* the mode without becoming a second contender for it. The collector's own
|
|
688
|
+
* start path calls {@link acquireRuntimeLock}, whose result is authoritative;
|
|
689
|
+
* this is the reporting view, and the two agree because both read the same
|
|
690
|
+
* file.
|
|
691
|
+
*/
|
|
692
|
+
export function resolveDispatchMode(userRoot?: string): ModeResolution {
|
|
693
|
+
if (killSwitchEngaged(userRoot)) {
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
mode: 'static' as const,
|
|
696
|
+
reason: 'kill switch engaged — the collector is stopped by operator action',
|
|
697
|
+
incumbent: null,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const raw = readLockPid(runtimeLockPath(userRoot));
|
|
701
|
+
if (raw === null) {
|
|
702
|
+
return Object.freeze({
|
|
703
|
+
mode: 'daemon' as const,
|
|
704
|
+
reason: 'no runtime lock held for this OS user',
|
|
705
|
+
incumbent: null,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
const incumbent = Number.parseInt(raw, 10);
|
|
709
|
+
if (Number.isFinite(incumbent) && pidIsAlive(incumbent)) {
|
|
710
|
+
return Object.freeze({
|
|
711
|
+
mode: 'static' as const,
|
|
712
|
+
reason: `a collector is already live for this OS user (pid ${incumbent})`,
|
|
713
|
+
incumbent,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze({
|
|
717
|
+
mode: 'daemon' as const,
|
|
718
|
+
reason: 'the runtime lock is stale — its owner is not alive and will be fenced',
|
|
719
|
+
incumbent: null,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* -------------------------------------------------------------------------- */
|
|
724
|
+
/* Supervisor detection — probed, never assumed */
|
|
725
|
+
/* -------------------------------------------------------------------------- */
|
|
726
|
+
|
|
727
|
+
export type SupervisorKind = 'launchd-user' | 'systemd-user' | 'none';
|
|
728
|
+
|
|
729
|
+
export interface SupervisorProbe {
|
|
730
|
+
readonly kind: SupervisorKind;
|
|
731
|
+
/** `supported` only where a user-scoped manager was positively detected. */
|
|
732
|
+
readonly tier: 'supported' | 'static-fallback';
|
|
733
|
+
readonly reason: string;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export interface ProbeEnvironment {
|
|
737
|
+
readonly platform?: NodeJS.Platform;
|
|
738
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
739
|
+
readonly exists?: (p: string) => boolean;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Probe for a user-scoped service manager.
|
|
744
|
+
*
|
|
745
|
+
* "Positively detected — probed, never assumed" is the supervisor blocker's own
|
|
746
|
+
* wording, and the Linux row is where it bites: `systemd` being *installed* is
|
|
747
|
+
* not the condition. A user session bus must exist, which is what
|
|
748
|
+
* `DBUS_SESSION_BUS_ADDRESS` or a live `XDG_RUNTIME_DIR/systemd/private`
|
|
749
|
+
* evidences. A container or a CI runner has neither, and both correctly resolve
|
|
750
|
+
* to static fallback.
|
|
751
|
+
*
|
|
752
|
+
* Windows returns `none` with a reason that says **unevaluated**, not refused —
|
|
753
|
+
* the platform table's wording, kept here because a probe that reported
|
|
754
|
+
* "unsupported" would harden an omission into a decision nobody made.
|
|
755
|
+
*/
|
|
756
|
+
export function detectSupervisor(environment: ProbeEnvironment = {}): SupervisorProbe {
|
|
757
|
+
const platform = environment.platform ?? process.platform;
|
|
758
|
+
const env = environment.env ?? process.env;
|
|
759
|
+
const exists = environment.exists ?? ((p: string) => fs.existsSync(p));
|
|
760
|
+
|
|
761
|
+
if (platform === 'darwin') {
|
|
762
|
+
// PROBED, not assumed (R2 finding 17). The first version returned
|
|
763
|
+
// `supported` unconditionally and computed this path only to interpolate
|
|
764
|
+
// it into the reason string — under a section heading that says
|
|
765
|
+
// "probed, never assumed", with the linux branch beside it doing the
|
|
766
|
+
// real thing. `launchctl` is the manager and `~/Library/LaunchAgents`
|
|
767
|
+
// is where a per-user agent is registered; a home directory without it,
|
|
768
|
+
// or without a writable parent, is not a row this package can claim.
|
|
769
|
+
const home = env.HOME ?? os.homedir();
|
|
770
|
+
const agents = path.join(home, 'Library', 'LaunchAgents');
|
|
771
|
+
// The AGENT DIRECTORY, not `~/Library` (R2 round-5 finding 3).
|
|
772
|
+
// `~/Library` exists on every macOS account, so accepting it made the
|
|
773
|
+
// "probed, never assumed" fix an assumption again on the one row it was
|
|
774
|
+
// written for — and `start()` maps `supported` to `mode: 'supervised'`,
|
|
775
|
+
// so a hand-started daemon wrote `supervised` into the operator-facing
|
|
776
|
+
// heartbeat while nothing supervised it.
|
|
777
|
+
if (exists(agents)) {
|
|
778
|
+
return Object.freeze({
|
|
779
|
+
kind: 'launchd-user' as const,
|
|
780
|
+
tier: 'supported' as const,
|
|
781
|
+
reason: `per-user launchd agent under ${agents}; no administrator privilege required`,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return Object.freeze({
|
|
785
|
+
kind: 'none' as const,
|
|
786
|
+
tier: 'static-fallback' as const,
|
|
787
|
+
reason:
|
|
788
|
+
`darwin without a per-user agent directory (${agents} absent) — nothing has `
|
|
789
|
+
+ 'registered a launchd agent here, so this is the static-fallback row',
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
if (platform === 'linux') {
|
|
793
|
+
const runtimeDir = env.XDG_RUNTIME_DIR;
|
|
794
|
+
const hasBusAddress = typeof env.DBUS_SESSION_BUS_ADDRESS === 'string'
|
|
795
|
+
&& env.DBUS_SESSION_BUS_ADDRESS.length > 0;
|
|
796
|
+
const hasPrivateSocket =
|
|
797
|
+
typeof runtimeDir === 'string'
|
|
798
|
+
&& runtimeDir.length > 0
|
|
799
|
+
&& exists(path.join(runtimeDir, 'systemd', 'private'));
|
|
800
|
+
if (hasBusAddress || hasPrivateSocket) {
|
|
801
|
+
return Object.freeze({
|
|
802
|
+
kind: 'systemd-user' as const,
|
|
803
|
+
tier: 'supported' as const,
|
|
804
|
+
reason: 'systemd --user reachable: a user session bus was positively detected',
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return Object.freeze({
|
|
808
|
+
kind: 'none' as const,
|
|
809
|
+
tier: 'static-fallback' as const,
|
|
810
|
+
reason:
|
|
811
|
+
'linux without a user session bus (container, minimal image, or CI runner) — '
|
|
812
|
+
+ 'systemd being installed is not the condition',
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
if (platform === 'win32') {
|
|
816
|
+
return Object.freeze({
|
|
817
|
+
kind: 'none' as const,
|
|
818
|
+
tier: 'static-fallback' as const,
|
|
819
|
+
reason: 'no user-scoped manager evaluated for Windows — unevaluated, not refused',
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return Object.freeze({
|
|
823
|
+
kind: 'none' as const,
|
|
824
|
+
tier: 'static-fallback' as const,
|
|
825
|
+
reason: `no supervisor detected for platform ${platform}`,
|
|
826
|
+
});
|
|
827
|
+
}
|