@event4u/agent-config 14.12.0 → 14.13.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 +234 -0
- 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 +109 -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-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 +247 -3
- package/dist/agent-src/scripts/guarded_baseline.ts +328 -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 +400 -0
- package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -4
- 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 +42 -10
- 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 +57 -57
- 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 +1836 -712
- 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 +4818 -2645
- 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/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 +247 -3
- package/src/agent-src/scripts/guarded_baseline.ts +328 -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 +400 -0
- package/src/agent-src/scripts/ticket_bundles.ts +84 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -4
- 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 +38 -36
- package/src/config/evaluator-budgets.json +14 -14
- package/src/config/gate-coverage.yml +579 -63
- package/src/config/gate-reachability-exemptions.json +21 -0
- package/src/config/gate-violation-baselines.json +37 -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 +8 -8
- 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 +126 -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 +27 -0
- package/src/scripts/_lib/activation_ladder.ts +211 -0
- package/src/scripts/_lib/activation_states.ts +88 -0
- package/src/scripts/_lib/adherence_detectors.ts +143 -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_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/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/council_settings_block.ts +6 -0
- package/src/scripts/_lib/curator_ops.ts +172 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +266 -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/eval_publication.ts +354 -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/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 +229 -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/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/prefix_stable_surfaces.ts +123 -0
- package/src/scripts/_lib/privacy_class.ts +47 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
- 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/runtime_journal.ts +1499 -0
- package/src/scripts/_lib/runtime_wiring_checks.ts +430 -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/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/disagreement_signal.ts +338 -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/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/response_record.ts +50 -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 +126 -0
- 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 +142 -1
- 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_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/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 +980 -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 +98 -11
- 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/concern_registry.ts +4 -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/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/lint_adr_sweep_routing.ts +12 -0
- package/src/scripts/lint_canonical_terms.ts +604 -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_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/mcp_server/consumer_tool_catalog.json +1 -1
- package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
- package/src/scripts/measure_docs_dead_links.ts +348 -0
- package/src/scripts/measure_host_capture.ts +461 -0
- package/src/scripts/migration_status.ts +3 -3
- package/src/scripts/model_rule_injection.ts +60 -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_turnaround.ts +384 -0
- package/src/scripts/probe_unblocked_ask.ts +279 -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,980 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* `evolution_lab` — the operator's command surface over the candidate loop.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-governed-harness-evolution` Phase 3, step 3.6.
|
|
6
|
+
*
|
|
7
|
+
* > *Give the operator a command surface. A verb set (`inspect`, `propose`,
|
|
8
|
+
* > `run`, `compare`, `explain`, `promote`, `clean`) with no background loop.
|
|
9
|
+
* > This is what makes "command-scoped, no daemon" enforced rather than
|
|
10
|
+
* > asserted.*
|
|
11
|
+
* > verify: **every phase's exit criterion is reachable through a named verb,
|
|
12
|
+
* > and no verb starts a resident process.**
|
|
13
|
+
*
|
|
14
|
+
* ## No daemon — what enforces it, not what asserts it
|
|
15
|
+
*
|
|
16
|
+
* ADR-124 prohibits a resident service in core, and a paragraph promising not
|
|
17
|
+
* to start one is the same class of guarantee the `merge-authority` council
|
|
18
|
+
* refused to accept. Four properties hold instead, and each is checked:
|
|
19
|
+
*
|
|
20
|
+
* 1. **No timer, no watcher, no unbounded loop.** There is no `setInterval`,
|
|
21
|
+
* `setTimeout`, `fs.watch`, `while (true)` or `for (;;)` in this module.
|
|
22
|
+
* `tests/scripts/evolution_lab.test.ts` scans this file's bytes for that
|
|
23
|
+
* construct set and is exercised in both polarities against a synthetic
|
|
24
|
+
* daemon-shaped source.
|
|
25
|
+
* 2. **No child process.** `node:child_process` is never imported. `run` and
|
|
26
|
+
* `compare` reach `bench_ab_clone` and `bench_ab_integrity` by direct
|
|
27
|
+
* function call, so there is nothing that could outlive this process.
|
|
28
|
+
* 3. **Every verb returns an exit code.** {@link main} is a pure
|
|
29
|
+
* `argv -> number`; the only `process.exit` is the CLI entry at the foot
|
|
30
|
+
* of the file.
|
|
31
|
+
* 4. **Observed, not only read.** Each verb is spawned under a hard timeout
|
|
32
|
+
* in the test. A verb that left a resident child holding stdio would keep
|
|
33
|
+
* the pipe open and time out; a timeout is therefore a positive detection,
|
|
34
|
+
* not a flake.
|
|
35
|
+
*
|
|
36
|
+
* ## `promote` refuses, and that is the verb working
|
|
37
|
+
*
|
|
38
|
+
* Phase 7 is gated on the OPEN `merge-authority` blocker (AI council
|
|
39
|
+
* 2026-08-29, anthropic + openai, 2/2: Phases 1-6 are legal because they
|
|
40
|
+
* promote nothing). So `promote` exists, is named, is documented — and always
|
|
41
|
+
* refuses with {@link EXIT_REFUSED}, naming the blocker.
|
|
42
|
+
*
|
|
43
|
+
* The refusal is not the only guard, because a refusal in a `switch` arm is one
|
|
44
|
+
* edit away from a promotion. The verb first routes the intended transition
|
|
45
|
+
* through `assertTransition(from, 'promoted')` with **no approval argument**,
|
|
46
|
+
* which is the mechanical half of the non-promotion condition: there is no
|
|
47
|
+
* expression anywhere in this module that constructs a `HumanApproval`, so even
|
|
48
|
+
* if the blocker were lifted and the refusal deleted, this path still could not
|
|
49
|
+
* promote without someone writing an approver in by hand. The test scans for
|
|
50
|
+
* that construction too.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import * as fs from 'node:fs';
|
|
54
|
+
import * as path from 'node:path';
|
|
55
|
+
import process from 'node:process';
|
|
56
|
+
import { fileURLToPath } from 'node:url';
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
CANDIDATE_PREFIX,
|
|
60
|
+
CANDIDATE_RECORD_FILE,
|
|
61
|
+
CLONES,
|
|
62
|
+
clone_candidate,
|
|
63
|
+
} from './bench_ab_clone.js';
|
|
64
|
+
import { main as integrity_main } from './bench_ab_integrity.js';
|
|
65
|
+
import {
|
|
66
|
+
type CandidateRecord,
|
|
67
|
+
CandidateSchemaError,
|
|
68
|
+
LIFECYCLE_SPINE,
|
|
69
|
+
LIFECYCLE_STATES,
|
|
70
|
+
type LifecycleState,
|
|
71
|
+
LifecycleTransitionError,
|
|
72
|
+
PathOwnershipError,
|
|
73
|
+
assertTransition,
|
|
74
|
+
isAccepted,
|
|
75
|
+
isLifecycleState,
|
|
76
|
+
parseCandidateRecord,
|
|
77
|
+
readCandidateRecord,
|
|
78
|
+
} from './_lib/candidate_record.js';
|
|
79
|
+
import {
|
|
80
|
+
RECIPES,
|
|
81
|
+
byteCompare,
|
|
82
|
+
candidateRecordFilename,
|
|
83
|
+
parseObservations,
|
|
84
|
+
proposeCandidates,
|
|
85
|
+
serialiseCandidateRecord,
|
|
86
|
+
} from './_lib/candidate_proposer.js';
|
|
87
|
+
import {
|
|
88
|
+
BudgetExceededError,
|
|
89
|
+
type DisclosureRecord,
|
|
90
|
+
type FieldVisibility,
|
|
91
|
+
HoldoutLeakError,
|
|
92
|
+
type RunBudget,
|
|
93
|
+
type RunPlan,
|
|
94
|
+
type VisibilityClass,
|
|
95
|
+
assertWithinBudget,
|
|
96
|
+
discloseToProposer,
|
|
97
|
+
} from './_lib/harness_evolution_guards.js';
|
|
98
|
+
|
|
99
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
100
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
101
|
+
|
|
102
|
+
/** The seven verbs, in the order step 3.6 names them. */
|
|
103
|
+
export const VERBS = [
|
|
104
|
+
'inspect',
|
|
105
|
+
'propose',
|
|
106
|
+
'run',
|
|
107
|
+
'compare',
|
|
108
|
+
'explain',
|
|
109
|
+
'promote',
|
|
110
|
+
'clean',
|
|
111
|
+
] as const;
|
|
112
|
+
export type Verb = (typeof VERBS)[number];
|
|
113
|
+
|
|
114
|
+
export function isVerb(v: unknown): v is Verb {
|
|
115
|
+
return typeof v === 'string' && (VERBS as readonly string[]).includes(v);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Everything worked. */
|
|
119
|
+
export const EXIT_OK = 0;
|
|
120
|
+
/** A real failure — unreadable record, rejected schema, integrity divergence. */
|
|
121
|
+
export const EXIT_ERROR = 1;
|
|
122
|
+
/** Bad invocation. Matches the sibling bench scripts' argparse exit code. */
|
|
123
|
+
export const EXIT_USAGE = 2;
|
|
124
|
+
/**
|
|
125
|
+
* Refused on governance grounds, not on error grounds.
|
|
126
|
+
*
|
|
127
|
+
* A distinct code because an operator script that treats "the blocker is open"
|
|
128
|
+
* as a transient failure would retry it, and a retried refusal looks like
|
|
129
|
+
* flakiness rather than like policy.
|
|
130
|
+
*/
|
|
131
|
+
export const EXIT_REFUSED = 3;
|
|
132
|
+
/**
|
|
133
|
+
* A pre-registered guard aborted the run.
|
|
134
|
+
*
|
|
135
|
+
* Distinct from {@link EXIT_ERROR} on purpose. `1` says "this invocation was
|
|
136
|
+
* malformed"; `4` says "the invocation was well-formed and a pre-registered
|
|
137
|
+
* invariant stopped it". Collapsing the two would make a budget abort
|
|
138
|
+
* indistinguishable from a typo in a path, and the whole subject of step 0.5 is
|
|
139
|
+
* that an abort must be legible as an abort rather than read as a degraded run.
|
|
140
|
+
*/
|
|
141
|
+
export const EXIT_GUARD_ABORT = 4;
|
|
142
|
+
|
|
143
|
+
// --- the pre-registered guards, at their call sites -------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The pre-registered budget, read from the committed config. Never defaulted.
|
|
147
|
+
*
|
|
148
|
+
* `road-to-governed-harness-evolution` step 0.5 and blocker
|
|
149
|
+
* `guard-call-site-integration`. Until this change the guards in
|
|
150
|
+
* `_lib/harness_evolution_guards.ts` had ZERO production call sites — measured
|
|
151
|
+
* 2026-08-30, three paths in the whole tree and all three were the guard, its
|
|
152
|
+
* unit test, and the config it reads. A guard nothing calls has no coverage,
|
|
153
|
+
* whatever its unit tests say; these two functions are the call sites.
|
|
154
|
+
*/
|
|
155
|
+
export const BUDGET_CONFIG_PATH = path.join(REPO_ROOT, 'src', 'config', 'harness-evolution-budget.json');
|
|
156
|
+
|
|
157
|
+
export class BudgetConfigError extends Error {
|
|
158
|
+
constructor(msg: string) {
|
|
159
|
+
super(`budget config: ${msg}`);
|
|
160
|
+
this.name = 'BudgetConfigError';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load the pre-registered ceilings. FAIL-CLOSED in every direction.
|
|
166
|
+
*
|
|
167
|
+
* A missing file, unparseable JSON, a missing key, a non-integer or a negative
|
|
168
|
+
* ceiling all THROW. There is deliberately no fallback budget: a default
|
|
169
|
+
* ceiling is a ceiling nobody registered, and a run that proceeds under one has
|
|
170
|
+
* exactly the property step 0.5 exists to prevent — it looks pre-registered and
|
|
171
|
+
* is not. Deleting the config must stop the lab, not un-cap it.
|
|
172
|
+
*/
|
|
173
|
+
export function loadRunBudget(file: string = BUDGET_CONFIG_PATH): RunBudget {
|
|
174
|
+
let raw: string;
|
|
175
|
+
try {
|
|
176
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
177
|
+
} catch {
|
|
178
|
+
throw new BudgetConfigError(
|
|
179
|
+
`not readable at ${file} — the budget is PRE-REGISTERED, so an absent config aborts the ` +
|
|
180
|
+
'run rather than un-capping it',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
let doc: unknown;
|
|
184
|
+
try {
|
|
185
|
+
doc = JSON.parse(raw);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
throw new BudgetConfigError(`${file} is not valid JSON: ${(e as Error).message}`);
|
|
188
|
+
}
|
|
189
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
|
|
190
|
+
throw new BudgetConfigError(`${file} must be a JSON object`);
|
|
191
|
+
}
|
|
192
|
+
const budget = (doc as Record<string, unknown>)['budget'];
|
|
193
|
+
if (typeof budget !== 'object' || budget === null || Array.isArray(budget)) {
|
|
194
|
+
throw new BudgetConfigError(`${file} carries no 'budget' object`);
|
|
195
|
+
}
|
|
196
|
+
const b = budget as Record<string, unknown>;
|
|
197
|
+
const ceiling = (key: string): number => {
|
|
198
|
+
const v = b[key];
|
|
199
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
200
|
+
throw new BudgetConfigError(`'budget.${key}' must be a non-negative integer`);
|
|
201
|
+
}
|
|
202
|
+
return v;
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
maxCandidates: ceiling('max_candidates'),
|
|
206
|
+
maxTrialsPerCandidate: ceiling('max_trials_per_candidate'),
|
|
207
|
+
maxSpendCents: ceiling('max_spend_cents'),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The visibility class of every field the observation schema declares.
|
|
213
|
+
*
|
|
214
|
+
* Step 0.4 asks for a per-field `visibility_class` on every observation. These
|
|
215
|
+
* three are the fields the deterministic proposer reads, and they are the only
|
|
216
|
+
* ones released by default. **Everything else fails closed to `holdout`** —
|
|
217
|
+
* that is `discloseToProposer`'s own default, and it is what makes an
|
|
218
|
+
* unclassified field an abort rather than a silent release.
|
|
219
|
+
*
|
|
220
|
+
* The gap this closes is real and was measurable before it: `parseObservations`
|
|
221
|
+
* ignores unknown keys, so an observations file carrying `holdoutScore: 0.83`
|
|
222
|
+
* next to a subject used to flow straight into proposer input with nothing
|
|
223
|
+
* looking at it. That is a holdout value reaching proposer context, which step
|
|
224
|
+
* 0.4 classes as invalidating rather than degrading.
|
|
225
|
+
*/
|
|
226
|
+
export const OBSERVATION_FIELD_VISIBILITY: readonly FieldVisibility[] = [
|
|
227
|
+
{ field: 'defectClass', visibility: 'proposer-visible' },
|
|
228
|
+
{ field: 'subject', visibility: 'proposer-visible' },
|
|
229
|
+
{ field: 'routeTo', visibility: 'proposer-visible' },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const VISIBILITY_CLASSES: readonly VisibilityClass[] = [
|
|
233
|
+
'proposer-visible',
|
|
234
|
+
'evaluator-private',
|
|
235
|
+
'holdout',
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
/** An observations document: the bare array, or one carrying its own field classes. */
|
|
239
|
+
export interface ObservationDocument {
|
|
240
|
+
readonly fieldVisibility: readonly FieldVisibility[];
|
|
241
|
+
readonly observations: readonly Record<string, unknown>[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Accept either shape, and never widen the default set silently.
|
|
246
|
+
*
|
|
247
|
+
* - a bare JSON array of observations — the declared three classes apply and
|
|
248
|
+
* every other field fails closed;
|
|
249
|
+
* - `{ "field_visibility": [{field, visibility_class}], "observations": [...] }`
|
|
250
|
+
* — the operator declares classes for their own extra fields, which is
|
|
251
|
+
* step 0.4's `visibility_class` in its literal form.
|
|
252
|
+
*
|
|
253
|
+
* A declaration may only ADD fields. Re-classifying one of the three declared
|
|
254
|
+
* fields is refused: letting an observations file relabel `subject` as
|
|
255
|
+
* `evaluator-private` would let the input decide its own trust boundary, which
|
|
256
|
+
* is the boundary-holds-until-the-first-convenient-exception failure the guard
|
|
257
|
+
* module names.
|
|
258
|
+
*/
|
|
259
|
+
export function parseObservationDocument(input: unknown): ObservationDocument {
|
|
260
|
+
const asObservations = (v: unknown): Record<string, unknown>[] => {
|
|
261
|
+
if (!Array.isArray(v)) {
|
|
262
|
+
throw new CandidateSchemaError('observations must be a JSON array');
|
|
263
|
+
}
|
|
264
|
+
return v.map((item) => {
|
|
265
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
266
|
+
throw new CandidateSchemaError('each observation must be a JSON object');
|
|
267
|
+
}
|
|
268
|
+
return item as Record<string, unknown>;
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
if (Array.isArray(input)) {
|
|
272
|
+
return { fieldVisibility: OBSERVATION_FIELD_VISIBILITY, observations: asObservations(input) };
|
|
273
|
+
}
|
|
274
|
+
if (typeof input !== 'object' || input === null) {
|
|
275
|
+
throw new CandidateSchemaError(
|
|
276
|
+
"observations must be a JSON array, or an object with 'observations' and optional 'field_visibility'",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const obj = input as Record<string, unknown>;
|
|
280
|
+
const extra: FieldVisibility[] = [];
|
|
281
|
+
const declared = obj['field_visibility'];
|
|
282
|
+
if (declared !== undefined) {
|
|
283
|
+
if (!Array.isArray(declared)) {
|
|
284
|
+
throw new CandidateSchemaError("'field_visibility' must be an array");
|
|
285
|
+
}
|
|
286
|
+
const reserved = new Set(OBSERVATION_FIELD_VISIBILITY.map((f) => f.field));
|
|
287
|
+
for (const item of declared) {
|
|
288
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
289
|
+
throw new CandidateSchemaError("'field_visibility' members must be objects");
|
|
290
|
+
}
|
|
291
|
+
const rec = item as Record<string, unknown>;
|
|
292
|
+
const field = rec['field'];
|
|
293
|
+
const visibility = rec['visibility_class'];
|
|
294
|
+
if (typeof field !== 'string' || field.trim() === '') {
|
|
295
|
+
throw new CandidateSchemaError("'field_visibility' member needs a non-empty 'field'");
|
|
296
|
+
}
|
|
297
|
+
if (!VISIBILITY_CLASSES.includes(visibility as VisibilityClass)) {
|
|
298
|
+
throw new CandidateSchemaError(
|
|
299
|
+
`'visibility_class' for '${field}' must be one of ${VISIBILITY_CLASSES.join(', ')}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (reserved.has(field)) {
|
|
303
|
+
throw new CandidateSchemaError(
|
|
304
|
+
`'${field}' is a declared proposer-visible field and an observations file may not ` +
|
|
305
|
+
're-classify it — an input that sets its own trust boundary is not a boundary',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
extra.push({ field, visibility: visibility as VisibilityClass });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
fieldVisibility: [...OBSERVATION_FIELD_VISIBILITY, ...extra],
|
|
313
|
+
observations: asObservations(obj['observations']),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Release every observation to proposer context, logging every field released.
|
|
319
|
+
*
|
|
320
|
+
* Step 0.4's three parts, in one place: the per-field class (the schema), the
|
|
321
|
+
* log of every field disclosed (written to stderr, and to the returned array),
|
|
322
|
+
* and the abort when holdout truth appears. The abort is a THROW here and a
|
|
323
|
+
* non-zero PROCESS exit at the verb — which is the distinction the split
|
|
324
|
+
* council turned on, so it is worth naming rather than leaving to the reader.
|
|
325
|
+
*
|
|
326
|
+
* @throws {HoldoutLeakError} on the first holdout or unclassified field.
|
|
327
|
+
*/
|
|
328
|
+
export function discloseObservations(
|
|
329
|
+
doc: ObservationDocument,
|
|
330
|
+
log: DisclosureRecord[],
|
|
331
|
+
emit: (line: string) => void,
|
|
332
|
+
): Record<string, unknown>[] {
|
|
333
|
+
const out: Record<string, unknown>[] = [];
|
|
334
|
+
for (let i = 0; i < doc.observations.length; i += 1) {
|
|
335
|
+
const perField: DisclosureRecord[] = [];
|
|
336
|
+
try {
|
|
337
|
+
out.push(discloseToProposer(doc.observations[i] as Record<string, unknown>, doc.fieldVisibility, perField));
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (e instanceof HoldoutLeakError) {
|
|
340
|
+
// The log names the field, which is the second half of 0.4's
|
|
341
|
+
// verify clause. `discloseToProposer` logs only what it
|
|
342
|
+
// RELEASED, by design, so the refusal is recorded here.
|
|
343
|
+
emit(`disclosure: REFUSED obs[${String(i)}] field=${e.field} class=holdout`);
|
|
344
|
+
}
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
for (const rec of perField) {
|
|
348
|
+
emit(`disclosure: obs[${String(i)}] field=${rec.field} class=${rec.visibility}`);
|
|
349
|
+
log.push(rec);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Which verb reaches which Phase-3 exit criterion.
|
|
357
|
+
*
|
|
358
|
+
* This is the machine-checkable half of step 3.6's verify clause. It covers
|
|
359
|
+
* **Phase 3 only**, deliberately and with the gap stated rather than implied:
|
|
360
|
+
* Phases 4-7 do not exist in the tree yet, so no verb can reach an exit
|
|
361
|
+
* criterion for them, and a map that listed a verb per unbuilt phase would be
|
|
362
|
+
* an assertion dressed as coverage. `tests/scripts/evolution_lab.test.ts`
|
|
363
|
+
* asserts every key here names a real verb and that all seven verbs appear.
|
|
364
|
+
*/
|
|
365
|
+
export const EXIT_CRITERION_COVERAGE: Readonly<Record<string, readonly Verb[]>> = {
|
|
366
|
+
'0.4 a run in which a holdout value reaches proposer context exits non-zero': ['propose'],
|
|
367
|
+
'0.5 a run configured past the ceiling exits non-zero before spending': ['run', 'propose'],
|
|
368
|
+
'3.1 five candidates materialised and destroyed, no diff in the original tree': ['run', 'clean'],
|
|
369
|
+
'3.1 sabotaging a path ownership makes the integrity check exit non-zero': ['compare'],
|
|
370
|
+
'3.2 the schema rejects a candidate touching two primary dimensions': ['inspect', 'run'],
|
|
371
|
+
'3.3 the schema rejects a mutation naming a fourth dimension': ['inspect', 'run'],
|
|
372
|
+
'3.4 no code path reads a candidate as accepted from the mere fact that it exists': ['inspect'],
|
|
373
|
+
'3.4 a state transition skipping a stage is refused': ['explain', 'promote'],
|
|
374
|
+
'3.5 the same input produces byte-identical candidates across two runs': ['propose'],
|
|
375
|
+
'3.6 every verb is named and none starts a resident process': ['explain'],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const USAGE = `usage: evolution_lab <verb> [options]
|
|
379
|
+
|
|
380
|
+
inspect [--record FILE]... [--records DIR] [--clones]
|
|
381
|
+
propose --observations FILE --out DIR [--force]
|
|
382
|
+
run --record FILE... [--refresh]
|
|
383
|
+
[--trials-per-candidate N] [--estimated-spend-cents N]
|
|
384
|
+
compare [--verbose]
|
|
385
|
+
explain [--record FILE [--to STATE]] [--criteria]
|
|
386
|
+
promote --record FILE (always refuses: blocker merge-authority)
|
|
387
|
+
clean [--yes]
|
|
388
|
+
|
|
389
|
+
No verb starts a resident process, a timer, or a watcher.
|
|
390
|
+
propose and run are gated by the pre-registered guards in
|
|
391
|
+
_lib/harness_evolution_guards.ts and exit 4 when one aborts the run.
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
function usageError(msg: string): number {
|
|
395
|
+
process.stderr.write(`evolution_lab: error: ${msg}\n`);
|
|
396
|
+
process.stderr.write(USAGE);
|
|
397
|
+
return EXIT_USAGE;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function fail(msg: string): number {
|
|
401
|
+
process.stderr.write(`evolution_lab: ${msg}\n`);
|
|
402
|
+
return EXIT_ERROR;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- flag parsing -----------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
interface Flags {
|
|
408
|
+
readonly bools: ReadonlySet<string>;
|
|
409
|
+
readonly values: ReadonlyMap<string, string[]>;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class FlagError extends Error {}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Parse `--flag` and `--flag value` / `--flag=value` against a declared shape.
|
|
416
|
+
*
|
|
417
|
+
* An unknown flag is an error rather than an ignored token. A silently ignored
|
|
418
|
+
* `--refresh` is the operator believing a clone was rebuilt when it was not.
|
|
419
|
+
*/
|
|
420
|
+
function parseFlags(argv: readonly string[], boolNames: readonly string[], valueNames: readonly string[]): Flags {
|
|
421
|
+
const bools = new Set<string>();
|
|
422
|
+
const values = new Map<string, string[]>();
|
|
423
|
+
let i = 0;
|
|
424
|
+
while (i < argv.length) {
|
|
425
|
+
const arg = argv[i] as string;
|
|
426
|
+
const eq = arg.indexOf('=');
|
|
427
|
+
const name = eq >= 0 ? arg.slice(0, eq) : arg;
|
|
428
|
+
if (!name.startsWith('--')) {
|
|
429
|
+
throw new FlagError(`unrecognized argument: ${arg}`);
|
|
430
|
+
}
|
|
431
|
+
const bare = name.slice(2);
|
|
432
|
+
if (boolNames.includes(bare)) {
|
|
433
|
+
if (eq >= 0) {
|
|
434
|
+
throw new FlagError(`--${bare} takes no value`);
|
|
435
|
+
}
|
|
436
|
+
bools.add(bare);
|
|
437
|
+
i += 1;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (valueNames.includes(bare)) {
|
|
441
|
+
let v: string;
|
|
442
|
+
if (eq >= 0) {
|
|
443
|
+
v = arg.slice(eq + 1);
|
|
444
|
+
i += 1;
|
|
445
|
+
} else {
|
|
446
|
+
const next = argv[i + 1];
|
|
447
|
+
if (next === undefined) {
|
|
448
|
+
throw new FlagError(`--${bare}: expected one argument`);
|
|
449
|
+
}
|
|
450
|
+
v = next;
|
|
451
|
+
i += 2;
|
|
452
|
+
}
|
|
453
|
+
const list = values.get(bare) ?? [];
|
|
454
|
+
list.push(v);
|
|
455
|
+
values.set(bare, list);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
throw new FlagError(`unrecognized argument: ${arg}`);
|
|
459
|
+
}
|
|
460
|
+
return { bools, values };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function one(flags: Flags, name: string): string | undefined {
|
|
464
|
+
const list = flags.values.get(name);
|
|
465
|
+
if (list === undefined) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
if (list.length > 1) {
|
|
469
|
+
throw new FlagError(`--${name} given more than once`);
|
|
470
|
+
}
|
|
471
|
+
return list[0];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// --- record loading ---------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Read and VALIDATE a candidate record file.
|
|
478
|
+
*
|
|
479
|
+
* Routes through `parseCandidateRecord` — the refusing parser — rather than
|
|
480
|
+
* through `readCandidateRecord`, because everything reached from here feeds a
|
|
481
|
+
* RUN. The forgiving reader exists for archives, and is used by `inspect
|
|
482
|
+
* --clones` where a historical record with an unknown dimension must stay
|
|
483
|
+
* readable. Keeping the two apart is the whole point of that asymmetry.
|
|
484
|
+
*
|
|
485
|
+
* @throws {CandidateSchemaError} on a rejected record.
|
|
486
|
+
*/
|
|
487
|
+
export function loadRecord(file: string): CandidateRecord {
|
|
488
|
+
let raw: string;
|
|
489
|
+
try {
|
|
490
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
491
|
+
} catch {
|
|
492
|
+
throw new CandidateSchemaError(`record not readable at ${file}`);
|
|
493
|
+
}
|
|
494
|
+
let parsed: unknown;
|
|
495
|
+
try {
|
|
496
|
+
parsed = JSON.parse(raw);
|
|
497
|
+
} catch (e) {
|
|
498
|
+
throw new CandidateSchemaError(`record at ${file} is not valid JSON: ${(e as Error).message}`);
|
|
499
|
+
}
|
|
500
|
+
return parseCandidateRecord(parsed);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** `*.json` under `dir`, sorted byte-wise. Never left in readdir order. */
|
|
504
|
+
export function recordFilesIn(dir: string): string[] {
|
|
505
|
+
return fs
|
|
506
|
+
.readdirSync(dir)
|
|
507
|
+
.filter((n) => n.endsWith('.json'))
|
|
508
|
+
.sort(byteCompare)
|
|
509
|
+
.map((n) => path.join(dir, n));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Candidate clone directories under `clones/`, sorted.
|
|
514
|
+
*
|
|
515
|
+
* Discovery by prefix, matching `bench_ab_integrity.discover_candidate_clones`
|
|
516
|
+
* — a clone the operator was not told about is exactly what an inspection verb
|
|
517
|
+
* has to be able to see.
|
|
518
|
+
*/
|
|
519
|
+
export function candidateCloneDirs(clonesRoot: string): string[] {
|
|
520
|
+
let entries: fs.Dirent[];
|
|
521
|
+
try {
|
|
522
|
+
entries = fs.readdirSync(clonesRoot, { withFileTypes: true });
|
|
523
|
+
} catch {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return entries
|
|
527
|
+
.filter((e) => e.isDirectory() && e.name.startsWith(CANDIDATE_PREFIX))
|
|
528
|
+
.map((e) => e.name)
|
|
529
|
+
.sort(byteCompare);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// --- verbs ------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
function verbInspect(argv: readonly string[]): number {
|
|
535
|
+
const flags = parseFlags(argv, ['clones'], ['record', 'records']);
|
|
536
|
+
const files: string[] = [...(flags.values.get('record') ?? [])];
|
|
537
|
+
const dir = one(flags, 'records');
|
|
538
|
+
if (dir !== undefined) {
|
|
539
|
+
let listed: string[];
|
|
540
|
+
try {
|
|
541
|
+
listed = recordFilesIn(dir);
|
|
542
|
+
} catch {
|
|
543
|
+
return fail(`records directory not readable at ${dir}`);
|
|
544
|
+
}
|
|
545
|
+
files.push(...listed);
|
|
546
|
+
}
|
|
547
|
+
const wantClones = flags.bools.has('clones') || files.length === 0;
|
|
548
|
+
let bad = 0;
|
|
549
|
+
for (const f of files.sort(byteCompare)) {
|
|
550
|
+
try {
|
|
551
|
+
const r = loadRecord(f);
|
|
552
|
+
process.stdout.write(
|
|
553
|
+
`record ${f}: id=${r.id} dimension=${r.dimension} lifecycle=${r.lifecycle} ` +
|
|
554
|
+
`accepted=${String(isAccepted(r))} mutations=${String(r.mutations.length)}\n`,
|
|
555
|
+
);
|
|
556
|
+
} catch (e) {
|
|
557
|
+
bad += 1;
|
|
558
|
+
process.stderr.write(`record ${f}: REJECTED — ${(e as Error).message}\n`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (wantClones) {
|
|
562
|
+
const dirs = candidateCloneDirs(CLONES);
|
|
563
|
+
if (dirs.length === 0) {
|
|
564
|
+
process.stdout.write(`clones: none under ${CLONES}\n`);
|
|
565
|
+
}
|
|
566
|
+
for (const name of dirs) {
|
|
567
|
+
const recordPath = path.join(CLONES, name, CANDIDATE_RECORD_FILE);
|
|
568
|
+
try {
|
|
569
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
|
|
570
|
+
const r = readCandidateRecord(parsed);
|
|
571
|
+
process.stdout.write(
|
|
572
|
+
`clone ${name}: id=${r.id} dimension=${r.dimension} lifecycle=${r.lifecycle} ` +
|
|
573
|
+
`accepted=${String(isAccepted(r))} unknownDimension=${String(r.unknownDimension)}\n`,
|
|
574
|
+
);
|
|
575
|
+
} catch (e) {
|
|
576
|
+
bad += 1;
|
|
577
|
+
process.stderr.write(`clone ${name}: unreadable record — ${(e as Error).message}\n`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return bad === 0 ? EXIT_OK : EXIT_ERROR;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Turn a pre-registered guard's throw into a non-zero PROCESS exit.
|
|
586
|
+
*
|
|
587
|
+
* This function is the whole substance of the `guard-call-site-integration`
|
|
588
|
+
* blocker. The council split on whether a unit test observing a throw proves
|
|
589
|
+
* the guard is integrated; the conservative seat held it does not, because
|
|
590
|
+
* nothing showed an executable runner CONVERTING that throw into a non-zero
|
|
591
|
+
* process exit. This is that conversion, and it is called from both verbs that
|
|
592
|
+
* can reach a guard.
|
|
593
|
+
*
|
|
594
|
+
* Returns `undefined` when the error is not a guard abort, so the caller falls
|
|
595
|
+
* through to its own handling rather than swallowing an unrelated failure as a
|
|
596
|
+
* governance stop.
|
|
597
|
+
*/
|
|
598
|
+
function guardAbort(e: unknown): number | undefined {
|
|
599
|
+
if (e instanceof BudgetExceededError) {
|
|
600
|
+
process.stderr.write(
|
|
601
|
+
`evolution_lab: ABORTED on the pre-registered budget (dimension: ${e.dimension})\n` +
|
|
602
|
+
` ${e.message}\n` +
|
|
603
|
+
` ceilings: ${BUDGET_CONFIG_PATH}\n` +
|
|
604
|
+
' Nothing was materialised and nothing was spent: the check runs before the first\n' +
|
|
605
|
+
' record is parsed and before the first clone is written.\n',
|
|
606
|
+
);
|
|
607
|
+
return EXIT_GUARD_ABORT;
|
|
608
|
+
}
|
|
609
|
+
if (e instanceof HoldoutLeakError) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
`evolution_lab: ABORTED on evaluator trust boundary (field: ${e.field})\n` +
|
|
612
|
+
` ${e.message}\n` +
|
|
613
|
+
' Nothing was proposed and nothing was written: disclosure is checked before the\n' +
|
|
614
|
+
' observation reaches the proposer.\n',
|
|
615
|
+
);
|
|
616
|
+
return EXIT_GUARD_ABORT;
|
|
617
|
+
}
|
|
618
|
+
if (e instanceof BudgetConfigError) {
|
|
619
|
+
process.stderr.write(`evolution_lab: ABORTED — ${(e as Error).message}\n`);
|
|
620
|
+
return EXIT_GUARD_ABORT;
|
|
621
|
+
}
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function verbPropose(argv: readonly string[]): number {
|
|
626
|
+
const flags = parseFlags(argv, ['force'], ['observations', 'out']);
|
|
627
|
+
const obsFile = one(flags, 'observations');
|
|
628
|
+
const outDir = one(flags, 'out');
|
|
629
|
+
if (obsFile === undefined || outDir === undefined) {
|
|
630
|
+
return usageError('propose requires --observations FILE and --out DIR');
|
|
631
|
+
}
|
|
632
|
+
let observations;
|
|
633
|
+
try {
|
|
634
|
+
const doc = parseObservationDocument(JSON.parse(fs.readFileSync(obsFile, 'utf-8')));
|
|
635
|
+
// GUARD 0.5, first. `propose` runs no trials and spends nothing, so the
|
|
636
|
+
// two placeholder dimensions are honest zeros rather than a pretence of
|
|
637
|
+
// a cost model — what it DOES decide is how many candidates exist, and
|
|
638
|
+
// `max_candidates` is a ceiling on exactly that. Checking here as well
|
|
639
|
+
// as in `run` closes the evasion of proposing six and running them in
|
|
640
|
+
// two batches of three.
|
|
641
|
+
const plan: RunPlan = {
|
|
642
|
+
candidates: doc.observations.length,
|
|
643
|
+
trialsPerCandidate: 1,
|
|
644
|
+
estimatedSpendCents: 0,
|
|
645
|
+
};
|
|
646
|
+
assertWithinBudget(plan, loadRunBudget());
|
|
647
|
+
// GUARD 0.4, before the proposer sees anything.
|
|
648
|
+
const log: DisclosureRecord[] = [];
|
|
649
|
+
const disclosed = discloseObservations(doc, log, (line) => process.stderr.write(`${line}\n`));
|
|
650
|
+
observations = parseObservations(disclosed);
|
|
651
|
+
} catch (e) {
|
|
652
|
+
const aborted = guardAbort(e);
|
|
653
|
+
if (aborted !== undefined) {
|
|
654
|
+
return aborted;
|
|
655
|
+
}
|
|
656
|
+
return fail(`observations at ${obsFile} rejected — ${(e as Error).message}`);
|
|
657
|
+
}
|
|
658
|
+
let records: CandidateRecord[];
|
|
659
|
+
try {
|
|
660
|
+
records = proposeCandidates(observations, (subject) =>
|
|
661
|
+
fs.readFileSync(path.join(REPO_ROOT, subject), 'utf-8'),
|
|
662
|
+
);
|
|
663
|
+
} catch (e) {
|
|
664
|
+
return fail(`proposal failed — ${(e as Error).message}`);
|
|
665
|
+
}
|
|
666
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
667
|
+
for (const r of records) {
|
|
668
|
+
const dest = path.join(outDir, candidateRecordFilename(r));
|
|
669
|
+
if (fs.existsSync(dest) && !flags.bools.has('force')) {
|
|
670
|
+
const existing = fs.readFileSync(dest, 'utf-8');
|
|
671
|
+
const proposed = serialiseCandidateRecord(r);
|
|
672
|
+
if (existing === proposed) {
|
|
673
|
+
process.stdout.write(`propose: ${dest} already identical\n`);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
return fail(`${dest} exists with different bytes — pass --force to overwrite`);
|
|
677
|
+
}
|
|
678
|
+
fs.writeFileSync(dest, serialiseCandidateRecord(r), 'utf-8');
|
|
679
|
+
process.stdout.write(`propose: wrote ${dest}\n`);
|
|
680
|
+
}
|
|
681
|
+
process.stdout.write(`propose: ${String(records.length)} candidate(s)\n`);
|
|
682
|
+
return EXIT_OK;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** A non-negative integer flag, or a usage error. Never a silent coercion. */
|
|
686
|
+
function intFlag(flags: Flags, name: string, fallback: number): number {
|
|
687
|
+
const raw = one(flags, name);
|
|
688
|
+
if (raw === undefined) {
|
|
689
|
+
return fallback;
|
|
690
|
+
}
|
|
691
|
+
if (!/^\d+$/.test(raw)) {
|
|
692
|
+
throw new FlagError(`--${name} must be a non-negative integer (got ${JSON.stringify(raw)})`);
|
|
693
|
+
}
|
|
694
|
+
return Number.parseInt(raw, 10);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function verbRun(argv: readonly string[]): number {
|
|
698
|
+
const flags = parseFlags(
|
|
699
|
+
argv,
|
|
700
|
+
['refresh'],
|
|
701
|
+
['record', 'records', 'trials-per-candidate', 'estimated-spend-cents'],
|
|
702
|
+
);
|
|
703
|
+
const files: string[] = [...(flags.values.get('record') ?? [])];
|
|
704
|
+
const dir = one(flags, 'records');
|
|
705
|
+
if (dir !== undefined) {
|
|
706
|
+
try {
|
|
707
|
+
files.push(...recordFilesIn(dir));
|
|
708
|
+
} catch {
|
|
709
|
+
return fail(`records directory not readable at ${dir}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (files.length === 0) {
|
|
713
|
+
return usageError('run requires at least one --record FILE (or --records DIR)');
|
|
714
|
+
}
|
|
715
|
+
// GUARD 0.5 — BEFORE the first record is parsed and before the first clone
|
|
716
|
+
// directory exists. `candidates` is DERIVED from the record set rather than
|
|
717
|
+
// declared: a declared count that disagrees with the record set is how a
|
|
718
|
+
// run gets truncated to fit a ceiling, and 0.5's whole point is that it
|
|
719
|
+
// aborts instead. The other two dimensions are declared because this verb
|
|
720
|
+
// cannot observe them — it runs no trials and spends nothing itself, so a
|
|
721
|
+
// default of zero is the honest reading and a caller that knows better
|
|
722
|
+
// passes the real figure.
|
|
723
|
+
try {
|
|
724
|
+
const plan: RunPlan = {
|
|
725
|
+
candidates: files.length,
|
|
726
|
+
trialsPerCandidate: intFlag(flags, 'trials-per-candidate', 1),
|
|
727
|
+
estimatedSpendCents: intFlag(flags, 'estimated-spend-cents', 0),
|
|
728
|
+
};
|
|
729
|
+
assertWithinBudget(plan, loadRunBudget());
|
|
730
|
+
} catch (e) {
|
|
731
|
+
const aborted = guardAbort(e);
|
|
732
|
+
if (aborted !== undefined) {
|
|
733
|
+
return aborted;
|
|
734
|
+
}
|
|
735
|
+
throw e;
|
|
736
|
+
}
|
|
737
|
+
const seen = new Set<string>();
|
|
738
|
+
for (const f of files.sort(byteCompare)) {
|
|
739
|
+
let record: CandidateRecord;
|
|
740
|
+
try {
|
|
741
|
+
record = loadRecord(f);
|
|
742
|
+
} catch (e) {
|
|
743
|
+
return fail(`${f} rejected — ${(e as Error).message}`);
|
|
744
|
+
}
|
|
745
|
+
if (seen.has(record.id)) {
|
|
746
|
+
return fail(`candidate id '${record.id}' given twice — ids name clone directories`);
|
|
747
|
+
}
|
|
748
|
+
seen.add(record.id);
|
|
749
|
+
try {
|
|
750
|
+
clone_candidate(record, { refresh: flags.bools.has('refresh') });
|
|
751
|
+
} catch (e) {
|
|
752
|
+
if (e instanceof PathOwnershipError) {
|
|
753
|
+
return fail(`candidate ${record.id} rejected — ${e.message}`);
|
|
754
|
+
}
|
|
755
|
+
return fail(`candidate ${record.id} failed — ${(e as Error).message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return EXIT_OK;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function verbCompare(argv: readonly string[]): number {
|
|
762
|
+
const flags = parseFlags(argv, ['verbose'], []);
|
|
763
|
+
// Direct call, never a subprocess: the integrity check is the second
|
|
764
|
+
// independent path-ownership guard and it must run in this process so its
|
|
765
|
+
// exit code IS this verb's exit code rather than a relayed one.
|
|
766
|
+
return integrity_main(flags.bools.has('verbose') ? ['--verbose'] : []);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Every state this record could legally move to right now, sorted. */
|
|
770
|
+
export function legalNextStates(from: LifecycleState): LifecycleState[] {
|
|
771
|
+
const out: LifecycleState[] = [];
|
|
772
|
+
for (const to of LIFECYCLE_STATES) {
|
|
773
|
+
try {
|
|
774
|
+
assertTransition(from, to);
|
|
775
|
+
out.push(to);
|
|
776
|
+
} catch {
|
|
777
|
+
// Refused — including `promoted`, which needs a named human and
|
|
778
|
+
// therefore is never a legal NEXT state from a bare transition call.
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return out.sort(byteCompare);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function verbExplain(argv: readonly string[]): number {
|
|
785
|
+
const flags = parseFlags(argv, ['criteria'], ['record', 'to']);
|
|
786
|
+
const file = one(flags, 'record');
|
|
787
|
+
const to = one(flags, 'to');
|
|
788
|
+
if (flags.bools.has('criteria')) {
|
|
789
|
+
for (const key of Object.keys(EXIT_CRITERION_COVERAGE).sort(byteCompare)) {
|
|
790
|
+
const verbs = EXIT_CRITERION_COVERAGE[key] as readonly Verb[];
|
|
791
|
+
process.stdout.write(`criterion ${key} -> ${verbs.join(', ')}\n`);
|
|
792
|
+
}
|
|
793
|
+
process.stdout.write(
|
|
794
|
+
'criterion coverage spans the phases that exist: Phases 4-7 are unbuilt, so no verb reaches them\n',
|
|
795
|
+
);
|
|
796
|
+
if (file === undefined) {
|
|
797
|
+
return EXIT_OK;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (file === undefined) {
|
|
801
|
+
return usageError('explain requires --record FILE or --criteria');
|
|
802
|
+
}
|
|
803
|
+
let record: CandidateRecord;
|
|
804
|
+
try {
|
|
805
|
+
record = loadRecord(file);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
return fail(`${file} rejected — ${(e as Error).message}`);
|
|
808
|
+
}
|
|
809
|
+
const recipe = Object.values(RECIPES).find((r) => r.dimension === record.dimension);
|
|
810
|
+
process.stdout.write(`explain ${record.id}\n`);
|
|
811
|
+
process.stdout.write(` dimension: ${record.dimension} (exactly one, per step 3.2)\n`);
|
|
812
|
+
process.stdout.write(` lifecycle: ${record.lifecycle}\n`);
|
|
813
|
+
process.stdout.write(` accepted: ${String(isAccepted(record))}\n`);
|
|
814
|
+
process.stdout.write(` spine: ${LIFECYCLE_SPINE.join(' -> ')}\n`);
|
|
815
|
+
process.stdout.write(` legal now: ${legalNextStates(record.lifecycle).join(', ') || '(none)'}\n`);
|
|
816
|
+
if (recipe !== undefined) {
|
|
817
|
+
process.stdout.write(` a ${record.dimension} defect reads: ${recipe.summary}\n`);
|
|
818
|
+
}
|
|
819
|
+
for (const m of record.mutations) {
|
|
820
|
+
process.stdout.write(` writes: ${m.path} (${String(m.content.length)} chars)\n`);
|
|
821
|
+
}
|
|
822
|
+
if (to === undefined) {
|
|
823
|
+
return EXIT_OK;
|
|
824
|
+
}
|
|
825
|
+
if (!isLifecycleState(to)) {
|
|
826
|
+
return usageError(`--to must be one of ${LIFECYCLE_STATES.join(', ')}`);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
assertTransition(record.lifecycle, to);
|
|
830
|
+
process.stdout.write(` transition ${record.lifecycle} -> ${to}: LEGAL\n`);
|
|
831
|
+
return EXIT_OK;
|
|
832
|
+
} catch (e) {
|
|
833
|
+
if (e instanceof LifecycleTransitionError) {
|
|
834
|
+
process.stdout.write(` transition ${record.lifecycle} -> ${to}: REFUSED — ${e.message}\n`);
|
|
835
|
+
return EXIT_ERROR;
|
|
836
|
+
}
|
|
837
|
+
throw e;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The promotion verb. It exists, it is named, and it always refuses.
|
|
843
|
+
*
|
|
844
|
+
* Two independent reasons, in the order they fire:
|
|
845
|
+
*
|
|
846
|
+
* 1. `assertTransition(from, 'promoted')` is called with NO approval, so the
|
|
847
|
+
* lifecycle's own gate refuses. This is the mechanical half of the
|
|
848
|
+
* non-promotion property the `merge-authority` council required.
|
|
849
|
+
* 2. Blocker `merge-authority` is OPEN. Phase 7 is gated on it and no council
|
|
850
|
+
* may resolve it: granting preauthorized merge authority weakens a
|
|
851
|
+
* human-in-the-loop guarantee, and refusing it settles an ADR-239 Decision
|
|
852
|
+
* recorded as open. Both are owner-reserved.
|
|
853
|
+
*
|
|
854
|
+
* There is no flag, no environment variable and no argument that turns this
|
|
855
|
+
* into a promotion. `--approver` is deliberately NOT a flag: adding one is what
|
|
856
|
+
* would let an unattended run name a human who never approved.
|
|
857
|
+
*/
|
|
858
|
+
function verbPromote(argv: readonly string[]): number {
|
|
859
|
+
const flags = parseFlags(argv, [], ['record']);
|
|
860
|
+
const file = one(flags, 'record');
|
|
861
|
+
if (file === undefined) {
|
|
862
|
+
return usageError('promote requires --record FILE');
|
|
863
|
+
}
|
|
864
|
+
let record: CandidateRecord;
|
|
865
|
+
try {
|
|
866
|
+
record = loadRecord(file);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
return fail(`${file} rejected — ${(e as Error).message}`);
|
|
869
|
+
}
|
|
870
|
+
let gate = 'the lifecycle gate did not refuse, which is itself a defect';
|
|
871
|
+
try {
|
|
872
|
+
assertTransition(record.lifecycle, 'promoted');
|
|
873
|
+
} catch (e) {
|
|
874
|
+
gate = (e as Error).message;
|
|
875
|
+
}
|
|
876
|
+
process.stderr.write(
|
|
877
|
+
`evolution_lab: promote REFUSED for ${record.id}\n` +
|
|
878
|
+
` lifecycle gate: ${gate}\n` +
|
|
879
|
+
' blocker: merge-authority is OPEN on road-to-governed-harness-evolution.\n' +
|
|
880
|
+
' Phases 1-6 are legal because they promote nothing (AI council 2026-08-29,\n' +
|
|
881
|
+
' anthropic + openai, 2/2). Phase 7 stays gated: granting merge authority\n' +
|
|
882
|
+
' weakens a human-in-the-loop guarantee and refusing it settles ADR-239\n' +
|
|
883
|
+
' Decision 3, and both are owner-reserved.\n' +
|
|
884
|
+
' Promotion into canonical agent-config remains a named human act performed\n' +
|
|
885
|
+
' outside this tool. This verb will not perform it.\n',
|
|
886
|
+
);
|
|
887
|
+
return EXIT_REFUSED;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Remove candidate clones — and only candidate clones.
|
|
892
|
+
*
|
|
893
|
+
* Scope is the `clones/candidate-*` directories this tool creates. The three
|
|
894
|
+
* fixed variants (`with`, `without`, `with-rdp`) are expensive to rebuild and
|
|
895
|
+
* belong to the value bench, not to the candidate loop, so they are never in
|
|
896
|
+
* range. Dry-run is the default; `--yes` is the confirmation.
|
|
897
|
+
*/
|
|
898
|
+
function verbClean(argv: readonly string[]): number {
|
|
899
|
+
const flags = parseFlags(argv, ['yes'], []);
|
|
900
|
+
const dirs = candidateCloneDirs(CLONES);
|
|
901
|
+
if (dirs.length === 0) {
|
|
902
|
+
process.stdout.write(`clean: no candidate clones under ${CLONES}\n`);
|
|
903
|
+
return EXIT_OK;
|
|
904
|
+
}
|
|
905
|
+
if (!flags.bools.has('yes')) {
|
|
906
|
+
for (const name of dirs) {
|
|
907
|
+
process.stdout.write(`clean: would remove ${path.join(CLONES, name)}\n`);
|
|
908
|
+
}
|
|
909
|
+
process.stdout.write(`clean: ${String(dirs.length)} candidate clone(s); pass --yes to remove\n`);
|
|
910
|
+
return EXIT_OK;
|
|
911
|
+
}
|
|
912
|
+
for (const name of dirs) {
|
|
913
|
+
// Re-check the prefix against the name actually about to be removed.
|
|
914
|
+
// The listing and the deletion are two steps, and a delete loop that
|
|
915
|
+
// trusts an earlier filter is one refactor away from removing `with`.
|
|
916
|
+
if (!name.startsWith(CANDIDATE_PREFIX)) {
|
|
917
|
+
return fail(`refusing to remove ${name}: not a candidate clone`);
|
|
918
|
+
}
|
|
919
|
+
const target = path.join(CLONES, name);
|
|
920
|
+
fs.rmSync(target, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
|
|
921
|
+
process.stdout.write(`clean: removed ${target}\n`);
|
|
922
|
+
}
|
|
923
|
+
return EXIT_OK;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// --- entry ------------------------------------------------------------------
|
|
927
|
+
|
|
928
|
+
export function main(argv?: string[]): number {
|
|
929
|
+
const args = argv ?? process.argv.slice(2);
|
|
930
|
+
const verb = args[0];
|
|
931
|
+
if (verb === undefined || verb === '-h' || verb === '--help') {
|
|
932
|
+
process.stdout.write(USAGE);
|
|
933
|
+
return verb === undefined ? EXIT_USAGE : EXIT_OK;
|
|
934
|
+
}
|
|
935
|
+
if (!isVerb(verb)) {
|
|
936
|
+
return usageError(`unknown verb '${verb}' (choose from ${VERBS.join(', ')})`);
|
|
937
|
+
}
|
|
938
|
+
const rest = args.slice(1);
|
|
939
|
+
try {
|
|
940
|
+
switch (verb) {
|
|
941
|
+
case 'inspect':
|
|
942
|
+
return verbInspect(rest);
|
|
943
|
+
case 'propose':
|
|
944
|
+
return verbPropose(rest);
|
|
945
|
+
case 'run':
|
|
946
|
+
return verbRun(rest);
|
|
947
|
+
case 'compare':
|
|
948
|
+
return verbCompare(rest);
|
|
949
|
+
case 'explain':
|
|
950
|
+
return verbExplain(rest);
|
|
951
|
+
case 'promote':
|
|
952
|
+
return verbPromote(rest);
|
|
953
|
+
case 'clean':
|
|
954
|
+
return verbClean(rest);
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {
|
|
957
|
+
if (e instanceof FlagError) {
|
|
958
|
+
return usageError(e.message);
|
|
959
|
+
}
|
|
960
|
+
if (e instanceof CandidateSchemaError || e instanceof PathOwnershipError) {
|
|
961
|
+
return fail((e as Error).message);
|
|
962
|
+
}
|
|
963
|
+
throw e;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function _isCliEntry(): boolean {
|
|
968
|
+
try {
|
|
969
|
+
if (process.argv[1] === undefined) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
973
|
+
} catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (_isCliEntry()) {
|
|
979
|
+
process.exit(main());
|
|
980
|
+
}
|