@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.
- package/.agent-src/commands/agent-handoff.md +14 -10
- package/.agent-src/commands/chat-history/import.md +170 -0
- package/.agent-src/commands/chat-history/learn.md +178 -0
- package/.agent-src/commands/chat-history/show.md +17 -18
- package/.agent-src/commands/chat-history.md +26 -25
- package/.agent-src/commands/council/default.md +77 -82
- package/.agent-src/commands/create-pr.md +28 -8
- 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/commands/sync-gitignore.md +1 -1
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
- 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/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 +22 -0
- package/.agent-src/rules/direct-answers.md +11 -2
- 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 +38 -6
- 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-attribution-footers.md +48 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +2 -1
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -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 +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -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 +50 -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 +22 -5
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/ai-council/SKILL.md +4 -5
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +21 -26
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
- 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 +110 -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/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +8 -4
- package/AGENTS.md +44 -3
- package/CHANGELOG.md +173 -0
- package/README.md +22 -22
- package/config/agent-settings.template.yml +42 -13
- package/config/gitignore-block.txt +4 -4
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +18 -13
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +348 -126
- package/docs/contracts/hook-architecture-v1.md +220 -0
- package/docs/contracts/memory-visibility-v1.md +122 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +2 -1
- package/docs/development.md +4 -1
- package/docs/getting-started.md +21 -29
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/hook-payload-capture.md +221 -0
- package/docs/migrations/commands-1.15.0.md +17 -12
- package/docs/skills-catalog.md +5 -4
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/scripts/agent-config +45 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +25 -9
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/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/pricing.py +10 -9
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +963 -597
- package/scripts/check_always_budget.py +7 -2
- package/scripts/check_references.py +12 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +407 -0
- package/scripts/hook_manifest.yaml +217 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +383 -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 +157 -0
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.py +725 -87
- package/scripts/install.sh +38 -1
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +217 -0
- package/scripts/lint_one_off_age.py +184 -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 +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +163 -1
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/verify_before_complete_hook.py +216 -0
- package/.agent-src/commands/chat-history/checkpoint.md +0 -126
- package/.agent-src/commands/chat-history/clear.md +0 -103
- package/.agent-src/commands/chat-history/resume.md +0 -183
- package/.agent-src/rules/chat-history-cadence.md +0 -109
- package/.agent-src/rules/chat-history-ownership.md +0 -123
- package/.agent-src/rules/chat-history-visibility.md +0 -96
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
- 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())
|
package/scripts/install-hooks.sh
CHANGED
|
@@ -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
|
|
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
|
|
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
|