@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.
Files changed (184) 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 +207 -77
  5. package/README.md +72 -150
  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/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -4,7 +4,7 @@
4
4
  # Replaces the four per-concern trampolines (augment-chat-history.sh,
5
5
  # augment-roadmap-progress.sh, augment-onboarding-gate.sh,
6
6
  # augment-context-hygiene.sh). One script, dispatched per (platform, event)
7
- # tuple via scripts/hooks/dispatch_hook.py reading scripts/hook_manifest.yaml.
7
+ # tuple via src/scripts/hooks/dispatch_hook.ts reading src/scripts/hook_manifest.yaml.
8
8
  #
9
9
  # Augment requires hook scripts to use the .sh extension and live at user
10
10
  # scope (~/.augment/hooks/) — same constraint as the legacy trampolines.
@@ -34,7 +34,7 @@ import * as fs from 'node:fs';
34
34
  import { fileURLToPath, pathToFileURL } from 'node:url';
35
35
  import * as path from 'node:path';
36
36
  import { readHookStdin } from './hook_stdin.js';
37
- import { substitutionPayloads } from './block_unauthorized_git.js';
37
+ import { substitutionPayloads } from './git_command_classifier.js';
38
38
 
39
39
  const _HERE = fileURLToPath(import.meta.url);
40
40
 
