@event4u/agent-config 1.18.0 → 1.20.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 (181) hide show
  1. package/.agent-src/commands/agent-handoff.md +14 -10
  2. package/.agent-src/commands/chat-history/import.md +170 -0
  3. package/.agent-src/commands/chat-history/learn.md +178 -0
  4. package/.agent-src/commands/chat-history/show.md +17 -18
  5. package/.agent-src/commands/chat-history.md +26 -25
  6. package/.agent-src/commands/council/default.md +77 -82
  7. package/.agent-src/commands/create-pr.md +28 -8
  8. package/.agent-src/commands/feature/roadmap.md +22 -0
  9. package/.agent-src/commands/roadmap/create.md +38 -6
  10. package/.agent-src/commands/roadmap/execute.md +36 -9
  11. package/.agent-src/commands/sync-gitignore.md +1 -1
  12. package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
  13. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
  14. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
  15. package/.agent-src/rules/agent-authority.md +1 -0
  16. package/.agent-src/rules/agent-docs.md +1 -0
  17. package/.agent-src/rules/analysis-skill-routing.md +1 -0
  18. package/.agent-src/rules/architecture.md +1 -0
  19. package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
  20. package/.agent-src/rules/artifact-engagement-recording.md +1 -0
  21. package/.agent-src/rules/ask-when-uncertain.md +1 -0
  22. package/.agent-src/rules/augment-portability.md +1 -0
  23. package/.agent-src/rules/augment-source-of-truth.md +1 -0
  24. package/.agent-src/rules/autonomous-execution.md +1 -0
  25. package/.agent-src/rules/capture-learnings.md +1 -0
  26. package/.agent-src/rules/cli-output-handling.md +2 -2
  27. package/.agent-src/rules/command-suggestion-policy.md +1 -0
  28. package/.agent-src/rules/commit-conventions.md +1 -0
  29. package/.agent-src/rules/commit-policy.md +1 -0
  30. package/.agent-src/rules/context-hygiene.md +22 -0
  31. package/.agent-src/rules/direct-answers.md +11 -2
  32. package/.agent-src/rules/docker-commands.md +1 -0
  33. package/.agent-src/rules/docs-sync.md +1 -0
  34. package/.agent-src/rules/downstream-changes.md +1 -0
  35. package/.agent-src/rules/e2e-testing.md +1 -0
  36. package/.agent-src/rules/guidelines.md +1 -0
  37. package/.agent-src/rules/improve-before-implement.md +1 -0
  38. package/.agent-src/rules/language-and-tone.md +38 -6
  39. package/.agent-src/rules/laravel-translations.md +1 -0
  40. package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
  41. package/.agent-src/rules/minimal-safe-diff.md +1 -0
  42. package/.agent-src/rules/missing-tool-handling.md +1 -0
  43. package/.agent-src/rules/model-recommendation.md +1 -0
  44. package/.agent-src/rules/no-attribution-footers.md +48 -0
  45. package/.agent-src/rules/no-cheap-questions.md +1 -0
  46. package/.agent-src/rules/no-roadmap-references.md +2 -1
  47. package/.agent-src/rules/non-destructive-by-default.md +1 -0
  48. package/.agent-src/rules/onboarding-gate.md +26 -0
  49. package/.agent-src/rules/package-ci-checks.md +1 -0
  50. package/.agent-src/rules/php-coding.md +1 -0
  51. package/.agent-src/rules/preservation-guard.md +1 -0
  52. package/.agent-src/rules/review-routing-awareness.md +1 -0
  53. package/.agent-src/rules/reviewer-awareness.md +1 -0
  54. package/.agent-src/rules/roadmap-progress-sync.md +22 -0
  55. package/.agent-src/rules/role-mode-adherence.md +2 -2
  56. package/.agent-src/rules/rule-type-governance.md +1 -0
  57. package/.agent-src/rules/runtime-safety.md +1 -0
  58. package/.agent-src/rules/scope-control.md +1 -0
  59. package/.agent-src/rules/security-sensitive-stop.md +1 -0
  60. package/.agent-src/rules/size-enforcement.md +1 -0
  61. package/.agent-src/rules/skill-improvement-trigger.md +1 -0
  62. package/.agent-src/rules/skill-quality.md +50 -0
  63. package/.agent-src/rules/slash-command-routing-policy.md +39 -0
  64. package/.agent-src/rules/think-before-action.md +1 -0
  65. package/.agent-src/rules/token-efficiency.md +1 -0
  66. package/.agent-src/rules/tool-safety.md +1 -0
  67. package/.agent-src/rules/ui-audit-gate.md +1 -0
  68. package/.agent-src/rules/upstream-proposal.md +1 -0
  69. package/.agent-src/rules/user-interaction.md +22 -5
  70. package/.agent-src/rules/verify-before-complete.md +1 -0
  71. package/.agent-src/skills/ai-council/SKILL.md +4 -5
  72. package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
  73. package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
  74. package/.agent-src/skills/md-language-check/SKILL.md +1 -1
  75. package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
  76. package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
  77. package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
  78. package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
  79. package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
  80. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
  81. package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
  82. package/.agent-src/templates/agent-settings.md +21 -26
  83. package/.agent-src/templates/roadmaps.md +8 -3
  84. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
  85. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
  86. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
  87. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
  88. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
  89. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
  90. package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
  91. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +110 -0
  92. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
  93. package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
  94. package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
  95. package/.agent-src/templates/skill.md +30 -1
  96. package/.claude-plugin/marketplace.json +8 -4
  97. package/AGENTS.md +44 -3
  98. package/CHANGELOG.md +173 -0
  99. package/README.md +22 -22
  100. package/config/agent-settings.template.yml +42 -13
  101. package/config/gitignore-block.txt +4 -4
  102. package/docs/architecture.md +3 -3
  103. package/docs/catalog.md +18 -13
  104. package/docs/contracts/adr-chat-history-split.md +10 -1
  105. package/docs/contracts/adr-settings-sync-engine.md +127 -0
  106. package/docs/contracts/command-clusters.md +1 -1
  107. package/docs/contracts/cross-wing-handoff.md +133 -0
  108. package/docs/contracts/decision-trace-v1.md +146 -0
  109. package/docs/contracts/file-ownership-matrix.json +348 -126
  110. package/docs/contracts/hook-architecture-v1.md +220 -0
  111. package/docs/contracts/memory-visibility-v1.md +122 -0
  112. package/docs/contracts/one-off-script-lifecycle.md +109 -0
  113. package/docs/contracts/rule-interactions.yml +22 -0
  114. package/docs/customization.md +2 -1
  115. package/docs/development.md +4 -1
  116. package/docs/getting-started.md +21 -29
  117. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
  118. package/docs/guidelines/agent-infra/layered-settings.md +32 -13
  119. package/docs/hook-payload-capture.md +221 -0
  120. package/docs/migrations/commands-1.15.0.md +17 -12
  121. package/docs/skills-catalog.md +5 -4
  122. package/llms.txt +4 -3
  123. package/package.json +1 -1
  124. package/scripts/agent-config +45 -1
  125. package/scripts/ai_council/_default_prices.py +4 -4
  126. package/scripts/ai_council/bundler.py +3 -3
  127. package/scripts/ai_council/clients.py +25 -9
  128. package/scripts/ai_council/modes.py +3 -4
  129. package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
  130. package/scripts/ai_council/one_off_archive/2026-05/_one_off_roundtrip.py +13 -8
  131. package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
  132. package/scripts/ai_council/pricing.py +10 -9
  133. package/scripts/ai_council/session.py +92 -0
  134. package/scripts/build_rule_trigger_matrix.py +1 -9
  135. package/scripts/capture_showcase_session.py +361 -0
  136. package/scripts/chat_history.py +963 -597
  137. package/scripts/check_always_budget.py +7 -2
  138. package/scripts/check_references.py +12 -2
  139. package/scripts/context_hygiene_hook.py +14 -6
  140. package/scripts/council_cli.py +407 -0
  141. package/scripts/hook_manifest.yaml +217 -0
  142. package/scripts/hooks/__init__.py +1 -0
  143. package/scripts/hooks/augment-chat-history.sh +10 -0
  144. package/scripts/hooks/augment-dispatcher.sh +72 -0
  145. package/scripts/hooks/cline-dispatcher.sh +86 -0
  146. package/scripts/hooks/cowork-dispatcher.sh +98 -0
  147. package/scripts/hooks/cursor-dispatcher.sh +76 -0
  148. package/scripts/hooks/dispatch_hook.py +383 -0
  149. package/scripts/hooks/envelope.py +98 -0
  150. package/scripts/hooks/gemini-dispatcher.sh +117 -0
  151. package/scripts/hooks/state_io.py +122 -0
  152. package/scripts/hooks/windsurf-dispatcher.sh +123 -0
  153. package/scripts/hooks_status.py +157 -0
  154. package/scripts/install-hooks.sh +2 -2
  155. package/scripts/install.py +725 -87
  156. package/scripts/install.sh +38 -1
  157. package/scripts/lint_handoffs.py +214 -0
  158. package/scripts/lint_hook_manifest.py +217 -0
  159. package/scripts/lint_one_off_age.py +184 -0
  160. package/scripts/lint_rule_tiers.py +78 -0
  161. package/scripts/lint_showcase_sessions.py +148 -0
  162. package/scripts/minimal_safe_diff_hook.py +245 -0
  163. package/scripts/onboarding_gate_hook.py +13 -8
  164. package/scripts/readme_linter.py +12 -3
  165. package/scripts/redact_hook_capture.py +148 -0
  166. package/scripts/roadmap_progress_hook.py +5 -0
  167. package/scripts/schemas/skill.schema.json +5 -0
  168. package/scripts/skill_linter.py +163 -1
  169. package/scripts/sync_agent_settings.py +32 -129
  170. package/scripts/sync_yaml_rt.py +734 -0
  171. package/scripts/update_prices.py +3 -3
  172. package/scripts/verify_before_complete_hook.py +216 -0
  173. package/.agent-src/commands/chat-history/checkpoint.md +0 -126
  174. package/.agent-src/commands/chat-history/clear.md +0 -103
  175. package/.agent-src/commands/chat-history/resume.md +0 -183
  176. package/.agent-src/rules/chat-history-cadence.md +0 -109
  177. package/.agent-src/rules/chat-history-ownership.md +0 -123
  178. package/.agent-src/rules/chat-history-visibility.md +0 -96
  179. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
  180. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
  181. package/scripts/check_phase_coupling.py +0 -148
