@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
@@ -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> = new Set(["claude"]);
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
- * Events where Claude Code honours exit 2 as a real block. Everywhere else
71
- * exit 2 either cannot block (PostToolUse: "the tool already ran";
72
- * SessionStart: "shows stderr to user only") or is far too blunt, so a block
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 of
87
- * `CLAUDE_BLOCK_CAPABLE_EVENTS` one module away is a copy that drifts, and the
88
- * drift direction here is the dangerous one: a stale copy would deny on a slot
89
- * where the deny is discarded, refusing nothing while looking like enforcement.
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
- if (!VERIFIED_PLATFORMS.has(platform)) return false;
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 = CLAUDE_HOOK_EVENT_NAME[event] ?? event;
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
- if (CLAUDE_BLOCK_CAPABLE_EVENTS.has(event)) {
173
- // Exit 2 + stderr: the ONLY documented way to make Claude Code
174
- // refuse the action and feed the reason back to the model.
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: 2, stdout: "", stderr: `${label}\n` };
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 {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * permission_gate — whether a `pre_tool_use` call gets an explicit
3
+ * `permissionDecision`, and which one.
4
+ *
5
+ * Four independent conditions, each able to withhold an allow on its own: the
6
+ * host offers the field on `pre_tool_use` only; the dispatcher's reduced
7
+ * severity must already be `allow`; no concern may have voted `ask` or `deny`;
8
+ * and the call itself must be category A. An allow is emitted only when all
9
+ * four hold, so widening any one of them is a visible edit rather than an
10
+ * emergent effect.
11
+ *
12
+ * Separate from `dispatch_hook` because that file is against the per-file
13
+ * source-size budget, and because the seam is real: the dispatcher runs
14
+ * concerns and reduces exit codes, this module reads the result and decides
15
+ * what the host is told about permission.
16
+ */
17
+ import { isCategoryA, type ToolInput } from "./category_a.js";
18
+ import {
19
+ composePermissionDecision,
20
+ type PermissionDecision,
21
+ type PermissionEmission,
22
+ } from "./host_semantics.js";
23
+
24
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
25
+ type JsonObject = { [k: string]: JsonValue };
26
+
27
+ function _isObject(v: unknown): v is JsonObject {
28
+ return typeof v === "object" && v !== null && !Array.isArray(v);
29
+ }
30
+
31
+ /** Mirrors `dispatch_hook.EXIT_ALLOW` / `EXIT_BLOCK`; imported would be a cycle. */
32
+ const EXIT_ALLOW = 0;
33
+ const EXIT_BLOCK = 1;
34
+
35
+ /** The host's `tool_name`, or `""` when the event carries none. */
36
+ export function _payload_tool_name(envelope: JsonObject): string {
37
+ const payload = envelope["payload"];
38
+ if (!_isObject(payload)) return "";
39
+ const raw = payload["tool_name"];
40
+ return typeof raw === "string" ? raw : "";
41
+ }
42
+
43
+ /** The host's `tool_input` object, or `{}` when the event carries none. */
44
+ export function _payload_tool_input(envelope: JsonObject): ToolInput {
45
+ const payload = envelope["payload"];
46
+ if (!_isObject(payload)) return {};
47
+ const raw = payload["tool_input"];
48
+ return _isObject(raw) ? (raw as ToolInput) : {};
49
+ }
50
+
51
+ /**
52
+ * The root every category-A path argument must resolve inside.
53
+ *
54
+ * The host's own `cwd` wins over `workspace_root` because the latter is the
55
+ * dispatcher's process cwd, which in a worktree is the parent checkout rather
56
+ * than the tree the user is working in — confining against it would admit a
57
+ * path outside the tree the call actually runs in.
58
+ */
59
+ export function _working_tree_root(envelope: JsonObject): string {
60
+ const payload = envelope["payload"];
61
+ if (_isObject(payload) && typeof payload["cwd"] === "string" && payload["cwd"].trim()) {
62
+ return payload["cwd"];
63
+ }
64
+ const workspace = envelope["workspace_root"];
65
+ return typeof workspace === "string" ? workspace : "";
66
+ }
67
+
68
+ /**
69
+ * One concern's permission verdict.
70
+ *
71
+ * A block is a deny. A concern may also say `{"decision":"ask"}` on stdout —
72
+ * the field has been recorded in the feedback file since v1 and read by
73
+ * nothing, so an advisory concern that wanted a confirmation got silence.
74
+ * `ask` is honoured regardless of the exit code: an advisory concern cannot
75
+ * block and must not have to in order to be heard. Everything else abstains,
76
+ * which composes as `allow`.
77
+ */
78
+ export function _concern_permission_verdict(
79
+ rc: number,
80
+ reply: JsonObject,
81
+ ): PermissionDecision {
82
+ if (rc === EXIT_BLOCK) return "deny";
83
+ const stated = String(reply["decision"] ?? "").trim().toLowerCase();
84
+ if (stated === "ask") return "ask";
85
+ if (stated === "deny") return "deny";
86
+ return "allow";
87
+ }
88
+
89
+ /**
90
+ * The permission field to hand the host, or `null` to keep the legacy envelope.
91
+ *
92
+ * Four independent conditions, each able to withhold the allow on its own:
93
+ * the host offers the field on `pre_tool_use` only; the reduced severity must
94
+ * already be `allow`; no concern may have voted `ask` or `deny`; and the call
95
+ * itself must be category A. An allow is emitted only when all four hold, so
96
+ * widening any one of them is a visible edit rather than an emergent effect.
97
+ */
98
+ export function _permission_for(
99
+ event: string,
100
+ envelope: JsonObject,
101
+ final_rc: number,
102
+ verdicts: readonly PermissionDecision[],
103
+ ): PermissionEmission | null {
104
+ if (event !== "pre_tool_use") return null;
105
+ if (final_rc !== EXIT_ALLOW) return null;
106
+ if (composePermissionDecision(verdicts) !== "allow") return null;
107
+ const tool_name = _payload_tool_name(envelope);
108
+ if (!isCategoryA(tool_name, _payload_tool_input(envelope), _working_tree_root(envelope))) {
109
+ return null;
110
+ }
111
+ return {
112
+ decision: "allow",
113
+ reason:
114
+ `agent-config: category-A ${tool_name} inside the working tree — ` +
115
+ `no concern gates it and none asked to.`,
116
+ };
117
+ }
@@ -45,7 +45,9 @@ const MANIFEST_PATH = path.join(
45
45
  "hook_manifest.yaml",
46
46
  );
47
47
 
48
- // (label, project-relative bridge path, install hint).
48
+ // (project-relative bridge path, one-line note shown when the bridge is not
49
+ // live). The note is printed verbatim, so a note that is not a command must
50
+ // not read as one.
49
51
  // Path may be a directory (cline) — existence => any file inside.
50
52
  //
51
53
  // Cowork has no project-scope bridge path: the Claude desktop app's
@@ -58,17 +60,17 @@ const MANIFEST_PATH = path.join(
58
60
  // Once upstream lands the fix and a stable settings location is
59
61
  // documented, swap the empty path here for that location.
60
62
  export const PLATFORM_BRIDGES: Record<string, [string, string]> = {
61
- augment: [".augment/settings.json", "src/scripts/install.py"],
62
- claude: [".claude/settings.json", "src/scripts/install.py"],
63
+ augment: [".augment/settings.json", "run `agent-config install`"],
64
+ claude: [".claude/settings.json", "run `agent-config install`"],
63
65
  cowork: [
64
66
  "",
65
67
  "upstream-blocked: anthropics/claude-code#40495 + #27398 (settings.json ignored in Cowork sandbox)",
66
68
  ],
67
- cursor: [".cursor/hooks.json", "src/scripts/install.py"],
68
- cline: [".clinerules/hooks", "src/scripts/install.py"],
69
- windsurf: [".windsurf/hooks.json", "src/scripts/install.py"],
70
- gemini: [".gemini/settings.json", "src/scripts/install.py"],
71
- copilot: ["", "rule-only fallback (no hook surface)"],
69
+ cursor: [".cursor/hooks.json", "run `agent-config install`"],
70
+ cline: [".clinerules/hooks", "run `agent-config install`"],
71
+ windsurf: [".windsurf/hooks.json", "run `agent-config install`"],
72
+ gemini: [".gemini/settings.json", "run `agent-config install`"],
73
+ copilot: ["", "rule-only fallback — this package binds no hook here"],
72
74
  };
73
75
 
74
76
  export interface PlatformRow {
@@ -180,11 +182,11 @@ export function _render_table(matrix: StatusMatrix): string {
180
182
  lines.push(` ${event.padEnd(22)} → ${concerns}`);
181
183
  }
182
184
  if (row.hint) {
183
- lines.push(` hint: run ${row.hint}`);
185
+ lines.push(` hint: ${row.hint}`);
184
186
  }
185
187
  }
186
188
  lines.push("");
187
- lines.push("Source of truth: scripts/hook_manifest.yaml");
189
+ lines.push("Source of truth: src/scripts/hook_manifest.yaml");
188
190
  lines.push("Contract: docs/contracts/hook-architecture-v1.md");
189
191
  return lines.join("\n");
190
192
  }