@event4u/agent-config 16.2.0 → 16.3.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 +89 -237
- package/README.md +14 -12
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
- package/dist/agent-src/commands/bug/fix.md +17 -2
- package/dist/agent-src/commands/feature/plan.md +1 -1
- package/dist/agent-src/commands/feature/roadmap.md +1 -1
- package/dist/agent-src/commands/fix/ci.md +1 -1
- package/dist/agent-src/commands/implement-ticket.md +1 -1
- package/dist/agent-src/commands/jira-ticket.md +1 -1
- package/dist/agent-src/commands/roadmap/create.md +1 -1
- package/dist/agent-src/commands/roadmap/materialize.md +1 -1
- package/dist/agent-src/commands/roadmap/process-full.md +4 -4
- package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
- package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
- package/dist/agent-src/commands/roadmap.md +18 -4
- package/dist/agent-src/commands/tests/create.md +17 -3
- package/dist/agent-src/commands/tests/execute.md +22 -5
- package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
- package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
- package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
- package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
- package/dist/agent-src/rules/active-remediation.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -0
- package/dist/agent-src/rules/broken-access-control.md +1 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
- package/dist/agent-src/rules/context-hygiene.md +1 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
- package/dist/agent-src/rules/design-fidelity.md +13 -16
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
- package/dist/agent-src/rules/domain-safety-pii.md +1 -0
- package/dist/agent-src/rules/domain-safety-retention.md +1 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
- package/dist/agent-src/rules/evaluator-independence.md +3 -2
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +1 -0
- package/dist/agent-src/rules/git-history-discipline.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -2
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +14 -17
- package/dist/agent-src/rules/persona-governance.md +2 -2
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
- package/dist/agent-src/rules/session-canary.md +8 -62
- package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/dist/agent-src/scripts/deferral_carry.ts +339 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
- package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
- package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
- package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/iconography/SKILL.md +1 -1
- package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
- package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
- package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
- package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
- package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
- package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
- package/dist/cli-delegate/check_update_banner.js +12 -1
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
- package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
- package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
- package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
- package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
- package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
- package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
- package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
- package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
- package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
- package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
- package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
- package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
- package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
- package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
- package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
- package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
- package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
- package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
- package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
- package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
- package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
- package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
- package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
- package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
- package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +1167 -234
- package/dist/cli-delegate/cmd_doctor.js +13 -6
- package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
- package/dist/cli-delegate/cmd_explain.js +5 -5
- package/dist/cli-delegate/cmd_export.js +11 -8
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +10 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +3 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +6 -6
- package/dist/cli-delegate/cmd_prune.js +11 -8
- package/dist/cli-delegate/cmd_refresh.js +2 -1
- package/dist/cli-delegate/cmd_route_audit.js +8 -6
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_session_recycle.js +4 -3
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
- package/dist/cli-delegate/cmd_settings_set.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +11 -8
- package/dist/cli-delegate/cmd_uninstall.js +11 -8
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +3 -2
- package/dist/cli-delegate/cmd_validate.js +11 -8
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/cli-delegate/deferral_carry.js +16 -0
- package/dist/cli-delegate/gate_execute.js +2 -2
- package/dist/cli-delegate/roadmap_gates.js +5 -5
- package/dist/cli-delegate/update_roadmap_progress.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -75
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +5357 -6656
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudePathsPlan.js +47 -6
- package/dist/install/claudeRuleRewrite.js +7 -0
- package/dist/install/emit_host_rules_cli.js +39 -36
- package/dist/install/globalRuleScope.js +87 -0
- package/dist/install/host_rule_ownership.js +160 -0
- package/dist/install/install.mjs +2080 -1024
- package/dist/install/installThinLayer.js +261 -0
- package/dist/install/installedRuleLinks.js +176 -0
- package/dist/install/wizard-plan.js +24 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +31965 -27154
- package/dist/scripts/_lib/agent_settings.js +11 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
- package/dist/scripts/_lib/installed_layer.js +378 -0
- package/dist/scripts/_lib/lean_projection_mode.js +218 -15
- package/dist/scripts/_lib/rule_consequence_class.js +45 -0
- package/dist/scripts/_lib/rule_injection.js +67 -10
- package/dist/scripts/_lib/rule_law_section.js +122 -0
- package/dist/scripts/_lib/thin_rules.js +458 -0
- package/dist/scripts/_lib/windsurf_render.js +60 -0
- package/dist/scripts/condense.js +14 -26
- package/dist/scripts/project_thin_rules.js +14 -294
- package/dist/server/schemas/settings.js +6 -0
- package/dist/shared/skillRanking.js +74 -13
- package/docs/contracts/settings-classes.md +7 -5
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
- package/src/agent-src/scripts/deferral_carry.ts +339 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
- package/src/config/agent-settings.template.yml +62 -22
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +96 -3
- package/src/config/gate-violation-baselines.json +17 -9
- package/src/config/hook-bundle-budget.json +25 -0
- package/src/config/hook-token-budget.json +6 -1
- package/src/config/host-instruction-limits.json +33 -0
- package/src/config/routing-coverage-seed.json +24 -3
- package/src/config/rule-activation-census.json +7 -3
- package/src/config/rule-consequence-class.json +161 -0
- package/src/config/rule-law-ceilings.json +99 -0
- package/src/install/claudePathsPlan.ts +46 -4
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/emit_host_rules_cli.ts +43 -34
- package/src/install/globalRuleScope.ts +109 -0
- package/src/install/host_rule_ownership.ts +170 -0
- package/src/install/installThinLayer.ts +312 -0
- package/src/install/installedRuleLinks.ts +220 -0
- package/src/install/wizard-plan.ts +24 -0
- package/src/scripts/_cli/cmd_doctor.ts +23 -71
- package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
- package/src/scripts/_dispatch.bash +16 -2
- package/src/scripts/_lib/agent_settings.ts +11 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
- package/src/scripts/_lib/estate_offsets.ts +241 -0
- package/src/scripts/_lib/exemption_shape.ts +143 -0
- package/src/scripts/_lib/fnv.ts +23 -0
- package/src/scripts/_lib/foreign_scan.ts +106 -0
- package/src/scripts/_lib/gitignore_block.ts +314 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
- package/src/scripts/_lib/graph_feeder_record.ts +214 -0
- package/src/scripts/_lib/hook_settings.ts +73 -0
- package/src/scripts/_lib/host_capability.ts +40 -19
- package/src/scripts/_lib/host_hook_merge.ts +165 -0
- package/src/scripts/_lib/installed_layer.ts +538 -0
- package/src/scripts/_lib/json_merge.ts +82 -0
- package/src/scripts/_lib/json_pointers.ts +93 -11
- package/src/scripts/_lib/lean_projection_mode.ts +275 -15
- package/src/scripts/_lib/machine_wake.ts +16 -3
- package/src/scripts/_lib/neighbour_census.ts +760 -0
- package/src/scripts/_lib/neighbour_scan.ts +122 -0
- package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
- package/src/scripts/_lib/prompt_shape.ts +46 -0
- package/src/scripts/_lib/release_migration_gate.ts +19 -3
- package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
- package/src/scripts/_lib/review_coverage.ts +134 -0
- package/src/scripts/_lib/rule_consequence_class.ts +71 -0
- package/src/scripts/_lib/rule_injection.ts +130 -10
- package/src/scripts/_lib/rule_law_section.ts +147 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
- package/src/scripts/_lib/skill_origin.ts +254 -0
- package/src/scripts/_lib/structured_ask.ts +64 -21
- package/src/scripts/_lib/thin_rules.ts +540 -0
- package/src/scripts/_lib/touched_file_quality.ts +434 -0
- package/src/scripts/_lib/turn_end_refusals.ts +77 -2
- package/src/scripts/_lib/windsurf_render.ts +63 -0
- package/src/scripts/ai_council/cli_call_budget.ts +13 -5
- package/src/scripts/ai_council/clients.ts +26 -57
- package/src/scripts/ai_council/config.ts +1 -1
- package/src/scripts/ai_council/vendor_defaults.ts +45 -0
- package/src/scripts/audit_skill_overlap.ts +261 -0
- package/src/scripts/before_complete_hook.ts +243 -12
- package/src/scripts/bench_touched_file_quality.ts +238 -0
- package/src/scripts/check_enforcement_matrix.ts +377 -20
- package/src/scripts/check_estate_count.ts +130 -153
- package/src/scripts/check_finding_dispositions.ts +116 -4
- package/src/scripts/check_hook_bundle_composition.ts +370 -0
- package/src/scripts/check_kernel_edit_ratified.ts +49 -11
- package/src/scripts/check_memory.ts +7 -1
- package/src/scripts/check_release_highlights.ts +10 -0
- package/src/scripts/check_release_pr_shape.ts +38 -15
- package/src/scripts/check_standing_rule_delivery.ts +33 -13
- package/src/scripts/check_verify_expectation_delta.ts +521 -0
- package/src/scripts/code_graph/detect.ts +127 -7
- package/src/scripts/code_graph/foreign.ts +194 -0
- package/src/scripts/code_graph/query.ts +217 -4
- package/src/scripts/code_graph/types.ts +26 -2
- package/src/scripts/code_graph/validate.ts +3 -1
- package/src/scripts/code_graph/verbs.ts +103 -0
- package/src/scripts/compile_hook_manifest.ts +33 -1
- package/src/scripts/condense.ts +14 -27
- package/src/scripts/estate_exemption_shape.ts +202 -0
- package/src/scripts/generate_host_cost_table.ts +1 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +150 -9
- package/src/scripts/hooks/block_config_weakening.ts +67 -2
- package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
- package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
- package/src/scripts/hooks/concern_failure_policy.ts +280 -0
- package/src/scripts/hooks/dispatch_hook.ts +52 -52
- package/src/scripts/hooks/exit_codes.ts +2 -1
- package/src/scripts/hooks/host_lowering.json +1 -1
- package/src/scripts/hooks/host_lowering.ts +19 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
- package/src/scripts/hooks/payload_stub.ts +10 -1
- package/src/scripts/hooks/rule_inject_hook.ts +757 -63
- package/src/scripts/hooks/table_fingerprint.ts +21 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
- package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
- package/src/scripts/hooks_status.ts +112 -0
- package/src/scripts/injection_scan_hook.ts +7 -0
- package/src/scripts/install.ts +72 -185
- package/src/scripts/installed_layer_report.ts +103 -0
- package/src/scripts/language_mirror_hook.ts +15 -6
- package/src/scripts/learning_sidecar.ts +119 -8
- package/src/scripts/lint_hook_manifest.ts +53 -0
- package/src/scripts/lint_major_migration_sections.ts +156 -26
- package/src/scripts/lint_pack_boundaries.ts +63 -5
- package/src/scripts/lint_rule_law_section.ts +517 -0
- package/src/scripts/lint_workflow_security.ts +161 -5
- package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
- package/src/scripts/mcp_server/graph_tools.ts +72 -3
- package/src/scripts/measure_routing_signal.ts +54 -16
- package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
- package/src/scripts/memory_learn_hook.ts +91 -6
- package/src/scripts/model_rule_injection.ts +52 -5
- package/src/scripts/project_thin_rules.ts +34 -327
- package/src/scripts/report_evidence_temperature.ts +657 -0
- package/src/scripts/report_installed_rule_links.ts +157 -0
- package/src/scripts/report_label_agreement.ts +233 -0
- package/src/scripts/report_obligation_mechanism.ts +230 -0
- package/src/scripts/report_single_token_triggers.ts +277 -0
- package/src/scripts/report_skill_ranker_confusion.ts +442 -0
- package/src/scripts/report_standing_payload_by_host.ts +188 -15
- package/src/scripts/report_verification_record_coverage.ts +306 -0
- package/src/scripts/rule_activation_census.ts +5 -2
- package/src/scripts/rule_trigger_eval.ts +9 -9
- package/src/scripts/schemas/review-findings.schema.json +3 -2
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/self_review_gate.ts +62 -7
- package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
- package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
- package/src/scripts/sync_gitignore.ts +29 -235
- package/src/scripts/trigger_eval_grandfather.json +0 -5
- package/src/server/schemas/settings.ts +10 -0
- package/src/shared/skillRanking.ts +117 -11
- package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
- package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* touched_file_quality — run the project's OWN quality commands, in shadow,
|
|
3
|
+
* over the files a turn actually edited.
|
|
4
|
+
*
|
|
5
|
+
* loss_class: ephemeral-lossy
|
|
6
|
+
*
|
|
7
|
+
* The one place this module shortens content is `advisoryLine`, whose 200-byte
|
|
8
|
+
* cap (roadmap 2.2) trims a summary it synthesised itself from `QualityRun[]`.
|
|
9
|
+
* The trimmed tail is not stored anywhere and is not meant to be: the runs are
|
|
10
|
+
* in-memory for one invocation, the line goes to stderr, and the verdict it
|
|
11
|
+
* summarises is reproduced by re-running the same resolver command. A recovery
|
|
12
|
+
* locator would promise a retrieval nothing performs.
|
|
13
|
+
*
|
|
14
|
+
* `road-to-touched-files-that-pass-their-own-tools` Phase 1. The project already
|
|
15
|
+
* computes a per-stack quality list — `resolve_toolchain().quality` in
|
|
16
|
+
* `src/agent-src/templates/scripts/work_engine/stack/runner.ts` — and nothing at
|
|
17
|
+
* stop calls it. This module is the caller. It owns no list of its own: D2 of the
|
|
18
|
+
* roadmap pins the resolver as the single authority, because a second list is a
|
|
19
|
+
* second authority that drifts from the first in silence.
|
|
20
|
+
*
|
|
21
|
+
* WHAT THIS IS NOT. It is not verification. A command recorded here never reaches
|
|
22
|
+
* `_lib/verification_command.ts`, never raises `verifications_this_turn`, and
|
|
23
|
+
* never sets `verified_this_turn` — `eslint` exits 0 over a clean file set, and a
|
|
24
|
+
* zero exit that reached the completion gate would mean a turn could claim "I
|
|
25
|
+
* verified" by having edited nothing interesting. The producer writes
|
|
26
|
+
* `quality_runs[]`; the classifier reads `verification_runs[]`. Two arrays, two
|
|
27
|
+
* readers, no overlap — pinned by a negative fixture rather than by this comment.
|
|
28
|
+
*
|
|
29
|
+
* FOUR RULES, AND EACH ONE EXISTS BECAUSE THE NAIVE VERSION IS HARMFUL.
|
|
30
|
+
*
|
|
31
|
+
* 1. A command runs only if the resolver listed it. Nothing is synthesised.
|
|
32
|
+
* 2. A command that WRITES runs only in its check form, or not at all. The
|
|
33
|
+
* resolver emits `vendor/bin/pint`, which rewrites the consumer's source; a
|
|
34
|
+
* shadow pass that reformats the tree it is observing is not a shadow pass.
|
|
35
|
+
* Where a check flag is known (`--test`) it is used; where it is not, the row
|
|
36
|
+
* is `skipped: mutating` and the command is never spawned.
|
|
37
|
+
* 3. A command with no file-scoped form is `skipped: unscoped`. `tsc --noEmit`
|
|
38
|
+
* cannot take a file list without discarding `tsconfig.json`, and running it
|
|
39
|
+
* project-wide at every stop makes stop the slowest hook in the chain. The
|
|
40
|
+
* resolver carries no cost field, so nothing here guesses that an unknown
|
|
41
|
+
* command is cheap: an unrecognised command is `unscoped` and is not run.
|
|
42
|
+
* 4. ENOENT or exit 127 is `skipped: absent`, counted apart from verdicts.
|
|
43
|
+
* Detection upstream is by manifest (`typescript` in `devDependencies`), not
|
|
44
|
+
* by presence on PATH, so "the tool is not installed" must never read as a red.
|
|
45
|
+
*
|
|
46
|
+
* Output per command: the command, its exit code, and the first 20 lines of its
|
|
47
|
+
* combined output. Bounded on purpose — this lands in a per-session state file
|
|
48
|
+
* that another hook reads on every turn.
|
|
49
|
+
*/
|
|
50
|
+
import { spawnSync } from 'node:child_process';
|
|
51
|
+
import * as fs from 'node:fs';
|
|
52
|
+
import * as path from 'node:path';
|
|
53
|
+
|
|
54
|
+
/** Lines of combined output retained per command. */
|
|
55
|
+
export const OUTPUT_HEAD_LINES = 20;
|
|
56
|
+
|
|
57
|
+
/** Default per-command wall-clock bound, milliseconds. */
|
|
58
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Why a command was not executed.
|
|
62
|
+
*
|
|
63
|
+
* `no_files` is the fourth reason and is not in the roadmap's prose list, which
|
|
64
|
+
* named three. It is needed and it is honest: running `eslint` over a turn that
|
|
65
|
+
* touched only `.php` files produces a verdict about nothing. 2.1 counts
|
|
66
|
+
* `skipped:*` separately from verdicts, and the glob is what makes a fourth
|
|
67
|
+
* reason additive rather than a contract change.
|
|
68
|
+
*/
|
|
69
|
+
export type SkipReason = 'mutating' | 'unscoped' | 'absent' | 'no_files';
|
|
70
|
+
|
|
71
|
+
/** One resolver command, classified into a runnable form or a skip reason. */
|
|
72
|
+
export interface QualityPlan {
|
|
73
|
+
/** The resolver's command string, verbatim — the provenance of this row. */
|
|
74
|
+
readonly source_command: string;
|
|
75
|
+
/** argv actually spawned, or `null` when the row is skipped. */
|
|
76
|
+
readonly argv: readonly string[] | null;
|
|
77
|
+
/** The touched files passed to it. */
|
|
78
|
+
readonly files: readonly string[];
|
|
79
|
+
readonly skipped: SkipReason | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One executed (or deliberately skipped) quality command. */
|
|
83
|
+
export interface QualityRun {
|
|
84
|
+
/** The command as run, or the resolver's command when skipped. */
|
|
85
|
+
readonly command: string;
|
|
86
|
+
readonly source_command: string;
|
|
87
|
+
/** `null` when the row was skipped — never `0`, which is a real verdict. */
|
|
88
|
+
readonly exit_code: number | null;
|
|
89
|
+
readonly output_head: readonly string[];
|
|
90
|
+
readonly skipped: SkipReason | null;
|
|
91
|
+
readonly files: readonly string[];
|
|
92
|
+
readonly at: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* How one resolver command is turned into a file-scoped, non-writing form.
|
|
97
|
+
*
|
|
98
|
+
* Keyed on the exact string the resolver emits. Keying on the whole command
|
|
99
|
+
* rather than on a parsed head is deliberate: the key IS the resolver's output,
|
|
100
|
+
* so a resolver change that this table has not seen falls through to `unscoped`
|
|
101
|
+
* and is not run, instead of being half-recognised and run in a form nobody
|
|
102
|
+
* chose.
|
|
103
|
+
*/
|
|
104
|
+
interface ToolSpec {
|
|
105
|
+
/** argv prefix; the touched files are appended. */
|
|
106
|
+
readonly argv: readonly string[];
|
|
107
|
+
/** Extensions this tool reads. Empty means "any file". */
|
|
108
|
+
readonly extensions: readonly string[];
|
|
109
|
+
/**
|
|
110
|
+
* The resolver's own command writes files, and `argv` above is its check
|
|
111
|
+
* form. Recorded so a reader can see that the executed command differs from
|
|
112
|
+
* the resolver's by exactly the flag that makes it read-only.
|
|
113
|
+
*/
|
|
114
|
+
readonly check_form: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const JS_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'] as const;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The scoped forms, one row per resolver command that HAS one.
|
|
121
|
+
*
|
|
122
|
+
* Everything the resolver can emit that is absent here — `npx tsc --noEmit`,
|
|
123
|
+
* `go vet ./...`, `cargo clippy` — is absent because it has no per-file form,
|
|
124
|
+
* and absence is what produces `skipped: unscoped`.
|
|
125
|
+
*/
|
|
126
|
+
const SCOPED_FORMS: ReadonlyMap<string, ToolSpec> = new Map<string, ToolSpec>([
|
|
127
|
+
['npx eslint .', { argv: ['npx', 'eslint'], extensions: JS_EXTENSIONS, check_form: false }],
|
|
128
|
+
[
|
|
129
|
+
'vendor/bin/phpstan analyse',
|
|
130
|
+
{ argv: ['vendor/bin/phpstan', 'analyse'], extensions: ['.php'], check_form: false },
|
|
131
|
+
],
|
|
132
|
+
// `vendor/bin/pint` REWRITES source. `--test` is its check form: same tool,
|
|
133
|
+
// same rules, reports instead of writing. Risk 3 of the roadmap is that this
|
|
134
|
+
// module reformats the consumer's tree; this row plus the fixture that proves
|
|
135
|
+
// no file changed is the mitigation.
|
|
136
|
+
[
|
|
137
|
+
'vendor/bin/pint',
|
|
138
|
+
{ argv: ['vendor/bin/pint', '--test'], extensions: ['.php'], check_form: true },
|
|
139
|
+
],
|
|
140
|
+
['ruff check', { argv: ['ruff', 'check'], extensions: ['.py'], check_form: false }],
|
|
141
|
+
['mypy .', { argv: ['mypy'], extensions: ['.py'], check_form: false }],
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolver commands known to write files for which NO check form is known.
|
|
146
|
+
*
|
|
147
|
+
* Empty today — every mutating command the resolver emits (`vendor/bin/pint`)
|
|
148
|
+
* has one, and it is in `SCOPED_FORMS` above. The map exists so that the day a
|
|
149
|
+
* writing command without a `--check` lands, the answer is a row here
|
|
150
|
+
* (`skipped: mutating`, never spawned) rather than a judgement call at the call
|
|
151
|
+
* site. A mutating command that fell through to `unscoped` would be recorded
|
|
152
|
+
* under the wrong reason and counted in the wrong bucket by 2.1.
|
|
153
|
+
*/
|
|
154
|
+
const MUTATING_WITHOUT_CHECK_FORM: ReadonlySet<string> = new Set<string>([]);
|
|
155
|
+
|
|
156
|
+
function extensionOf(file: string): string {
|
|
157
|
+
const base = path.basename(file);
|
|
158
|
+
const dot = base.lastIndexOf('.');
|
|
159
|
+
return dot <= 0 ? '' : base.slice(dot).toLowerCase();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Classify the resolver's list against the turn's touched files. PURE.
|
|
164
|
+
*
|
|
165
|
+
* Separated from execution so the four rules above are testable without
|
|
166
|
+
* spawning anything, and so a reader can see the whole policy in one return
|
|
167
|
+
* value before any process starts.
|
|
168
|
+
*/
|
|
169
|
+
export function planQualityRuns(
|
|
170
|
+
commands: readonly string[],
|
|
171
|
+
files: readonly string[],
|
|
172
|
+
): QualityPlan[] {
|
|
173
|
+
return commands.map((source_command): QualityPlan => {
|
|
174
|
+
if (MUTATING_WITHOUT_CHECK_FORM.has(source_command)) {
|
|
175
|
+
return { source_command, argv: null, files: [], skipped: 'mutating' };
|
|
176
|
+
}
|
|
177
|
+
const spec = SCOPED_FORMS.get(source_command);
|
|
178
|
+
if (spec === undefined) {
|
|
179
|
+
return { source_command, argv: null, files: [], skipped: 'unscoped' };
|
|
180
|
+
}
|
|
181
|
+
const matched =
|
|
182
|
+
spec.extensions.length === 0
|
|
183
|
+
? [...files]
|
|
184
|
+
: files.filter((f) => spec.extensions.includes(extensionOf(f)));
|
|
185
|
+
if (matched.length === 0) {
|
|
186
|
+
return { source_command, argv: null, files: [], skipped: 'no_files' };
|
|
187
|
+
}
|
|
188
|
+
return { source_command, argv: [...spec.argv, ...matched], files: matched, skipped: null };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `argv` rendered for the record. Not re-parsed — display and audit only. */
|
|
193
|
+
export function renderCommand(argv: readonly string[]): string {
|
|
194
|
+
return argv.join(' ');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function headLines(text: string, limit: number): string[] {
|
|
198
|
+
if (text === '') return [];
|
|
199
|
+
return text
|
|
200
|
+
.split(/\r\n|\r|\n/)
|
|
201
|
+
.filter((line, i, all) => !(line === '' && i === all.length - 1))
|
|
202
|
+
.slice(0, limit);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** ISO-8601 to seconds, matching the record's other timestamps. */
|
|
206
|
+
function now(): string {
|
|
207
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface RunOptions {
|
|
211
|
+
readonly root: string;
|
|
212
|
+
readonly commands: readonly string[];
|
|
213
|
+
readonly files: readonly string[];
|
|
214
|
+
readonly timeout_ms?: number;
|
|
215
|
+
/** Injected in tests so the table's behaviour is observable without a toolchain. */
|
|
216
|
+
readonly spawn?: (
|
|
217
|
+
argv: readonly string[],
|
|
218
|
+
cwd: string,
|
|
219
|
+
timeout_ms: number,
|
|
220
|
+
) => { status: number | null; output: string; enoent: boolean };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function defaultSpawn(
|
|
224
|
+
argv: readonly string[],
|
|
225
|
+
cwd: string,
|
|
226
|
+
timeout_ms: number,
|
|
227
|
+
): { status: number | null; output: string; enoent: boolean } {
|
|
228
|
+
const [head, ...rest] = argv;
|
|
229
|
+
if (head === undefined) return { status: null, output: '', enoent: true };
|
|
230
|
+
const res = spawnSync(head, rest, {
|
|
231
|
+
cwd,
|
|
232
|
+
timeout: timeout_ms,
|
|
233
|
+
encoding: 'utf-8',
|
|
234
|
+
// No shell. The argv comes from a committed table, and a shell would
|
|
235
|
+
// make a file path with a space into an injection surface.
|
|
236
|
+
shell: false,
|
|
237
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
238
|
+
});
|
|
239
|
+
const enoent =
|
|
240
|
+
res.error !== undefined &&
|
|
241
|
+
(res.error as NodeJS.ErrnoException).code === 'ENOENT';
|
|
242
|
+
const output = `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
243
|
+
return { status: res.status, output, enoent };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Execute the plan and return one record per resolver command.
|
|
248
|
+
*
|
|
249
|
+
* Never throws. A spawn that fails for any reason becomes a row, because a
|
|
250
|
+
* shadow pass that can take the stop hook down with it is worse than no shadow
|
|
251
|
+
* pass at all.
|
|
252
|
+
*/
|
|
253
|
+
export function runTouchedFileQuality(opts: RunOptions): QualityRun[] {
|
|
254
|
+
const timeout_ms = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
255
|
+
const spawn = opts.spawn ?? defaultSpawn;
|
|
256
|
+
const out: QualityRun[] = [];
|
|
257
|
+
for (const plan of planQualityRuns(opts.commands, opts.files)) {
|
|
258
|
+
if (plan.argv === null) {
|
|
259
|
+
out.push({
|
|
260
|
+
command: plan.source_command,
|
|
261
|
+
source_command: plan.source_command,
|
|
262
|
+
exit_code: null,
|
|
263
|
+
output_head: [],
|
|
264
|
+
skipped: plan.skipped,
|
|
265
|
+
files: [],
|
|
266
|
+
at: now(),
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
let status: number | null = null;
|
|
271
|
+
let output = '';
|
|
272
|
+
let enoent = false;
|
|
273
|
+
try {
|
|
274
|
+
const res = spawn(plan.argv, opts.root, timeout_ms);
|
|
275
|
+
status = res.status;
|
|
276
|
+
output = res.output;
|
|
277
|
+
enoent = res.enoent;
|
|
278
|
+
} catch {
|
|
279
|
+
enoent = true;
|
|
280
|
+
}
|
|
281
|
+
// Exit 127 is the shell's "command not found" and reaches us when the
|
|
282
|
+
// tool is a wrapper script whose own dependency is missing. Same meaning
|
|
283
|
+
// as ENOENT for this module's purposes, and the same bucket in 2.1.
|
|
284
|
+
const absent = enoent || status === 127;
|
|
285
|
+
out.push({
|
|
286
|
+
command: renderCommand(plan.argv),
|
|
287
|
+
source_command: plan.source_command,
|
|
288
|
+
exit_code: absent ? null : status,
|
|
289
|
+
output_head: absent ? [] : headLines(output, OUTPUT_HEAD_LINES),
|
|
290
|
+
skipped: absent ? 'absent' : null,
|
|
291
|
+
files: plan.files,
|
|
292
|
+
at: now(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The single advisory line `warn` mode emits — command, first file, verdict.
|
|
300
|
+
*
|
|
301
|
+
* `null` when nothing failed. Capped at 200 bytes by the roadmap's 2.2, and the
|
|
302
|
+
* cap is enforced on the UTF-8 byte length rather than on `String.length`, which
|
|
303
|
+
* counts code units and would let a path with non-ASCII characters through at
|
|
304
|
+
* nearly twice the budget.
|
|
305
|
+
*/
|
|
306
|
+
export const WARN_LINE_MAX_BYTES = 200;
|
|
307
|
+
|
|
308
|
+
export function advisoryLine(runs: readonly QualityRun[]): string | null {
|
|
309
|
+
const failed = runs.find((r) => r.skipped === null && r.exit_code !== null && r.exit_code !== 0);
|
|
310
|
+
if (failed === undefined) return null;
|
|
311
|
+
const file = failed.files[0] ?? '(no file)';
|
|
312
|
+
const more = failed.files.length > 1 ? ` (+${String(failed.files.length - 1)} more)` : '';
|
|
313
|
+
const line = `touched-file quality: ${failed.source_command} exited ${String(failed.exit_code)} on ${file}${more}`;
|
|
314
|
+
return truncateToBytes(line, WARN_LINE_MAX_BYTES);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Cut `text` so its UTF-8 encoding is at most `max` bytes, never mid-character. */
|
|
318
|
+
export function truncateToBytes(text: string, max: number): string {
|
|
319
|
+
const buf = Buffer.from(text, 'utf-8');
|
|
320
|
+
if (buf.length <= max) return text;
|
|
321
|
+
let end = max;
|
|
322
|
+
// Back off into the start of a character: continuation bytes are 10xxxxxx.
|
|
323
|
+
while (end > 0 && (buf[end] ?? 0) >= 0x80 && (buf[end] ?? 0) < 0xc0) end -= 1;
|
|
324
|
+
return buf.subarray(0, end).toString('utf-8');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Which files the turn touched
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/** Where the touched-path set came from, recorded beside the runs. */
|
|
332
|
+
export type FileSource = 'minimal-safe-diff∩worktree' | 'none';
|
|
333
|
+
|
|
334
|
+
export interface TouchedFiles {
|
|
335
|
+
readonly files: readonly string[];
|
|
336
|
+
readonly source: FileSource;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The turn's touched files, intersected with what the working tree says changed.
|
|
341
|
+
*
|
|
342
|
+
* RISK 4 OF THE ROADMAP, AND THE REASON THIS IS NOT JUST A READ.
|
|
343
|
+
* `agents/state/minimal-safe-diff.json` is ONE FILE PER PROJECT ROOT, not per
|
|
344
|
+
* session — and under this repository's worktree workflow `CLAUDE_PROJECT_DIR`
|
|
345
|
+
* resolves to the parent checkout, so concurrent runs share it. Reading it
|
|
346
|
+
* straight would let a neighbouring session's edits become this session's
|
|
347
|
+
* quality target.
|
|
348
|
+
*
|
|
349
|
+
* Two independent narrowings, and both are needed:
|
|
350
|
+
*
|
|
351
|
+
* - OWNERSHIP. A state file whose `session_id` is not this session's is
|
|
352
|
+
* refused outright, which is the same stance `owns_session_state` takes on
|
|
353
|
+
* the verification record.
|
|
354
|
+
* - THE WORKING TREE. A path is kept only if git still reports it as changed.
|
|
355
|
+
* A path the recorder saw but the tree has since reverted is not a quality
|
|
356
|
+
* target, and an edit from a different checkout is not in THIS tree's status.
|
|
357
|
+
*
|
|
358
|
+
* An absent, foreign or unreadable recorder state yields `none` and NO files,
|
|
359
|
+
* which makes the shadow pass a no-op. The alternative — falling back to the
|
|
360
|
+
* whole working tree — is exactly the cross-session leak this is guarding, so
|
|
361
|
+
* the degradation is toward measuring nothing rather than toward measuring
|
|
362
|
+
* somebody else's work.
|
|
363
|
+
*/
|
|
364
|
+
export function touchedFilesAtStop(
|
|
365
|
+
root: string,
|
|
366
|
+
session_id: string,
|
|
367
|
+
gitStatus: (root: string) => readonly string[] = defaultGitStatus,
|
|
368
|
+
): TouchedFiles {
|
|
369
|
+
let recorded: string[];
|
|
370
|
+
try {
|
|
371
|
+
const raw = fs.readFileSync(
|
|
372
|
+
path.join(root, 'agents', 'state', 'minimal-safe-diff.json'),
|
|
373
|
+
'utf-8',
|
|
374
|
+
);
|
|
375
|
+
const decoded: unknown = JSON.parse(raw);
|
|
376
|
+
if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) {
|
|
377
|
+
return { files: [], source: 'none' };
|
|
378
|
+
}
|
|
379
|
+
const state = decoded as Record<string, unknown>;
|
|
380
|
+
if (state['session_id'] !== session_id || session_id === '') {
|
|
381
|
+
return { files: [], source: 'none' };
|
|
382
|
+
}
|
|
383
|
+
const touched = state['files_touched_this_turn'];
|
|
384
|
+
if (!Array.isArray(touched)) return { files: [], source: 'none' };
|
|
385
|
+
recorded = touched.filter((x): x is string => typeof x === 'string');
|
|
386
|
+
} catch {
|
|
387
|
+
return { files: [], source: 'none' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const changed = new Set(gitStatus(root));
|
|
391
|
+
const files = recorded.filter((f) => changed.has(normalizePath(f)));
|
|
392
|
+
return { files, source: 'minimal-safe-diff∩worktree' };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Repo-relative, forward slashes, no leading `./` — the shape git prints. */
|
|
396
|
+
export function normalizePath(p: string): string {
|
|
397
|
+
return p.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function defaultGitStatus(root: string): string[] {
|
|
401
|
+
const res = spawnSync('git', ['status', '--porcelain', '-z'], {
|
|
402
|
+
cwd: root,
|
|
403
|
+
encoding: 'utf-8',
|
|
404
|
+
timeout: 10_000,
|
|
405
|
+
shell: false,
|
|
406
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
407
|
+
});
|
|
408
|
+
if (res.status !== 0 || typeof res.stdout !== 'string') return [];
|
|
409
|
+
return parsePorcelainZ(res.stdout);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Paths out of `git status --porcelain -z`.
|
|
414
|
+
*
|
|
415
|
+
* `-z` rather than the line-oriented form: without it git QUOTES a path holding
|
|
416
|
+
* a space or a non-ASCII byte, and the quoted form never matches a recorded
|
|
417
|
+
* path, so the intersection would silently drop exactly the files most likely
|
|
418
|
+
* to be mis-handled downstream. A rename entry carries two NUL-separated paths
|
|
419
|
+
* (`R <new>\0<old>`); the new path is the one that exists to be checked.
|
|
420
|
+
*/
|
|
421
|
+
export function parsePorcelainZ(stdout: string): string[] {
|
|
422
|
+
const out: string[] = [];
|
|
423
|
+
const parts = stdout.split('\0');
|
|
424
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
425
|
+
const entry = parts[i];
|
|
426
|
+
if (entry === undefined || entry.length < 4) continue;
|
|
427
|
+
const status = entry.slice(0, 2);
|
|
428
|
+
out.push(normalizePath(entry.slice(3)));
|
|
429
|
+
// Rename / copy: the NEXT NUL-separated field is the ORIGIN path, which
|
|
430
|
+
// is consumed here so it is not read as a status entry of its own.
|
|
431
|
+
if (status.startsWith('R') || status.startsWith('C')) i += 1;
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
@@ -366,12 +366,44 @@ export function readInstallBoundary(): InstallBoundary {
|
|
|
366
366
|
* would let another concern's retries inflate Q1's numerator against a
|
|
367
367
|
* denominator that only counts this gate's refusals.
|
|
368
368
|
*/
|
|
369
|
+
/**
|
|
370
|
+
* Who set the `stop_hook_active` flag this row was recorded under.
|
|
371
|
+
*
|
|
372
|
+
* `layer` already says WHICH allow path produced the row, and the doc above
|
|
373
|
+
* says the host sets `stop_hook_active` on ANY stop hook's block — including a
|
|
374
|
+
* neighbour's. What it could not say is which: a `stop_hook_active` row and a
|
|
375
|
+
* `refused_turn` row were the only two things the record distinguished, so a
|
|
376
|
+
* retry caused by a foreign concern and a retry caused by this gate were
|
|
377
|
+
* indistinguishable once the layer was `stop_hook_active`.
|
|
378
|
+
*
|
|
379
|
+
* - `ours` — this session's refusal record exists and parses, so this gate
|
|
380
|
+
* refused at some point before this stop.
|
|
381
|
+
* - `foreign` — the record is ABSENT. This gate never refused in this session,
|
|
382
|
+
* and the host set the flag anyway, so something else did.
|
|
383
|
+
* - `unknown` — the record exists and could not be read. The distinction from
|
|
384
|
+
* `foreign` is the whole point: "we did not refuse" and "we
|
|
385
|
+
* cannot tell whether we refused" are different findings, and
|
|
386
|
+
* collapsing them would let an unreadable state directory read
|
|
387
|
+
* as evidence of a neighbour.
|
|
388
|
+
*
|
|
389
|
+
* On the `refused_turn` layer the answer is `ours` by construction — that
|
|
390
|
+
* marker is this gate's own and no other writer sets it.
|
|
391
|
+
*/
|
|
392
|
+
export type ShadowSetBy = 'ours' | 'foreign' | 'unknown';
|
|
393
|
+
|
|
369
394
|
export interface WouldRefuseAgainRow {
|
|
370
395
|
detector: RefusalDetectorId;
|
|
371
396
|
/** The turn ordinal the shadow ran on — joins to `refused_turn`. */
|
|
372
397
|
turn: number;
|
|
373
398
|
at: string;
|
|
374
399
|
layer: ShadowLayer;
|
|
400
|
+
/**
|
|
401
|
+
* Absent on rows written before this field existed, which parse as
|
|
402
|
+
* `unknown` rather than being dropped — an old row is still a real
|
|
403
|
+
* observation of a retry, and discarding it would silently lower Q1's
|
|
404
|
+
* numerator.
|
|
405
|
+
*/
|
|
406
|
+
set_by: ShadowSetBy;
|
|
375
407
|
}
|
|
376
408
|
|
|
377
409
|
/**
|
|
@@ -514,7 +546,12 @@ export function parseShadowRecord(raw: string): ShadowRecord | null {
|
|
|
514
546
|
if (typeof turn !== 'number' || !Number.isFinite(turn)) continue;
|
|
515
547
|
if (typeof at !== 'string') continue;
|
|
516
548
|
if (layer !== 'stop_hook_active' && layer !== 'refused_turn') continue;
|
|
517
|
-
|
|
549
|
+
const setByRaw = ro['set_by'];
|
|
550
|
+
const set_by: ShadowSetBy =
|
|
551
|
+
setByRaw === 'ours' || setByRaw === 'foreign' || setByRaw === 'unknown'
|
|
552
|
+
? setByRaw
|
|
553
|
+
: 'unknown';
|
|
554
|
+
rows.push({ detector: det, turn, at, layer, set_by });
|
|
518
555
|
}
|
|
519
556
|
const num = (v: unknown): number =>
|
|
520
557
|
typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -548,11 +585,24 @@ export function foldShadow(
|
|
|
548
585
|
turnOrdinal: number;
|
|
549
586
|
at: string;
|
|
550
587
|
layer: ShadowLayer;
|
|
588
|
+
/**
|
|
589
|
+
* Omitted by a caller that cannot establish it, which is `unknown` —
|
|
590
|
+
* never `foreign`, because a missing answer is not evidence of a
|
|
591
|
+
* neighbour.
|
|
592
|
+
*/
|
|
593
|
+
setBy?: ShadowSetBy;
|
|
551
594
|
},
|
|
552
595
|
): ShadowRecord {
|
|
553
596
|
const rows = [...(prev?.would_refuse_again ?? [])];
|
|
597
|
+
const set_by: ShadowSetBy = input.setBy ?? 'unknown';
|
|
554
598
|
for (const d of input.detectors) {
|
|
555
|
-
rows.push({
|
|
599
|
+
rows.push({
|
|
600
|
+
detector: d,
|
|
601
|
+
turn: input.turnOrdinal,
|
|
602
|
+
at: input.at,
|
|
603
|
+
layer: input.layer,
|
|
604
|
+
set_by,
|
|
605
|
+
});
|
|
556
606
|
}
|
|
557
607
|
let dropped = prev?.dropped ?? 0;
|
|
558
608
|
if (rows.length > SHADOW_MAX_ROWS) {
|
|
@@ -571,6 +621,31 @@ export function foldShadow(
|
|
|
571
621
|
};
|
|
572
622
|
}
|
|
573
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Who set `stop_hook_active`, decided from this gate's OWN refusal record.
|
|
626
|
+
*
|
|
627
|
+
* Three states, and the third is not a hedge. `ENOENT` means this gate never
|
|
628
|
+
* refused in this session, so a flag the host set anyway came from somewhere
|
|
629
|
+
* else — `foreign`. Any OTHER read failure means the answer is unavailable, and
|
|
630
|
+
* reporting that as `foreign` would turn a broken state directory into evidence
|
|
631
|
+
* that a neighbour acted. The lane's own framing is the reason: what this suite
|
|
632
|
+
* can say about a neighbour is only what it observes, and an unreadable file is
|
|
633
|
+
* not an observation.
|
|
634
|
+
*
|
|
635
|
+
* `parseRecord` returning `null` on a present file is also `unknown`: the file
|
|
636
|
+
* exists, so something wrote it, and we cannot say what.
|
|
637
|
+
*/
|
|
638
|
+
export function shadowSetBy(workspaceRoot: string, sessionKey: string): ShadowSetBy {
|
|
639
|
+
let raw: string;
|
|
640
|
+
try {
|
|
641
|
+
raw = fs.readFileSync(sessionRefusalFile(workspaceRoot, sessionKey), 'utf-8');
|
|
642
|
+
} catch (err) {
|
|
643
|
+
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
644
|
+
return code === 'ENOENT' ? 'foreign' : 'unknown';
|
|
645
|
+
}
|
|
646
|
+
return parseRecord(raw) === null ? 'unknown' : 'ours';
|
|
647
|
+
}
|
|
648
|
+
|
|
574
649
|
/** This session's shadow record, or `null` when it has never retried. */
|
|
575
650
|
export function readShadowRecord(workspaceRoot: string, sessionKey: string): ShadowRecord | null {
|
|
576
651
|
try {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The windsurf single-file surface, rendered — one renderer, two callers.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-a-hook-bundle-with-one-yaml-reader` 3.1. `condense.generate_windsurfrules`
|
|
5
|
+
* WRITES `.windsurfrules`; the standing-payload census needs the same bytes to
|
|
6
|
+
* report windsurf's figure without reading that file, because it is an untracked
|
|
7
|
+
* generated projection and reading it published a number that tracked when the
|
|
8
|
+
* checkout last ran `task generate-tools` rather than anything about the tree.
|
|
9
|
+
*
|
|
10
|
+
* Both callers share THIS function. A second renderer living in the census would
|
|
11
|
+
* have measured a file nobody receives, which is Risk 3 of that roadmap.
|
|
12
|
+
*
|
|
13
|
+
* WHY A SEPARATE MODULE rather than an export from `condense.ts`: that file sits
|
|
14
|
+
* 1,228 lines over the 1,500-line cap `check_source_size_budget` ratchets, so
|
|
15
|
+
* every line added there is a new violation. Splitting the renderer out moves
|
|
16
|
+
* `condense.ts` DOWN. The file is small on purpose and carries only what the
|
|
17
|
+
* render needs.
|
|
18
|
+
*
|
|
19
|
+
* `strip_frontmatter` moved here with it — it is the render's one non-trivial
|
|
20
|
+
* dependency, it has exactly one caller left inside `condense.ts`, and leaving it
|
|
21
|
+
* behind would have meant either an import cycle or a second copy.
|
|
22
|
+
* `condense.ts` re-exports it, so `src/install/emit_host_rules_cli.ts` and every
|
|
23
|
+
* other importer keeps its path.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
|
|
28
|
+
/** Python `str.lstrip("\n")` — strip only leading newlines. Mirrors `condense._lstripNewlines`. */
|
|
29
|
+
function lstripNewlines(s: string): string {
|
|
30
|
+
return s.replace(/^\n+/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Python `str.strip()` with no args — strip leading/trailing Unicode whitespace. Mirrors `condense._strip`. */
|
|
34
|
+
function strip(s: string): string {
|
|
35
|
+
return s.replace(/^\s+/u, '').replace(/\s+$/u, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Drop a leading `---` frontmatter block, if the content opens with one. */
|
|
39
|
+
export function strip_frontmatter(content: string): string {
|
|
40
|
+
if (content.startsWith('---')) {
|
|
41
|
+
const end = content.indexOf('---', 3);
|
|
42
|
+
if (end !== -1) {
|
|
43
|
+
content = lstripNewlines(content.slice(end + 3));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return content;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The exact bytes `generate_windsurfrules` writes, for a given rule set.
|
|
51
|
+
*
|
|
52
|
+
* `rulesSource` is a parameter rather than `condense.MODULE_STATE.RULES_SOURCE`
|
|
53
|
+
* so a caller can render a tree it names — the census renders the projection
|
|
54
|
+
* source directly, with no module state to set up.
|
|
55
|
+
*/
|
|
56
|
+
export function render_windsurfrules(rulesSource: string, rules: readonly string[]): string {
|
|
57
|
+
const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
|
|
58
|
+
for (const rule of rules) {
|
|
59
|
+
const content = strip_frontmatter(fs.readFileSync(path.join(rulesSource, rule), 'utf-8'));
|
|
60
|
+
parts.push(`---\n\n${strip(content)}\n`);
|
|
61
|
+
}
|
|
62
|
+
return parts.join('\n') + '\n';
|
|
63
|
+
}
|
|
@@ -29,15 +29,23 @@ import * as path from 'node:path';
|
|
|
29
29
|
export const CLI_CALLS_ATTRIBUTION_SUFFIX = '.attribution.json';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Who booked a call. Enumerated because the consumer set is closed: exactly
|
|
33
|
-
* files in the tree construct a `CliClient`.
|
|
32
|
+
* Who booked a call. Enumerated because the consumer set is closed: exactly
|
|
33
|
+
* three files in the tree construct a `CliClient`.
|
|
34
34
|
*
|
|
35
|
-
* `unknown` is a FINDING when it appears — it means a
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* `unknown` is a FINDING when it appears — it means a booking path exists that
|
|
36
|
+
* this enumeration does not know about. That is how the test suite was caught
|
|
37
|
+
* spending the operator's live budget.
|
|
38
38
|
*/
|
|
39
39
|
export const CLI_CONSUMER_COUNCIL = 'council';
|
|
40
40
|
export const CLI_CONSUMER_TEAM = 'team';
|
|
41
|
+
/**
|
|
42
|
+
* The dogfooded self-review gate. Third because `self_review_gate.ts` is
|
|
43
|
+
* CLI-first since this constant was added: the subscription transport is tried
|
|
44
|
+
* before the API one, so a local or self-hosted run books here rather than
|
|
45
|
+
* spending API credit. A hosted CI runner has no vendor CLI on PATH and falls
|
|
46
|
+
* through to the API transport, which books nothing against this counter.
|
|
47
|
+
*/
|
|
48
|
+
export const CLI_CONSUMER_SELF_REVIEW = 'self-review';
|
|
41
49
|
export const CLI_CONSUMER_UNKNOWN = 'unknown';
|
|
42
50
|
|
|
43
51
|
/**
|