@event4u/agent-config 14.18.0 → 14.19.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.
Files changed (146) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +169 -130
  5. package/README.md +71 -149
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/review/changes.md +19 -19
  9. package/dist/agent-src/commands/review.md +2 -2
  10. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  11. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  12. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  13. package/dist/agent-src/rules/guidelines.md +1 -1
  14. package/dist/agent-src/rules/session-canary.md +2 -2
  15. package/dist/agent-src/rules/token-efficiency.md +1 -1
  16. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  17. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  19. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  20. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  21. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  22. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  23. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  24. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  25. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  26. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  27. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  28. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  29. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  30. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  31. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  32. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  33. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  34. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  35. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  36. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  37. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  38. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
  39. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  40. package/dist/cli/commands/recordTriggerEval.js +2 -2
  41. package/dist/cli/main.js +1 -1
  42. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  43. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  44. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  45. package/dist/cli-delegate/cmd_conformance.js +420 -16
  46. package/dist/cli-delegate/cmd_doctor.js +1 -1
  47. package/dist/cli-delegate/cmd_export.js +1 -1
  48. package/dist/cli-delegate/cmd_handoff.js +1 -1
  49. package/dist/cli-delegate/cmd_prune.js +1 -1
  50. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  51. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  52. package/dist/cli-delegate/cmd_sync.js +1 -1
  53. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  54. package/dist/cli-delegate/cmd_validate.js +1 -1
  55. package/dist/discovery/deprecation-report.md +1 -1
  56. package/dist/discovery/discovery-manifest.json +28 -27
  57. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  58. package/dist/discovery/discovery-manifest.summary.md +1 -1
  59. package/dist/discovery/dormancy-report.md +1 -1
  60. package/dist/discovery/orphan-report.md +1 -1
  61. package/dist/discovery/packs.json +2 -2
  62. package/dist/discovery/trust-report.md +1 -1
  63. package/dist/discovery/workspaces.json +2 -2
  64. package/dist/hooks/dispatch.js +2132 -1462
  65. package/dist/install/install.mjs +588 -527
  66. package/dist/mcp/registry-manifest.json +1 -1
  67. package/dist/mcp/server.json +2 -2
  68. package/dist/mcp/server.mjs +1619 -1045
  69. package/dist/scripts/_lib/model_tier.js +1 -1
  70. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  71. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  72. package/docs/guidelines/design-antipatterns.md +87 -64
  73. package/package.json +1 -1
  74. package/src/agent-src/README.md +1 -1
  75. package/src/config/ci-local-parity.yml +6 -0
  76. package/src/config/gate-coverage.yml +170 -0
  77. package/src/config/gate-violation-baselines.json +3 -3
  78. package/src/config/release-gate-locality.yml +12 -0
  79. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  80. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  81. package/src/scripts/_cli/cmd_update.ts +1 -1
  82. package/src/scripts/_cli/cmd_versions.ts +1 -1
  83. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  84. package/src/scripts/_lib/consequence_objects.ts +152 -0
  85. package/src/scripts/_lib/exec_evidence.ts +7 -0
  86. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  87. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  88. package/src/scripts/_lib/model_tier.ts +1 -1
  89. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  90. package/src/scripts/_lib/release_highlights.ts +93 -0
  91. package/src/scripts/_lib/release_material.ts +64 -1
  92. package/src/scripts/_lib/runtime_journal.ts +26 -3
  93. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  94. package/src/scripts/_lib/session_register.ts +11 -6
  95. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  96. package/src/scripts/_lib/surface.ts +86 -0
  97. package/src/scripts/_lib/ui_authority.ts +116 -0
  98. package/src/scripts/adr/regenerate_index.ts +54 -1
  99. package/src/scripts/adr_cite_check.ts +71 -15
  100. package/src/scripts/ai_council/clients.ts +1 -1
  101. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  102. package/src/scripts/build_proof.ts +15 -2
  103. package/src/scripts/check_beta_review_markers.ts +141 -17
  104. package/src/scripts/check_enforcement_coverage.ts +1 -1
  105. package/src/scripts/check_enforcement_denominator.ts +121 -11
  106. package/src/scripts/check_finding_dispositions.ts +66 -9
  107. package/src/scripts/check_gate_coverage.ts +96 -0
  108. package/src/scripts/check_release_highlights.ts +74 -1
  109. package/src/scripts/check_skill_activation_claim.ts +179 -0
  110. package/src/scripts/check_trigger_evals.ts +1 -1
  111. package/src/scripts/conformance_scan.ts +44 -3
  112. package/src/scripts/explain_run.ts +1 -1
  113. package/src/scripts/generate_capability_matrix.ts +1 -1
  114. package/src/scripts/git_authorization_hook.ts +75 -1
  115. package/src/scripts/hook_manifest.json +1 -1
  116. package/src/scripts/hook_manifest.yaml +14 -11
  117. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  118. package/src/scripts/hooks/category_a.ts +291 -0
  119. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  120. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  121. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  122. package/src/scripts/hooks/git_command_classifier.ts +156 -1
  123. package/src/scripts/hooks/host_lowering.ts +215 -0
  124. package/src/scripts/hooks/host_lowering.yaml +185 -0
  125. package/src/scripts/hooks/host_semantics.ts +105 -37
  126. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  127. package/src/scripts/hooks/permission_gate.ts +117 -0
  128. package/src/scripts/hooks_status.ts +12 -10
  129. package/src/scripts/install.ts +53 -82
  130. package/src/scripts/install_anthropic_key.sh +2 -2
  131. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  132. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  133. package/src/scripts/lint_hook_manifest.ts +87 -3
  134. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  135. package/src/scripts/lint_readme_size.ts +158 -12
  136. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  137. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  138. package/src/scripts/measure_sibling_naming.ts +150 -0
  139. package/src/scripts/redact_hook_capture.ts +1 -1
  140. package/src/scripts/report_skill_activation.ts +319 -7
  141. package/src/scripts/schemas/review-findings.schema.json +5 -0
  142. package/src/scripts/schemas/skill.schema.json +2 -2
  143. package/src/scripts/session_canary_hook.ts +1 -1
  144. package/src/scripts/session_register_hook.ts +4 -3
  145. package/src/scripts/trigger_eval_grandfather.json +5 -1
  146. package/src/scripts/validate_agent_settings.ts +1 -1