@@ -400,7 +400,7 @@ function _is_blocked(git_tokens: string[]): [boolean, string] {
400
400
  * genuinely unparseable command that DOES invoke git. So the test moves from
401
401
  * "the string contains git" to "some command position is git", with heredoc
402
402
  * bodies removed first — the same data-not-command distinction
403
- * `block_unauthorized_git` already makes.
403
+ * `git_command_classifier` already makes.
404
404
  *
405
405
  * Deliberately regex-shaped rather than tokenised: shlex has already failed by
406
406
  * the time this runs, so no reliable token stream exists. It therefore stays
@@ -450,7 +450,7 @@ export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
450
450
  * `_git_base` never sees git. The more parseable the command, the less it was
451
451
  * protected. Classification inside the payload is by command position, so a
452
452
  * substitution that INVOKES git is checked and one that merely names it in an
453
- * argument is not — matching what `block_unauthorized_git` does one level up.
453
+ * argument is not — matching what `git_command_classifier` does one level up.
454
454
  *
455
455
  * Heredoc bodies are removed before extraction: a backtick or `$(` inside a
456
456
  * commit message being written to a file is data, and feeding it back through
@@ -0,0 +1,291 @@
1
+ /**
2
+ * category_a — which `pre_tool_use` calls this package is willing to hand the
3
+ * host an explicit `permissionDecision: allow` for.
4
+ *
5
+ * WHY THIS EXISTS. `host_semantics` built exactly one envelope shape, so a
6
+ * call this package does not gate at all still cost the user a host
7
+ * confirmation. `permissionDecision` is offered by the host contract and was
8
+ * never emitted, so the prompt is ours to remove — but only for calls where
9
+ * "we do not gate this" is a fact about the call rather than about our
10
+ * coverage. That is what category A names: reads, navigation, build, test and
11
+ * lint inside the working tree with no consequence operation attached.
12
+ *
13
+ * THE CLASSIFIER IS AN ALLOWLIST AND MUST STAY ONE. A denylist answers "is
14
+ * this one of the bad shapes I thought of", so every shape nobody thought of
15
+ * would be auto-allowed. Everything below answers the opposite question, and an
16
+ * unrecognised call is not category A. The failure direction of a bug here is
17
+ * then a prompt that did not go away, never a confirmation that was skipped.
18
+ *
19
+ * WHAT AN ALLOW DOES NOT DO. It never overrides a verdict from this package:
20
+ * composition lives in `host_semantics.composePermissionDecision`, where one
21
+ * `ask` or `deny` beats every `allow`, and the dispatcher reaches the allow
22
+ * path only when the reduced severity is already `allow`. It is also not a
23
+ * grant — the host's own strings record `permissionDecision=allow ignored: a
24
+ * confined session takes grants only from its command line`.
25
+ */
26
+ import * as path from "node:path";
27
+
28
+ /** A JSON-ish tool input as it arrives in the dispatcher envelope. */
29
+ export type ToolInput = Record<string, unknown>;
30
+
31
+ /**
32
+ * Tools whose every documented effect is a read. `WebFetch` and `WebSearch`
33
+ * are deliberately absent — they reach the network, which is the egress leg of
34
+ * the lethal trifecta and not something to auto-allow.
35
+ */
36
+ const READ_ONLY_TOOLS: ReadonlySet<string> = new Set([
37
+ "Read",
38
+ "Glob",
39
+ "Grep",
40
+ "NotebookRead",
41
+ "LS",
42
+ "TodoRead",
43
+ ]);
44
+
45
+ /** Per-tool input keys that carry a filesystem path needing confinement. */
46
+ const PATH_KEYS: readonly string[] = [
47
+ "file_path",
48
+ "notebook_path",
49
+ "path",
50
+ "directory",
51
+ ];
52
+
53
+ /**
54
+ * Any of these in a Bash command string disqualifies it before the argv is
55
+ * looked at. Chaining, substitution, redirection and backgrounding all let a
56
+ * safe head token carry an arbitrary second command, and a classifier that
57
+ * tries to parse past them is a shell parser with a security boundary attached.
58
+ * Refusing the whole shape is both smaller and correct.
59
+ */
60
+ const SHELL_METACHARACTERS = /[;&|`$><\n\r(){}\\]/;
61
+
62
+ /**
63
+ * Words naming a consequence operation, matched as whole tokens anywhere in
64
+ * the command. Defence in depth: the metacharacter rule already prevents a
65
+ * second command, so a hit here means the FIRST command is consequential.
66
+ */
67
+ const CONSEQUENCE_WORDS: ReadonlySet<string> = new Set([
68
+ "push", "publish", "deploy", "release", "merge", "rebase", "reset",
69
+ "revert", "restore", "checkout", "commit", "tag", "fetch", "pull", "clone",
70
+ "rm", "rmdir", "unlink", "mv", "cp", "chmod", "chown", "kill", "sudo",
71
+ "curl", "wget", "ssh", "scp", "rsync", "nc", "telnet",
72
+ "drop", "truncate", "delete", "insert", "migrate", "seed", "restore-db",
73
+ "install", "uninstall", "upgrade", "prune", "clean", "apply", "destroy",
74
+ "terraform", "kubectl", "helm", "aws", "gcloud", "az", "docker", "podman",
75
+ "systemctl", "launchctl", "crontab", "mail", "sendmail",
76
+ ]);
77
+
78
+ /** Read-only `git` subcommands; every consequential one is absent by design. */
79
+ const GIT_READ_SUBCOMMANDS: ReadonlySet<string> = new Set([
80
+ "status", "log", "diff", "show", "branch", "blame", "describe",
81
+ "shortlog", "rev-parse", "ls-files", "ls-tree", "cat-file", "whatchanged",
82
+ ]);
83
+
84
+ /**
85
+ * Head tokens that cannot write outside their arguments and whose named
86
+ * operation is a read, a navigation, a build, a test or a lint.
87
+ *
88
+ * `node`, `python`, `ruby`, `php` and `sh` are deliberately absent: their named
89
+ * operation is "run this program", which establishes nothing about what the
90
+ * program does. A test runner is admitted by name because the name IS the
91
+ * operation.
92
+ */
93
+ const SAFE_HEADS: ReadonlySet<string> = new Set([
94
+ "ls", "pwd", "cat", "head", "tail", "wc", "file", "stat", "find",
95
+ "grep", "rg", "tree", "du", "df", "which", "basename", "dirname",
96
+ "realpath", "sort", "uniq", "cut", "diff", "cmp", "date", "echo",
97
+ "tsc", "eslint", "prettier", "biome", "stylelint",
98
+ "vitest", "jest", "mocha", "pytest", "mypy", "ruff", "flake8", "black",
99
+ "phpstan", "phpunit", "psalm", "pest", "rspec", "rubocop",
100
+ "golangci-lint", "shellcheck", "markdownlint", "hadolint",
101
+ ]);
102
+
103
+ /**
104
+ * Multi-purpose runners: the head token decides nothing, so the subcommand
105
+ * must be declared. `npm run <script>` executes whatever the manifest says,
106
+ * which is why the script name is checked separately below.
107
+ */
108
+ const RUNNER_SUBCOMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
109
+ npm: new Set(["test", "run", "ls", "list", "why", "outdated", "view"]),
110
+ pnpm: new Set(["test", "run", "ls", "list", "why", "outdated"]),
111
+ yarn: new Set(["test", "run", "list", "why", "info"]),
112
+ npx: new Set(["tsc", "eslint", "prettier", "vitest", "jest"]),
113
+ composer: new Set(["test", "run-script", "show", "validate", "diagnose"]),
114
+ cargo: new Set(["test", "build", "check", "clippy", "fmt", "tree"]),
115
+ go: new Set(["test", "build", "vet", "list", "version"]),
116
+ make: new Set(["test", "build", "lint", "check", "typecheck"]),
117
+ task: new Set(["test", "build", "lint", "check", "typecheck", "--list"]),
118
+ just: new Set(["test", "build", "lint", "check"]),
119
+ bundle: new Set(["exec"]),
120
+ dotnet: new Set(["test", "build"]),
121
+ mvn: new Set(["test", "compile", "verify"]),
122
+ gradle: new Set(["test", "build", "check"]),
123
+ };
124
+
125
+ /** Script names a `run`-style subcommand may name and stay category A. */
126
+ const SAFE_SCRIPT_NAMES: ReadonlySet<string> = new Set([
127
+ "test", "tests", "lint", "typecheck", "types", "build", "check",
128
+ "format", "fmt", "unit", "e2e", "coverage", "compile",
129
+ ]);
130
+
131
+ /** Subcommands that take a script name rather than acting themselves. */
132
+ const SCRIPT_TAKING_SUBCOMMANDS: ReadonlySet<string> = new Set([
133
+ "run", "run-script", "exec",
134
+ ]);
135
+
136
+ /**
137
+ * Is `candidate` inside `root`?
138
+ *
139
+ * Pure `path` arithmetic, no `fs`: the classifier must be decidable from the
140
+ * envelope alone, and a stat call would make it depend on what happens to
141
+ * exist on the machine that runs the hook.
142
+ */
143
+ export function isInsideWorkingTree(candidate: string, root: string): boolean {
144
+ if (!candidate.trim() || !root.trim()) return false;
145
+ const abs = path.resolve(root, candidate);
146
+ const rel = path.relative(path.resolve(root), abs);
147
+ if (rel === "") return true;
148
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
149
+ }
150
+
151
+ /** Split a command on whitespace; the caller has already refused metacharacters. */
152
+ function _argv(command: string): string[] {
153
+ return command.trim().split(/\s+/).filter((t) => t.length > 0);
154
+ }
155
+
156
+ /** Strip a leading `-`/`--` so a flag is never mistaken for a subcommand. */
157
+ function _isFlag(token: string): boolean {
158
+ return token.startsWith("-");
159
+ }
160
+
161
+ /** First non-flag token after the head, or `""`. */
162
+ function _firstSubcommand(argv: readonly string[]): string {
163
+ for (const token of argv.slice(1)) {
164
+ if (!_isFlag(token)) return token;
165
+ }
166
+ return "";
167
+ }
168
+
169
+ /**
170
+ * `git` global options that consume the NEXT token as their value.
171
+ *
172
+ * Without this, `git -C sub status` resolves its subcommand to `sub` and the
173
+ * whole directory-flag form — the shape the canon now teaches in place of
174
+ * `cd X && …` — falls out of category A. That defect was found by the friction
175
+ * corpus on its first run, which is the reason the corpus counts confirmations
176
+ * rather than gates: a gate count would have looked identical.
177
+ *
178
+ * The `--flag=value` spelling consumes no extra token and needs no entry.
179
+ */
180
+ const GIT_VALUE_FLAGS: ReadonlySet<string> = new Set([
181
+ "-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path",
182
+ ]);
183
+
184
+ /**
185
+ * The `git` subcommand, with global options skipped.
186
+ *
187
+ * Returns `""` when a global option's VALUE escapes the working tree — an
188
+ * absolute path or a `..` traversal. The check is textual rather than resolved
189
+ * against a root because this function is pure and receives no root; what it
190
+ * can decide is exactly the property that matters, and a relative path with no
191
+ * `..` cannot escape.
192
+ */
193
+ function _gitSubcommand(argv: readonly string[]): string {
194
+ for (let i = 1; i < argv.length; i++) {
195
+ const token = argv[i] as string;
196
+ if (!_isFlag(token)) return token;
197
+ const [flag, inlineValue] = token.includes("=")
198
+ ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)]
199
+ : [token, null];
200
+ const value = inlineValue ?? (GIT_VALUE_FLAGS.has(flag) ? argv[++i] : undefined);
201
+ if (value !== undefined && (path.isAbsolute(value) || value.split("/").includes(".."))) {
202
+ return "";
203
+ }
204
+ }
205
+ return "";
206
+ }
207
+
208
+ /** Non-flag token after the subcommand, or `""`. */
209
+ function _secondSubcommand(argv: readonly string[]): string {
210
+ const nonFlags = argv.slice(1).filter((t) => !_isFlag(t));
211
+ return nonFlags[1] ?? "";
212
+ }
213
+
214
+ /**
215
+ * Does any token in the command name a consequence operation?
216
+ *
217
+ * Tokenised rather than substring-matched: a substring test would refuse
218
+ * `git log --format=…` for containing `mat`, and a classifier that refuses
219
+ * everything is indistinguishable from one that was never wired.
220
+ */
221
+ export function namesConsequenceOperation(command: string): boolean {
222
+ for (const raw of _argv(command)) {
223
+ const token = raw.replace(/^-+/, "").toLowerCase();
224
+ if (CONSEQUENCE_WORDS.has(token)) return true;
225
+ // `npm:publish`, `db:seed`, `deploy:prod` — a namespaced script name
226
+ // hides the consequential half behind a separator.
227
+ for (const part of token.split(/[:/=,]/)) {
228
+ if (part && CONSEQUENCE_WORDS.has(part)) return true;
229
+ }
230
+ }
231
+ return false;
232
+ }
233
+
234
+ /** Is this Bash command string category A? */
235
+ export function isCategoryABashCommand(command: string): boolean {
236
+ if (typeof command !== "string" || !command.trim()) return false;
237
+ if (SHELL_METACHARACTERS.test(command)) return false;
238
+ if (namesConsequenceOperation(command)) return false;
239
+
240
+ const argv = _argv(command);
241
+ const head = (argv[0] ?? "").toLowerCase();
242
+ if (!head) return false;
243
+
244
+ if (SAFE_HEADS.has(head)) return true;
245
+
246
+ if (head === "git") {
247
+ return GIT_READ_SUBCOMMANDS.has(_gitSubcommand(argv));
248
+ }
249
+
250
+ const allowed = RUNNER_SUBCOMMANDS[head];
251
+ if (!allowed) return false;
252
+ const sub = _firstSubcommand(argv);
253
+ if (!sub || !allowed.has(sub)) return false;
254
+ if (SCRIPT_TAKING_SUBCOMMANDS.has(sub)) {
255
+ const script = _secondSubcommand(argv);
256
+ return script !== "" && SAFE_SCRIPT_NAMES.has(script.toLowerCase());
257
+ }
258
+ return true;
259
+ }
260
+
261
+ /**
262
+ * Is this tool call category A?
263
+ *
264
+ * `workingTree` is the root every path argument must resolve inside. An empty
265
+ * root means the dispatcher could not establish one, and an unestablished
266
+ * boundary is not a boundary — the call is not category A.
267
+ */
268
+ export function isCategoryA(
269
+ toolName: string,
270
+ toolInput: ToolInput,
271
+ workingTree: string,
272
+ ): boolean {
273
+ if (!workingTree.trim()) return false;
274
+ if (typeof toolName !== "string" || !toolName) return false;
275
+
276
+ if (READ_ONLY_TOOLS.has(toolName)) {
277
+ for (const key of PATH_KEYS) {
278
+ const value = toolInput?.[key];
279
+ if (typeof value !== "string") continue;
280
+ if (!isInsideWorkingTree(value, workingTree)) return false;
281
+ }
282
+ return true;
283
+ }
284
+
285
+ if (toolName === "Bash") {
286
+ const command = toolInput?.["command"];
287
+ return typeof command === "string" && isCategoryABashCommand(command);
288
+ }
289
+
290
+ return false;
291
+ }
@@ -4,7 +4,7 @@
4
4
  # Routes user-scope `~/Documents/Cline/Hooks/<HookName>` events into
5
5
  # the active workspace's `./agent-config dispatch:hook`. Project-scope
6
6
  # `.clinerules/hooks/<HookName>` does NOT need this trampoline —
7
- # install.py `ensure_cline_bridge()` writes per-event scripts there
7
+ # the installer's `ensure_cline_bridge()` writes per-event scripts there
8
8
  # that dispatch directly because Cline fires project hooks with the
9
9
  # workspace as cwd.
10
10
  #
@@ -41,6 +41,7 @@ import { main as wrapperFreshnessMain } from '../wrapper_freshness_hook.js';
41
41
  import { main as surfaceProbeMain } from '../surface_probe_hook.js';
42
42
  import { main as teamReviewGateMain } from '../team_review_gate_hook.js';
43
43
  import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
44
+ import { main as pushSettleMain } from './push_settle_hook.js';
44
45
  import { main as sessionCanaryMain } from '../session_canary_hook.js';
45
46
  import { main as councilAvailabilityMain } from '../council_availability_hook.js';
46
47
  import { main as telemetryDisclosureMain } from '../telemetry_disclosure_hook.js';
@@ -48,7 +49,6 @@ import { main as selfRepairMain } from '../self_repair_hook.js';
48
49
  import { main as sessionRegisterMain } from '../session_register_hook.js';
49
50
  import { main as languageMirrorMain } from '../language_mirror_hook.js';
50
51
  import { main as gitAuthorizationMain } from '../git_authorization_hook.js';
51
- import { main as blockUnauthorizedGitMain } from './block_unauthorized_git.js';
52
52
  import { main as evidenceIndependenceMain } from './evidence_independence.js';
53
53
  import { main as blockNoVerifyMain } from './block_no_verify.js';
54
54
  import { main as blockKernelRuleWritesMain } from './block_kernel_rule_writes.js';
@@ -102,6 +102,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
102
102
  'src/scripts/surface_probe_hook.ts': surfaceProbeMain as ConcernMain,
103
103
  'src/scripts/team_review_gate_hook.ts': teamReviewGateMain as ConcernMain,
104
104
  'src/scripts/pr_url_reminder_hook.ts': prUrlReminderMain as ConcernMain,
105
+ 'src/scripts/hooks/push_settle_hook.ts': pushSettleMain as ConcernMain,
105
106
  'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
106
107
  'src/scripts/council_availability_hook.ts': councilAvailabilityMain as ConcernMain,
107
108
  'src/scripts/telemetry_disclosure_hook.ts': telemetryDisclosureMain as ConcernMain,
@@ -109,7 +110,6 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
109
110
  'src/scripts/session_register_hook.ts': sessionRegisterMain as ConcernMain,
110
111
  'src/scripts/language_mirror_hook.ts': languageMirrorMain as ConcernMain,
111
112
  'src/scripts/git_authorization_hook.ts': gitAuthorizationMain as ConcernMain,
112
- 'src/scripts/hooks/block_unauthorized_git.ts': blockUnauthorizedGitMain as ConcernMain,
113
113
  'src/scripts/hooks/evidence_independence.ts': evidenceIndependenceMain as ConcernMain,
114
114
  'src/scripts/hooks/block_no_verify.ts': blockNoVerifyMain as ConcernMain,
115
115
  'src/scripts/hooks/block_kernel_rule_writes.ts': blockKernelRuleWritesMain as ConcernMain,
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Routes user-scope `~/.cursor/hooks.json` events into the active
5
5
  # workspace's `./agent-config dispatch:hook`. Project-scope
6
- # `.cursor/hooks.json` does NOT need this trampoline — install.py
6
+ # `.cursor/hooks.json` does NOT need this trampoline — the installer
7
7
  # `ensure_cursor_bridge()` writes direct dispatch:hook commands there
8
8
  # because the project hooks fire with the workspace as cwd.
9
9
  #
@@ -20,10 +20,16 @@
20
20
  # - Re-pipe the original JSON into
21
21
  # ./agent-config dispatch:hook --platform cursor \
22
22
  # --event $1 --native-event $2
23
- # - Always exit 0 — Cursor's pre-hooks can block via exit code, but
24
- # none of our concerns block; chat-history / roadmap-progress /
25
- # context-hygiene are observe-only and onboarding-gate writes
26
- # state without denying sessionStart.
23
+ # - Always exit 0 — Cursor's pre-hooks can block via exit code. The
24
+ # manifest declares 7 concerns at `severity: blocking`, and this host
25
+ # binds none of them: cursor's platform row carries session_start,
26
+ # session_end, stop, user_prompt_submit and post_tool_use, and every
27
+ # blocking concern sits in a slot or platform row outside that set.
28
+ # The count is a YAML parse of the concerns map, never `grep -c
29
+ # 'severity: blocking'`, which returns 8 by also counting the prose
30
+ # line that quotes the key. What IS bound here — chat-history /
31
+ # roadmap-progress / context-hygiene — is observe-only, and
32
+ # onboarding-gate writes state without denying sessionStart.
27
33
 
28
34
  set -u
29
35
 
@@ -9,12 +9,10 @@
9
9
  * stdout/stderr split, and exit codes. Helper names keep snake_case for
10
10
  * fidelity (concern authors rely on the documented surface).
11
11
  *
12
- * Per `docs/contracts/hook-architecture-v1.md`. Reads the manifest at
13
- * `scripts/hook_manifest.yaml`, resolves which concerns fire on the given
14
- * (platform, event) tuple, and runs each concern sequentially with the
15
- * stdin envelope contract. Reduces concern exit codes per the spec
16
- * (0=allow, 1=block, 2=warn, ≥3=error → fail-open unless concern is
17
- * fail_closed).
12
+ * Per `docs/contracts/hook-architecture-v1.md`. Reads `src/scripts/hook_manifest.yaml`,
13
+ * resolves which concerns fire on the given (platform, event) tuple, and runs each
14
+ * sequentially with the stdin envelope contract. Reduces concern exit codes per the
15
+ * spec (0=allow, 1=block, 2=warn, ≥3=error → fail-open unless concern is fail_closed).
18
16
  *
19
17
  * Invocation:
20
18
  *
@@ -50,13 +48,16 @@ import {
50
48
  setHookStdinOverride,
51
49
  clearHookStdinOverride,
52
50
  } from "./hook_stdin.js";
53
- import { emitFor, type Severity } from "./host_semantics.js";
51
+ import { emitFor, type PermissionDecision, type Severity } from "./host_semantics.js";
52
+ import { _concern_permission_verdict, _payload_tool_name, _permission_for } from "./permission_gate.js";
53
+ export { _concern_permission_verdict, _payload_tool_input, _payload_tool_name, _permission_for, _working_tree_root } from "./permission_gate.js";
54
54
  import { _concern_body_classes, planPayloadShapes } from "./payload_stub.js";
55
55
  import { resolveSessionRole, type SessionRole } from "../_lib/session_role.js";
56
56
  import { stdinReadFailure, denyOnStdinFailure } from './stdin_failure_policy.js';
57
57
  export { stdinReadFailure, denyOnStdinFailure, _is_fail_closed_blocking } from './stdin_failure_policy.js';
58
58
  import { _py_json_dumps } from './py_json_dumps.js';
59
59
  import { _fallback_yaml } from './fallback_yaml.js';
60
+ import { detectSurface } from '../_lib/surface.js';
60
61
  import { recordCapture, recordOpportunity } from "../_lib/collector_denominator.js";
61
62
  export { _fallback_yaml } from './fallback_yaml.js';
62
63
 
@@ -255,20 +256,6 @@ interface ConcernDef extends JsonObject {
255
256
  name: string;
256
257
  }
257
258
 
258
- /**
259
- * The tool name the host reported for this event, or `""`.
260
- *
261
- * Only tool events carry one. Read defensively: the payload is host-shaped and
262
- * a missing / non-string field must degrade to "unknown tool", never throw.
263
- */
264
- export function _payload_tool_name(envelope: JsonObject): string {
265
- const payload = envelope["payload"];
266
- if (!_isObject(payload)) {
267
- return "";
268
- }
269
- const raw = payload["tool_name"];
270
- return typeof raw === "string" ? raw : "";
271
- }
272
259
 
273
260
  /**
274
261
  * Per-concern tool filter — the `tools:` key.
@@ -290,7 +277,7 @@ export function _payload_tool_name(envelope: JsonObject): string {
290
277
  * - A host matcher would only help the two hosts that support one (Claude,
291
278
  * Gemini). The in-process skip helps all eight platforms in the manifest.
292
279
  *
293
- * ## What this does NOT claim
280
+ * What this does NOT claim:
294
281
  *
295
282
  * It does not claim a latency win. The measured hook cost that was fixed was
296
283
  * the *invocation path* (~370 of ~450–500 ms was eager CLI imports); nothing in
@@ -509,6 +496,7 @@ export function _build_envelope(args: Args, payload_text: string): JsonObject {
509
496
  process.env["AGENT_SESSION_ID"] ||
510
497
  "",
511
498
  workspace_root: process.cwd(),
499
+ surface: detectSurface(payloadObj),
512
500
  payload: payloadObj,
513
501
  settings: {},
514
502
  };
@@ -1280,6 +1268,11 @@ export function main(argv?: string[]): number {
1280
1268
  // fixed-field schema (PII-exclusion-by-construction), and a concern's raw
1281
1269
  // stderr is free-form content. This array is in-memory only.
1282
1270
  const concern_messages: ConcernMessage[] = [];
1271
+ // Per-concern permission verdicts for the host's `permissionDecision` field.
1272
+ // Separate from `rcs` because the two ladders are not the same: an advisory
1273
+ // concern can ask for a confirmation without blocking, and a reduced `allow`
1274
+ // must not become a grant when one of them did.
1275
+ const permission_verdicts: PermissionDecision[] = [];
1283
1276
  // Per-concern `tools:` filter (see _concern_matches_tool). Applied here so it
1284
1277
  // is one place, after the envelope exists and before any concern is spawned.
1285
1278
  const tool_name = _payload_tool_name(envelope);
@@ -1339,6 +1332,7 @@ export function main(argv?: string[]): number {
1339
1332
  }
1340
1333
  rcs.push(rc);
1341
1334
  const reply = _parse_concern_stdout(stdout_text);
1335
+ permission_verdicts.push(_concern_permission_verdict(rc, reply));
1342
1336
  // A concern states its reason either as JSON {"reason": …} on stdout
1343
1337
  // (advisory concerns) or as a formatted stderr line (the block guards, e.g.
1344
1338
  // `block-no-verify: BLOCKED — …`). Both are captured here so the emission
@@ -1423,6 +1417,7 @@ export function main(argv?: string[]): number {
1423
1417
  _severity_for(final_rc) as Severity,
1424
1418
  decidingReasons,
1425
1419
  final_rc,
1420
+ _permission_for(args.event, envelope, final_rc, permission_verdicts),
1426
1421
  );
1427
1422
  if (emission.stdout) {
1428
1423
  process.stdout.write(emission.stdout);
@@ -365,8 +365,21 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
365
365
  });
366
366
  try {
367
367
  atomic_write_json(target, state);
368
- } catch {
369
- /* observability only */
368
+ } catch (err) {
369
+ /* Unlike its two siblings in `git_authorization_hook`, this one fails
370
+ OPEN, which is why it gets a diagnostic rather than a shrug. The turn's
371
+ evaluation count lives only in this file: a failed write leaves
372
+ `evaluations` one short, so the NEXT self-commissioned evaluation in the
373
+ same turn reads `priorEvaluations === 0` and the verdict-shopping warn
374
+ does not fire. The item-1 pre-loaded-verdict BLOCK is unaffected — it
375
+ reads the prompt, not this state — so the degradation is bounded to the
376
+ advisory half. Silent was the wrong call for a guard that loses a check
377
+ when it fails. */
378
+ process.stderr.write(
379
+ `evidence-independence: evaluation-count write failed (${_errText(err)}) — ` +
380
+ "this turn's count did NOT advance, so a second self-scoped evaluation " +
381
+ "will not be warned about. The pre-loaded-verdict block is unaffected.\n",
382
+ );
370
383
  }
371
384
  }
372
385
 
@@ -379,6 +392,12 @@ export function run(stdin_text: string, options: { consumer_root: string }): num
379
392
  return decision.exit;
380
393
  }
381
394
 
395
+ /** One-line, bounded rendering of a caught value for a stderr diagnostic. */
396
+ function _errText(err: unknown): string {
397
+ const t = err instanceof Error ? err.message : String(err);
398
+ return t.replace(/\s+/g, " ").trim().slice(0, 200);
399
+ }
400
+
382
401
  export function main(argv?: string[]): number {
383
402
  const args = argv ?? process.argv.slice(2);
384
403
  let consumer_root = process.cwd();