@event4u/agent-config 15.0.0 → 16.1.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 +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- 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 +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -69,10 +69,12 @@ import { main as delegationNudgeMain } from './delegation_nudge_hook.js';
|
|
|
69
69
|
import { main as skillRouteMain } from './skill_route_hook.js';
|
|
70
70
|
import { main as ruleInjectMain } from './rule_inject_hook.js';
|
|
71
71
|
import { main as endReviewNudgeMain } from './end_review_nudge_hook.js';
|
|
72
|
+
import { main as reviewBaselineMain } from './review_baseline_hook.js';
|
|
72
73
|
import { main as turnEndGateMain } from './turn_end_gate_hook.js';
|
|
73
74
|
import { main as editShapeMain } from './edit_shape_hook.js';
|
|
74
75
|
import { main as commentDisciplineMain } from './comment_discipline_hook.js';
|
|
75
76
|
import { main as rereadGuardMain } from './reread_guard_hook.js';
|
|
77
|
+
import { main as memoryRecallMain } from './memory_recall_hook.js';
|
|
76
78
|
import { main as sessionEolMain } from './session_eol_hook.js';
|
|
77
79
|
import { main as subagentLedgerMain } from './subagent_ledger_hook.js';
|
|
78
80
|
import { main as toolResultBytesMain } from './tool_result_bytes_hook.js';
|
|
@@ -81,6 +83,7 @@ import { main as sourceFirstGateMain } from './source_first_gate_hook.js';
|
|
|
81
83
|
import { main as interruptionLedgerMain } from './interruption_ledger_hook.js';
|
|
82
84
|
import { main as runContinuationMain } from './run_continuation_hook.js';
|
|
83
85
|
import { main as journalRecordMain } from './journal_record_hook.js';
|
|
86
|
+
import { main as obligationSettleMain } from './obligation_settle_hook.js';
|
|
84
87
|
|
|
85
88
|
/** A concern `main` — argv-taking or not; both shapes exist. */
|
|
86
89
|
export type ConcernMain = (argv?: string[]) => number | undefined | void;
|
|
@@ -132,10 +135,13 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
132
135
|
'src/scripts/hooks/skill_route_hook.ts': skillRouteMain as ConcernMain,
|
|
133
136
|
'src/scripts/hooks/rule_inject_hook.ts': ruleInjectMain as ConcernMain,
|
|
134
137
|
'src/scripts/hooks/end_review_nudge_hook.ts': endReviewNudgeMain as ConcernMain,
|
|
138
|
+
'src/scripts/hooks/obligation_settle_hook.ts': obligationSettleMain as ConcernMain,
|
|
139
|
+
'src/scripts/hooks/review_baseline_hook.ts': reviewBaselineMain as ConcernMain,
|
|
135
140
|
'src/scripts/hooks/turn_end_gate_hook.ts': turnEndGateMain as ConcernMain,
|
|
136
141
|
'src/scripts/hooks/edit_shape_hook.ts': editShapeMain as ConcernMain,
|
|
137
142
|
'src/scripts/hooks/comment_discipline_hook.ts': commentDisciplineMain as ConcernMain,
|
|
138
143
|
'src/scripts/hooks/reread_guard_hook.ts': rereadGuardMain as ConcernMain,
|
|
144
|
+
'src/scripts/hooks/memory_recall_hook.ts': memoryRecallMain as ConcernMain,
|
|
139
145
|
'src/scripts/hooks/session_eol_hook.ts': sessionEolMain as ConcernMain,
|
|
140
146
|
'src/scripts/hooks/subagent_ledger_hook.ts': subagentLedgerMain as ConcernMain,
|
|
141
147
|
'src/scripts/hooks/tool_result_bytes_hook.ts': toolResultBytesMain as ConcernMain,
|
|
@@ -56,6 +56,7 @@ import fs from 'node:fs';
|
|
|
56
56
|
import path from 'node:path';
|
|
57
57
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
58
58
|
|
|
59
|
+
import { appendDischarge, stamp } from '../_lib/obligations.js';
|
|
59
60
|
import { isUiPath, isUiTreePath } from '../_lib/ui_surface.js';
|
|
60
61
|
import { loadDesignContext, scanFile } from '../lint_design_slop.js';
|
|
61
62
|
import { readHookStdin } from './hook_stdin.js';
|
|
@@ -65,6 +66,7 @@ const EXIT_WARN = 2;
|
|
|
65
66
|
const SETTINGS_FILE = '.agent-settings.yml';
|
|
66
67
|
const STATE_REL = path.join('agents', 'runtime', 'state', 'design-pass-hook.json');
|
|
67
68
|
const AUDIT_REL = path.join('agents', 'runtime', 'state', 'ui-audit.json');
|
|
69
|
+
const CONFORMANCE_REL = path.join('agents', 'runtime', 'state', 'ui-conformance.json');
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* The P0 set — objective floors only, and the list is closed on purpose.
|
|
@@ -128,6 +130,77 @@ export function enabled(root: string, key = 'design_pass'): boolean {
|
|
|
128
130
|
|
|
129
131
|
export const isUiSurface = (p: string): boolean => isUiPath(p) || isUiTreePath(p);
|
|
130
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The shadow read of `ui-conformance.json` — Phase 5 of
|
|
135
|
+
* `road-to-behaviour-evidence-over-pixels`.
|
|
136
|
+
*
|
|
137
|
+
* SHADOW MEANS SHADOW. This produces a line in the reported verdict and
|
|
138
|
+
* touches nothing else: it is not consulted by `decide`, never reaches
|
|
139
|
+
* `blocked`, and cannot change an exit code. The roadmap adds evidence, not
|
|
140
|
+
* enforcement, and the block path is deliberately untouched by this change.
|
|
141
|
+
*
|
|
142
|
+
* An ABSENT artefact is reported as absent, never as clean — the same rule the
|
|
143
|
+
* probe applies to its own dimensions. A malformed one is reported as
|
|
144
|
+
* unreadable for the same reason.
|
|
145
|
+
*/
|
|
146
|
+
export interface ConformanceVerdict {
|
|
147
|
+
/**
|
|
148
|
+
* Whether this verdict is worth surfacing on its own. An ABSENT artefact is
|
|
149
|
+
* not: most repositories never run the probe, and a line on every clean UI
|
|
150
|
+
* write is the noise that gets a carrier switched off — Risk 1 of the parent
|
|
151
|
+
* roadmap. Absent is still reported honestly whenever the pass speaks for
|
|
152
|
+
* another reason.
|
|
153
|
+
*/
|
|
154
|
+
noteworthy: boolean;
|
|
155
|
+
line: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function conformanceVerdict(root: string): ConformanceVerdict {
|
|
159
|
+
let raw: string;
|
|
160
|
+
try {
|
|
161
|
+
raw = fs.readFileSync(path.join(root, CONFORMANCE_REL), 'utf-8');
|
|
162
|
+
} catch {
|
|
163
|
+
return {
|
|
164
|
+
noteworthy: false,
|
|
165
|
+
line: 'ui-conformance: absent — no probe artefact for this change (run `ui_conformance_probe --target <file> --reference <file>`). Absent is not clean.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const a = JSON.parse(raw) as {
|
|
170
|
+
structure_gate?: string;
|
|
171
|
+
dimensions?: { dimension: string; status: string; findings: number | null; reason?: string }[];
|
|
172
|
+
findings?: unknown[];
|
|
173
|
+
};
|
|
174
|
+
const rows = a.dimensions ?? [];
|
|
175
|
+
const exercised = rows.filter((r) => r.status === 'exercised');
|
|
176
|
+
const notApplicable = rows.filter((r) => r.status !== 'exercised');
|
|
177
|
+
const counts = exercised.map((r) => `${r.dimension}=${r.findings}`).join(' ');
|
|
178
|
+
const parts = [
|
|
179
|
+
`ui-conformance: ${(a.findings ?? []).length} behavioural finding(s) — ${counts || 'no dimension exercised'}`,
|
|
180
|
+
];
|
|
181
|
+
if (notApplicable.length) {
|
|
182
|
+
parts.push(
|
|
183
|
+
` · not applicable: ${notApplicable
|
|
184
|
+
.map((r) => `${r.dimension} (${r.reason ?? 'no reason given'})`)
|
|
185
|
+
.join('; ')}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (a.structure_gate === 'stopped') {
|
|
189
|
+
parts.push(' · structure gate stopped on at least one node — style comparison was skipped for it');
|
|
190
|
+
}
|
|
191
|
+
parts.push(' (shadow verdict: reported, never enforced — this pass does not block on it)');
|
|
192
|
+
return {
|
|
193
|
+
noteworthy: (a.findings ?? []).length > 0 || a.structure_gate === 'stopped' || notApplicable.length > 0,
|
|
194
|
+
line: parts.join('\n'),
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return {
|
|
198
|
+
noteworthy: true,
|
|
199
|
+
line: 'ui-conformance: unreadable — the artefact exists but did not parse. Reported rather than ignored.',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
/**
|
|
132
205
|
* E2.2 — is the audit artefact newer than the target?
|
|
133
206
|
*
|
|
@@ -135,6 +208,43 @@ export const isUiSurface = (p: string): boolean => isUiPath(p) || isUiTreePath(p
|
|
|
135
208
|
* because it looks like evidence. An artefact older than the file it is
|
|
136
209
|
* supposed to describe has not seen the change.
|
|
137
210
|
*/
|
|
211
|
+
/**
|
|
212
|
+
* Record that the UI-audit obligation was discharged, when it was.
|
|
213
|
+
*
|
|
214
|
+
* This concern is the only thing in the tree that knows. It already decides
|
|
215
|
+
* audit freshness for its own advisory line; until now that decision was spent
|
|
216
|
+
* on one message and thrown away, so a turn-end reader asking "was the audit
|
|
217
|
+
* done?" had to re-derive a verdict it does not own. The decision existed —
|
|
218
|
+
* only the write was missing.
|
|
219
|
+
*
|
|
220
|
+
* Discharged means: at least one target was touched AND every one of them has
|
|
221
|
+
* a fresh audit. A partial pass records nothing, because `ui-audit-gate`'s
|
|
222
|
+
* obligation is about the surfaces this turn wrote, not about a quorum of them.
|
|
223
|
+
*
|
|
224
|
+
* NO PER-WRITE VALIDATOR IS ADDED. The write is one small append behind the
|
|
225
|
+
* same lock the ledger already uses, on a path that has an mtime comparison in
|
|
226
|
+
* front of it — deliberately, because this concern sits on `post_tool_use`,
|
|
227
|
+
* the hottest slot it binds, and a validation pass per write is exactly the
|
|
228
|
+
* cost the phase forbids.
|
|
229
|
+
*/
|
|
230
|
+
export function recordAuditDischarge(
|
|
231
|
+
root: string,
|
|
232
|
+
targets: readonly string[],
|
|
233
|
+
auditMissing: readonly string[],
|
|
234
|
+
): void {
|
|
235
|
+
if (targets.length === 0 || auditMissing.length > 0) return;
|
|
236
|
+
const session = (process.env["CLAUDE_CODE_SESSION_ID"] ?? "").trim();
|
|
237
|
+
if (session === "") return;
|
|
238
|
+
try {
|
|
239
|
+
appendDischarge(root, session, [
|
|
240
|
+
{ rule: "ui-audit-gate", by: "design-pass", at: stamp() },
|
|
241
|
+
]);
|
|
242
|
+
} catch {
|
|
243
|
+
// A discharge that cannot be recorded is a missing reading, never a
|
|
244
|
+
// reason to fail the write that prompted it.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
138
248
|
export function auditIsFresh(root: string, target: string): boolean {
|
|
139
249
|
try {
|
|
140
250
|
const a = fs.statSync(path.join(root, AUDIT_REL)).mtimeMs;
|
|
@@ -252,7 +362,7 @@ export function targetPath(payload: unknown): string | null {
|
|
|
252
362
|
return null;
|
|
253
363
|
}
|
|
254
364
|
|
|
255
|
-
export function render(result: PassResult): string {
|
|
365
|
+
export function render(result: PassResult, conformance?: string): string {
|
|
256
366
|
const lines: string[] = [];
|
|
257
367
|
if (result.blocked.length) {
|
|
258
368
|
lines.push(
|
|
@@ -272,6 +382,9 @@ export function render(result: PassResult): string {
|
|
|
272
382
|
lines.push(` · no ui-audit.json newer than ${m} — run \`agent-config ui:audit <path>\``);
|
|
273
383
|
lines.push(`verification: ${result.verification}`);
|
|
274
384
|
if (result.degradation_reason) lines.push(`degradation_reason: ${result.degradation_reason}`);
|
|
385
|
+
// Appended last and never consulted above: the conformance verdict is a
|
|
386
|
+
// report, not an input to any decision this pass makes.
|
|
387
|
+
if (conformance) lines.push(conformance);
|
|
275
388
|
return lines.join('\n');
|
|
276
389
|
}
|
|
277
390
|
|
|
@@ -329,6 +442,7 @@ function main(): number {
|
|
|
329
442
|
}
|
|
330
443
|
|
|
331
444
|
const auditMissing = targets.filter((t) => !auditIsFresh(root, t));
|
|
445
|
+
recordAuditDischarge(root, targets, auditMissing);
|
|
332
446
|
let result = decide(slot, findings, auditMissing, new Set(state.surfaced), renderAvailable(root));
|
|
333
447
|
if (scanFailed) {
|
|
334
448
|
result = { ...result, verification: 'unverified', degradation_reason: `detector unavailable: ${scanFailed}` };
|
|
@@ -338,11 +452,22 @@ function main(): number {
|
|
|
338
452
|
if (slot === 'stop') state.touched = [];
|
|
339
453
|
writeState(root, state);
|
|
340
454
|
|
|
341
|
-
|
|
455
|
+
// Shadow mount. The verdict can make the pass SPEAK when it would otherwise
|
|
456
|
+
// be silent — a behavioural finding is exactly the thing no static scan can
|
|
457
|
+
// see — but it cannot make it refuse. `result.blocked` above is computed
|
|
458
|
+
// without it and the exit code below is unchanged.
|
|
459
|
+
const conformance = conformanceVerdict(root);
|
|
460
|
+
|
|
461
|
+
if (
|
|
462
|
+
!result.findings.length &&
|
|
463
|
+
!result.audit_missing.length &&
|
|
464
|
+
result.verification === 'verified' &&
|
|
465
|
+
!conformance.noteworthy
|
|
466
|
+
) {
|
|
342
467
|
return EXIT_ALLOW;
|
|
343
468
|
}
|
|
344
469
|
|
|
345
|
-
process.stdout.write(`${JSON.stringify({ reason: render(result) })}\n`);
|
|
470
|
+
process.stdout.write(`${JSON.stringify({ reason: render(result, conformance.line) })}\n`);
|
|
346
471
|
// Deliberately EXIT_WARN even with a P0 verdict. EXIT_BLOCK is unreachable
|
|
347
472
|
// for an advisory concern (the dispatcher downgrades it), so returning it
|
|
348
473
|
// would claim a refusal the transport discards.
|
|
@@ -28,17 +28,26 @@
|
|
|
28
28
|
* (`git ls-files --others --exclude-standard`, each one READ from the
|
|
29
29
|
* filesystem rather than diffed in a subprocess — see "Untracked files" below;
|
|
30
30
|
* fixes a review finding, F6, that a brand-new file was previously invisible
|
|
31
|
-
* to this count entirely).
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
31
|
+
* to this count entirely).
|
|
32
|
+
*
|
|
33
|
+
* SINCE 2026-09-11 THIS READING IS BASELINE-CORRECTED ON `claude`, and this
|
|
34
|
+
* block used to say the opposite — that a session-start baseline was rejected
|
|
35
|
+
* "because no such baseline exists anywhere a `stop` concern can read it". That
|
|
36
|
+
* was true of a one-concern roadmap phase and stale by the time it mattered:
|
|
37
|
+
* `session_start` is bound on seven hosts, and `review-baseline` now records
|
|
38
|
+
* HEAD plus the already-dirty non-doc line count there. `_lib/review_baseline.ts`
|
|
39
|
+
* cites this paragraph as the defect it closes, so leaving the paragraph
|
|
40
|
+
* standing would have made the two files contradict each other.
|
|
41
|
+
*
|
|
42
|
+
* What the correction does and does not cover:
|
|
43
|
+
* - Uncommitted changes that PREDATE the session are SUBTRACTED where a
|
|
44
|
+
* baseline is readable and its HEAD and measure still match. Absent,
|
|
45
|
+
* unreadable, head-moved or mixed-measure → the unsubtracted count, with
|
|
46
|
+
* the reason carried in the `review_skipped` row.
|
|
47
|
+
* - The baseline is written once per session and never refreshed, so from a
|
|
48
|
+
* session's first commit onward `head-moved` holds and the pre-existing
|
|
49
|
+
* tree is charged again. Over-reporting is the safe direction; the limit is
|
|
50
|
+
* real and is stated in `review_baseline.ts` rather than implied away.
|
|
42
51
|
* - A session that DID commit mid-turn (one of `commit-policy`'s four
|
|
43
52
|
* exceptions) moves HEAD, so mutations already committed are invisible
|
|
44
53
|
* to this diff. Declared, not silently assumed away.
|
|
@@ -207,11 +216,20 @@
|
|
|
207
216
|
* exit-2 warn onto an unverified propagation path is exactly the
|
|
208
217
|
* speculative mapping `host_semantics.ts` exists to avoid.
|
|
209
218
|
*
|
|
210
|
-
* CONTRACT: never
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
219
|
+
* CONTRACT: never REFUSES a turn. The DISPATCHER-INTERNAL exit is 2 on a fire
|
|
220
|
+
* (never 1/BLOCK, never >=3), `fail_closed: false`; the HOST-FACING exit on
|
|
221
|
+
* `claude` is 0 either way, per the proof above. Doc-only diffs, a reviewer
|
|
222
|
+
* having run, or any unreadable/malformed input all resolve to silence
|
|
223
|
+
* (dispatcher-internal exit 0), not a crash.
|
|
224
|
+
*
|
|
225
|
+
* WHAT THE PROOF ABOVE DOES NOT COVER. Every term of it is an exit code, so it
|
|
226
|
+
* settles refusal and nothing else. Whether the `additionalContext` payload
|
|
227
|
+
* this concern emits at `stop` causes the host to run the agent AGAIN is a
|
|
228
|
+
* separate question, and this tree does not answer it — see
|
|
229
|
+
* `docs/contracts/hook-architecture-v1.md` under the exit-code table, which
|
|
230
|
+
* names the measurement that would. So do not read the proof above as
|
|
231
|
+
* establishing that this concern leaves the turn alone; it establishes only
|
|
232
|
+
* that the concern never refuses it.
|
|
215
233
|
*/
|
|
216
234
|
import { spawnSync } from 'node:child_process';
|
|
217
235
|
import * as crypto from 'node:crypto';
|
|
@@ -220,7 +238,18 @@ import * as os from 'node:os';
|
|
|
220
238
|
import * as path from 'node:path';
|
|
221
239
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
222
240
|
|
|
223
|
-
import {
|
|
241
|
+
import {
|
|
242
|
+
applyBaseline,
|
|
243
|
+
currentHeadSha,
|
|
244
|
+
readBaseline,
|
|
245
|
+
type BaselineFallback,
|
|
246
|
+
type BaselineOutcome,
|
|
247
|
+
} from '../_lib/review_baseline.js';
|
|
248
|
+
import {
|
|
249
|
+
buildReviewSkippedLine,
|
|
250
|
+
type BaselineApplication,
|
|
251
|
+
type MutationMeasure,
|
|
252
|
+
} from '../_lib/review_skipped_record.js';
|
|
224
253
|
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
225
254
|
import { readHookStdin } from './hook_stdin.js';
|
|
226
255
|
import { atomic_write_json, is_replay_mode } from './state_io.js';
|
|
@@ -606,6 +635,7 @@ export function appendReviewSkippedTelemetry(
|
|
|
606
635
|
workspaceRoot: string,
|
|
607
636
|
diffLines: number,
|
|
608
637
|
mutationMeasure: MutationMeasure,
|
|
638
|
+
baseline?: BaselineApplication,
|
|
609
639
|
): void {
|
|
610
640
|
if (is_replay_mode()) return;
|
|
611
641
|
const ts = new Date().toISOString();
|
|
@@ -614,6 +644,7 @@ export function appendReviewSkippedTelemetry(
|
|
|
614
644
|
mutation_measure: mutationMeasure,
|
|
615
645
|
ts,
|
|
616
646
|
id: crypto.randomUUID(),
|
|
647
|
+
baseline,
|
|
617
648
|
});
|
|
618
649
|
if (errors.length || line === null) return; // refused input — never write a broken line
|
|
619
650
|
try {
|
|
@@ -694,6 +725,23 @@ function _read_stdin(): string {
|
|
|
694
725
|
return readHookStdin();
|
|
695
726
|
}
|
|
696
727
|
|
|
728
|
+
/**
|
|
729
|
+
* The one place the two spellings meet.
|
|
730
|
+
*
|
|
731
|
+
* `BaselineFallback` is kebab-cased because it is a TypeScript union read only
|
|
732
|
+
* in this process; `BaselineApplication` is snake_cased because it is written
|
|
733
|
+
* into an audit line and every other enum in that stream is. A hand-written
|
|
734
|
+
* ternary translated them until a review pointed out that `head-moved` and
|
|
735
|
+
* `head_moved` differ by one character in a conditional nobody would re-read —
|
|
736
|
+
* an exhaustive record makes a new fallback a compile error instead.
|
|
737
|
+
*/
|
|
738
|
+
const FALLBACK_TO_TELEMETRY: Readonly<Record<BaselineFallback, BaselineApplication>> = {
|
|
739
|
+
absent: 'absent',
|
|
740
|
+
unreadable: 'unreadable',
|
|
741
|
+
'head-moved': 'head_moved',
|
|
742
|
+
'mixed-measure': 'mixed_measure',
|
|
743
|
+
};
|
|
744
|
+
|
|
697
745
|
export function main(): number {
|
|
698
746
|
const [envelope, payload] = unwrap(_read_stdin(), 'claude');
|
|
699
747
|
|
|
@@ -705,15 +753,43 @@ export function main(): number {
|
|
|
705
753
|
const workspace_root =
|
|
706
754
|
String(envelope['workspace_root'] ?? '').trim() || process.cwd();
|
|
707
755
|
|
|
708
|
-
let
|
|
756
|
+
let measuredLines: number;
|
|
709
757
|
let mutationMeasure: MutationMeasure;
|
|
710
758
|
try {
|
|
711
759
|
const measured = totalNonDocMutatedLinesWithMeasure(workspace_root);
|
|
712
|
-
|
|
760
|
+
measuredLines = measured.lines;
|
|
713
761
|
mutationMeasure = measured.measure;
|
|
714
762
|
} catch {
|
|
715
763
|
return 0; // fail-open — never block the agent loop
|
|
716
764
|
}
|
|
765
|
+
|
|
766
|
+
// The session key is derived HERE rather than after the threshold test,
|
|
767
|
+
// because the threshold is now taken on the SUBTRACTED count and the
|
|
768
|
+
// baseline is keyed on the session. That is a real reordering: one extra
|
|
769
|
+
// `sha256` of a string and one `readFileSync` of a ~100-byte JSON file now
|
|
770
|
+
// run on every stop, including the majority that end below threshold. The
|
|
771
|
+
// alternative — testing the unsubtracted count first as a cheap
|
|
772
|
+
// pre-filter — is wrong in the one direction that matters: on an
|
|
773
|
+
// accumulator branch EVERY turn clears the unsubtracted threshold, so the
|
|
774
|
+
// pre-filter would pass exactly when the subtraction is needed and save
|
|
775
|
+
// nothing.
|
|
776
|
+
const sessionKey = deriveSessionKey(envelope, payload);
|
|
777
|
+
const baselineOutcome: BaselineOutcome = applyBaseline(
|
|
778
|
+
measuredLines,
|
|
779
|
+
readBaseline(workspace_root, sessionKey),
|
|
780
|
+
// A THUNK, not a value: on the `absent` and `unreadable` branches the
|
|
781
|
+
// result is discarded, and those two are the steady state on every host
|
|
782
|
+
// without this nudge and on every session before its first baseline
|
|
783
|
+
// write. Passing the value eagerly spent an 11-14 ms `git rev-parse` on
|
|
784
|
+
// each of those stops for nothing.
|
|
785
|
+
() => currentHeadSha(workspace_root),
|
|
786
|
+
mutationMeasure,
|
|
787
|
+
);
|
|
788
|
+
const diffLines = baselineOutcome.lines;
|
|
789
|
+
const baselineApplication: BaselineApplication = baselineOutcome.applied
|
|
790
|
+
? 'applied'
|
|
791
|
+
: FALLBACK_TO_TELEMETRY[baselineOutcome.fallback];
|
|
792
|
+
|
|
717
793
|
if (diffLines <= MUTATION_LINE_THRESHOLD) {
|
|
718
794
|
return 0; // below threshold, or a doc-only diff — nothing to nudge
|
|
719
795
|
}
|
|
@@ -723,7 +799,6 @@ export function main(): number {
|
|
|
723
799
|
// JSON parse below. A session that already fired stops here, at a
|
|
724
800
|
// single `fs.existsSync` call, without re-reading a (potentially
|
|
725
801
|
// large) transcript file it already knows the answer for.
|
|
726
|
-
const sessionKey = deriveSessionKey(envelope, payload);
|
|
727
802
|
if (hasFiredThisSession(workspace_root, sessionKey)) {
|
|
728
803
|
return 0;
|
|
729
804
|
}
|
|
@@ -756,7 +831,12 @@ export function main(): number {
|
|
|
756
831
|
markFiredThisSession(workspace_root, sessionKey, new Date().toISOString());
|
|
757
832
|
|
|
758
833
|
try {
|
|
759
|
-
appendReviewSkippedTelemetry(
|
|
834
|
+
appendReviewSkippedTelemetry(
|
|
835
|
+
workspace_root,
|
|
836
|
+
diffLines,
|
|
837
|
+
mutationMeasure,
|
|
838
|
+
baselineApplication,
|
|
839
|
+
);
|
|
760
840
|
} catch {
|
|
761
841
|
// never let a telemetry failure block or fail the turn
|
|
762
842
|
}
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*
|
|
44
44
|
* Exit codes: 0 allow · 2 block (stderr carries the reason).
|
|
45
45
|
*/
|
|
46
|
+
import { spawnSync } from "node:child_process";
|
|
46
47
|
import crypto from "node:crypto";
|
|
47
48
|
import fs from "node:fs";
|
|
48
49
|
import path from "node:path";
|
|
@@ -50,6 +51,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
50
51
|
|
|
51
52
|
import { atomic_write_json } from "./state_io.js";
|
|
52
53
|
import { readHookStdin } from "./hook_stdin.js";
|
|
54
|
+
import { isCodePath, isTestPath } from "../_lib/test_delta.js";
|
|
53
55
|
|
|
54
56
|
const EXIT_ALLOW = 0;
|
|
55
57
|
// MUST equal dispatch_hook.EXIT_BLOCK. The dispatcher's internal ladder is
|
|
@@ -324,6 +326,79 @@ export function decide(
|
|
|
324
326
|
};
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
/**
|
|
330
|
+
* The one-commit test-authorship flag —
|
|
331
|
+
* `road-to-adversarial-verification-and-long-runs` 2.4, third clause.
|
|
332
|
+
*
|
|
333
|
+
* A commit staging BOTH a test and code it could cover is the shape of L0 on the
|
|
334
|
+
* independence scale — the implementer wrote the evaluator — and it is the one
|
|
335
|
+
* case nobody sees, because the diff looks exactly like a well-tested change.
|
|
336
|
+
*
|
|
337
|
+
* **It reads the STAGED SET and the command, and nothing else.** It does NOT
|
|
338
|
+
* read a session id, an author or a timestamp, so it cannot distinguish an
|
|
339
|
+
* implementer who wrote both from one committing a test another session authored
|
|
340
|
+
* — an independent review of this function caught an earlier docstring here
|
|
341
|
+
* claiming a session boundary the code never reads, which is exactly the
|
|
342
|
+
* over-claim `evaluator-independence` exists over. The honest reading is: this
|
|
343
|
+
* is the shape L0 takes in a diff, surfaced so the level is stated rather than
|
|
344
|
+
* inferred.
|
|
345
|
+
*
|
|
346
|
+
* **WARN, never block.** L0 is explicitly permitted as a fallback, so refusing it
|
|
347
|
+
* would forbid a legal state; and a signal this coarse may surface, never refuse.
|
|
348
|
+
*
|
|
349
|
+
* Returns `null` when nothing is worth saying — i.e. this is not a commit.
|
|
350
|
+
*/
|
|
351
|
+
export function sameSessionTestFlag(command: string | null): string | null {
|
|
352
|
+
if (command === null || !/\bgit\b[^|;&]*\bcommit\b/.test(command)) return null;
|
|
353
|
+
return SAME_SESSION_TEXT;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const SAME_SESSION_TEXT =
|
|
357
|
+
"This commit stages both a test and code it could cover — the shape L0 takes " +
|
|
358
|
+
"in a diff, where the implementer authored the evaluator. This check reads the " +
|
|
359
|
+
"staged set only: it cannot tell who wrote the test, so it is a prompt to STATE " +
|
|
360
|
+
"the level, not a finding that you are at L0. L0 is a permitted FALLBACK, not " +
|
|
361
|
+
"the default: where a second session, a second model or a second provider was " +
|
|
362
|
+
"available, the test should have come from one of them. Say which in the commit " +
|
|
363
|
+
"or the PR body — a diff looks identical either way. See evaluator-independence " +
|
|
364
|
+
"§ Tests are evaluators.";
|
|
365
|
+
|
|
366
|
+
/** Both a test path and a production path in one changed set. */
|
|
367
|
+
export function touchesTestAndCode(paths: readonly string[]): boolean {
|
|
368
|
+
return paths.some(isTestPath) && paths.some(isCodePath);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The shell command this envelope is about, across the key names hosts use. */
|
|
372
|
+
function _commandOf(envelope: JsonObject): string | null {
|
|
373
|
+
const payload = _isObject(envelope["payload"]) ? envelope["payload"] : envelope;
|
|
374
|
+
const input = _isObject(payload["tool_input"]) ? payload["tool_input"] : payload;
|
|
375
|
+
for (const key of ["command", "cmd", "shell_command"]) {
|
|
376
|
+
const v = input[key];
|
|
377
|
+
if (typeof v === "string" && v !== "") return v;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Staged paths, or `null` when git cannot answer.
|
|
384
|
+
*
|
|
385
|
+
* `null` is not "nothing staged" — it is "no reading", and the caller must not
|
|
386
|
+
* turn an unreadable index into a silent clean bill. A hook that cannot measure
|
|
387
|
+
* says nothing rather than implying a measurement.
|
|
388
|
+
*/
|
|
389
|
+
function _stagedPaths(consumer_root: string): string[] | null {
|
|
390
|
+
const r = spawnSync("git", ["diff", "--cached", "--name-only"], {
|
|
391
|
+
cwd: consumer_root,
|
|
392
|
+
encoding: "utf8",
|
|
393
|
+
});
|
|
394
|
+
if (r.status !== 0) return null;
|
|
395
|
+
const out = r.stdout
|
|
396
|
+
.split("\n")
|
|
397
|
+
.map((s) => s.trim())
|
|
398
|
+
.filter((s) => s !== "");
|
|
399
|
+
return out.length === 0 ? null : out;
|
|
400
|
+
}
|
|
401
|
+
|
|
327
402
|
export function run(stdin_text: string, options: { consumer_root: string }): number {
|
|
328
403
|
let envelope: JsonObject = {};
|
|
329
404
|
if (stdin_text.trim()) {
|
|
@@ -355,6 +430,27 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
|
|
|
355
430
|
}
|
|
356
431
|
|
|
357
432
|
const [tool, prompt] = extractDispatch(envelope);
|
|
433
|
+
|
|
434
|
+
// 2.4 — the one-commit test-authorship flag, checked BEFORE the dispatch
|
|
435
|
+
// branch and returning early. A commit envelope carries no evaluation prompt,
|
|
436
|
+
// so `decide` would classify it as not-an-evaluation and allow it silently;
|
|
437
|
+
// running both would mean the commit path fell through to a counter that is
|
|
438
|
+
// about evaluation dispatches and would be wrong to advance here.
|
|
439
|
+
//
|
|
440
|
+
// CHEAP TEST FIRST, and that ordering is the finding rather than a style
|
|
441
|
+
// preference: `_stagedPaths` spawns `git diff --cached`, and this concern is
|
|
442
|
+
// bound on `pre_tool_use`, so reading the index before the regex made every
|
|
443
|
+
// Read, Edit, Grep and Task dispatch in every session pay a subprocess. The
|
|
444
|
+
// regex answers "is this even a commit" for free.
|
|
445
|
+
const flag = sameSessionTestFlag(_commandOf(envelope));
|
|
446
|
+
if (flag !== null) {
|
|
447
|
+
const staged = _stagedPaths(options.consumer_root);
|
|
448
|
+
if (staged !== null && touchesTestAndCode(staged)) {
|
|
449
|
+
process.stdout.write(`${JSON.stringify({ decision: "warn", reason: flag })}\n`);
|
|
450
|
+
return EXIT_ALLOW;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
358
454
|
const decision = decide(tool, prompt, state.evaluations.length);
|
|
359
455
|
|
|
360
456
|
if (decision.evaluations > state.evaluations.length) {
|