@@ -90,6 +90,7 @@ import * as catalogue from './_lib/skill_catalogue.js';
90
90
  import { isInteractiveSession, type SessionProbe } from '../shared/interactiveContext.js';
91
91
  import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
92
92
  import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
93
+ import { hostBindings } from './hooks/host_lowering.js';
93
94
  import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
94
95
  import {
95
96
  LEGACY_ALL,
@@ -417,7 +418,7 @@ function yamlSafeLoad(text: string): unknown {
417
418
  }
418
419
 
419
420
  // ---------------------------------------------------------------------------
420
- // Module-level constants (install.py:51-116)
421
+ // Module-level constants.
421
422
  // ---------------------------------------------------------------------------
422
423
 
423
424
  const DEFAULT_PROFILE = 'balanced';
@@ -1022,14 +1023,6 @@ const AUGMENT_LEGACY_TRAMPOLINES: readonly string[] = [
1022
1023
  'augment-onboarding-gate.sh',
1023
1024
  'augment-context-hygiene.sh',
1024
1025
  ];
1025
- const AUGMENT_DISPATCHER_BINDINGS: ReadonlyArray<readonly [string, string]> = [
1026
- ['session_start', 'SessionStart'],
1027
- ['session_end', 'SessionEnd'],
1028
- ['stop', 'Stop'],
1029
- ['pre_tool_use', 'PreToolUse'],
1030
- ['post_tool_use', 'PostToolUse'],
1031
- ];
1032
-
1033
1026
  function _deploy_augment_trampoline(package_root: string, name: string, force: boolean): string | null {
1034
1027
  const src = path.join(package_root, 'scripts', 'hooks', name);
1035
1028
  if (!pathExists(src)) {
@@ -1073,8 +1066,8 @@ function ensure_augment_user_hooks(package_root: string, force: boolean): Record
1073
1066
  _remove_legacy_augment_trampolines();
1074
1067
 
1075
1068
  const per_event: Record<string, unknown[]> = {};
1076
- for (const [ac_event, native] of AUGMENT_DISPATCHER_BINDINGS) {
1077
- const cmd = `${dst} ${ac_event} ${native}`;
1069
+ for (const { slot, native } of hostBindings('augment')) {
1070
+ const cmd = `${dst} ${slot} ${native}`;
1078
1071
  const entry = { hooks: [{ type: 'command', command: cmd }] };
1079
1072
  (per_event[native] ??= []).push(entry);
1080
1073
  }
@@ -1130,14 +1123,6 @@ const _prune_tier_b_modules = (
1130
1123
  ): [number, Record<string, DeployResult>] =>
1131
1124
  _prune_modules_by(dr, mcp_bridge.tierBPrunePredicate(tier_b));
1132
1125
 
1133
- const CURSOR_DISPATCHER_BINDINGS: ReadonlyArray<readonly [string, string]> = [
1134
- ['session_start', 'sessionStart'],
1135
- ['session_end', 'sessionEnd'],
1136
- ['stop', 'stop'],
1137
- ['user_prompt_submit', 'beforeSubmitPrompt'],
1138
- ['post_tool_use', 'postToolUse'],
1139
- ];
1140
-
1141
1126
  function _cursor_dispatch_command(ac_event: string, native: string): string {
1142
1127
  // Hook-resilience shim (P1.4): missing binary → silent exit 0; a
1143
1128
  // present dispatcher's exit code propagates (fail_closed concerns
@@ -1152,8 +1137,8 @@ function _cursor_dispatch_command(ac_event: string, native: string): string {
1152
1137
 
1153
1138
  function ensure_cursor_bridge(project_root: string, force: boolean): Record<string, unknown>[] {
1154
1139
  const hooks: Record<string, unknown[]> = {};
1155
- for (const [ac_event, native] of CURSOR_DISPATCHER_BINDINGS) {
1156
- (hooks[native] ??= []).push({ command: _cursor_dispatch_command(ac_event, native) });
1140
+ for (const { slot, native } of hostBindings('cursor')) {
1141
+ (hooks[native] ??= []).push({ command: _cursor_dispatch_command(slot, native) });
1157
1142
  }
1158
1143
  const bridge = { version: 1, hooks };
1159
1144
  return merge_json_file(
@@ -1187,8 +1172,8 @@ function ensure_cursor_user_hooks(package_root: string, force: boolean): Record<
1187
1172
  }
1188
1173
 
1189
1174
  const hooks: Record<string, unknown[]> = {};
1190
- for (const [ac_event, native] of CURSOR_DISPATCHER_BINDINGS) {
1191
- (hooks[native] ??= []).push({ command: `${dst} ${ac_event} ${native}` });
1175
+ for (const { slot, native } of hostBindings('cursor')) {
1176
+ (hooks[native] ??= []).push({ command: `${dst} ${slot} ${native}` });
1192
1177
  }
1193
1178
 
1194
1179
  const settings_patch = { version: 1, hooks };
@@ -1200,15 +1185,6 @@ function ensure_cursor_user_hooks(package_root: string, force: boolean): Record<
1200
1185
  );
1201
1186
  }
1202
1187
 
1203
- const CLINE_DISPATCHER_BINDINGS: ReadonlyArray<readonly [string, string]> = [
1204
- ['session_start', 'TaskStart'],
1205
- ['session_start', 'TaskResume'],
1206
- ['session_end', 'TaskComplete'],
1207
- ['stop', 'TaskCancel'],
1208
- ['user_prompt_submit', 'UserPromptSubmit'],
1209
- ['post_tool_use', 'PostToolUse'],
1210
- ];
1211
-
1212
1188
  /** `shlex.quote` — POSIX shell single-quote escaping. */
1213
1189
  function shlexQuote(s: string): string {
1214
1190
  if (s === '') return "''";
@@ -1221,7 +1197,7 @@ function clineProjectHookBody(native_event: string, ac_event: string, workspace_
1221
1197
  // template uses doubled braces ({{ }}) that .format() collapses to single.
1222
1198
  return (
1223
1199
  '#!/usr/bin/env bash\n' +
1224
- '# Generated by event4u/agent-config install.py — DO NOT EDIT.\n' +
1200
+ '# Generated by event4u/agent-config — DO NOT EDIT.\n' +
1225
1201
  `# Project-scope Cline hook for ${native_event} → agent-config ${ac_event}.\n` +
1226
1202
  '# Phase 7.6 (docs/contracts/hook-architecture-v1.md).\n' +
1227
1203
  'set -u\n' +
@@ -1249,14 +1225,14 @@ function ensure_cline_bridge(project_root: string, force: boolean): void {
1249
1225
 
1250
1226
  const workspace_quoted = shlexQuote(resolvePath(project_root));
1251
1227
  let written = 0;
1252
- for (const [ac_event, native_event] of CLINE_DISPATCHER_BINDINGS) {
1253
- const target = path.join(hooks_dir, native_event);
1254
- const body = clineProjectHookBody(native_event, ac_event, workspace_quoted);
1228
+ for (const { slot, native } of hostBindings('cline')) {
1229
+ const target = path.join(hooks_dir, native);
1230
+ const body = clineProjectHookBody(native, slot, workspace_quoted);
1255
1231
  if (pathExists(target) && readText(target) === body && !force) {
1256
1232
  continue;
1257
1233
  }
1258
1234
  if (pathExists(target) && !force) {
1259
- skip(`.clinerules/hooks/${native_event} exists, needs update (use --force)`);
1235
+ skip(`.clinerules/hooks/${native} exists, needs update (use --force)`);
1260
1236
  continue;
1261
1237
  }
1262
1238
  writeText(target, body);
@@ -1292,13 +1268,13 @@ function ensure_cline_user_hooks(package_root: string, force: boolean): void {
1292
1268
  }
1293
1269
 
1294
1270
  const trampoline_quoted = shlexQuote(trampoline);
1295
- for (const [ac_event, native_event] of CLINE_DISPATCHER_BINDINGS) {
1296
- const wrapper = path.join(CLINE_USER_DIR, native_event);
1271
+ for (const { slot, native } of hostBindings('cline')) {
1272
+ const wrapper = path.join(CLINE_USER_DIR, native);
1297
1273
  const body =
1298
1274
  '#!/usr/bin/env bash\n' +
1299
- '# Generated by event4u/agent-config install.py — DO NOT EDIT.\n' +
1300
- `# User-scope Cline hook for ${native_event} → agent-config ${ac_event}.\n` +
1301
- `exec ${trampoline_quoted} ${ac_event} ${native_event}\n`;
1275
+ '# Generated by event4u/agent-config — DO NOT EDIT.\n' +
1276
+ `# User-scope Cline hook for ${native} → agent-config ${slot}.\n` +
1277
+ `exec ${trampoline_quoted} ${slot} ${native}\n`;
1302
1278
  if (pathExists(wrapper) && readText(wrapper) === body && !force) {
1303
1279
  continue;
1304
1280
  }
@@ -1307,12 +1283,6 @@ function ensure_cline_user_hooks(package_root: string, force: boolean): void {
1307
1283
  }
1308
1284
  }
1309
1285
 
1310
- const WINDSURF_DISPATCHER_BINDINGS: ReadonlyArray<readonly [string, string]> = [
1311
- ['session_start', 'post_setup_worktree'],
1312
- ['user_prompt_submit', 'pre_user_prompt'],
1313
- ['stop', 'post_cascade_response'],
1314
- ];
1315
-
1316
1286
  function _windsurf_dispatch_command(ac_event: string, native: string): string {
1317
1287
  // Hook-resilience shim (P1.4): missing binary → silent exit 0; a
1318
1288
  // present dispatcher's exit code propagates (fail_closed concerns
@@ -1327,9 +1297,9 @@ function _windsurf_dispatch_command(ac_event: string, native: string): string {
1327
1297
 
1328
1298
  function ensure_windsurf_bridge(project_root: string, force: boolean): Record<string, unknown>[] {
1329
1299
  const hooks: Record<string, unknown[]> = {};
1330
- for (const [ac_event, native] of WINDSURF_DISPATCHER_BINDINGS) {
1300
+ for (const { slot, native } of hostBindings('windsurf')) {
1331
1301
  (hooks[native] ??= []).push({
1332
- command: _windsurf_dispatch_command(ac_event, native),
1302
+ command: _windsurf_dispatch_command(slot, native),
1333
1303
  show_output: false,
1334
1304
  });
1335
1305
  }
@@ -1365,9 +1335,9 @@ function ensure_windsurf_user_hooks(package_root: string, force: boolean): Recor
1365
1335
  }
1366
1336
 
1367
1337
  const hooks: Record<string, unknown[]> = {};
1368
- for (const [ac_event, native] of WINDSURF_DISPATCHER_BINDINGS) {
1338
+ for (const { slot, native } of hostBindings('windsurf')) {
1369
1339
  (hooks[native] ??= []).push({
1370
- command: `${dst} ${ac_event} ${native}`,
1340
+ command: `${dst} ${slot} ${native}`,
1371
1341
  show_output: false,
1372
1342
  });
1373
1343
  }
@@ -1381,14 +1351,6 @@ function ensure_windsurf_user_hooks(package_root: string, force: boolean): Recor
1381
1351
  );
1382
1352
  }
1383
1353
 
1384
- const GEMINI_DISPATCHER_BINDINGS: ReadonlyArray<readonly [string, string, string]> = [
1385
- ['session_start', 'SessionStart', ''],
1386
- ['session_end', 'SessionEnd', ''],
1387
- ['stop', 'AfterAgent', ''],
1388
- ['user_prompt_submit', 'BeforeAgent', ''],
1389
- ['post_tool_use', 'AfterTool', '.*'],
1390
- ];
1391
-
1392
1354
  function _gemini_dispatch_command(ac_event: string, native: string): string {
1393
1355
  // Hook-resilience shim (P1.4): missing binary → silent exit 0; a
1394
1356
  // present dispatcher's exit code propagates (fail_closed concerns
@@ -1405,10 +1367,10 @@ function _gemini_hooks_dict(
1405
1367
  command_factory: (ac_event: string, native: string) => string,
1406
1368
  ): Record<string, unknown[]> {
1407
1369
  const out: Record<string, unknown[]> = {};
1408
- for (const [ac_event, native, matcher] of GEMINI_DISPATCHER_BINDINGS) {
1370
+ for (const { slot, native, matcher } of hostBindings('gemini')) {
1409
1371
  (out[native] ??= []).push({
1410
1372
  matcher,
1411
- hooks: [{ type: 'command', command: command_factory(ac_event, native) }],
1373
+ hooks: [{ type: 'command', command: command_factory(slot, native) }],
1412
1374
  });
1413
1375
  }
1414
1376
  return out;
@@ -1724,15 +1686,37 @@ function ensure_kiro_bridge(project_root: string, force: boolean): void {
1724
1686
 
1725
1687
  // --- Post-install smoke test ---
1726
1688
 
1727
- const SMOKE_PROBE_EVENTS: ReadonlyArray<readonly [string, string]> = [
1689
+ /**
1690
+ * Platforms whose bridge the smoke probe exercises, with the slot it probes.
1691
+ *
1692
+ * The NATIVE event is derived from `host_lowering.yaml`, never written here.
1693
+ * The hand-written list this replaced probed `['cursor', 'beforeShellExecution']`
1694
+ * — an event no cursor bridge has ever written — so the probe could not fail on
1695
+ * a real binding regression, which is the only thing it exists to catch. A
1696
+ * platform whose table row does not carry the probed slot is a hard error, not
1697
+ * a skip: a silent skip is how the previous list stayed wrong.
1698
+ */
1699
+ const SMOKE_PROBE_SLOTS: ReadonlyArray<readonly [string, string]> = [
1728
1700
  ['augment', 'session_start'],
1729
- ['claude', 'SessionStart'],
1730
- ['cursor', 'beforeShellExecution'],
1701
+ ['claude', 'session_start'],
1702
+ ['cursor', 'session_start'],
1731
1703
  ['cline', 'session_start'],
1732
- ['windsurf', 'post_setup_worktree'],
1733
- ['gemini', 'SessionStart'],
1704
+ ['windsurf', 'session_start'],
1705
+ ['gemini', 'session_start'],
1734
1706
  ];
1735
1707
 
1708
+ export function smokeProbeEvents(): ReadonlyArray<readonly [string, string]> {
1709
+ return SMOKE_PROBE_SLOTS.map(([platform, slot]) => {
1710
+ const binding = hostBindings(platform).find((b) => b.slot === slot);
1711
+ if (binding === undefined) {
1712
+ throw new Error(
1713
+ `install smoke probe: host_lowering.yaml has no \`${slot}\` binding for \`${platform}\``,
1714
+ );
1715
+ }
1716
+ return [platform, binding.native] as const;
1717
+ });
1718
+ }
1719
+
1736
1720
  const SMOKE_BRIDGE_PATHS: Record<string, string> = {
1737
1721
  augment: '.augment/settings.json',
1738
1722
  claude: '.claude/settings.json',
@@ -1787,7 +1771,7 @@ function _smoke_test_hooks(project_root: string, package_root: string): number {
1787
1771
  const skipped: string[] = [];
1788
1772
  const passed: string[] = [];
1789
1773
 
1790
- for (const [platform, native] of SMOKE_PROBE_EVENTS) {
1774
+ for (const [platform, native] of smokeProbeEvents()) {
1791
1775
  const rel_bridge = SMOKE_BRIDGE_PATHS[platform] ?? '';
1792
1776
  const bridge_path = rel_bridge ? path.join(project_root, rel_bridge) : null;
1793
1777
  const bridge_present = Boolean(
@@ -2619,15 +2603,6 @@ function _is_agent_config_source_repo(project_root: string): [boolean, string] {
2619
2603
  }
2620
2604
  }
2621
2605
 
2622
- const installer_self = path.join(project_root, 'scripts', 'install.py');
2623
- try {
2624
- if (isFile(installer_self) && resolvePath(installer_self) === resolvePath(_HERE)) {
2625
- return [true, 'src/scripts/install.py (self)'];
2626
- }
2627
- } catch {
2628
- /* OSError → pass */
2629
- }
2630
-
2631
2606
  return [false, ''];
2632
2607
  }
2633
2608
 
@@ -4028,7 +4003,7 @@ function _merge_tools_aliases(tools: string | null, ai: string | null): string {
4028
4003
  return items.length > 0 ? items.join(',') : 'all';
4029
4004
  }
4030
4005
 
4031
- const PROG = 'install.py';
4006
+ const PROG = 'agent-config install';
4032
4007
  // Verbatim argparse usage block at COLUMNS=80 (captured from the .py). The
4033
4008
  // `--help` BODY (per-flag descriptions) is a documented divergence — argparse
4034
4009
  // re-wraps it to the live terminal width, which is not worth reproducing; the
@@ -5364,10 +5339,6 @@ export {
5364
5339
  ROOCODE_MARKER,
5365
5340
  ZED_MARKER,
5366
5341
  // ADR-200 py2ts: re-exported for test_install_snapshot.ts twin
5367
- CURSOR_DISPATCHER_BINDINGS,
5368
- CLINE_DISPATCHER_BINDINGS,
5369
- WINDSURF_DISPATCHER_BINDINGS,
5370
- GEMINI_DISPATCHER_BINDINGS,
5371
5342
  ensure_cursor_bridge,
5372
5343
  ensure_cline_bridge,
5373
5344
  ensure_mcp_bridge,
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Interactive Anthropic-API-key installer for scripts/skill_trigger_eval.py.
2
+ # Interactive Anthropic-API-key installer for src/scripts/skill_trigger_eval.ts.
3
3
  #
4
4
  # Reads the key with `read -s` so it never echoes to the terminal and
5
5
  # never lands in shell history or scrollback. Writes atomically to
@@ -7,7 +7,7 @@
7
7
  # ~/.config/agent-config/anthropic.key is read as a fallback by the
8
8
  # loaders so pre-2.4 installs keep working until the namespace shim runs.
9
9
  #
10
- # Contract — companion to scripts/skill_trigger_eval.py:
10
+ # Contract — companion to src/scripts/skill_trigger_eval.ts:
11
11
  # - File path: $HOME/.event4u/agent-config/anthropic.key
12
12
  # - File mode: 0600 (owner read/write only)
13
13
  # - Key format: must start with `sk-ant-`
@@ -60,6 +60,43 @@ const STATUS_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|\s*([a-z-]*)\s*\|([^|]*)\|/g
60
60
  const STATUS_SECTION = /^## Detector status$([\s\S]*?)(?=^## )/m;
61
61
  const CATALOG_ID = /^\s+catalogId:\s*"([A-Z]+\d+)"/gm;
62
62
 
63
+ /** The five classes a catalog row may claim (road-to-one-motion-authority 2.1). */
64
+ export const CLASSES = ['floor', 'invariant', 'craft-presumption', 'style-preference', 'reference-constraint'] as const;
65
+ /** How a fix is checked. `feel` is the perceptual mode `evidence-artifact-types.md` adds. */
66
+ export const VERIFICATIONS = ['static', 'render', 'feel', 'judgment'] as const;
67
+ /** Remediation names an order, worst-first, drawn from these verbs. */
68
+ export const REMEDIATION_VERBS = ['delete', 'reduce', 'retune', 'replace'] as const;
69
+
70
+ export interface CensusCells { class: string; remediation: string; verification: string }
71
+
72
+ const CENSUS_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|(.*)$/gm;
73
+
74
+ /**
75
+ * The three census cells per catalog entry, read from the LAST three columns.
76
+ *
77
+ * Read positionally from the end rather than by header index because the four
78
+ * original columns carry free prose containing pipes inside inline code, and a
79
+ * header-indexed read would mis-align on exactly the rows whose prose is
80
+ * richest. The three census cells are short closed-vocabulary tokens, so the
81
+ * tail of the row is the one part that parses reliably.
82
+ */
83
+ export function censusCells(catalogMd: string): Map<string, CensusCells> {
84
+ const section = STATUS_SECTION.exec(catalogMd)?.[0] ?? '';
85
+ const outside = section === '' ? catalogMd : catalogMd.replace(section, '');
86
+ const out = new Map<string, CensusCells>();
87
+ for (const m of outside.matchAll(CENSUS_ROW)) {
88
+ const cells = (m[2] ?? '').split('|').map((c) => c.trim());
89
+ while (cells.length > 0 && cells[cells.length - 1] === '') cells.pop();
90
+ if (cells.length < 6) continue;
91
+ out.set(m[1] as string, {
92
+ class: cells[cells.length - 3] ?? '',
93
+ remediation: cells[cells.length - 2] ?? '',
94
+ verification: cells[cells.length - 1] ?? '',
95
+ });
96
+ }
97
+ return out;
98
+ }
99
+
63
100
  /** Catalog entry ids outside the § Detector status section. */
64
101
  export function catalogEntries(catalogMd: string): string[] {
65
102
  const section = STATUS_SECTION.exec(catalogMd)?.[0] ?? '';
@@ -131,6 +168,49 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
131
168
  }
132
169
  }
133
170
 
171
+ // F — the census columns: present, non-empty, and from the closed vocabularies.
172
+ const census = censusCells(catalogMd);
173
+ for (const id of seen) {
174
+ const cells = census.get(id);
175
+ if (cells === undefined) {
176
+ out.push({ kind: 'missing-census', id, msg: `${id}: catalog row carries no class / remediation / verification cells` });
177
+ continue;
178
+ }
179
+ if (!(CLASSES as readonly string[]).includes(cells.class)) {
180
+ out.push({ kind: 'bad-class', id, msg: `${id}: class '${cells.class || '(empty)'}' is not one of ${CLASSES.join(', ')}` });
181
+ }
182
+ if (!(VERIFICATIONS as readonly string[]).includes(cells.verification)) {
183
+ out.push({ kind: 'bad-verification', id, msg: `${id}: verification '${cells.verification || '(empty)'}' is not one of ${VERIFICATIONS.join(', ')}` });
184
+ }
185
+ const verbs = cells.remediation.split('→').map((v) => v.trim()).filter((v) => v !== '');
186
+ if (verbs.length === 0 || !verbs.every((v) => (REMEDIATION_VERBS as readonly string[]).includes(v))) {
187
+ out.push({ kind: 'bad-remediation', id, msg: `${id}: remediation '${cells.remediation || '(empty)'}' is not a → order over ${REMEDIATION_VERBS.join(', ')}` });
188
+ }
189
+ }
190
+
191
+ // G — the two axes are INDEPENDENT (road-to-one-motion-authority Risk 3).
192
+ // Class is a property of the rule, status a property of its enforcement.
193
+ // A `floor` that nothing can detect is legitimately `judgment-only`, and a
194
+ // `style-preference` that happens to be greppable is legitimately `backed`.
195
+ // This gate asserts the independence positively rather than merely allowing
196
+ // it: a run in which class turned out to determine status would mean the
197
+ // second axis had collapsed into the first and is carrying no information.
198
+ const pairs = [...seen]
199
+ .map((id) => ({ cls: census.get(id)?.class ?? '', status: rows.get(id)?.status ?? '' }))
200
+ .filter((p) => p.cls !== '' && p.status !== '');
201
+ const byClass = new Map<string, Set<string>>();
202
+ for (const p of pairs) {
203
+ const set = byClass.get(p.cls) ?? new Set<string>();
204
+ set.add(p.status);
205
+ byClass.set(p.cls, set);
206
+ }
207
+ if (pairs.length > 0 && [...byClass.values()].every((statuses) => statuses.size === 1)) {
208
+ out.push({
209
+ kind: 'axes-collapsed',
210
+ msg: 'every class maps to exactly one status — the class column is carrying no information the status column does not already carry. Class describes the rule, status describes its enforcement; if they have become the same axis, one of them is wrong.',
211
+ });
212
+ }
213
+
134
214
  // C — backed set equals the registry set, both directions.
135
215
  const backed = new Set([...rows].filter(([, r]) => r.status === 'backed').map(([id]) => id));
136
216
  const registry = new Set(registryCatalogIds(registryTs));
@@ -55,15 +55,38 @@ import { parseHonestNull, parseMarkerLine, parseSkipDeclaration } from './check_
55
55
 
56
56
  export const EVIDENCE_ROOT = path.join('agents', 'evidence');
57
57
 
58
- /** The five types the contract defines. Anything else is an error, not a warning. */
58
+ /** The types the contract defines. Anything else is an error, not a warning. */
59
59
  export const EVIDENCE_TYPES = [
60
60
  'original-review',
61
61
  'current-binding',
62
62
  'declared-skip',
63
63
  'honest-null',
64
64
  'analysis',
65
+ 'feel',
65
66
  ] as const;
66
67
 
68
+ /**
69
+ * The closed method vocabulary a `feel` artifact must name.
70
+ *
71
+ * Closed on purpose. An evidence class nothing can emit is a vocabulary entry,
72
+ * not a control, and the cheapest way to satisfy a perceptual floor is to write
73
+ * the word "feel" and move on. Requiring one of four named methods makes the
74
+ * claim falsifiable: each says how the motion was actually looked at.
75
+ */
76
+ export const FEEL_METHODS = ['slow-motion', 'frame-step', 'device', 'next-day'] as const;
77
+ export type FeelMethod = (typeof FEEL_METHODS)[number];
78
+
79
+ const FEEL_LINE_RE = new RegExp(
80
+ String.raw`^\*\*Feel:\*\*\s+(${FEEL_METHODS.join('|')})\s+[\u2014-]\s+(\S.*)$`,
81
+ );
82
+
83
+ /** The method and outcome a `feel` line declares, or `null` when it declares neither. */
84
+ export function parseFeelLine(line: string): { method: FeelMethod; outcome: string } | null {
85
+ const m = FEEL_LINE_RE.exec(line.trim());
86
+ if (m === null) return null;
87
+ return { method: m[1] as FeelMethod, outcome: (m[2] ?? '').trim() };
88
+ }
89
+
67
90
  export type EvidenceType = (typeof EVIDENCE_TYPES)[number];
68
91
 
69
92
  /** The explicit marker, for the two types with no pre-existing grammar. */
@@ -202,6 +225,27 @@ export function checkFiles(root: string, files: readonly string[], ledger?: Gate
202
225
  });
203
226
  continue;
204
227
  }
228
+ // Risk 5 of road-to-one-motion-authority: the floor is the METHOD
229
+ // token, not the word. An outcome of `unbacked` is legal — a
230
+ // perceptual check that found nothing is still a check that ran — but
231
+ // an absent line is not, and neither is a method outside the four.
232
+ if (res.type === 'feel') {
233
+ const declared = contents
234
+ .split('\n')
235
+ .slice(0, MARKER_SCAN_LINES)
236
+ .some((l) => parseFeelLine(l) !== null);
237
+ if (!declared) {
238
+ ledger?.fail(rel, 'feel artifact with no method line');
239
+ findings.push({
240
+ file: rel,
241
+ reason:
242
+ 'declares `evidence-type: feel` but carries no `**Feel:** <method> — <outcome>` '
243
+ + `line naming one of ${FEEL_METHODS.join(' | ')}. An outcome of \`unbacked\` is legal; `
244
+ + 'an absent method is not',
245
+ });
246
+ continue;
247
+ }
248
+ }
205
249
  if (res.type === null) {
206
250
  ledger?.fail(rel, 'no evidence type declared');
207
251
  findings.push({
@@ -13,8 +13,11 @@
13
13
  * - a platform binding referencing an unknown event
14
14
  * - a native_event_aliases block referencing an unknown event or platform
15
15
  * - an orphan `<platform>-dispatcher.sh` trampoline without a manifest block
16
+ * - a `host_lowering.yaml` row whose `verified.expires` has passed (or which
17
+ * was never verified) while it still carries a blocking binding
16
18
  *
17
- * Soft-warns on placeholder platform blocks and dead concerns.
19
+ * Soft-warns on placeholder platform blocks, dead concerns, an expired row
20
+ * carrying no blocking binding, and an incomplete `verified` provenance block.
18
21
  *
19
22
  * Exit codes:
20
23
  * 0 — clean (warnings allowed)
@@ -29,6 +32,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
29
32
  import { parse as parseYaml } from "yaml";
30
33
 
31
34
  import { assertScanned, DeadScopeError } from "./_lib/scan_scope.js";
35
+ import { asOf } from "./_lib/as_of.js";
36
+ import { parseHostLowering, type HostLowering } from "./hooks/host_lowering.js";
32
37
  import { RE_ARM_EVENTS } from "./_lib/prefix_stable_surfaces.js";
33
38
 
34
39
  // src/scripts/lint_hook_manifest.ts → two levels up is the repo root.
@@ -44,6 +49,7 @@ const DEFAULT_MANIFEST = path.join(
44
49
  "hook_manifest.yaml",
45
50
  );
46
51
  const HOOKS_DIR = path.join(REPO_ROOT, "src", "scripts", "hooks");
52
+ const DEFAULT_HOST_LOWERING = path.join(HOOKS_DIR, "host_lowering.yaml");
47
53
 
48
54
  // Canonical event vocabulary — keep in lock-step with
49
55
  // docs/contracts/hook-architecture-v1.md and dispatch_hook.EVENT_VOCABULARY.
@@ -635,7 +641,80 @@ export function _check_roles(
635
641
  }
636
642
  }
637
643
 
638
- export function lint(manifestPath: string, strict: boolean): number {
644
+ /**
645
+ * A capability fact that cannot expire is a capability fact nobody re-checks.
646
+ *
647
+ * Three rounds of corrections to the same host claims is what this gate exists
648
+ * to stop, so the rule is mechanical rather than editorial: an unverified row —
649
+ * and an expired one is unverified, by the same definition `host_semantics`
650
+ * uses — may not carry a blocking binding. Everything softer warns, because a
651
+ * lapse on an advisory row must not red an unrelated pull request.
652
+ */
653
+ export function _check_host_lowering(
654
+ tablePath: string,
655
+ errors: string[],
656
+ warnings: string[],
657
+ today: string = asOf().toISOString().slice(0, 10),
658
+ ): void {
659
+ let table: HostLowering;
660
+ try {
661
+ table = parseHostLowering(fs.readFileSync(tablePath, "utf-8"));
662
+ } catch (exc) {
663
+ errors.push(
664
+ `host_lowering: ${tablePath} could not be read or parsed: ` +
665
+ `${exc instanceof Error ? exc.message : String(exc)}`,
666
+ );
667
+ return;
668
+ }
669
+ for (const [host, surfaces] of table) {
670
+ for (const [surface, row] of surfaces) {
671
+ const blocking = [...row.slots.entries()]
672
+ .filter(([, s]) => s.block_exit !== null)
673
+ .map(([slot]) => slot);
674
+ const expired = row.verified !== null && row.verified.expires < today;
675
+ const where = `host_lowering ${host}/${surface}`;
676
+
677
+ if (row.verified === null) {
678
+ if (blocking.length > 0) {
679
+ errors.push(
680
+ `${where}: carries a blocking binding (${blocking.join(", ")}) with \`verified: null\`. ` +
681
+ "Establish the row with a live deny probe and a dated `verified` block, or set " +
682
+ "`block_exit: null` on those slots.",
683
+ );
684
+ }
685
+ continue;
686
+ }
687
+ if (expired && blocking.length > 0) {
688
+ errors.push(
689
+ `${where}: \`verified.expires\` was ${row.verified.expires} and today is ${today}, ` +
690
+ `while the row still carries a blocking binding (${blocking.join(", ")}). ` +
691
+ "Re-establish it with a live deny probe on that host and move `expires` forward, " +
692
+ "or set `block_exit: null` on those slots — an expired row reads as unverified.",
693
+ );
694
+ } else if (expired) {
695
+ warnings.push(
696
+ `${where}: \`verified.expires\` was ${row.verified.expires} and today is ${today}. ` +
697
+ "The row carries no blocking binding, so nothing is degraded; re-establish it " +
698
+ "before attaching one.",
699
+ );
700
+ }
701
+ for (const field of ["docs_at", "host_version"] as const) {
702
+ if (row.verified[field] === null) {
703
+ warnings.push(`${where}: \`verified.${field}\` is null — the row is admissible but not fully cited.`);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ }
709
+
710
+ export function lint(
711
+ manifestPath: string,
712
+ strict: boolean,
713
+ hostLoweringPath: string = DEFAULT_HOST_LOWERING,
714
+ // Threaded so a fixture test can pin the day the verdict is read against.
715
+ // Left undefined the check resolves it through `asOf()`.
716
+ today?: string,
717
+ ): number {
639
718
  if (!_isFile(manifestPath)) {
640
719
  process.stderr.write(
641
720
  `lint_hook_manifest: file not found: ${manifestPath}\n`,
@@ -664,6 +743,7 @@ export function lint(manifestPath: string, strict: boolean): number {
664
743
  _check_aliases(manifest, errors);
665
744
  _check_orphan_trampolines(manifest, errors);
666
745
  _check_dead_concerns(concernNames, bound, warnings);
746
+ _check_host_lowering(hostLoweringPath, errors, warnings, today);
667
747
 
668
748
  for (const w of warnings) {
669
749
  process.stderr.write(`warn: ${w}\n`);
@@ -703,17 +783,21 @@ export function lint(manifestPath: string, strict: boolean): number {
703
783
 
704
784
  export function main(argv: string[]): number {
705
785
  let manifest = DEFAULT_MANIFEST;
786
+ let hostLowering = DEFAULT_HOST_LOWERING;
706
787
  let strict = false;
707
788
  for (let i = 0; i < argv.length; i += 1) {
708
789
  const a = argv[i];
709
790
  if (a === "--manifest") {
710
791
  manifest = argv[i + 1] ?? manifest;
711
792
  i += 1;
793
+ } else if (a === "--host-lowering") {
794
+ hostLowering = argv[i + 1] ?? hostLowering;
795
+ i += 1;
712
796
  } else if (a === "--strict") {
713
797
  strict = true;
714
798
  }
715
799
  }
716
- return lint(manifest, strict);
800
+ return lint(manifest, strict, hostLowering);
717
801
  }
718
802
 
719
803
  function _isCliEntry(): boolean {