@event4u/agent-config 1.18.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.
Files changed (126) hide show
  1. package/.agent-src/commands/council/default.md +74 -76
  2. package/.agent-src/commands/feature/roadmap.md +22 -0
  3. package/.agent-src/commands/roadmap/create.md +38 -6
  4. package/.agent-src/commands/roadmap/execute.md +36 -9
  5. package/.agent-src/rules/agent-authority.md +1 -0
  6. package/.agent-src/rules/agent-docs.md +1 -0
  7. package/.agent-src/rules/analysis-skill-routing.md +1 -0
  8. package/.agent-src/rules/architecture.md +1 -0
  9. package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
  10. package/.agent-src/rules/artifact-engagement-recording.md +1 -0
  11. package/.agent-src/rules/ask-when-uncertain.md +1 -0
  12. package/.agent-src/rules/augment-portability.md +1 -0
  13. package/.agent-src/rules/augment-source-of-truth.md +1 -0
  14. package/.agent-src/rules/autonomous-execution.md +1 -0
  15. package/.agent-src/rules/capture-learnings.md +1 -0
  16. package/.agent-src/rules/chat-history-cadence.md +34 -0
  17. package/.agent-src/rules/chat-history-ownership.md +1 -0
  18. package/.agent-src/rules/chat-history-visibility.md +1 -0
  19. package/.agent-src/rules/cli-output-handling.md +2 -2
  20. package/.agent-src/rules/command-suggestion-policy.md +1 -0
  21. package/.agent-src/rules/commit-conventions.md +1 -0
  22. package/.agent-src/rules/commit-policy.md +1 -0
  23. package/.agent-src/rules/context-hygiene.md +22 -0
  24. package/.agent-src/rules/direct-answers.md +1 -0
  25. package/.agent-src/rules/docker-commands.md +1 -0
  26. package/.agent-src/rules/docs-sync.md +1 -0
  27. package/.agent-src/rules/downstream-changes.md +1 -0
  28. package/.agent-src/rules/e2e-testing.md +1 -0
  29. package/.agent-src/rules/guidelines.md +1 -0
  30. package/.agent-src/rules/improve-before-implement.md +1 -0
  31. package/.agent-src/rules/language-and-tone.md +1 -0
  32. package/.agent-src/rules/laravel-translations.md +1 -0
  33. package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
  34. package/.agent-src/rules/minimal-safe-diff.md +1 -0
  35. package/.agent-src/rules/missing-tool-handling.md +1 -0
  36. package/.agent-src/rules/model-recommendation.md +1 -0
  37. package/.agent-src/rules/no-cheap-questions.md +1 -0
  38. package/.agent-src/rules/no-roadmap-references.md +1 -0
  39. package/.agent-src/rules/non-destructive-by-default.md +1 -0
  40. package/.agent-src/rules/onboarding-gate.md +26 -0
  41. package/.agent-src/rules/package-ci-checks.md +1 -0
  42. package/.agent-src/rules/php-coding.md +1 -0
  43. package/.agent-src/rules/preservation-guard.md +1 -0
  44. package/.agent-src/rules/review-routing-awareness.md +1 -0
  45. package/.agent-src/rules/reviewer-awareness.md +1 -0
  46. package/.agent-src/rules/roadmap-progress-sync.md +22 -0
  47. package/.agent-src/rules/role-mode-adherence.md +2 -2
  48. package/.agent-src/rules/rule-type-governance.md +1 -0
  49. package/.agent-src/rules/runtime-safety.md +1 -0
  50. package/.agent-src/rules/scope-control.md +1 -0
  51. package/.agent-src/rules/security-sensitive-stop.md +1 -0
  52. package/.agent-src/rules/size-enforcement.md +1 -0
  53. package/.agent-src/rules/skill-improvement-trigger.md +1 -0
  54. package/.agent-src/rules/skill-quality.md +1 -0
  55. package/.agent-src/rules/slash-command-routing-policy.md +39 -0
  56. package/.agent-src/rules/think-before-action.md +1 -0
  57. package/.agent-src/rules/token-efficiency.md +1 -0
  58. package/.agent-src/rules/tool-safety.md +1 -0
  59. package/.agent-src/rules/ui-audit-gate.md +1 -0
  60. package/.agent-src/rules/upstream-proposal.md +1 -0
  61. package/.agent-src/rules/user-interaction.md +1 -0
  62. package/.agent-src/rules/verify-before-complete.md +1 -0
  63. package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
  64. package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
  65. package/.agent-src/templates/agent-settings.md +16 -0
  66. package/.agent-src/templates/roadmaps.md +8 -3
  67. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
  68. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
  69. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
  70. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
  71. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
  72. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
  73. package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
  74. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
  75. package/.claude-plugin/marketplace.json +1 -1
  76. package/CHANGELOG.md +62 -0
  77. package/README.md +19 -19
  78. package/config/agent-settings.template.yml +23 -0
  79. package/docs/catalog.md +5 -2
  80. package/docs/contracts/adr-settings-sync-engine.md +127 -0
  81. package/docs/contracts/decision-trace-v1.md +146 -0
  82. package/docs/contracts/file-ownership-matrix.json +7 -0
  83. package/docs/contracts/hook-architecture-v1.md +213 -0
  84. package/docs/contracts/memory-visibility-v1.md +138 -0
  85. package/docs/contracts/one-off-script-lifecycle.md +109 -0
  86. package/docs/contracts/rule-interactions.yml +22 -0
  87. package/docs/customization.md +1 -0
  88. package/docs/development.md +4 -1
  89. package/docs/guidelines/agent-infra/layered-settings.md +32 -13
  90. package/package.json +1 -1
  91. package/scripts/agent-config +44 -0
  92. package/scripts/ai_council/bundler.py +3 -3
  93. package/scripts/ai_council/clients.py +24 -8
  94. package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
  95. package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
  96. package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
  97. package/scripts/ai_council/session.py +92 -0
  98. package/scripts/capture_showcase_session.py +361 -0
  99. package/scripts/chat_history.py +11 -1
  100. package/scripts/check_always_budget.py +7 -2
  101. package/scripts/context_hygiene_hook.py +14 -6
  102. package/scripts/council_cli.py +357 -0
  103. package/scripts/hook_manifest.yaml +184 -0
  104. package/scripts/hooks/__init__.py +1 -0
  105. package/scripts/hooks/augment-dispatcher.sh +72 -0
  106. package/scripts/hooks/cline-dispatcher.sh +86 -0
  107. package/scripts/hooks/cursor-dispatcher.sh +76 -0
  108. package/scripts/hooks/dispatch_hook.py +348 -0
  109. package/scripts/hooks/envelope.py +98 -0
  110. package/scripts/hooks/gemini-dispatcher.sh +117 -0
  111. package/scripts/hooks/state_io.py +122 -0
  112. package/scripts/hooks/windsurf-dispatcher.sh +123 -0
  113. package/scripts/hooks_status.py +146 -0
  114. package/scripts/install.py +725 -87
  115. package/scripts/install.sh +1 -1
  116. package/scripts/lint_hook_manifest.py +216 -0
  117. package/scripts/lint_one_off_age.py +184 -0
  118. package/scripts/lint_rule_tiers.py +78 -0
  119. package/scripts/lint_showcase_sessions.py +148 -0
  120. package/scripts/minimal_safe_diff_hook.py +245 -0
  121. package/scripts/onboarding_gate_hook.py +13 -8
  122. package/scripts/readme_linter.py +12 -3
  123. package/scripts/roadmap_progress_hook.py +5 -0
  124. package/scripts/sync_agent_settings.py +32 -129
  125. package/scripts/sync_yaml_rt.py +734 -0
  126. package/scripts/verify_before_complete_hook.py +216 -0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Gemini CLI universal hook trampoline (Phase 7.8,
3
+ # hook-architecture-v1.md).
4
+ #
5
+ # Routes Gemini hook events — fired from either the project-scope
6
+ # `.gemini/settings.json` or the user-scope `~/.gemini/settings.json`
7
+ # — into the active workspace's `./agent-config dispatch:hook`.
8
+ #
9
+ # Gemini event payload (per geminicli.com/docs/hooks/reference/):
10
+ # { "session_id": "...", "cwd": "...",
11
+ # "hook_event_name": "SessionStart" | "BeforeAgent" | ...,
12
+ # <event-specific fields: source, prompt, tool_name, ...> }
13
+ #
14
+ # Workspace resolution — Gemini does NOT pass a workspace_roots array
15
+ # the way Cursor/Cline do. Instead:
16
+ # 1. Project-scope hook → cwd is the workspace root (Gemini fires
17
+ # hooks with the project as cwd).
18
+ # `$PWD` containing `./agent-config` is the happy path.
19
+ # 2. User-scope hook → cwd may be the workspace, but for some
20
+ # events Gemini executes from `$HOME` or a tmp dir. Fall back to:
21
+ # - the JSON payload's `cwd` field
22
+ # - walk up to nearest .agent-settings.yml
23
+ # 3. Bail silently when no resolution succeeds — concerns are
24
+ # observe-only at this layer; chat-history / roadmap-progress /
25
+ # context-hygiene never block, and onboarding-gate writes state,
26
+ # not exit code.
27
+ #
28
+ # Output — none on stdout. Gemini consumes JSON on stdout for
29
+ # context injection / decision; we don't inject anything from this
30
+ # layer (concerns stream their own state via agents/state/.dispatcher/).
31
+ # SessionStart / SessionEnd are advisory in Gemini (continue/decision
32
+ # ignored), so we always exit 0.
33
+
34
+ set -u
35
+
36
+ # Args from the platform's settings.json command string:
37
+ # $1 = agent-config event name (session_start, stop, user_prompt_submit, ...)
38
+ # $2 = Gemini-native event name (SessionStart, BeforeAgent, ...)
39
+ EVENT="${1-}"
40
+ NATIVE_EVENT="${2-}"
41
+
42
+ if [ -z "$EVENT" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ EVENT_DATA="$(cat)"
47
+
48
+ # 1. $PWD wins when it already looks like an agent-config workspace.
49
+ WORKSPACE=""
50
+ if [ -x "$PWD/agent-config" ] || [ -f "$PWD/.agent-settings.yml" ]; then
51
+ WORKSPACE="$PWD"
52
+ fi
53
+
54
+ # 2. Walk up from $PWD looking for .agent-settings.yml (covers
55
+ # sub-directory invocations).
56
+ if [ -z "$WORKSPACE" ]; then
57
+ candidate="$PWD"
58
+ while [ -n "$candidate" ] && [ "$candidate" != "/" ]; do
59
+ if [ -f "$candidate/.agent-settings.yml" ]; then
60
+ WORKSPACE="$candidate"
61
+ break
62
+ fi
63
+ candidate="$(dirname "$candidate")"
64
+ done
65
+ fi
66
+
67
+ # 3. Parse JSON `cwd` field from the payload.
68
+ if [ -z "$WORKSPACE" ]; then
69
+ if command -v jq >/dev/null 2>&1; then
70
+ EXTRACTED="$(printf '%s' "$EVENT_DATA" \
71
+ | jq -r '.cwd // empty' 2>/dev/null)"
72
+ elif command -v python3 >/dev/null 2>&1; then
73
+ EXTRACTED="$(printf '%s' "$EVENT_DATA" | python3 -c '
74
+ import json, sys
75
+ try:
76
+ data = json.load(sys.stdin)
77
+ except Exception:
78
+ sys.exit(0)
79
+ print(data.get("cwd") or "")
80
+ ' 2>/dev/null)"
81
+ else
82
+ EXTRACTED=""
83
+ fi
84
+ EXTRACTED="${EXTRACTED%$'\r'}"
85
+ if [ -n "$EXTRACTED" ]; then
86
+ candidate="$EXTRACTED"
87
+ if [ -f "$candidate" ]; then
88
+ candidate="$(dirname "$candidate")"
89
+ fi
90
+ while [ -n "$candidate" ] && [ "$candidate" != "/" ]; do
91
+ if [ -f "$candidate/.agent-settings.yml" ]; then
92
+ WORKSPACE="$candidate"
93
+ break
94
+ fi
95
+ candidate="$(dirname "$candidate")"
96
+ done
97
+ fi
98
+ fi
99
+
100
+ if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
101
+ exit 0
102
+ fi
103
+
104
+ cd "$WORKSPACE" 2>/dev/null || exit 0
105
+
106
+ if [ ! -x ./agent-config ]; then
107
+ exit 0
108
+ fi
109
+
110
+ printf '%s' "$EVENT_DATA" \
111
+ | ./agent-config dispatch:hook \
112
+ --platform gemini \
113
+ --event "$EVENT" \
114
+ --native-event "$NATIVE_EVENT" \
115
+ >/dev/null 2>&1 || true
116
+
117
+ exit 0
@@ -0,0 +1,122 @@
1
+ """Concurrency-safe state writes for hook concerns.
2
+
3
+ Per `docs/contracts/hook-architecture-v1.md` § Concurrency, every concern
4
+ that writes under `agents/state/` MUST:
5
+
6
+ 1. Acquire `fcntl.flock(LOCK_EX)` on `agents/state/.dispatcher.lock`.
7
+ 2. Write to a sibling `<dest>.tmp.<pid>` file in the same directory.
8
+ 3. `os.replace(tmp, dest)` — POSIX-atomic on the same filesystem.
9
+ 4. Release the lock.
10
+
11
+ The single shared lock is intentional: serialising state writes across
12
+ concerns is cheaper than per-file locks, and concerns already run
13
+ sequentially within one dispatcher invocation. Concurrent dispatcher
14
+ invocations (e.g. two platforms firing into the same workspace) are the
15
+ case this lock guards.
16
+
17
+ Cross-platform notes
18
+ --------------------
19
+ - `fcntl` is POSIX-only. On Windows the contract degrades gracefully:
20
+ the lock acquire is a no-op, atomic replace via `os.replace` still
21
+ holds, and torn-write risk is accepted (Windows is not a primary
22
+ agent-config platform — Cline tracks the upstream Windows-path issue
23
+ separately).
24
+ - The lock file lives under `agents/state/` which is gitignored.
25
+ - The lock is process-scoped, not session-scoped: each call opens,
26
+ locks, writes, releases, closes. No long-lived file handles.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ try:
36
+ import fcntl # type: ignore[import-not-found]
37
+ _HAS_FCNTL = True
38
+ except ImportError: # pragma: no cover — Windows
39
+ _HAS_FCNTL = False
40
+
41
+ LOCK_BASENAME = ".dispatcher.lock"
42
+
43
+
44
+ def _lock_path(state_dir: Path) -> Path:
45
+ return state_dir / LOCK_BASENAME
46
+
47
+
48
+ def atomic_write_json(target: Path, payload: Any, *, indent: int = 2) -> None:
49
+ """Write `payload` as JSON to `target` atomically and concurrency-safely.
50
+
51
+ `target` MUST sit under an `agents/state/` directory (or any other
52
+ directory the caller treats as the lock scope). The lock file is
53
+ `<target.parent>/.dispatcher.lock`. Caller does not need to create
54
+ the directory in advance — this function ensures it.
55
+ """
56
+ target = Path(target)
57
+ state_dir = target.parent
58
+ state_dir.mkdir(parents=True, exist_ok=True)
59
+ body = json.dumps(payload, indent=indent) + "\n"
60
+ _atomic_write_text(target, body)
61
+
62
+
63
+ def atomic_write_text(target: Path, text: str) -> None:
64
+ """Write text to `target` atomically and concurrency-safely. Same
65
+ locking discipline as `atomic_write_json` — useful for non-JSON
66
+ state payloads (chat-history transcript, status text)."""
67
+ target = Path(target)
68
+ state_dir = target.parent
69
+ state_dir.mkdir(parents=True, exist_ok=True)
70
+ _atomic_write_text(target, text)
71
+
72
+
73
+ def _atomic_write_text(target: Path, text: str) -> None:
74
+ tmp = target.with_suffix(target.suffix + f".tmp.{os.getpid()}")
75
+ lock_path = _lock_path(target.parent)
76
+ # `os.O_CREAT | os.O_RDWR` — we don't truncate the lock file, just
77
+ # need an fd to flock. Mode 0o644 is fine; the file holds no data.
78
+ if _HAS_FCNTL:
79
+ fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
80
+ try:
81
+ fcntl.flock(fd, fcntl.LOCK_EX)
82
+ try:
83
+ tmp.write_text(text, encoding="utf-8")
84
+ os.replace(str(tmp), str(target))
85
+ finally:
86
+ fcntl.flock(fd, fcntl.LOCK_UN)
87
+ finally:
88
+ os.close(fd)
89
+ else: # pragma: no cover — Windows fallback, no flock
90
+ tmp.write_text(text, encoding="utf-8")
91
+ os.replace(str(tmp), str(target))
92
+
93
+
94
+ FEEDBACK_DIRNAME = ".dispatcher"
95
+
96
+
97
+ def feedback_dir(state_root: Path, session_id: str) -> Path:
98
+ """Return the per-session feedback directory under state_root.
99
+
100
+ Layout:
101
+ <state_root>/.dispatcher/<session_id>/
102
+ <concern>.json — one per concern that ran
103
+ summary.json — rollup written by the dispatcher
104
+
105
+ Per Council Round 2 (2026-05-04): exit-code reduction collapses
106
+ multiple concern signals into a single platform-native code; the
107
+ feedback dir surfaces the per-concern detail to humans and
108
+ `task hooks-status` without re-routing control flow.
109
+ """
110
+ safe_session = session_id or "unknown-session"
111
+ # Defence-in-depth: refuse path traversal in session_id.
112
+ safe_session = safe_session.replace("/", "_").replace("\\", "_").replace("..", "_")
113
+ return Path(state_root) / FEEDBACK_DIRNAME / safe_session
114
+
115
+
116
+ __all__ = [
117
+ "atomic_write_json",
118
+ "atomic_write_text",
119
+ "feedback_dir",
120
+ "LOCK_BASENAME",
121
+ "FEEDBACK_DIRNAME",
122
+ ]
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # Windsurf (Cascade) universal hook trampoline (Phase 7.7,
3
+ # hook-architecture-v1.md).
4
+ #
5
+ # Routes Windsurf hook events — fired from either the project-scope
6
+ # `.windsurf/hooks.json` or the user-scope `~/.codeium/windsurf/hooks.json`
7
+ # — into the active workspace's `./agent-config dispatch:hook`.
8
+ #
9
+ # Windsurf event payload (per docs.windsurf.com/windsurf/cascade/hooks):
10
+ # { "agent_action_name": "<event>",
11
+ # "tool_info": { "cwd": "...", "file_path": "...", ... } }
12
+ #
13
+ # Workspace resolution — Windsurf does NOT pass a workspace_roots array
14
+ # the way Cursor/Cline do. Instead:
15
+ # 1. Project-scope hook → cwd is the workspace root (Cascade convention).
16
+ # `$PWD` containing `./agent-config` is the happy path.
17
+ # 2. User-scope hook → cwd may be the workspace, but for some events
18
+ # Windsurf executes from `$HOME` or a tmp dir. Fall back to:
19
+ # - tool_info.cwd from the JSON payload
20
+ # - tool_info.file_path → walk up to nearest .agent-settings.yml
21
+ # - $ROOT_WORKSPACE_PATH (only set on post_setup_worktree)
22
+ # 3. Bail silently when no resolution succeeds — concerns are
23
+ # observe-only at this layer; chat-history / roadmap-progress /
24
+ # context-hygiene never block, and onboarding-gate writes state,
25
+ # not exit code.
26
+ #
27
+ # Output — none. Windsurf does not consume stdout from hooks (post hooks
28
+ # are async, pre hooks block via exit code 2). We always exit 0 since
29
+ # none of our concerns block.
30
+
31
+ set -u
32
+
33
+ # Args from the platform's hooks.json command string:
34
+ # $1 = agent-config event name (session_start, stop, user_prompt_submit)
35
+ # $2 = Windsurf-native event name (pre_user_prompt, post_cascade_response, …)
36
+ EVENT="${1-}"
37
+ NATIVE_EVENT="${2-}"
38
+
39
+ if [ -z "$EVENT" ]; then
40
+ exit 0
41
+ fi
42
+
43
+ EVENT_DATA="$(cat)"
44
+
45
+ # 1. $PWD wins when it already looks like an agent-config workspace.
46
+ WORKSPACE=""
47
+ if [ -x "$PWD/agent-config" ] || [ -f "$PWD/.agent-settings.yml" ]; then
48
+ WORKSPACE="$PWD"
49
+ fi
50
+
51
+ # 2. Walk up from $PWD looking for .agent-settings.yml (covers
52
+ # sub-directory invocations).
53
+ if [ -z "$WORKSPACE" ]; then
54
+ candidate="$PWD"
55
+ while [ -n "$candidate" ] && [ "$candidate" != "/" ]; do
56
+ if [ -f "$candidate/.agent-settings.yml" ]; then
57
+ WORKSPACE="$candidate"
58
+ break
59
+ fi
60
+ candidate="$(dirname "$candidate")"
61
+ done
62
+ fi
63
+
64
+ # 3. Parse JSON tool_info for cwd / file_path.
65
+ if [ -z "$WORKSPACE" ]; then
66
+ if command -v jq >/dev/null 2>&1; then
67
+ EXTRACTED="$(printf '%s' "$EVENT_DATA" \
68
+ | jq -r '.tool_info.cwd // .tool_info.file_path // empty' 2>/dev/null)"
69
+ elif command -v python3 >/dev/null 2>&1; then
70
+ EXTRACTED="$(printf '%s' "$EVENT_DATA" | python3 -c '
71
+ import json, sys
72
+ try:
73
+ data = json.load(sys.stdin)
74
+ except Exception:
75
+ sys.exit(0)
76
+ info = data.get("tool_info") or {}
77
+ print(info.get("cwd") or info.get("file_path") or "")
78
+ ' 2>/dev/null)"
79
+ else
80
+ EXTRACTED=""
81
+ fi
82
+ EXTRACTED="${EXTRACTED%$'\r'}"
83
+ if [ -n "$EXTRACTED" ]; then
84
+ # Walk up looking for .agent-settings.yml.
85
+ candidate="$EXTRACTED"
86
+ if [ -f "$candidate" ]; then
87
+ candidate="$(dirname "$candidate")"
88
+ fi
89
+ while [ -n "$candidate" ] && [ "$candidate" != "/" ]; do
90
+ if [ -f "$candidate/.agent-settings.yml" ]; then
91
+ WORKSPACE="$candidate"
92
+ break
93
+ fi
94
+ candidate="$(dirname "$candidate")"
95
+ done
96
+ fi
97
+ fi
98
+
99
+ # 4. $ROOT_WORKSPACE_PATH is set only on post_setup_worktree.
100
+ if [ -z "$WORKSPACE" ] && [ -n "${ROOT_WORKSPACE_PATH-}" ]; then
101
+ if [ -f "$ROOT_WORKSPACE_PATH/.agent-settings.yml" ]; then
102
+ WORKSPACE="$ROOT_WORKSPACE_PATH"
103
+ fi
104
+ fi
105
+
106
+ if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
107
+ exit 0
108
+ fi
109
+
110
+ cd "$WORKSPACE" 2>/dev/null || exit 0
111
+
112
+ if [ ! -x ./agent-config ]; then
113
+ exit 0
114
+ fi
115
+
116
+ printf '%s' "$EVENT_DATA" \
117
+ | ./agent-config dispatch:hook \
118
+ --platform windsurf \
119
+ --event "$EVENT" \
120
+ --native-event "$NATIVE_EVENT" \
121
+ >/dev/null 2>&1 || true
122
+
123
+ exit 0
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """Print the runtime hook matrix per `docs/contracts/hook-architecture-v1.md`.
3
+
4
+ For each platform in `scripts/hook_manifest.yaml`, prints whether the
5
+ project-scope bridge files exist on disk, which (event → concerns)
6
+ bindings the manifest declares, and a one-line install hint when the
7
+ bridge is missing. Copilot has no native hook surface — its row carries
8
+ the `degraded: rule-only fallback` marker per Phase 7.12 / Round 2.
9
+
10
+ This is a **read-only** report. It never installs, modifies, or fires
11
+ anything; that is the contract callers depend on (`task hooks-status`,
12
+ post-install smoke, CI).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parents[1]
22
+ sys.path.insert(0, str(REPO_ROOT / "scripts" / "hooks"))
23
+
24
+ import dispatch_hook # noqa: E402 — reuse the manifest loader
25
+
26
+ # (label, project-relative bridge path, install hint).
27
+ # Path may be a directory (cline) — existence => any file inside.
28
+ PLATFORM_BRIDGES: dict[str, tuple[str, str]] = {
29
+ "augment": (".augment/settings.json", "scripts/install.py"),
30
+ "claude": (".claude/settings.json", "scripts/install.py"),
31
+ "cursor": (".cursor/hooks.json", "scripts/install.py"),
32
+ "cline": (".clinerules/hooks", "scripts/install.py"),
33
+ "windsurf": (".windsurf/hooks.json", "scripts/install.py"),
34
+ "gemini": (".gemini/settings.json", "scripts/install.py"),
35
+ "copilot": ("", "rule-only fallback (no hook surface)"),
36
+ }
37
+
38
+
39
+ def _bridge_status(project_root: Path, rel_path: str) -> str:
40
+ if not rel_path:
41
+ return "n/a"
42
+ target = project_root / rel_path
43
+ if target.is_dir():
44
+ return "installed" if any(target.iterdir()) else "empty"
45
+ return "installed" if target.is_file() else "missing"
46
+
47
+
48
+ def collect(project_root: Path, manifest: dict) -> dict:
49
+ """Build the runtime matrix as a plain dict — JSON-serialisable."""
50
+ platforms = manifest.get("platforms") or {}
51
+ rows: list[dict] = []
52
+ for platform in PLATFORM_BRIDGES:
53
+ rel, hint = PLATFORM_BRIDGES[platform]
54
+ block = platforms.get(platform) or {}
55
+ fallback_only = bool(block.get("fallback_only"))
56
+ bindings = (
57
+ {} if fallback_only
58
+ else {ev: list(c) for ev, c in block.items()
59
+ if isinstance(c, list)}
60
+ )
61
+ status = "degraded" if fallback_only else _bridge_status(project_root, rel)
62
+ rows.append({
63
+ "platform": platform,
64
+ "status": status,
65
+ "bridge_path": rel or None,
66
+ "fallback_only": fallback_only,
67
+ "bindings": bindings,
68
+ "hint": hint if status in {"missing", "empty", "degraded"} else None,
69
+ })
70
+ return {"schema_version": 1, "platforms": rows}
71
+
72
+
73
+ def _render_table(matrix: dict) -> str:
74
+ lines: list[str] = []
75
+ lines.append("agent-config hook matrix")
76
+ lines.append("=" * 60)
77
+ for row in matrix["platforms"]:
78
+ marker = {
79
+ "installed": "✅ ",
80
+ "missing": "❌ ",
81
+ "empty": "⚠️ ",
82
+ "degraded": "⚠️ ",
83
+ "n/a": "· ",
84
+ }.get(row["status"], "? ")
85
+ head = f"{marker}{row['platform']:<9} {row['status']}"
86
+ if row["bridge_path"]:
87
+ head += f" ({row['bridge_path']})"
88
+ lines.append(head)
89
+ if row["fallback_only"]:
90
+ lines.append(" degraded: rule-only fallback "
91
+ "— hooks are not auto-firing on this platform.")
92
+ continue
93
+ if not row["bindings"]:
94
+ lines.append(" (no bindings declared in manifest)")
95
+ continue
96
+ for event in sorted(row["bindings"]):
97
+ concerns = ", ".join(row["bindings"][event]) or "—"
98
+ lines.append(f" {event:<22} → {concerns}")
99
+ if row["hint"]:
100
+ lines.append(f" hint: run {row['hint']}")
101
+ lines.append("")
102
+ lines.append("Source of truth: scripts/hook_manifest.yaml")
103
+ lines.append("Contract: docs/contracts/hook-architecture-v1.md")
104
+ return "\n".join(lines)
105
+
106
+
107
+ def _final_exit_code(matrix: dict, strict: bool) -> int:
108
+ if not strict:
109
+ return 0
110
+ # Strict mode: any platform with declared bindings whose bridge is
111
+ # missing is a CI failure. `degraded`/`n/a` never fail (Copilot is
112
+ # an explicit no-hook platform; n/a means no bridge expected).
113
+ for row in matrix["platforms"]:
114
+ if row["status"] == "missing" and row["bindings"]:
115
+ return 1
116
+ return 0
117
+
118
+
119
+ def main(argv: list[str] | None = None) -> int:
120
+ parser = argparse.ArgumentParser(description=__doc__)
121
+ parser.add_argument("--format", choices=["table", "json"], default="table")
122
+ parser.add_argument("--project-root", default=".",
123
+ help="Project root to inspect (default: cwd)")
124
+ parser.add_argument("--manifest", default=str(dispatch_hook.MANIFEST_PATH))
125
+ parser.add_argument("--strict", action="store_true",
126
+ help="Exit non-zero if any platform with bindings is "
127
+ "missing its bridge (CI-friendly).")
128
+ args = parser.parse_args(argv)
129
+
130
+ manifest_path = Path(args.manifest)
131
+ if not manifest_path.exists():
132
+ sys.stderr.write(f"hooks_status: manifest missing at {manifest_path}\n")
133
+ return 2
134
+ manifest = dispatch_hook._load_yaml(manifest_path)
135
+ project_root = Path(args.project_root).resolve()
136
+ matrix = collect(project_root, manifest)
137
+
138
+ if args.format == "json":
139
+ print(json.dumps(matrix, indent=2, sort_keys=True))
140
+ else:
141
+ print(_render_table(matrix))
142
+ return _final_exit_code(matrix, args.strict)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ raise SystemExit(main())