@@ -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,157 @@
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
+ #
29
+ # Cowork has no project-scope bridge path: the Claude desktop app's
30
+ # local-agent-mode runtime is upstream-blocked from reading any of
31
+ # Claude Code's three settings sources (anthropics/claude-code#40495,
32
+ # #27398). We register cowork here so the manifest's `cowork:`
33
+ # bindings are surfaced in the status report, but the empty bridge
34
+ # path resolves to status="n/a" — strict mode does not fail on
35
+ # n/a (see _final_exit_code), matching Copilot's no-bridge posture.
36
+ # Once upstream lands the fix and a stable settings location is
37
+ # documented, swap the empty path here for that location.
38
+ PLATFORM_BRIDGES: dict[str, tuple[str, str]] = {
39
+ "augment": (".augment/settings.json", "scripts/install.py"),
40
+ "claude": (".claude/settings.json", "scripts/install.py"),
41
+ "cowork": ("", "upstream-blocked: anthropics/claude-code#40495 + #27398 (settings.json ignored in Cowork sandbox)"),
42
+ "cursor": (".cursor/hooks.json", "scripts/install.py"),
43
+ "cline": (".clinerules/hooks", "scripts/install.py"),
44
+ "windsurf": (".windsurf/hooks.json", "scripts/install.py"),
45
+ "gemini": (".gemini/settings.json", "scripts/install.py"),
46
+ "copilot": ("", "rule-only fallback (no hook surface)"),
47
+ }
48
+
49
+
50
+ def _bridge_status(project_root: Path, rel_path: str) -> str:
51
+ if not rel_path:
52
+ return "n/a"
53
+ target = project_root / rel_path
54
+ if target.is_dir():
55
+ return "installed" if any(target.iterdir()) else "empty"
56
+ return "installed" if target.is_file() else "missing"
57
+
58
+
59
+ def collect(project_root: Path, manifest: dict) -> dict:
60
+ """Build the runtime matrix as a plain dict — JSON-serialisable."""
61
+ platforms = manifest.get("platforms") or {}
62
+ rows: list[dict] = []
63
+ for platform in PLATFORM_BRIDGES:
64
+ rel, hint = PLATFORM_BRIDGES[platform]
65
+ block = platforms.get(platform) or {}
66
+ fallback_only = bool(block.get("fallback_only"))
67
+ bindings = (
68
+ {} if fallback_only
69
+ else {ev: list(c) for ev, c in block.items()
70
+ if isinstance(c, list)}
71
+ )
72
+ status = "degraded" if fallback_only else _bridge_status(project_root, rel)
73
+ rows.append({
74
+ "platform": platform,
75
+ "status": status,
76
+ "bridge_path": rel or None,
77
+ "fallback_only": fallback_only,
78
+ "bindings": bindings,
79
+ "hint": hint if status in {"missing", "empty", "degraded", "n/a"} else None,
80
+ })
81
+ return {"schema_version": 1, "platforms": rows}
82
+
83
+
84
+ def _render_table(matrix: dict) -> str:
85
+ lines: list[str] = []
86
+ lines.append("agent-config hook matrix")
87
+ lines.append("=" * 60)
88
+ for row in matrix["platforms"]:
89
+ marker = {
90
+ "installed": "✅ ",
91
+ "missing": "❌ ",
92
+ "empty": "⚠️ ",
93
+ "degraded": "⚠️ ",
94
+ "n/a": "· ",
95
+ }.get(row["status"], "? ")
96
+ head = f"{marker}{row['platform']:<9} {row['status']}"
97
+ if row["bridge_path"]:
98
+ head += f" ({row['bridge_path']})"
99
+ lines.append(head)
100
+ if row["fallback_only"]:
101
+ lines.append(" degraded: rule-only fallback "
102
+ "— hooks are not auto-firing on this platform.")
103
+ continue
104
+ if not row["bindings"]:
105
+ lines.append(" (no bindings declared in manifest)")
106
+ continue
107
+ for event in sorted(row["bindings"]):
108
+ concerns = ", ".join(row["bindings"][event]) or "—"
109
+ lines.append(f" {event:<22} → {concerns}")
110
+ if row["hint"]:
111
+ lines.append(f" hint: run {row['hint']}")
112
+ lines.append("")
113
+ lines.append("Source of truth: scripts/hook_manifest.yaml")
114
+ lines.append("Contract: docs/contracts/hook-architecture-v1.md")
115
+ return "\n".join(lines)
116
+
117
+
118
+ def _final_exit_code(matrix: dict, strict: bool) -> int:
119
+ if not strict:
120
+ return 0
121
+ # Strict mode: any platform with declared bindings whose bridge is
122
+ # missing is a CI failure. `degraded`/`n/a` never fail (Copilot is
123
+ # an explicit no-hook platform; n/a means no bridge expected).
124
+ for row in matrix["platforms"]:
125
+ if row["status"] == "missing" and row["bindings"]:
126
+ return 1
127
+ return 0
128
+
129
+
130
+ def main(argv: list[str] | None = None) -> int:
131
+ parser = argparse.ArgumentParser(description=__doc__)
132
+ parser.add_argument("--format", choices=["table", "json"], default="table")
133
+ parser.add_argument("--project-root", default=".",
134
+ help="Project root to inspect (default: cwd)")
135
+ parser.add_argument("--manifest", default=str(dispatch_hook.MANIFEST_PATH))
136
+ parser.add_argument("--strict", action="store_true",
137
+ help="Exit non-zero if any platform with bindings is "
138
+ "missing its bridge (CI-friendly).")
139
+ args = parser.parse_args(argv)
140
+
141
+ manifest_path = Path(args.manifest)
142
+ if not manifest_path.exists():
143
+ sys.stderr.write(f"hooks_status: manifest missing at {manifest_path}\n")
144
+ return 2
145
+ manifest = dispatch_hook._load_yaml(manifest_path)
146
+ project_root = Path(args.project_root).resolve()
147
+ matrix = collect(project_root, manifest)
148
+
149
+ if args.format == "json":
150
+ print(json.dumps(matrix, indent=2, sort_keys=True))
151
+ else:
152
+ print(_render_table(matrix))
153
+ return _final_exit_code(matrix, args.strict)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ raise SystemExit(main())
@@ -63,7 +63,7 @@ echo "✅ Pre-commit hook installed."
63
63
  # lifecycle hooks) cannot fire SessionStart/Stop/PostToolUse. Git hooks
64
64
  # are the platform-agnostic lifecycle surface that fires regardless of
65
65
  # IDE — every commit, merge, checkout, and rewrite turns into a phase
66
- # boundary in .agent-chat-history when an agent session is active.
66
+ # boundary in agents/.agent-chat-history when an agent session is active.
67
67
  #
68
68
  # The hooks are silent no-ops when no agent session is active (the
69
69
  # chat_history.py hook-append script returns "skipped_no_sidecar" with
@@ -75,7 +75,7 @@ write_chat_history_hook() {
75
75
  local phase_tag="$2"
76
76
  cat > "$HOOKS_DIR/$name" << EOF
77
77
  #!/usr/bin/env bash
78
- # $name: append a phase boundary to .agent-chat-history when an agent
78
+ # $name: append a phase boundary to agents/.agent-chat-history when an agent
79
79
  # session is active. Silent no-op otherwise. Never blocks git.
80
80
 
81
81
  if [ -x ./agent-config ]; then