@event4u/agent-config 14.17.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host_lowering — the reader for `host_lowering.yaml`.
|
|
3
|
+
*
|
|
4
|
+
* The table is the single place a host's block-exit, emission shape, fail
|
|
5
|
+
* policy and bridge bindings are written. Before it, those five facts lived in
|
|
6
|
+
* `host_semantics.ts`, in five private per-host binding constants in
|
|
7
|
+
* `install.ts`, and in prose; three rounds of prose corrections did not stop
|
|
8
|
+
* the drift, because prose has no single reader.
|
|
9
|
+
*
|
|
10
|
+
* LOADING IS STRICT. A missing or malformed table throws rather than
|
|
11
|
+
* degrading to an empty one. An empty table would resolve `VERIFIED_PLATFORMS`
|
|
12
|
+
* to the empty set, which silently downgrades the one host that actually
|
|
13
|
+
* enforces from blocking to advisory — the exact failure this roadmap's risk
|
|
14
|
+
* register ranks first. A throw is loud; a silent downgrade is not. The
|
|
15
|
+
* dispatcher already reads `hook_manifest.yaml` unconditionally from the same
|
|
16
|
+
* tree, so this adds no new class of failure.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import { parse as parseYaml } from 'yaml';
|
|
23
|
+
|
|
24
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
25
|
+
const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
26
|
+
const _REPO_ROOT = path.resolve(
|
|
27
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
28
|
+
...(_IN_BUNDLE ? ['..', '..'] : ['..', '..', '..']),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const HOST_LOWERING_PATH = path.join(_REPO_ROOT, 'src', 'scripts', 'hooks', 'host_lowering.yaml');
|
|
32
|
+
|
|
33
|
+
/** Provenance of a row. Every field may be null; `expires` may not. */
|
|
34
|
+
export interface VerifiedBlock {
|
|
35
|
+
docs_at: string | null;
|
|
36
|
+
docs_url: string | null;
|
|
37
|
+
probe_at: string | null;
|
|
38
|
+
host_version: string | null;
|
|
39
|
+
expires: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SlotRow {
|
|
43
|
+
/** Host-native event name(s) for this slot, in emission order. */
|
|
44
|
+
native: string[];
|
|
45
|
+
/** Exit code the host honours as a refusal here, or null if unestablished. */
|
|
46
|
+
block_exit: number | null;
|
|
47
|
+
/** Entry-shape-specific extra (gemini's per-binding matcher). */
|
|
48
|
+
matcher?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SurfaceRow {
|
|
52
|
+
entry_shape: string;
|
|
53
|
+
json_shape: string;
|
|
54
|
+
fail_policy: string;
|
|
55
|
+
timeout_unit: string;
|
|
56
|
+
timeout_default: number | null;
|
|
57
|
+
verified: VerifiedBlock | null;
|
|
58
|
+
/** Insertion-ordered: bridge entries are written in this order. */
|
|
59
|
+
slots: Map<string, SlotRow>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type HostLowering = Map<string, Map<string, SurfaceRow>>;
|
|
63
|
+
|
|
64
|
+
/** The surface every row carries today — see the table header. */
|
|
65
|
+
export const DEFAULT_SURFACE = 'any';
|
|
66
|
+
|
|
67
|
+
function _asString(v: unknown): string | null {
|
|
68
|
+
if (v === null || v === undefined) return null;
|
|
69
|
+
// A bare `2027-09-06` parses as a Date under YAML 1.1 timestamp rules.
|
|
70
|
+
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
|
71
|
+
return String(v);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _parse(text: string): HostLowering {
|
|
75
|
+
const raw = parseYaml(text) as Record<string, unknown> | null;
|
|
76
|
+
const hostsRaw = raw?.['hosts'];
|
|
77
|
+
if (typeof hostsRaw !== 'object' || hostsRaw === null) {
|
|
78
|
+
throw new Error('host_lowering.yaml: missing `hosts:` map');
|
|
79
|
+
}
|
|
80
|
+
const out: HostLowering = new Map();
|
|
81
|
+
for (const [host, hv] of Object.entries(hostsRaw as Record<string, unknown>)) {
|
|
82
|
+
const surfacesRaw = (hv as Record<string, unknown> | null)?.['surfaces'];
|
|
83
|
+
if (typeof surfacesRaw !== 'object' || surfacesRaw === null) {
|
|
84
|
+
throw new Error(`host_lowering.yaml: ${host} has no \`surfaces:\` map`);
|
|
85
|
+
}
|
|
86
|
+
const surfaces = new Map<string, SurfaceRow>();
|
|
87
|
+
for (const [surface, sv] of Object.entries(surfacesRaw as Record<string, unknown>)) {
|
|
88
|
+
const s = sv as Record<string, unknown>;
|
|
89
|
+
const verifiedRaw = s['verified'] as Record<string, unknown> | null | undefined;
|
|
90
|
+
let verified: VerifiedBlock | null = null;
|
|
91
|
+
if (verifiedRaw) {
|
|
92
|
+
const expires = _asString(verifiedRaw['expires']);
|
|
93
|
+
if (!expires) {
|
|
94
|
+
throw new Error(`host_lowering.yaml: ${host}/${surface} verified block has no \`expires\``);
|
|
95
|
+
}
|
|
96
|
+
verified = {
|
|
97
|
+
docs_at: _asString(verifiedRaw['docs_at']),
|
|
98
|
+
docs_url: _asString(verifiedRaw['docs_url']),
|
|
99
|
+
probe_at: _asString(verifiedRaw['probe_at']),
|
|
100
|
+
host_version: _asString(verifiedRaw['host_version']),
|
|
101
|
+
expires,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const slots = new Map<string, SlotRow>();
|
|
105
|
+
for (const [slot, rvRaw] of Object.entries((s['slots'] ?? {}) as Record<string, unknown>)) {
|
|
106
|
+
const rv = rvRaw as Record<string, unknown>;
|
|
107
|
+
const nativeRaw = rv['native'];
|
|
108
|
+
const native = Array.isArray(nativeRaw) ? nativeRaw.map(String) : [String(nativeRaw)];
|
|
109
|
+
const be = rv['block_exit'];
|
|
110
|
+
const row: SlotRow = {
|
|
111
|
+
native,
|
|
112
|
+
block_exit: typeof be === 'number' ? be : null,
|
|
113
|
+
};
|
|
114
|
+
if (typeof rv['matcher'] === 'string') row.matcher = rv['matcher'];
|
|
115
|
+
slots.set(slot, row);
|
|
116
|
+
}
|
|
117
|
+
surfaces.set(surface, {
|
|
118
|
+
entry_shape: String(s['entry_shape'] ?? 'none'),
|
|
119
|
+
json_shape: String(s['json_shape'] ?? 'none'),
|
|
120
|
+
fail_policy: String(s['fail_policy'] ?? 'discard'),
|
|
121
|
+
timeout_unit: String(s['timeout_unit'] ?? 'unknown'),
|
|
122
|
+
timeout_default: typeof s['timeout_default'] === 'number' ? s['timeout_default'] : null,
|
|
123
|
+
verified,
|
|
124
|
+
slots,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
out.set(host, surfaces);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let _cache: HostLowering | null = null;
|
|
133
|
+
|
|
134
|
+
/** Parse the table once per process. Throws on a missing or malformed file. */
|
|
135
|
+
export function loadHostLowering(): HostLowering {
|
|
136
|
+
if (_cache === null) _cache = _parse(fs.readFileSync(HOST_LOWERING_PATH, 'utf-8'));
|
|
137
|
+
return _cache;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Test seam — drop the memoized table so a fixture can be loaded instead. */
|
|
141
|
+
export function _resetHostLoweringCache(table?: HostLowering): void {
|
|
142
|
+
_cache = table ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse a fixture table without touching the committed file. */
|
|
146
|
+
export function parseHostLowering(text: string): HostLowering {
|
|
147
|
+
return _parse(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function surfaceRow(
|
|
151
|
+
host: string,
|
|
152
|
+
surface: string = DEFAULT_SURFACE,
|
|
153
|
+
table: HostLowering = loadHostLowering(),
|
|
154
|
+
): SurfaceRow | null {
|
|
155
|
+
return table.get(host)?.get(surface) ?? table.get(host)?.get(DEFAULT_SURFACE) ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Is the row's provenance still current?
|
|
160
|
+
*
|
|
161
|
+
* A row past `expires` is treated exactly as `verified: null` — it cannot
|
|
162
|
+
* carry a blocking binding. Comparison is on the calendar date so the answer
|
|
163
|
+
* does not depend on the runner's clock time of day.
|
|
164
|
+
*/
|
|
165
|
+
export function isVerifiedNow(row: SurfaceRow | null, today: string = new Date().toISOString().slice(0, 10)): boolean {
|
|
166
|
+
if (row?.verified == null) return false;
|
|
167
|
+
return row.verified.expires >= today;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Hosts carrying an unexpired `verified` block. */
|
|
171
|
+
export function verifiedPlatforms(table: HostLowering = loadHostLowering()): ReadonlySet<string> {
|
|
172
|
+
const out = new Set<string>();
|
|
173
|
+
for (const [host, surfaces] of table) {
|
|
174
|
+
for (const row of surfaces.values()) {
|
|
175
|
+
if (isVerifiedNow(row)) {
|
|
176
|
+
out.add(host);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Exit code this host honours as a refusal on `slot`, or null. */
|
|
185
|
+
export function blockExitFor(host: string, slot: string, table: HostLowering = loadHostLowering()): number | null {
|
|
186
|
+
const row = surfaceRow(host, DEFAULT_SURFACE, table);
|
|
187
|
+
if (!isVerifiedNow(row)) return null;
|
|
188
|
+
return row?.slots.get(slot)?.block_exit ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One bridge binding: an agent-config slot lowered onto one native event. */
|
|
192
|
+
export interface HostBinding {
|
|
193
|
+
slot: string;
|
|
194
|
+
native: string;
|
|
195
|
+
matcher: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Ordered bridge bindings for a host.
|
|
200
|
+
*
|
|
201
|
+
* Order is the table's `slots:` insertion order and is part of the generated
|
|
202
|
+
* output — the emitted bridge JSON is keyed by native event, so re-ordering
|
|
203
|
+
* changes bytes. Never sort this.
|
|
204
|
+
*/
|
|
205
|
+
export function hostBindings(host: string, table: HostLowering = loadHostLowering()): HostBinding[] {
|
|
206
|
+
const row = surfaceRow(host, DEFAULT_SURFACE, table);
|
|
207
|
+
if (!row) return [];
|
|
208
|
+
const out: HostBinding[] = [];
|
|
209
|
+
for (const [slot, sr] of row.slots) {
|
|
210
|
+
for (const native of sr.native) {
|
|
211
|
+
out.push({ slot, native, matcher: sr.matcher ?? '' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# host_lowering — how each host lowers a dispatcher verdict into its own hook
|
|
2
|
+
# contract, and what this tree has actually established about that lowering.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS FILE EXISTS. Before it, five facts about a host lived in five
|
|
5
|
+
# places: `VERIFIED_PLATFORMS` and the block-capable event set in
|
|
6
|
+
# `hooks/host_semantics.ts`, five private per-host binding constants in
|
|
7
|
+
# `install.ts`, and prose in half a dozen docs. Three rounds of corrections to
|
|
8
|
+
# that prose did not stop it drifting, because prose has no single reader. This
|
|
9
|
+
# table is the single reader.
|
|
10
|
+
#
|
|
11
|
+
# WHAT A ROW IS. A row is keyed `(host, surface, slot)`. It describes what this
|
|
12
|
+
# package does and what this package has established — never what the host is
|
|
13
|
+
# capable of. `verified: null` means nobody established anything; it does NOT
|
|
14
|
+
# mean the host cannot enforce.
|
|
15
|
+
#
|
|
16
|
+
# THE `surface` AXIS IS PRESENT AND DEGENERATE, DELIBERATELY. Every row today
|
|
17
|
+
# is `any`, because nothing in this tree has measured an IDE-versus-CLI
|
|
18
|
+
# difference in hook lowering on any host. The key carries the dimension so a
|
|
19
|
+
# future measurement lands as a new row rather than as a rewrite; inventing
|
|
20
|
+
# `ide:`/`cli:` rows nobody measured is the defect this roadmap exists to stop.
|
|
21
|
+
#
|
|
22
|
+
# CONSUMED BY:
|
|
23
|
+
# - src/scripts/hooks/host_semantics.ts (VERIFIED_PLATFORMS, isBlockCapable, emission shape)
|
|
24
|
+
# - src/scripts/_lib/host_bindings.ts (per-host bridge bindings + the install smoke probe)
|
|
25
|
+
# - src/scripts/lint_hook_manifest.ts (schema + expiry gate)
|
|
26
|
+
#
|
|
27
|
+
# COLUMNS
|
|
28
|
+
# block_exit exit code this host honours as a refusal on that slot;
|
|
29
|
+
# `null` = this tree has not established that it honours one.
|
|
30
|
+
# json_shape structured-output envelope the dispatcher may emit.
|
|
31
|
+
# entry_shape shape of the bridge entry the installer writes.
|
|
32
|
+
# fail_policy `propagate` = the dispatcher's exit code reaches the host;
|
|
33
|
+
# `discard` = the trampoline exits 0 regardless.
|
|
34
|
+
# timeout_unit unit the host's own hook-timeout setting is expressed in.
|
|
35
|
+
# timeout_default host default for that timeout. `null` = not established here.
|
|
36
|
+
# verified provenance of the row, or `null`. `expires` is mandatory
|
|
37
|
+
# when the block is present — see lint_hook_manifest.
|
|
38
|
+
# slots ORDERED. Bridge entries are written in this order, so the
|
|
39
|
+
# order is part of the generated output and must not be
|
|
40
|
+
# sorted. `native` may be a list where one slot has two
|
|
41
|
+
# native events (cline session_start).
|
|
42
|
+
schema_version: 1
|
|
43
|
+
|
|
44
|
+
hosts:
|
|
45
|
+
claude:
|
|
46
|
+
surfaces:
|
|
47
|
+
any:
|
|
48
|
+
entry_shape: claude-settings-hooks
|
|
49
|
+
json_shape: hook-specific-output
|
|
50
|
+
fail_policy: propagate
|
|
51
|
+
timeout_unit: seconds
|
|
52
|
+
timeout_default: null
|
|
53
|
+
# Transcribed from the behaviour host_semantics.ts already implements —
|
|
54
|
+
# this row is a description of the status quo, not a new claim.
|
|
55
|
+
# docs_at/host_version are null because the original read was never
|
|
56
|
+
# dated; that absence is the record, and the lint reports it.
|
|
57
|
+
verified:
|
|
58
|
+
docs_at: null
|
|
59
|
+
docs_url: https://docs.claude.com/en/docs/claude-code/hooks
|
|
60
|
+
probe_at: null
|
|
61
|
+
host_version: null
|
|
62
|
+
expires: 2027-09-06
|
|
63
|
+
slots:
|
|
64
|
+
session_start: { native: SessionStart, block_exit: null }
|
|
65
|
+
session_end: { native: SessionEnd, block_exit: null }
|
|
66
|
+
stop: { native: Stop, block_exit: 2 }
|
|
67
|
+
user_prompt_submit: { native: UserPromptSubmit, block_exit: 2 }
|
|
68
|
+
pre_tool_use: { native: PreToolUse, block_exit: 2 }
|
|
69
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
70
|
+
pre_compact: { native: PreCompact, block_exit: null }
|
|
71
|
+
subagent_start: { native: SubagentStart, block_exit: null }
|
|
72
|
+
subagent_stop: { native: SubagentStop, block_exit: null }
|
|
73
|
+
|
|
74
|
+
augment:
|
|
75
|
+
surfaces:
|
|
76
|
+
any:
|
|
77
|
+
entry_shape: augment-settings-hooks
|
|
78
|
+
json_shape: none
|
|
79
|
+
# augment-dispatcher.sh exits 0 unconditionally: a verdict reaches the
|
|
80
|
+
# trampoline and stops there.
|
|
81
|
+
fail_policy: discard
|
|
82
|
+
timeout_unit: unknown
|
|
83
|
+
timeout_default: null
|
|
84
|
+
verified: null
|
|
85
|
+
slots:
|
|
86
|
+
session_start: { native: SessionStart, block_exit: null }
|
|
87
|
+
session_end: { native: SessionEnd, block_exit: null }
|
|
88
|
+
stop: { native: Stop, block_exit: null }
|
|
89
|
+
pre_tool_use: { native: PreToolUse, block_exit: null }
|
|
90
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
91
|
+
|
|
92
|
+
cursor:
|
|
93
|
+
surfaces:
|
|
94
|
+
any:
|
|
95
|
+
entry_shape: cursor-hooks-json
|
|
96
|
+
json_shape: none
|
|
97
|
+
fail_policy: propagate
|
|
98
|
+
timeout_unit: unknown
|
|
99
|
+
timeout_default: null
|
|
100
|
+
verified: null
|
|
101
|
+
slots:
|
|
102
|
+
session_start: { native: sessionStart, block_exit: null }
|
|
103
|
+
session_end: { native: sessionEnd, block_exit: null }
|
|
104
|
+
stop: { native: stop, block_exit: null }
|
|
105
|
+
user_prompt_submit: { native: beforeSubmitPrompt, block_exit: null }
|
|
106
|
+
post_tool_use: { native: postToolUse, block_exit: null }
|
|
107
|
+
|
|
108
|
+
cline:
|
|
109
|
+
surfaces:
|
|
110
|
+
any:
|
|
111
|
+
entry_shape: cline-per-event-script
|
|
112
|
+
json_shape: none
|
|
113
|
+
# The generated per-event script swallows the dispatcher's exit code
|
|
114
|
+
# and prints `{}`.
|
|
115
|
+
fail_policy: discard
|
|
116
|
+
timeout_unit: unknown
|
|
117
|
+
timeout_default: null
|
|
118
|
+
verified: null
|
|
119
|
+
slots:
|
|
120
|
+
session_start: { native: [TaskStart, TaskResume], block_exit: null }
|
|
121
|
+
session_end: { native: TaskComplete, block_exit: null }
|
|
122
|
+
stop: { native: TaskCancel, block_exit: null }
|
|
123
|
+
user_prompt_submit: { native: UserPromptSubmit, block_exit: null }
|
|
124
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
125
|
+
|
|
126
|
+
windsurf:
|
|
127
|
+
surfaces:
|
|
128
|
+
any:
|
|
129
|
+
entry_shape: windsurf-hooks-json
|
|
130
|
+
json_shape: none
|
|
131
|
+
fail_policy: propagate
|
|
132
|
+
timeout_unit: unknown
|
|
133
|
+
timeout_default: null
|
|
134
|
+
verified: null
|
|
135
|
+
slots:
|
|
136
|
+
session_start: { native: post_setup_worktree, block_exit: null }
|
|
137
|
+
user_prompt_submit: { native: pre_user_prompt, block_exit: null }
|
|
138
|
+
stop: { native: post_cascade_response, block_exit: null }
|
|
139
|
+
|
|
140
|
+
gemini:
|
|
141
|
+
surfaces:
|
|
142
|
+
any:
|
|
143
|
+
entry_shape: gemini-settings-hooks
|
|
144
|
+
json_shape: none
|
|
145
|
+
fail_policy: propagate
|
|
146
|
+
timeout_unit: unknown
|
|
147
|
+
timeout_default: null
|
|
148
|
+
# A live deny probe was attempted on 2026-09-06 against Gemini CLI
|
|
149
|
+
# 0.50.0 in an isolated scratch workspace and could not run: the CLI
|
|
150
|
+
# aborts with IneligibleTierError before any turn begins, so no tool
|
|
151
|
+
# call and therefore no BeforeTool hook is reachable. An unrunnable
|
|
152
|
+
# probe establishes nothing, so this stays null.
|
|
153
|
+
verified: null
|
|
154
|
+
slots:
|
|
155
|
+
session_start: { native: SessionStart, block_exit: null, matcher: "" }
|
|
156
|
+
session_end: { native: SessionEnd, block_exit: null, matcher: "" }
|
|
157
|
+
stop: { native: AfterAgent, block_exit: null, matcher: "" }
|
|
158
|
+
user_prompt_submit: { native: BeforeAgent, block_exit: null, matcher: "" }
|
|
159
|
+
post_tool_use: { native: AfterTool, block_exit: null, matcher: ".*" }
|
|
160
|
+
|
|
161
|
+
cowork:
|
|
162
|
+
surfaces:
|
|
163
|
+
any:
|
|
164
|
+
entry_shape: none
|
|
165
|
+
json_shape: none
|
|
166
|
+
# cowork-dispatcher.sh exits 0 unconditionally, and the host is
|
|
167
|
+
# upstream-blocked from reading any settings source this package writes.
|
|
168
|
+
fail_policy: discard
|
|
169
|
+
timeout_unit: unknown
|
|
170
|
+
timeout_default: null
|
|
171
|
+
verified: null
|
|
172
|
+
slots: {}
|
|
173
|
+
|
|
174
|
+
copilot:
|
|
175
|
+
surfaces:
|
|
176
|
+
any:
|
|
177
|
+
entry_shape: none
|
|
178
|
+
json_shape: none
|
|
179
|
+
fail_policy: discard
|
|
180
|
+
timeout_unit: unknown
|
|
181
|
+
timeout_default: null
|
|
182
|
+
# `fallback_only: true` in hook_manifest.yaml. This package binds
|
|
183
|
+
# nothing here and has measured nothing here.
|
|
184
|
+
verified: null
|
|
185
|
+
slots: {}
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* this module exists to remove.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import { blockExitFor, DEFAULT_SURFACE, loadHostLowering, surfaceRow, verifiedPlatforms } from "./host_lowering.js";
|
|
38
|
+
|
|
37
39
|
/** Internal severity, mirroring dispatch_hook's EXIT_ALLOW / BLOCK / WARN. */
|
|
38
40
|
export type Severity = "allow" | "warn" | "block";
|
|
39
41
|
|
|
@@ -50,53 +52,38 @@ export interface Emission {
|
|
|
50
52
|
/**
|
|
51
53
|
* Hosts with a documented, verified native hook contract. Anything absent
|
|
52
54
|
* from this set falls through to the legacy pass-through.
|
|
55
|
+
*
|
|
56
|
+
* Read from `host_lowering.yaml`: a host is here when it carries a `verified`
|
|
57
|
+
* block whose `expires` has not passed. An expired row is therefore
|
|
58
|
+
* indistinguishable from `verified: null` at this boundary, which is what
|
|
59
|
+
* makes the expiry a real control rather than a comment.
|
|
53
60
|
*/
|
|
54
|
-
export const VERIFIED_PLATFORMS: ReadonlySet<string> =
|
|
55
|
-
|
|
56
|
-
/** Internal event name -> Claude Code `hookEventName` for JSON output. */
|
|
57
|
-
const CLAUDE_HOOK_EVENT_NAME: Record<string, string> = {
|
|
58
|
-
pre_tool_use: "PreToolUse",
|
|
59
|
-
post_tool_use: "PostToolUse",
|
|
60
|
-
user_prompt_submit: "UserPromptSubmit",
|
|
61
|
-
session_start: "SessionStart",
|
|
62
|
-
session_end: "SessionEnd",
|
|
63
|
-
stop: "Stop",
|
|
64
|
-
pre_compact: "PreCompact",
|
|
65
|
-
subagent_start: "SubagentStart",
|
|
66
|
-
subagent_stop: "SubagentStop",
|
|
67
|
-
};
|
|
61
|
+
export const VERIFIED_PLATFORMS: ReadonlySet<string> = verifiedPlatforms();
|
|
68
62
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* degrades to visible advisory context instead of a silent no-op.
|
|
74
|
-
*/
|
|
75
|
-
const CLAUDE_BLOCK_CAPABLE_EVENTS: ReadonlySet<string> = new Set([
|
|
76
|
-
"pre_tool_use",
|
|
77
|
-
"user_prompt_submit",
|
|
78
|
-
"stop",
|
|
79
|
-
]);
|
|
63
|
+
/** Internal event name -> host-native event name, for structured output. */
|
|
64
|
+
function nativeEventName(platform: string, event: string): string {
|
|
65
|
+
return surfaceRow(platform, DEFAULT_SURFACE, loadHostLowering())?.slots.get(event)?.native[0] ?? event;
|
|
66
|
+
}
|
|
80
67
|
|
|
81
68
|
/**
|
|
82
69
|
* Does this (platform, event) pair let a hook actually REFUSE the action?
|
|
83
70
|
*
|
|
84
71
|
* Exported because a second consumer now needs the same fact and must not
|
|
85
72
|
* re-derive it: `dispatch_hook`'s stdin-read-failure policy denies only where a
|
|
86
|
-
* deny is honoured (`b-stdin-read-failure-policy`, option (c)). A copy
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
73
|
+
* deny is honoured (`b-stdin-read-failure-policy`, option (c)). A second copy
|
|
74
|
+
* of the block-capable slot set would be a copy that drifts, and the drift
|
|
75
|
+
* direction here is the dangerous one: a stale copy would deny on a slot where
|
|
76
|
+
* the deny is discarded, refusing nothing while looking like enforcement. Both
|
|
77
|
+
* consumers now read `host_lowering.yaml`, so there is one set to drift from.
|
|
90
78
|
*
|
|
91
79
|
* An unverified platform is NOT block-capable by this predicate, and that is
|
|
92
80
|
* deliberate rather than incidental: `emitFor` hands such a platform its legacy
|
|
93
81
|
* exit code verbatim, so this tree has no evidence its host honours a deny at
|
|
94
|
-
* all. Claiming capability without evidence is the over-claim the hook-coverage
|
|
82
|
+
* all. A row whose `verified` block has expired reads as unverified here. Claiming capability without evidence is the over-claim the hook-coverage
|
|
95
83
|
* corrections in this estate exist to remove.
|
|
96
84
|
*/
|
|
97
85
|
export function isBlockCapable(platform: string, event: string): boolean {
|
|
98
|
-
|
|
99
|
-
return CLAUDE_BLOCK_CAPABLE_EVENTS.has(event);
|
|
86
|
+
return blockExitFor(platform, event) !== null;
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
function _joinReasons(reasons: readonly string[]): string {
|
|
@@ -112,12 +99,70 @@ function _joinReasons(reasons: readonly string[]): string {
|
|
|
112
99
|
* advisory path) always intended.
|
|
113
100
|
*/
|
|
114
101
|
export function claudeAdditionalContext(event: string, text: string): string {
|
|
115
|
-
const hookEventName =
|
|
102
|
+
const hookEventName = nativeEventName("claude", event);
|
|
116
103
|
return `${JSON.stringify({
|
|
117
104
|
hookSpecificOutput: { hookEventName, additionalContext: text },
|
|
118
105
|
})}\n`;
|
|
119
106
|
}
|
|
120
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A host permission verdict for a `pre_tool_use` call.
|
|
110
|
+
*
|
|
111
|
+
* Probed against the running host on 2026-09-06 (Claude Code **2.1.263**,
|
|
112
|
+
* `/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe`):
|
|
113
|
+
* the binary's own strings read `` `permissionDecision` - "allow", "deny", or
|
|
114
|
+
* "ask" (PreToolUse only) ``. The pin is the point — an unpinned capability
|
|
115
|
+
* claim rots the way the one it replaces did.
|
|
116
|
+
*/
|
|
117
|
+
export type PermissionDecision = "allow" | "deny" | "ask";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compose the concerns' permission verdicts into the one the host receives.
|
|
121
|
+
*
|
|
122
|
+
* ONE `ask` OR `deny` BEATS EVERY `allow`. This is the composition policy the
|
|
123
|
+
* host contract left undecided, and leaving it undecided is what stalled the
|
|
124
|
+
* emission path before: a dispatcher that reduces many concerns to one exit
|
|
125
|
+
* code has to say what happens when they disagree, and the only answer that
|
|
126
|
+
* cannot lower a floor is the most restrictive one.
|
|
127
|
+
*
|
|
128
|
+
* `null` means emit no permission field at all — nobody voted, so the legacy
|
|
129
|
+
* `additionalContext` envelope stands. That is deliberately distinct from
|
|
130
|
+
* `"allow"`: an absent field leaves the host's own permission machinery in
|
|
131
|
+
* charge, while an `allow` asserts this package looked at the call and has
|
|
132
|
+
* nothing to stop. Collapsing the two would turn "no concern matched this tool"
|
|
133
|
+
* into an affirmative grant, which is the widening failure the category-A
|
|
134
|
+
* classifier exists to prevent.
|
|
135
|
+
*/
|
|
136
|
+
export function composePermissionDecision(
|
|
137
|
+
verdicts: readonly PermissionDecision[],
|
|
138
|
+
): PermissionDecision | null {
|
|
139
|
+
if (verdicts.length === 0) return null;
|
|
140
|
+
if (verdicts.includes("deny")) return "deny";
|
|
141
|
+
if (verdicts.includes("ask")) return "ask";
|
|
142
|
+
return "allow";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build the Claude Code `hookSpecificOutput.permissionDecision` envelope.
|
|
147
|
+
*
|
|
148
|
+
* Exit 0 carries it, like every other JSON reply — the host parses stdout only
|
|
149
|
+
* on exit 0, so a decision emitted with any other exit code is discarded.
|
|
150
|
+
*/
|
|
151
|
+
export function claudePermissionDecision(
|
|
152
|
+
event: string,
|
|
153
|
+
decision: PermissionDecision,
|
|
154
|
+
reason: string,
|
|
155
|
+
): string {
|
|
156
|
+
const hookEventName = nativeEventName("claude", event);
|
|
157
|
+
return `${JSON.stringify({
|
|
158
|
+
hookSpecificOutput: {
|
|
159
|
+
hookEventName,
|
|
160
|
+
permissionDecision: decision,
|
|
161
|
+
permissionDecisionReason: reason,
|
|
162
|
+
},
|
|
163
|
+
})}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
/**
|
|
122
167
|
* Will `emitFor` actually put the reasons on a stream?
|
|
123
168
|
*
|
|
@@ -146,11 +191,25 @@ export function emissionCarriesReasons(platform: string, severity: Severity): bo
|
|
|
146
191
|
return severity !== "allow";
|
|
147
192
|
}
|
|
148
193
|
|
|
194
|
+
/** An explicit permission verdict the dispatcher wants the host to receive. */
|
|
195
|
+
export interface PermissionEmission {
|
|
196
|
+
decision: PermissionDecision;
|
|
197
|
+
reason: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
149
200
|
/**
|
|
150
201
|
* Translate (platform, event, severity, reasons) into a native emission.
|
|
151
202
|
*
|
|
152
203
|
* `legacyExit` is the internal reduced code; unverified platforms get it back
|
|
153
204
|
* verbatim so their behaviour is byte-identical to before this module landed.
|
|
205
|
+
*
|
|
206
|
+
* `permission` is emitted ONLY on a `pre_tool_use` event whose reduced severity
|
|
207
|
+
* is already `allow`. Both halves of that guard are load-bearing. The event
|
|
208
|
+
* guard is the host's — the field is documented "(PreToolUse only)". The
|
|
209
|
+
* severity guard is this package's: a `warn` or `block` reaching here means a
|
|
210
|
+
* concern had something to say, and an `allow` alongside it would be exactly
|
|
211
|
+
* the case `composePermissionDecision` refuses. So an allow can never travel
|
|
212
|
+
* with a finding, by construction rather than by the caller remembering.
|
|
154
213
|
*/
|
|
155
214
|
export function emitFor(
|
|
156
215
|
platform: string,
|
|
@@ -158,6 +217,7 @@ export function emitFor(
|
|
|
158
217
|
severity: Severity,
|
|
159
218
|
reasons: readonly string[],
|
|
160
219
|
legacyExit: number,
|
|
220
|
+
permission?: PermissionEmission | null,
|
|
161
221
|
): Emission {
|
|
162
222
|
if (!VERIFIED_PLATFORMS.has(platform)) {
|
|
163
223
|
return { exit: legacyExit, stdout: "", stderr: "" };
|
|
@@ -165,15 +225,23 @@ export function emitFor(
|
|
|
165
225
|
const reason = _joinReasons(reasons);
|
|
166
226
|
|
|
167
227
|
if (severity === "allow") {
|
|
228
|
+
if (permission && event === "pre_tool_use") {
|
|
229
|
+
return {
|
|
230
|
+
exit: 0,
|
|
231
|
+
stdout: claudePermissionDecision(event, permission.decision, permission.reason),
|
|
232
|
+
stderr: "",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
168
235
|
return { exit: 0, stdout: "", stderr: "" };
|
|
169
236
|
}
|
|
170
237
|
|
|
171
238
|
if (severity === "block") {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
239
|
+
const blockExit = blockExitFor(platform, event);
|
|
240
|
+
if (blockExit !== null) {
|
|
241
|
+
// The host's own refusal code + stderr: the ONLY documented way to
|
|
242
|
+
// make it refuse the action and feed the reason back to the model.
|
|
175
243
|
const label = reason || "blocked by agent-config hook policy";
|
|
176
|
-
return { exit:
|
|
244
|
+
return { exit: blockExit, stdout: "", stderr: `${label}\n` };
|
|
177
245
|
}
|
|
178
246
|
// Not block-capable (post_tool_use, session_start, …). Exit 2 here
|
|
179
247
|
// would discard stdout and still not block, so surface the reason as
|
|
@@ -93,6 +93,7 @@ import * as path from 'node:path';
|
|
|
93
93
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
94
94
|
|
|
95
95
|
import { hookSectionEnabled } from '../_lib/hook_settings.js';
|
|
96
|
+
import { detectSurface, readSurface, type Surface } from '../_lib/surface.js';
|
|
96
97
|
import {
|
|
97
98
|
RECORDED_EVENTS,
|
|
98
99
|
isJournalAvailable,
|
|
@@ -182,6 +183,8 @@ export interface JournalInput {
|
|
|
182
183
|
task_id: string | null;
|
|
183
184
|
prompt_id: string | null;
|
|
184
185
|
capability: string;
|
|
186
|
+
/** Closed vocabulary. The dispatcher resolves it; re-derived only as a fallback. */
|
|
187
|
+
surface: Surface;
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/**
|
|
@@ -208,6 +211,11 @@ export function reduceEnvelope(envelope: JsonObject): JournalInput | null {
|
|
|
208
211
|
task_id: firstString(envelope, payload, ['task_id', 'taskId']),
|
|
209
212
|
prompt_id: firstString(envelope, payload, ['prompt_id', 'promptId']),
|
|
210
213
|
capability: capabilityFor(event, envelope, payload),
|
|
214
|
+
// The dispatcher writes it into the envelope; a direct invocation
|
|
215
|
+
// (`agent-config …:hook < event.json`) has no dispatcher, so fall back
|
|
216
|
+
// to the same detector rather than recording a field the caller never
|
|
217
|
+
// had a chance to set.
|
|
218
|
+
surface: readSurface(envelope['surface']) === 'unknown' ? detectSurface(payload) : readSurface(envelope['surface']),
|
|
211
219
|
};
|
|
212
220
|
}
|
|
213
221
|
|
|
@@ -248,6 +256,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string): numb
|
|
|
248
256
|
task_id: input.task_id,
|
|
249
257
|
prompt_id: input.prompt_id,
|
|
250
258
|
capability: input.capability,
|
|
259
|
+
surface: input.surface,
|
|
251
260
|
});
|
|
252
261
|
});
|
|
253
262
|
} catch {
|
|
@@ -294,6 +303,7 @@ export function recordedFor(envelope: JsonValue, consumerRoot: string): SkipReas
|
|
|
294
303
|
task_id: input.task_id,
|
|
295
304
|
prompt_id: input.prompt_id,
|
|
296
305
|
capability: input.capability,
|
|
306
|
+
surface: input.surface,
|
|
297
307
|
});
|
|
298
308
|
});
|
|
299
309
|
} catch {
|