@event4u/agent-config 1.17.0 → 1.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/.agent-src/commands/council/default.md +74 -76
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/chat-history-cadence.md +34 -0
- package/.agent-src/rules/chat-history-ownership.md +1 -0
- package/.agent-src/rules/chat-history-visibility.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +28 -0
- package/.agent-src/rules/direct-answers.md +18 -26
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +1 -0
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-cheap-questions.md +15 -21
- package/.agent-src/rules/no-roadmap-references.md +1 -0
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +33 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +49 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +29 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +1 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +1 -0
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +12 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +97 -0
- package/README.md +20 -20
- package/config/agent-settings.template.yml +23 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -2
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +7 -0
- package/docs/contracts/hook-architecture-v1.md +213 -0
- package/docs/contracts/load-context-budget-model.md +80 -0
- package/docs/contracts/load-context-schema.md +20 -0
- package/docs/contracts/memory-visibility-v1.md +138 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/roadmap-complexity-standard.md +137 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +1 -0
- package/docs/development.md +4 -1
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
- package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
- package/package.json +1 -1
- package/scripts/agent-config +64 -0
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +24 -8
- package/scripts/ai_council/one_off_archive/2026-05/README.md +67 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
- package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +360 -0
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +11 -1
- package/scripts/check_always_budget.py +46 -2
- package/scripts/check_one_off_location.py +81 -0
- package/scripts/check_references.py +6 -0
- package/scripts/compress.py +5 -2
- package/scripts/context_hygiene_hook.py +181 -0
- package/scripts/council_cli.py +357 -0
- package/scripts/hook_manifest.yaml +184 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-context-hygiene.sh +55 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/augment-onboarding-gate.sh +55 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +348 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +146 -0
- package/scripts/install.py +728 -51
- package/scripts/install.sh +1 -1
- package/scripts/lint_examples.py +98 -0
- package/scripts/lint_hook_manifest.py +216 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_roadmap_complexity.py +127 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +142 -0
- package/scripts/readme_linter.py +12 -3
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/rule.schema.json +5 -0
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/verify_before_complete_hook.py +216 -0
- /package/scripts/ai_council/{_one_off_2a4_acceptance.py → one_off_archive/2026-05/_one_off_2a4_acceptance.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_estimate.py → one_off_archive/2026-05/_one_off_context_layer_v1_estimate.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_review.py → one_off_archive/2026-05/_one_off_context_layer_v1_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_followups_review.py → one_off_archive/2026-05/_one_off_followups_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_nondestructive_inline_audit.py → one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py} +0 -0
- /package/scripts/{_one_off_phase4_dispatch_latency.py → ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py} +0 -0
- /package/scripts/{_one_off_phase6_trigger_jaccard.py → ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_budget_rebalance.py → one_off_archive/2026-05/_one_off_phase_2a_budget_rebalance.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_post_revert.py → one_off_archive/2026-05/_one_off_phase_2a_post_revert.py} +0 -0
- /package/scripts/ai_council/{_one_off_rebalancing_audit.py → one_off_archive/2026-05/_one_off_rebalancing_audit.py} +0 -0
- /package/scripts/ai_council/{_one_off_rule_hardening_v1.py → one_off_archive/2026-05/_one_off_rule_hardening_v1.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_open_questions.py → one_off_archive/2026-05/_one_off_structural_open_questions.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_optimization.py → one_off_archive/2026-05/_one_off_structural_optimization.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_gaps.py → one_off_archive/2026-05/_one_off_structural_v3_gaps.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_review.py → one_off_archive/2026-05/_one_off_structural_v3_review.py} +0 -0
package/scripts/install.py
CHANGED
|
@@ -29,6 +29,8 @@ import copy
|
|
|
29
29
|
import json
|
|
30
30
|
import os
|
|
31
31
|
import re
|
|
32
|
+
import shlex
|
|
33
|
+
import subprocess
|
|
32
34
|
import sys
|
|
33
35
|
from pathlib import Path
|
|
34
36
|
|
|
@@ -464,17 +466,31 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
|
|
|
464
466
|
# .augment/settings.json is plugin enablement, not hooks.
|
|
465
467
|
AUGMENT_USER_DIR = Path.home() / ".augment"
|
|
466
468
|
AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
469
|
+
|
|
470
|
+
# Phase 7.3 (hook-architecture-v1.md): one universal trampoline per
|
|
471
|
+
# platform replaces the per-concern fan-out. The trampoline cd's into
|
|
472
|
+
# the consumer workspace and pipes stdin into
|
|
473
|
+
# `./agent-config dispatch:hook`, which reads scripts/hook_manifest.yaml
|
|
474
|
+
# to resolve which concerns fire on (platform, event).
|
|
475
|
+
AUGMENT_DISPATCHER_TRAMPOLINE = "augment-dispatcher.sh"
|
|
476
|
+
|
|
477
|
+
# Pre-Phase-7 trampolines deployed at ~/.augment/hooks/ — install removes
|
|
478
|
+
# them on rerun so the manifest stays the single source of truth.
|
|
479
|
+
AUGMENT_LEGACY_TRAMPOLINES = (
|
|
480
|
+
"augment-chat-history.sh",
|
|
481
|
+
"augment-roadmap-progress.sh",
|
|
482
|
+
"augment-onboarding-gate.sh",
|
|
483
|
+
"augment-context-hygiene.sh",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# (agent-config event, Augment native event). Augment fires the same
|
|
487
|
+
# trampoline once per binding; the trampoline forwards both names to the
|
|
488
|
+
# dispatcher so concerns can branch on either.
|
|
489
|
+
AUGMENT_DISPATCHER_BINDINGS = (
|
|
490
|
+
("session_start", "SessionStart"),
|
|
491
|
+
("session_end", "SessionEnd"),
|
|
492
|
+
("stop", "Stop"),
|
|
493
|
+
("post_tool_use", "PostToolUse"),
|
|
478
494
|
)
|
|
479
495
|
|
|
480
496
|
|
|
@@ -495,33 +511,60 @@ def _deploy_augment_trampoline(package_root: Path, name: str, force: bool) -> Pa
|
|
|
495
511
|
return dst
|
|
496
512
|
|
|
497
513
|
|
|
498
|
-
def
|
|
499
|
-
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
time.
|
|
507
|
-
|
|
508
|
-
Two trampolines are deployed:
|
|
509
|
-
- augment-chat-history.sh → SessionStart/SessionEnd/Stop/PostToolUse
|
|
510
|
-
- augment-roadmap-progress.sh → PostToolUse (path-filtered to
|
|
511
|
-
agents/roadmaps/ — see scripts/roadmap_progress_hook.py)
|
|
514
|
+
def _remove_legacy_augment_trampolines() -> None:
|
|
515
|
+
"""Phase 7.3 cleanup: drop pre-dispatcher trampolines on rerun.
|
|
516
|
+
|
|
517
|
+
The manifest is now the single source of truth; leaving the old
|
|
518
|
+
per-concern .sh files at ~/.augment/hooks/ would not break anything
|
|
519
|
+
(settings.json no longer references them), but it produces stale
|
|
520
|
+
artefacts that confuse `task hooks-status` and look like a partial
|
|
521
|
+
install. Removal is best-effort and silent on missing files.
|
|
512
522
|
"""
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
523
|
+
for name in AUGMENT_LEGACY_TRAMPOLINES:
|
|
524
|
+
legacy = AUGMENT_USER_HOOKS_DIR / name
|
|
525
|
+
try:
|
|
526
|
+
if legacy.is_file():
|
|
527
|
+
legacy.unlink()
|
|
528
|
+
skip(f"removed legacy ~/.augment/hooks/{name}")
|
|
529
|
+
except OSError:
|
|
530
|
+
pass
|
|
531
|
+
|
|
521
532
|
|
|
522
|
-
|
|
533
|
+
def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
534
|
+
"""Deploy the Augment universal-dispatcher trampoline at user scope.
|
|
535
|
+
|
|
536
|
+
Phase 7.3 (hook-architecture-v1.md): one trampoline replaces the
|
|
537
|
+
four per-concern .sh files. The trampoline reads the event JSON
|
|
538
|
+
from stdin, extracts workspace_roots[0], cd's there, and pipes the
|
|
539
|
+
payload into `./agent-config dispatch:hook --platform augment
|
|
540
|
+
--event <agent-config-event> --native-event <native>`. The
|
|
541
|
+
dispatcher then loads scripts/hook_manifest.yaml and runs the
|
|
542
|
+
resolved concern chain.
|
|
543
|
+
|
|
544
|
+
Augment hook scripts must use the .sh extension and be referenced
|
|
545
|
+
by absolute path; user scope is the only surface that fires for
|
|
546
|
+
both the CLI and the IDE plugins. Installs once per developer.
|
|
547
|
+
|
|
548
|
+
Settings entries (Phase 7.3, see AUGMENT_DISPATCHER_BINDINGS):
|
|
549
|
+
- SessionStart → augment-dispatcher.sh session_start SessionStart
|
|
550
|
+
- SessionEnd → augment-dispatcher.sh session_end SessionEnd
|
|
551
|
+
- Stop → augment-dispatcher.sh stop Stop
|
|
552
|
+
- PostToolUse → augment-dispatcher.sh post_tool_use PostToolUse
|
|
553
|
+
"""
|
|
554
|
+
dst = _deploy_augment_trampoline(package_root, AUGMENT_DISPATCHER_TRAMPOLINE, force)
|
|
555
|
+
if dst is None:
|
|
523
556
|
return
|
|
524
557
|
|
|
558
|
+
_remove_legacy_augment_trampolines()
|
|
559
|
+
|
|
560
|
+
per_event: dict[str, list] = {}
|
|
561
|
+
for ac_event, native in AUGMENT_DISPATCHER_BINDINGS:
|
|
562
|
+
# Augment's `command` is a shell line — pass agent-config event
|
|
563
|
+
# and Augment-native event as positional args.
|
|
564
|
+
cmd = f"{dst} {ac_event} {native}"
|
|
565
|
+
entry = {"hooks": [{"type": "command", "command": cmd}]}
|
|
566
|
+
per_event.setdefault(native, []).append(entry)
|
|
567
|
+
|
|
525
568
|
settings_patch: dict = {"hooks": per_event}
|
|
526
569
|
merge_json_file(
|
|
527
570
|
AUGMENT_USER_DIR / "settings.json",
|
|
@@ -531,40 +574,525 @@ def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
|
531
574
|
)
|
|
532
575
|
|
|
533
576
|
|
|
534
|
-
|
|
535
|
-
|
|
577
|
+
# Claude Code lifecycle events → agent-config event vocabulary.
|
|
578
|
+
# Phase 7.3: one universal dispatch:hook entry per event replaces the
|
|
579
|
+
# per-concern subcommand fan-out. The dispatcher reads
|
|
580
|
+
# scripts/hook_manifest.yaml to resolve which concerns fire on each
|
|
581
|
+
# (platform, event) tuple. Mirrors AUGMENT_DISPATCHER_BINDINGS so each
|
|
582
|
+
# concern fires on the same logical surface across platforms — the
|
|
583
|
+
# contract from agents/contexts/hardening-pattern.md § Cross-platform
|
|
584
|
+
# parity.
|
|
585
|
+
CLAUDE_DISPATCHER_BINDINGS = (
|
|
586
|
+
("session_start", "SessionStart"),
|
|
587
|
+
("session_end", "SessionEnd"),
|
|
588
|
+
("stop", "Stop"),
|
|
589
|
+
("user_prompt_submit", "UserPromptSubmit"),
|
|
590
|
+
("post_tool_use", "PostToolUse"),
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _claude_dispatch_block(ac_event: str, native: str) -> dict:
|
|
595
|
+
"""Single hook entry routing the event through the universal dispatcher."""
|
|
536
596
|
return {
|
|
537
597
|
"hooks": [
|
|
538
598
|
{
|
|
539
599
|
"type": "command",
|
|
540
|
-
"command":
|
|
600
|
+
"command": (
|
|
601
|
+
f"./agent-config dispatch:hook "
|
|
602
|
+
f"--platform claude --event {ac_event} "
|
|
603
|
+
f"--native-event {native}"
|
|
604
|
+
),
|
|
541
605
|
},
|
|
542
606
|
],
|
|
543
607
|
}
|
|
544
608
|
|
|
545
609
|
|
|
546
610
|
def ensure_claude_bridge(project_root: Path, force: bool) -> None:
|
|
547
|
-
"""Deploy .claude/settings.json with plugin enablement and
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
611
|
+
"""Deploy .claude/settings.json with plugin enablement and the Phase 7
|
|
612
|
+
universal dispatcher hooks.
|
|
613
|
+
|
|
614
|
+
Each Claude Code lifecycle event is wired to a single
|
|
615
|
+
`./agent-config dispatch:hook` invocation. The dispatcher reads
|
|
616
|
+
scripts/hook_manifest.yaml at runtime and runs the resolved concern
|
|
617
|
+
chain — concerns are no-ops when the relevant feature is disabled
|
|
618
|
+
in .agent-settings.yml. Idempotent: reruns merge cleanly without
|
|
619
|
+
duplicating entries (deep_merge replaces hook arrays rather than
|
|
620
|
+
appending).
|
|
553
621
|
"""
|
|
554
|
-
|
|
622
|
+
per_event: dict[str, list] = {}
|
|
623
|
+
for ac_event, native in CLAUDE_DISPATCHER_BINDINGS:
|
|
624
|
+
per_event.setdefault(native, []).append(
|
|
625
|
+
_claude_dispatch_block(ac_event, native)
|
|
626
|
+
)
|
|
627
|
+
|
|
555
628
|
bridge = {
|
|
556
629
|
"enabledPlugins": {"agent-conf@event4u": True},
|
|
557
|
-
"hooks":
|
|
558
|
-
"SessionStart": [claude_hook],
|
|
559
|
-
"UserPromptSubmit": [claude_hook],
|
|
560
|
-
"PostToolUse": [claude_hook],
|
|
561
|
-
"Stop": [claude_hook],
|
|
562
|
-
"SessionEnd": [claude_hook],
|
|
563
|
-
},
|
|
630
|
+
"hooks": per_event,
|
|
564
631
|
}
|
|
565
632
|
merge_json_file(project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json")
|
|
566
633
|
|
|
567
634
|
|
|
635
|
+
# Cursor lifecycle events → agent-config event vocabulary.
|
|
636
|
+
# Phase 7.5 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
637
|
+
# Cursor's project-scope `.cursor/hooks.json` fires hooks with the
|
|
638
|
+
# project as cwd, so the dispatch:hook command runs directly with no
|
|
639
|
+
# trampoline. User-scope `~/.cursor/hooks.json` is a separate opt-in
|
|
640
|
+
# (--cursor-user-hooks) and routes through cursor-dispatcher.sh because
|
|
641
|
+
# the user-scope hooks fire across all projects.
|
|
642
|
+
#
|
|
643
|
+
# Native event names per https://cursor.com/docs/reference/third-party-hooks
|
|
644
|
+
# (camelCase). UserPromptSubmit lives at `beforeSubmitPrompt`. Stop is
|
|
645
|
+
# IDE-only — CLI-only Cursor users get the rule-only checkpoint
|
|
646
|
+
# fallback per agents/contexts/chat-history-platform-hooks.md.
|
|
647
|
+
CURSOR_DISPATCHER_BINDINGS = (
|
|
648
|
+
("session_start", "sessionStart"),
|
|
649
|
+
("session_end", "sessionEnd"),
|
|
650
|
+
("stop", "stop"),
|
|
651
|
+
("user_prompt_submit", "beforeSubmitPrompt"),
|
|
652
|
+
("post_tool_use", "postToolUse"),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _cursor_dispatch_command(ac_event: str, native: str) -> str:
|
|
657
|
+
return (
|
|
658
|
+
f"./agent-config dispatch:hook "
|
|
659
|
+
f"--platform cursor --event {ac_event} "
|
|
660
|
+
f"--native-event {native}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def ensure_cursor_bridge(project_root: Path, force: bool) -> None:
|
|
665
|
+
"""Deploy `.cursor/hooks.json` (project scope) with the Phase 7
|
|
666
|
+
universal dispatcher hooks.
|
|
667
|
+
|
|
668
|
+
Each Cursor lifecycle event is wired to a single
|
|
669
|
+
`./agent-config dispatch:hook` invocation. Cursor fires project
|
|
670
|
+
hooks with the project as cwd, so no trampoline is needed at this
|
|
671
|
+
scope — concerns are no-ops when disabled in .agent-settings.yml.
|
|
672
|
+
Idempotent: deep_merge replaces hook arrays on rerun rather than
|
|
673
|
+
appending duplicates.
|
|
674
|
+
"""
|
|
675
|
+
hooks: dict[str, list] = {}
|
|
676
|
+
for ac_event, native in CURSOR_DISPATCHER_BINDINGS:
|
|
677
|
+
hooks.setdefault(native, []).append(
|
|
678
|
+
{"command": _cursor_dispatch_command(ac_event, native)}
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
bridge = {"version": 1, "hooks": hooks}
|
|
682
|
+
merge_json_file(project_root / ".cursor" / "hooks.json", bridge, force, ".cursor/hooks.json")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
# Cursor user-scope hooks fire across every project the developer opens
|
|
686
|
+
# in the Cursor IDE / CLI. The trampoline reads `workspace_roots[0]`
|
|
687
|
+
# from the event payload (per https://cursor.com/docs/hooks) and routes
|
|
688
|
+
# the JSON into the active project's `./agent-config dispatch:hook`,
|
|
689
|
+
# silent no-op when the workspace is not an agent-config consumer.
|
|
690
|
+
CURSOR_USER_DIR = Path.home() / ".cursor"
|
|
691
|
+
CURSOR_USER_HOOKS_DIR = CURSOR_USER_DIR / "hooks"
|
|
692
|
+
CURSOR_DISPATCHER_TRAMPOLINE = "cursor-dispatcher.sh"
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def ensure_cursor_user_hooks(package_root: Path, force: bool) -> None:
|
|
696
|
+
"""Deploy the Cursor universal-dispatcher trampoline at user scope.
|
|
697
|
+
|
|
698
|
+
Phase 7.5 (hook-architecture-v1.md): mirrors ensure_augment_user_hooks
|
|
699
|
+
for the Cursor surface. Writes:
|
|
700
|
+
- ~/.cursor/hooks/cursor-dispatcher.sh (trampoline)
|
|
701
|
+
- ~/.cursor/hooks.json (event → trampoline call)
|
|
702
|
+
|
|
703
|
+
Each hooks.json command line is `<dispatcher> <ac_event> <native>`
|
|
704
|
+
so the trampoline can forward both names to the dispatcher for
|
|
705
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
706
|
+
"""
|
|
707
|
+
src = package_root / "scripts" / "hooks" / CURSOR_DISPATCHER_TRAMPOLINE
|
|
708
|
+
if not src.exists():
|
|
709
|
+
skip(f"cursor trampoline missing in package: {src}")
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
CURSOR_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
713
|
+
dst = CURSOR_USER_HOOKS_DIR / CURSOR_DISPATCHER_TRAMPOLINE
|
|
714
|
+
src_text = src.read_text(encoding="utf-8")
|
|
715
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
716
|
+
skip(f"~/.cursor/hooks/{CURSOR_DISPATCHER_TRAMPOLINE} already up to date")
|
|
717
|
+
else:
|
|
718
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
719
|
+
dst.chmod(0o755)
|
|
720
|
+
success(f"~/.cursor/hooks/{CURSOR_DISPATCHER_TRAMPOLINE} installed")
|
|
721
|
+
|
|
722
|
+
hooks: dict[str, list] = {}
|
|
723
|
+
for ac_event, native in CURSOR_DISPATCHER_BINDINGS:
|
|
724
|
+
hooks.setdefault(native, []).append(
|
|
725
|
+
{"command": f"{dst} {ac_event} {native}"}
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
settings_patch: dict = {"version": 1, "hooks": hooks}
|
|
729
|
+
merge_json_file(
|
|
730
|
+
CURSOR_USER_DIR / "hooks.json",
|
|
731
|
+
settings_patch,
|
|
732
|
+
force,
|
|
733
|
+
"~/.cursor/hooks.json",
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# Cline lifecycle events → agent-config event vocabulary.
|
|
738
|
+
# Phase 7.6 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
739
|
+
# Cline reads scripts at `.clinerules/hooks/<HookName>` (project) or
|
|
740
|
+
# `~/Documents/Cline/Hooks/<HookName>` (global) — file names match
|
|
741
|
+
# the hook type exactly, no extension, executable bit required.
|
|
742
|
+
# Both TaskStart (new) and TaskResume (resumed) map to session_start;
|
|
743
|
+
# TaskCancel maps to stop because the session is interrupted with
|
|
744
|
+
# partial state (mirrors Augment Stop semantics).
|
|
745
|
+
CLINE_DISPATCHER_BINDINGS = (
|
|
746
|
+
("session_start", "TaskStart"),
|
|
747
|
+
("session_start", "TaskResume"),
|
|
748
|
+
("session_end", "TaskComplete"),
|
|
749
|
+
("stop", "TaskCancel"),
|
|
750
|
+
("user_prompt_submit", "UserPromptSubmit"),
|
|
751
|
+
("post_tool_use", "PostToolUse"),
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Each project-scope script is generated from this template — one file
|
|
755
|
+
# per native hook name. The script reads stdin (Cline's payload), forwards
|
|
756
|
+
# it into `./agent-config dispatch:hook`, then emits the empty JSON
|
|
757
|
+
# envelope Cline expects (`{}` = no cancel, no context modification).
|
|
758
|
+
# `cd "$WORKSPACE_ROOT"` is intentional even though Cline fires project
|
|
759
|
+
# hooks with cwd already set: the workspace path lands in $WORKSPACE_ROOT
|
|
760
|
+
# at install time and the cd guards against future Cline behaviour
|
|
761
|
+
# changes (cline#8073-class shifts in cwd handling).
|
|
762
|
+
CLINE_PROJECT_HOOK_TEMPLATE = """\
|
|
763
|
+
#!/usr/bin/env bash
|
|
764
|
+
# Generated by event4u/agent-config install.py — DO NOT EDIT.
|
|
765
|
+
# Project-scope Cline hook for {native_event} → agent-config {ac_event}.
|
|
766
|
+
# Phase 7.6 (docs/contracts/hook-architecture-v1.md).
|
|
767
|
+
set -u
|
|
768
|
+
EVENT_DATA="$(cat)"
|
|
769
|
+
WORKSPACE_ROOT={workspace_quoted}
|
|
770
|
+
cd "$WORKSPACE_ROOT" 2>/dev/null || {{ printf '%s\\n' '{{}}'; exit 0; }}
|
|
771
|
+
if [ ! -x ./agent-config ]; then
|
|
772
|
+
printf '%s\\n' '{{}}'
|
|
773
|
+
exit 0
|
|
774
|
+
fi
|
|
775
|
+
printf '%s' "$EVENT_DATA" \\
|
|
776
|
+
| ./agent-config dispatch:hook \\
|
|
777
|
+
--platform cline \\
|
|
778
|
+
--event {ac_event} \\
|
|
779
|
+
--native-event {native_event} \\
|
|
780
|
+
>/dev/null 2>&1 || true
|
|
781
|
+
printf '%s\\n' '{{}}'
|
|
782
|
+
exit 0
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def ensure_cline_bridge(project_root: Path, force: bool) -> None:
|
|
787
|
+
"""Deploy `.clinerules/hooks/<HookName>` per-event scripts.
|
|
788
|
+
|
|
789
|
+
Phase 7.6: Cline project hooks are individual executable scripts
|
|
790
|
+
named exactly after the hook (no extension). install writes one
|
|
791
|
+
script per (ac_event, native_event) tuple in
|
|
792
|
+
CLINE_DISPATCHER_BINDINGS; rerunning is idempotent — the script
|
|
793
|
+
body is overwritten only when content differs (or --force).
|
|
794
|
+
"""
|
|
795
|
+
hooks_dir = project_root / ".clinerules" / "hooks"
|
|
796
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
|
|
798
|
+
workspace_quoted = shlex.quote(str(project_root.resolve()))
|
|
799
|
+
written = 0
|
|
800
|
+
for ac_event, native_event in CLINE_DISPATCHER_BINDINGS:
|
|
801
|
+
target = hooks_dir / native_event
|
|
802
|
+
body = CLINE_PROJECT_HOOK_TEMPLATE.format(
|
|
803
|
+
native_event=native_event,
|
|
804
|
+
ac_event=ac_event,
|
|
805
|
+
workspace_quoted=workspace_quoted,
|
|
806
|
+
)
|
|
807
|
+
if target.exists() and target.read_text(encoding="utf-8") == body and not force:
|
|
808
|
+
continue
|
|
809
|
+
if target.exists() and not force:
|
|
810
|
+
skip(f".clinerules/hooks/{native_event} exists, needs update (use --force)")
|
|
811
|
+
continue
|
|
812
|
+
target.write_text(body, encoding="utf-8")
|
|
813
|
+
target.chmod(0o755)
|
|
814
|
+
written += 1
|
|
815
|
+
if written:
|
|
816
|
+
success(f".clinerules/hooks/ — {written} script(s) installed")
|
|
817
|
+
else:
|
|
818
|
+
skip(".clinerules/hooks/ already up to date")
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# Cline user-scope hooks live at `~/Documents/Cline/Hooks/<HookName>`
|
|
822
|
+
# (per docs.cline.bot/customization/hooks) and fire across every
|
|
823
|
+
# project the developer opens. The trampoline reads `workspaceRoots[0]`
|
|
824
|
+
# from the event payload and routes the JSON into the active project's
|
|
825
|
+
# `./agent-config dispatch:hook`. Silent no-op when the workspace is
|
|
826
|
+
# not an agent-config consumer.
|
|
827
|
+
CLINE_USER_DIR = Path.home() / "Documents" / "Cline" / "Hooks"
|
|
828
|
+
CLINE_DISPATCHER_TRAMPOLINE = "cline-dispatcher.sh"
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def ensure_cline_user_hooks(package_root: Path, force: bool) -> None:
|
|
832
|
+
"""Deploy the Cline universal-dispatcher trampoline at user scope.
|
|
833
|
+
|
|
834
|
+
Phase 7.6 (hook-architecture-v1.md): mirrors ensure_cursor_user_hooks
|
|
835
|
+
for Cline. Writes:
|
|
836
|
+
- ~/Documents/Cline/Hooks/cline-dispatcher.sh (shared trampoline)
|
|
837
|
+
- ~/Documents/Cline/Hooks/<HookName> (per-event wrapper)
|
|
838
|
+
|
|
839
|
+
Each per-event wrapper is a tiny shim that exec's the trampoline
|
|
840
|
+
with `<ac_event> <native_event>` arguments and re-pipes stdin —
|
|
841
|
+
this matches Cline's "file name == hook name, no extension"
|
|
842
|
+
convention while still routing through one shared dispatcher.
|
|
843
|
+
"""
|
|
844
|
+
src = package_root / "scripts" / "hooks" / CLINE_DISPATCHER_TRAMPOLINE
|
|
845
|
+
if not src.exists():
|
|
846
|
+
skip(f"cline trampoline missing in package: {src}")
|
|
847
|
+
return
|
|
848
|
+
|
|
849
|
+
CLINE_USER_DIR.mkdir(parents=True, exist_ok=True)
|
|
850
|
+
trampoline = CLINE_USER_DIR / CLINE_DISPATCHER_TRAMPOLINE
|
|
851
|
+
src_text = src.read_text(encoding="utf-8")
|
|
852
|
+
if trampoline.exists() and trampoline.read_text(encoding="utf-8") == src_text and not force:
|
|
853
|
+
skip(f"~/Documents/Cline/Hooks/{CLINE_DISPATCHER_TRAMPOLINE} already up to date")
|
|
854
|
+
else:
|
|
855
|
+
trampoline.write_text(src_text, encoding="utf-8")
|
|
856
|
+
trampoline.chmod(0o755)
|
|
857
|
+
success(f"~/Documents/Cline/Hooks/{CLINE_DISPATCHER_TRAMPOLINE} installed")
|
|
858
|
+
|
|
859
|
+
trampoline_quoted = shlex.quote(str(trampoline))
|
|
860
|
+
for ac_event, native_event in CLINE_DISPATCHER_BINDINGS:
|
|
861
|
+
wrapper = CLINE_USER_DIR / native_event
|
|
862
|
+
body = (
|
|
863
|
+
"#!/usr/bin/env bash\n"
|
|
864
|
+
"# Generated by event4u/agent-config install.py — DO NOT EDIT.\n"
|
|
865
|
+
f"# User-scope Cline hook for {native_event} → agent-config {ac_event}.\n"
|
|
866
|
+
f"exec {trampoline_quoted} {ac_event} {native_event}\n"
|
|
867
|
+
)
|
|
868
|
+
if wrapper.exists() and wrapper.read_text(encoding="utf-8") == body and not force:
|
|
869
|
+
continue
|
|
870
|
+
wrapper.write_text(body, encoding="utf-8")
|
|
871
|
+
wrapper.chmod(0o755)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# Windsurf (Cascade) lifecycle events → agent-config event vocabulary.
|
|
875
|
+
# Phase 7.7 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
876
|
+
# Windsurf reads `.windsurf/hooks.json` (project) or
|
|
877
|
+
# `~/.codeium/windsurf/hooks.json` (user). Cascade has no generic
|
|
878
|
+
# post-tool-use surface — concerns gated to that slot don't fire on
|
|
879
|
+
# Windsurf (documented platform limitation in chat-history-platform-hooks.md).
|
|
880
|
+
WINDSURF_DISPATCHER_BINDINGS = (
|
|
881
|
+
("session_start", "post_setup_worktree"),
|
|
882
|
+
("user_prompt_submit", "pre_user_prompt"),
|
|
883
|
+
("stop", "post_cascade_response"),
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _windsurf_dispatch_command(ac_event: str, native: str) -> str:
|
|
888
|
+
return (
|
|
889
|
+
f"./agent-config dispatch:hook "
|
|
890
|
+
f"--platform windsurf --event {ac_event} "
|
|
891
|
+
f"--native-event {native}"
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def ensure_windsurf_bridge(project_root: Path, force: bool) -> None:
|
|
896
|
+
"""Deploy `.windsurf/hooks.json` (project scope) with the Phase 7
|
|
897
|
+
universal dispatcher hooks.
|
|
898
|
+
|
|
899
|
+
Each Windsurf lifecycle event is wired to a single
|
|
900
|
+
`./agent-config dispatch:hook` invocation. Cascade fires project
|
|
901
|
+
hooks with the workspace as cwd, so no trampoline is needed at this
|
|
902
|
+
scope. Idempotent via deep_merge — rerunning replaces hook arrays
|
|
903
|
+
rather than appending duplicates. `show_output: false` keeps post
|
|
904
|
+
hooks silent (per Windsurf docs); concerns stream their own output
|
|
905
|
+
via agents/state/.dispatcher/.
|
|
906
|
+
"""
|
|
907
|
+
hooks: dict[str, list] = {}
|
|
908
|
+
for ac_event, native in WINDSURF_DISPATCHER_BINDINGS:
|
|
909
|
+
hooks.setdefault(native, []).append({
|
|
910
|
+
"command": _windsurf_dispatch_command(ac_event, native),
|
|
911
|
+
"show_output": False,
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
bridge = {"hooks": hooks}
|
|
915
|
+
merge_json_file(
|
|
916
|
+
project_root / ".windsurf" / "hooks.json",
|
|
917
|
+
bridge,
|
|
918
|
+
force,
|
|
919
|
+
".windsurf/hooks.json",
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# Windsurf user-scope hooks live at `~/.codeium/windsurf/hooks.json`
|
|
924
|
+
# (per docs.windsurf.com/windsurf/cascade/hooks). The trampoline
|
|
925
|
+
# resolves the active workspace from $PWD / .agent-settings.yml /
|
|
926
|
+
# tool_info.cwd|file_path / $ROOT_WORKSPACE_PATH and routes the JSON
|
|
927
|
+
# into that project's `./agent-config dispatch:hook`. Silent no-op
|
|
928
|
+
# when the workspace is not an agent-config consumer.
|
|
929
|
+
WINDSURF_USER_DIR = Path.home() / ".codeium" / "windsurf"
|
|
930
|
+
WINDSURF_USER_HOOKS_DIR = WINDSURF_USER_DIR / "hooks"
|
|
931
|
+
WINDSURF_DISPATCHER_TRAMPOLINE = "windsurf-dispatcher.sh"
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def ensure_windsurf_user_hooks(package_root: Path, force: bool) -> None:
|
|
935
|
+
"""Deploy the Windsurf universal-dispatcher trampoline at user scope.
|
|
936
|
+
|
|
937
|
+
Phase 7.7 (hook-architecture-v1.md): mirrors ensure_cursor_user_hooks
|
|
938
|
+
for the Windsurf surface. Writes:
|
|
939
|
+
- ~/.codeium/windsurf/hooks/windsurf-dispatcher.sh (trampoline)
|
|
940
|
+
- ~/.codeium/windsurf/hooks.json (event → trampoline call)
|
|
941
|
+
|
|
942
|
+
Each hooks.json command line is `<dispatcher> <ac_event> <native>`
|
|
943
|
+
so the trampoline forwards both names to the dispatcher for
|
|
944
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
945
|
+
"""
|
|
946
|
+
src = package_root / "scripts" / "hooks" / WINDSURF_DISPATCHER_TRAMPOLINE
|
|
947
|
+
if not src.exists():
|
|
948
|
+
skip(f"windsurf trampoline missing in package: {src}")
|
|
949
|
+
return
|
|
950
|
+
|
|
951
|
+
WINDSURF_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
952
|
+
dst = WINDSURF_USER_HOOKS_DIR / WINDSURF_DISPATCHER_TRAMPOLINE
|
|
953
|
+
src_text = src.read_text(encoding="utf-8")
|
|
954
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
955
|
+
skip(f"~/.codeium/windsurf/hooks/{WINDSURF_DISPATCHER_TRAMPOLINE} already up to date")
|
|
956
|
+
else:
|
|
957
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
958
|
+
dst.chmod(0o755)
|
|
959
|
+
success(f"~/.codeium/windsurf/hooks/{WINDSURF_DISPATCHER_TRAMPOLINE} installed")
|
|
960
|
+
|
|
961
|
+
hooks: dict[str, list] = {}
|
|
962
|
+
for ac_event, native in WINDSURF_DISPATCHER_BINDINGS:
|
|
963
|
+
hooks.setdefault(native, []).append({
|
|
964
|
+
"command": f"{dst} {ac_event} {native}",
|
|
965
|
+
"show_output": False,
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
settings_patch: dict = {"hooks": hooks}
|
|
969
|
+
merge_json_file(
|
|
970
|
+
WINDSURF_USER_DIR / "hooks.json",
|
|
971
|
+
settings_patch,
|
|
972
|
+
force,
|
|
973
|
+
"~/.codeium/windsurf/hooks.json",
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
# Gemini CLI lifecycle events → agent-config event vocabulary.
|
|
978
|
+
# Phase 7.8 (hook-architecture-v1.md, scripts/hook_manifest.yaml):
|
|
979
|
+
# Gemini reads `.gemini/settings.json` (project) or
|
|
980
|
+
# `~/.gemini/settings.json` (user). Each event maps to an array of
|
|
981
|
+
# hook groups; each group has a `matcher` (exact string for lifecycle,
|
|
982
|
+
# regex for tool events) and a `hooks` array of `{type: "command",
|
|
983
|
+
# command: "..."}`.
|
|
984
|
+
#
|
|
985
|
+
# Native event names per geminicli.com/docs/hooks/reference/
|
|
986
|
+
# (PascalCase). BeforeAgent fires after the user submits a prompt
|
|
987
|
+
# and before agent planning — our user_prompt_submit slot. AfterAgent
|
|
988
|
+
# fires when the agent loop ends — our stop slot. SessionStart /
|
|
989
|
+
# SessionEnd are advisory (continue/decision ignored). For lifecycle
|
|
990
|
+
# events the matcher filters on `source` ("startup"|"resume"|"clear"
|
|
991
|
+
# for SessionStart, etc.); empty matcher == match all.
|
|
992
|
+
GEMINI_DISPATCHER_BINDINGS = (
|
|
993
|
+
("session_start", "SessionStart", ""),
|
|
994
|
+
("session_end", "SessionEnd", ""),
|
|
995
|
+
("stop", "AfterAgent", ""),
|
|
996
|
+
("user_prompt_submit", "BeforeAgent", ""),
|
|
997
|
+
("post_tool_use", "AfterTool", ".*"),
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _gemini_dispatch_command(ac_event: str, native: str) -> str:
|
|
1002
|
+
return (
|
|
1003
|
+
f"./agent-config dispatch:hook "
|
|
1004
|
+
f"--platform gemini --event {ac_event} "
|
|
1005
|
+
f"--native-event {native}"
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _gemini_hooks_dict(command_factory) -> dict[str, list]:
|
|
1010
|
+
"""Build the nested {event: [{matcher, hooks: [{type, command}]}]}
|
|
1011
|
+
payload Gemini expects. command_factory(ac_event, native) returns
|
|
1012
|
+
the command string for one binding."""
|
|
1013
|
+
out: dict[str, list] = {}
|
|
1014
|
+
for ac_event, native, matcher in GEMINI_DISPATCHER_BINDINGS:
|
|
1015
|
+
out.setdefault(native, []).append({
|
|
1016
|
+
"matcher": matcher,
|
|
1017
|
+
"hooks": [
|
|
1018
|
+
{
|
|
1019
|
+
"type": "command",
|
|
1020
|
+
"command": command_factory(ac_event, native),
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
})
|
|
1024
|
+
return out
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def ensure_gemini_bridge(project_root: Path, force: bool) -> None:
|
|
1028
|
+
"""Deploy `.gemini/settings.json` (project scope) with the Phase 7
|
|
1029
|
+
universal dispatcher hooks.
|
|
1030
|
+
|
|
1031
|
+
Each Gemini lifecycle event is wired to a single
|
|
1032
|
+
`./agent-config dispatch:hook` invocation. Project-scope hooks
|
|
1033
|
+
fire with the project as cwd, so no trampoline is needed at this
|
|
1034
|
+
scope. Idempotent via deep_merge — rerunning replaces hook arrays
|
|
1035
|
+
rather than appending duplicates.
|
|
1036
|
+
"""
|
|
1037
|
+
bridge = {"hooks": _gemini_hooks_dict(_gemini_dispatch_command)}
|
|
1038
|
+
merge_json_file(
|
|
1039
|
+
project_root / ".gemini" / "settings.json",
|
|
1040
|
+
bridge,
|
|
1041
|
+
force,
|
|
1042
|
+
".gemini/settings.json",
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
# Gemini user-scope hooks live at `~/.gemini/settings.json` and fire
|
|
1047
|
+
# across every project the developer opens. The trampoline resolves
|
|
1048
|
+
# the active workspace from $PWD / .agent-settings.yml / payload.cwd
|
|
1049
|
+
# and routes the JSON into that project's `./agent-config dispatch:hook`.
|
|
1050
|
+
# Silent no-op when the workspace is not an agent-config consumer.
|
|
1051
|
+
GEMINI_USER_DIR = Path.home() / ".gemini"
|
|
1052
|
+
GEMINI_USER_HOOKS_DIR = GEMINI_USER_DIR / "hooks"
|
|
1053
|
+
GEMINI_DISPATCHER_TRAMPOLINE = "gemini-dispatcher.sh"
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def ensure_gemini_user_hooks(package_root: Path, force: bool) -> None:
|
|
1057
|
+
"""Deploy the Gemini universal-dispatcher trampoline at user scope.
|
|
1058
|
+
|
|
1059
|
+
Phase 7.8 (hook-architecture-v1.md): mirrors ensure_windsurf_user_hooks
|
|
1060
|
+
for the Gemini surface. Writes:
|
|
1061
|
+
- ~/.gemini/hooks/gemini-dispatcher.sh (trampoline)
|
|
1062
|
+
- ~/.gemini/settings.json (event → trampoline call)
|
|
1063
|
+
|
|
1064
|
+
Each settings.json command line is `<dispatcher> <ac_event> <native>`
|
|
1065
|
+
so the trampoline forwards both names to the dispatcher for
|
|
1066
|
+
traceability. Hooks fire across all projects the developer opens.
|
|
1067
|
+
"""
|
|
1068
|
+
src = package_root / "scripts" / "hooks" / GEMINI_DISPATCHER_TRAMPOLINE
|
|
1069
|
+
if not src.exists():
|
|
1070
|
+
skip(f"gemini trampoline missing in package: {src}")
|
|
1071
|
+
return
|
|
1072
|
+
|
|
1073
|
+
GEMINI_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1074
|
+
dst = GEMINI_USER_HOOKS_DIR / GEMINI_DISPATCHER_TRAMPOLINE
|
|
1075
|
+
src_text = src.read_text(encoding="utf-8")
|
|
1076
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
1077
|
+
skip(f"~/.gemini/hooks/{GEMINI_DISPATCHER_TRAMPOLINE} already up to date")
|
|
1078
|
+
else:
|
|
1079
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
1080
|
+
dst.chmod(0o755)
|
|
1081
|
+
success(f"~/.gemini/hooks/{GEMINI_DISPATCHER_TRAMPOLINE} installed")
|
|
1082
|
+
|
|
1083
|
+
settings_patch = {
|
|
1084
|
+
"hooks": _gemini_hooks_dict(
|
|
1085
|
+
lambda ac_event, native: f"{dst} {ac_event} {native}",
|
|
1086
|
+
),
|
|
1087
|
+
}
|
|
1088
|
+
merge_json_file(
|
|
1089
|
+
GEMINI_USER_DIR / "settings.json",
|
|
1090
|
+
settings_patch,
|
|
1091
|
+
force,
|
|
1092
|
+
"~/.gemini/settings.json",
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
568
1096
|
def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
569
1097
|
target = project_root / ".github" / "plugin" / "marketplace.json"
|
|
570
1098
|
|
|
@@ -588,6 +1116,107 @@ def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
|
588
1116
|
success(".github/plugin/marketplace.json created")
|
|
589
1117
|
|
|
590
1118
|
|
|
1119
|
+
# --- Post-install smoke test ---
|
|
1120
|
+
|
|
1121
|
+
# (platform, native event used for the dry-fire). Probe events are
|
|
1122
|
+
# chosen so the dispatcher resolves at least one concern per platform
|
|
1123
|
+
# from the canonical manifest. Copilot is intentionally excluded —
|
|
1124
|
+
# rule-only fallback per Phase 7.9.
|
|
1125
|
+
SMOKE_PROBE_EVENTS = (
|
|
1126
|
+
("augment", "session_start"),
|
|
1127
|
+
("claude", "SessionStart"),
|
|
1128
|
+
("cursor", "beforeShellExecution"),
|
|
1129
|
+
("cline", "session_start"),
|
|
1130
|
+
("windsurf", "post_setup_worktree"),
|
|
1131
|
+
("gemini", "SessionStart"),
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1134
|
+
# Map platform → bridge file/dir we expect to exist before probing.
|
|
1135
|
+
# Mirrors PLATFORM_BRIDGES in scripts/hooks_status.py.
|
|
1136
|
+
SMOKE_BRIDGE_PATHS = {
|
|
1137
|
+
"augment": ".augment/settings.json",
|
|
1138
|
+
"claude": ".claude/settings.json",
|
|
1139
|
+
"cursor": ".cursor/hooks.json",
|
|
1140
|
+
"cline": ".clinerules/hooks",
|
|
1141
|
+
"windsurf": ".windsurf/hooks.json",
|
|
1142
|
+
"gemini": ".gemini/settings.json",
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def _smoke_test_hooks(project_root: Path, package_root: Path) -> int:
|
|
1147
|
+
"""Dry-fire dispatch_hook.py against every installed bridge.
|
|
1148
|
+
|
|
1149
|
+
Per Phase 7.12: uses `--dry-run` so resolution-only — no concern
|
|
1150
|
+
invocation, no state writes outside the dispatcher's own report.
|
|
1151
|
+
Failure is non-fatal (warn only); install always exits 0 even
|
|
1152
|
+
when smoke fails so consumers in restricted CI sandboxes are not
|
|
1153
|
+
blocked. CI-side strict mode lives in `hooks_status --strict`.
|
|
1154
|
+
"""
|
|
1155
|
+
dispatcher = package_root / "scripts" / "hooks" / "dispatch_hook.py"
|
|
1156
|
+
manifest = package_root / "scripts" / "hook_manifest.yaml"
|
|
1157
|
+
if not dispatcher.is_file() or not manifest.is_file():
|
|
1158
|
+
return 0 # package layout doesn't ship the dispatcher; skip silently
|
|
1159
|
+
|
|
1160
|
+
failed: list[str] = []
|
|
1161
|
+
skipped: list[str] = []
|
|
1162
|
+
passed: list[str] = []
|
|
1163
|
+
|
|
1164
|
+
for platform, native in SMOKE_PROBE_EVENTS:
|
|
1165
|
+
rel_bridge = SMOKE_BRIDGE_PATHS.get(platform, "")
|
|
1166
|
+
bridge_path = project_root / rel_bridge if rel_bridge else None
|
|
1167
|
+
bridge_present = bool(
|
|
1168
|
+
bridge_path and (bridge_path.is_file() or
|
|
1169
|
+
(bridge_path.is_dir() and any(bridge_path.iterdir())))
|
|
1170
|
+
)
|
|
1171
|
+
if not bridge_present:
|
|
1172
|
+
skipped.append(platform)
|
|
1173
|
+
continue
|
|
1174
|
+
# Map native → agent-config event using the dispatcher's own
|
|
1175
|
+
# alias resolution. We re-use the dispatcher in --dry-run mode,
|
|
1176
|
+
# passing both --platform + --event=<canonical>. Since the
|
|
1177
|
+
# canonical event is what the manifest binds against, we feed
|
|
1178
|
+
# it directly: 'session_start' is the cross-platform anchor
|
|
1179
|
+
# that every bridge wires up. This avoids re-implementing
|
|
1180
|
+
# alias resolution here.
|
|
1181
|
+
cmd = [
|
|
1182
|
+
sys.executable, str(dispatcher),
|
|
1183
|
+
"--manifest", str(manifest),
|
|
1184
|
+
"--platform", platform,
|
|
1185
|
+
"--event", "session_start",
|
|
1186
|
+
"--native-event", native,
|
|
1187
|
+
"--dry-run",
|
|
1188
|
+
]
|
|
1189
|
+
try:
|
|
1190
|
+
proc = subprocess.run(
|
|
1191
|
+
cmd, input="{}", capture_output=True, text=True,
|
|
1192
|
+
cwd=str(project_root), timeout=10, check=False,
|
|
1193
|
+
)
|
|
1194
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
1195
|
+
failed.append(f"{platform}: {exc}")
|
|
1196
|
+
continue
|
|
1197
|
+
if proc.returncode != 0:
|
|
1198
|
+
failed.append(f"{platform}: exit={proc.returncode} {proc.stderr.strip()[:120]}")
|
|
1199
|
+
continue
|
|
1200
|
+
try:
|
|
1201
|
+
plan = json.loads(proc.stdout or "{}")
|
|
1202
|
+
except json.JSONDecodeError:
|
|
1203
|
+
failed.append(f"{platform}: dispatcher did not emit JSON plan")
|
|
1204
|
+
continue
|
|
1205
|
+
if not isinstance(plan.get("concerns"), list):
|
|
1206
|
+
failed.append(f"{platform}: plan.concerns missing or not a list")
|
|
1207
|
+
continue
|
|
1208
|
+
passed.append(platform)
|
|
1209
|
+
|
|
1210
|
+
if not QUIET:
|
|
1211
|
+
if passed:
|
|
1212
|
+
success(f"hook smoke passed: {', '.join(passed)}")
|
|
1213
|
+
if skipped:
|
|
1214
|
+
skip(f"hook smoke skipped (bridge not installed): {', '.join(skipped)}")
|
|
1215
|
+
for line in failed:
|
|
1216
|
+
warn(f"hook smoke failed — {line}")
|
|
1217
|
+
return 1 if failed else 0
|
|
1218
|
+
|
|
1219
|
+
|
|
591
1220
|
# --- Argument parsing ---
|
|
592
1221
|
|
|
593
1222
|
def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
@@ -608,9 +1237,34 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
608
1237
|
action="store_true",
|
|
609
1238
|
help="also deploy ~/.augment/settings.json + ~/.augment/hooks/ (user-scope, all projects)",
|
|
610
1239
|
)
|
|
1240
|
+
parser.add_argument(
|
|
1241
|
+
"--cursor-user-hooks",
|
|
1242
|
+
action="store_true",
|
|
1243
|
+
help="also deploy ~/.cursor/hooks.json + ~/.cursor/hooks/cursor-dispatcher.sh (user-scope, all projects)",
|
|
1244
|
+
)
|
|
1245
|
+
parser.add_argument(
|
|
1246
|
+
"--cline-user-hooks",
|
|
1247
|
+
action="store_true",
|
|
1248
|
+
help="also deploy ~/Documents/Cline/Hooks/ trampoline + per-event wrappers (user-scope, all projects)",
|
|
1249
|
+
)
|
|
1250
|
+
parser.add_argument(
|
|
1251
|
+
"--windsurf-user-hooks",
|
|
1252
|
+
action="store_true",
|
|
1253
|
+
help="also deploy ~/.codeium/windsurf/hooks.json + hooks/windsurf-dispatcher.sh (user-scope, all projects)",
|
|
1254
|
+
)
|
|
1255
|
+
parser.add_argument(
|
|
1256
|
+
"--gemini-user-hooks",
|
|
1257
|
+
action="store_true",
|
|
1258
|
+
help="also deploy ~/.gemini/settings.json + ~/.gemini/hooks/gemini-dispatcher.sh (user-scope, all projects)",
|
|
1259
|
+
)
|
|
611
1260
|
parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
|
|
612
1261
|
parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
|
|
613
1262
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
1263
|
+
parser.add_argument(
|
|
1264
|
+
"--no-smoke",
|
|
1265
|
+
action="store_true",
|
|
1266
|
+
help="skip the post-install hook smoke test (default: dry-fire dispatch:hook against every installed bridge)",
|
|
1267
|
+
)
|
|
614
1268
|
return parser.parse_args(argv)
|
|
615
1269
|
|
|
616
1270
|
|
|
@@ -651,11 +1305,33 @@ def main(argv: list[str]) -> int:
|
|
|
651
1305
|
ensure_vscode_bridge(project_root, package_type, opts.force)
|
|
652
1306
|
ensure_augment_bridge(project_root, opts.force)
|
|
653
1307
|
ensure_claude_bridge(project_root, opts.force)
|
|
1308
|
+
ensure_cursor_bridge(project_root, opts.force)
|
|
1309
|
+
ensure_cline_bridge(project_root, opts.force)
|
|
1310
|
+
ensure_windsurf_bridge(project_root, opts.force)
|
|
1311
|
+
ensure_gemini_bridge(project_root, opts.force)
|
|
654
1312
|
ensure_copilot_bridge(project_root, opts.force)
|
|
655
1313
|
|
|
656
1314
|
if opts.augment_user_hooks:
|
|
657
1315
|
ensure_augment_user_hooks(package_root, opts.force)
|
|
658
1316
|
|
|
1317
|
+
if opts.cursor_user_hooks:
|
|
1318
|
+
ensure_cursor_user_hooks(package_root, opts.force)
|
|
1319
|
+
|
|
1320
|
+
if opts.cline_user_hooks:
|
|
1321
|
+
ensure_cline_user_hooks(package_root, opts.force)
|
|
1322
|
+
|
|
1323
|
+
if opts.windsurf_user_hooks:
|
|
1324
|
+
ensure_windsurf_user_hooks(package_root, opts.force)
|
|
1325
|
+
|
|
1326
|
+
if opts.gemini_user_hooks:
|
|
1327
|
+
ensure_gemini_user_hooks(package_root, opts.force)
|
|
1328
|
+
|
|
1329
|
+
if not opts.skip_bridges and not opts.no_smoke:
|
|
1330
|
+
if not QUIET:
|
|
1331
|
+
print()
|
|
1332
|
+
info("Smoke-testing installed hook bridges (dry-run)")
|
|
1333
|
+
_smoke_test_hooks(project_root, package_root)
|
|
1334
|
+
|
|
659
1335
|
if not QUIET:
|
|
660
1336
|
print()
|
|
661
1337
|
success("Done.")
|
|
@@ -668,6 +1344,7 @@ def main(argv: list[str]) -> int:
|
|
|
668
1344
|
print(" Next steps:")
|
|
669
1345
|
print(" • Commit .agent-settings.yml and bridge files to your repo")
|
|
670
1346
|
print(" • New team members just run composer install / npm install — done")
|
|
1347
|
+
print(" • Inspect hook coverage: ./agent-config hooks:status")
|
|
671
1348
|
print(" • Full walkthrough: https://github.com/event4u-app/agent-config/blob/main/docs/getting-started.md")
|
|
672
1349
|
print()
|
|
673
1350
|
return 0
|