@event4u/agent-config 1.39.0 → 1.40.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 (45) hide show
  1. package/.agent-src/commands/orchestrate.md +123 -0
  2. package/.agent-src/commands/sync-gitignore/fix.md +135 -0
  3. package/.agent-src/commands/sync-gitignore.md +31 -5
  4. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
  5. package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
  6. package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  7. package/.agent-src/templates/agent-settings.md +9 -0
  8. package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
  9. package/.claude-plugin/marketplace.json +3 -1
  10. package/CHANGELOG.md +42 -0
  11. package/README.md +5 -5
  12. package/bin/install.php +13 -6
  13. package/config/agent-settings.template.yml +21 -0
  14. package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
  15. package/docs/architecture.md +1 -1
  16. package/docs/catalog.md +3 -2
  17. package/docs/contracts/audit-log-v1.md +142 -0
  18. package/docs/contracts/command-clusters.md +2 -0
  19. package/docs/contracts/file-ownership-matrix.json +20 -0
  20. package/docs/contracts/orchestration-dsl-v1.md +152 -0
  21. package/docs/getting-started.md +1 -1
  22. package/docs/installation.md +132 -0
  23. package/docs/setup/per-ide/aider.md +48 -0
  24. package/docs/setup/per-ide/claude-code.md +108 -0
  25. package/docs/setup/per-ide/claude-desktop.md +148 -0
  26. package/docs/setup/per-ide/cline.md +43 -0
  27. package/docs/setup/per-ide/codex.md +46 -0
  28. package/docs/setup/per-ide/copilot.md +80 -0
  29. package/docs/setup/per-ide/cursor.md +125 -0
  30. package/docs/setup/per-ide/gemini-cli.md +45 -0
  31. package/docs/setup/per-ide/windsurf.md +120 -0
  32. package/package.json +1 -1
  33. package/scripts/compress.py +153 -1
  34. package/scripts/extract_audit_patterns.py +202 -0
  35. package/scripts/install +156 -1
  36. package/scripts/install.py +270 -10
  37. package/scripts/install.sh +52 -7
  38. package/scripts/lint_orchestration_dsl.py +214 -0
  39. package/scripts/skill_linter.py +9 -0
  40. package/scripts/sync_gitignore.py +56 -1
  41. package/templates/claude_desktop_config.json.template +21 -0
  42. package/templates/cursor-rule.mdc.j2 +7 -0
  43. package/templates/global-install-manifest.yml +91 -0
  44. package/templates/marketing-copy.yml +64 -0
  45. package/templates/windsurf-rule.md.j2 +7 -0
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """Mine repeated phase patterns from ``agents/state/audit/*.jsonl``.
3
+
4
+ Consumer side of `audit-log-v1` (see
5
+ `docs/contracts/audit-log-v1.md`). Reads append-only JSONL audit
6
+ lines emitted by the `work_engine` phase hook and surfaces patterns
7
+ that repeat across **independent** runs — i.e. distinct `work_id`
8
+ values — so the human reviewer can promote them via the
9
+ `learning-to-rule-or-skill` skill.
10
+
11
+ Read-only: never mutates the JSONL, never writes outside `--output`.
12
+
13
+ Pattern shape (one per row):
14
+
15
+ {
16
+ "summary": "<phase>:<outcome>:<rules_hash>",
17
+ "phase": "verify",
18
+ "outcome": "success",
19
+ "rules_applied": ["verify-before-complete", "commit-policy"],
20
+ "count": 7, # distinct work_ids
21
+ "line_ids": ["01HXY...", ...],
22
+ "first_seen": "2026-05-01T...",
23
+ "last_seen": "2026-05-11T..."
24
+ }
25
+
26
+ Repetition gate: a pattern is emitted only when ``count >= 2`` and
27
+ the two contributing lines come from **different** ``work_id`` values
28
+ (independence floor — same gate as the skill's evidence rule).
29
+
30
+ Usage::
31
+
32
+ python3 scripts/extract_audit_patterns.py # human table
33
+ python3 scripts/extract_audit_patterns.py --json # machine
34
+ python3 scripts/extract_audit_patterns.py --min-count 3
35
+ python3 scripts/extract_audit_patterns.py --month 2026-05 # one file
36
+ python3 scripts/extract_audit_patterns.py --audit-dir <p> # override
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import argparse
41
+ import json
42
+ import sys
43
+ from collections import defaultdict
44
+ from dataclasses import dataclass, field, asdict
45
+ from pathlib import Path
46
+ from typing import Iterable
47
+
48
+ ROOT = Path(__file__).resolve().parent.parent
49
+ DEFAULT_AUDIT_DIR = ROOT / "agents" / "state" / "audit"
50
+ SCHEMA_VERSION = 1
51
+
52
+
53
+ @dataclass
54
+ class Pattern:
55
+ summary: str
56
+ phase: str
57
+ outcome: str
58
+ rules_applied: list[str]
59
+ count: int = 0
60
+ line_ids: list[str] = field(default_factory=list)
61
+ work_ids: set[str] = field(default_factory=set)
62
+ first_seen: str = ""
63
+ last_seen: str = ""
64
+
65
+ def to_dict(self) -> dict:
66
+ d = asdict(self)
67
+ d["work_ids"] = sorted(self.work_ids)
68
+ return d
69
+
70
+
71
+ def _iter_lines(audit_dir: Path, month: str | None) -> Iterable[dict]:
72
+ """Yield parsed JSONL records from the audit directory.
73
+
74
+ Silently skips malformed lines (forward-compat per contract § 86).
75
+ """
76
+ if not audit_dir.exists():
77
+ return
78
+ files = (
79
+ [audit_dir / f"{month}.jsonl"] if month
80
+ else sorted(audit_dir.glob("*.jsonl"))
81
+ )
82
+ for path in files:
83
+ if not path.exists():
84
+ continue
85
+ with path.open("r", encoding="utf-8") as fh:
86
+ for raw in fh:
87
+ raw = raw.strip()
88
+ if not raw:
89
+ continue
90
+ try:
91
+ rec = json.loads(raw)
92
+ except json.JSONDecodeError:
93
+ continue
94
+ if rec.get("schema_version") != SCHEMA_VERSION:
95
+ continue
96
+ yield rec
97
+
98
+
99
+ def _pattern_key(rec: dict) -> tuple[str, str, tuple[str, ...]]:
100
+ rules = tuple(sorted(rec.get("rules_applied") or []))
101
+ return (rec.get("phase", ""), rec.get("outcome", ""), rules)
102
+
103
+
104
+ def _resolve_supersedes(records: list[dict]) -> list[dict]:
105
+ """Apply supersede chains: drop records whose id is superseded."""
106
+ superseded: set[str] = set()
107
+ for rec in records:
108
+ if rec.get("type") == "supersede" and rec.get("supersedes"):
109
+ superseded.add(rec["supersedes"])
110
+ return [r for r in records if r.get("id") not in superseded]
111
+
112
+
113
+ def mine(audit_dir: Path, month: str | None, min_count: int) -> list[dict]:
114
+ """Group records into patterns; enforce independence floor."""
115
+ records = _resolve_supersedes(list(_iter_lines(audit_dir, month)))
116
+ groups: dict[tuple, Pattern] = {}
117
+ for rec in records:
118
+ if rec.get("type") not in (None, "phase"):
119
+ continue
120
+ key = _pattern_key(rec)
121
+ if key not in groups:
122
+ phase, outcome, rules = key
123
+ rules_hash = "+".join(rules) or "<none>"
124
+ groups[key] = Pattern(
125
+ summary=f"{phase}:{outcome}:{rules_hash}",
126
+ phase=phase,
127
+ outcome=outcome,
128
+ rules_applied=list(rules),
129
+ )
130
+ pat = groups[key]
131
+ ts = rec.get("ts", "")
132
+ wid = rec.get("work_id", "")
133
+ if wid:
134
+ pat.work_ids.add(wid)
135
+ line_id = rec.get("id", "")
136
+ if line_id:
137
+ pat.line_ids.append(line_id)
138
+ pat.count = len(pat.work_ids)
139
+ if not pat.first_seen or ts < pat.first_seen:
140
+ pat.first_seen = ts
141
+ if not pat.last_seen or ts > pat.last_seen:
142
+ pat.last_seen = ts
143
+ out = [p.to_dict() for p in groups.values() if p.count >= min_count]
144
+ out.sort(key=lambda d: (-d["count"], d["summary"]))
145
+ return out
146
+
147
+
148
+ def _render_table(patterns: list[dict]) -> str:
149
+ if not patterns:
150
+ return "(no patterns at or above the min-count threshold)"
151
+ lines = [
152
+ f"{'count':>5} {'phase':<10} {'outcome':<8} {'rules':<40} summary",
153
+ f"{'-' * 5} {'-' * 10} {'-' * 8} {'-' * 40} -------",
154
+ ]
155
+ for p in patterns:
156
+ rules = ",".join(p["rules_applied"]) or "<none>"
157
+ if len(rules) > 38:
158
+ rules = rules[:35] + "..."
159
+ lines.append(
160
+ f"{p['count']:>5} {p['phase']:<10} {p['outcome']:<8} "
161
+ f"{rules:<40} {p['summary']}"
162
+ )
163
+ return "\n".join(lines)
164
+
165
+
166
+ def main(argv: list[str] | None = None) -> int:
167
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
168
+ ap.add_argument(
169
+ "--audit-dir", type=Path, default=DEFAULT_AUDIT_DIR,
170
+ help="Override audit-log directory (default: %(default)s).",
171
+ )
172
+ ap.add_argument(
173
+ "--month", help="Single YYYY-MM file instead of all months.",
174
+ )
175
+ ap.add_argument(
176
+ "--min-count", type=int, default=2,
177
+ help="Minimum distinct work_ids required (default: 2).",
178
+ )
179
+ ap.add_argument(
180
+ "--json", action="store_true", help="Emit machine-readable JSON.",
181
+ )
182
+ args = ap.parse_args(argv)
183
+
184
+ if args.min_count < 2:
185
+ print(
186
+ "❌ --min-count must be >= 2 (independence floor per "
187
+ "audit-log-v1 § Privacy floor).",
188
+ file=sys.stderr,
189
+ )
190
+ return 2
191
+
192
+ patterns = mine(args.audit_dir, args.month, args.min_count)
193
+ if args.json:
194
+ json.dump(patterns, sys.stdout, indent=2, sort_keys=True)
195
+ sys.stdout.write("\n")
196
+ else:
197
+ print(_render_table(patterns))
198
+ return 0
199
+
200
+
201
+ if __name__ == "__main__":
202
+ sys.exit(main())
package/scripts/install CHANGED
@@ -15,13 +15,30 @@
15
15
  # --source <dir> Package source directory (default: auto-detect)
16
16
  # --target <dir> Target project root (default: cwd)
17
17
  # --profile <name> Cost profile for bridges (minimal|balanced|full)
18
+ # --tools <list> Comma-separated tool IDs to install (default: all).
19
+ # Valid: claude-code,claude-desktop,cursor,windsurf,
20
+ # cline,gemini-cli,copilot,augment,aider,codex,all
21
+ # --list-tools Print supported tool IDs with descriptions, then exit
22
+ # --yes, -y Non-interactive mode: do not prompt (default for non-TTY)
18
23
  # --force Overwrite existing bridge files
19
24
  # --dry-run Show what payload sync would do (does not run bridges)
20
25
  # --verbose Detailed payload sync output
21
26
  # --quiet Suppress non-error output
22
27
  # --skip-sync Skip payload sync (install.sh)
23
28
  # --skip-bridges Skip bridge files (install.py)
29
+ # --global Phase-3: ship kernel rules + curated skills to user-scope
30
+ # dirs (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/,
31
+ # ~/.config/agent-config/) so the agent has them in every
32
+ # project. Pair with --tools to scope surfaces; default = all.
33
+ # --uninstall With --global: remove the event4u/ namespace dir from each
34
+ # enabled surface (no effect on user-added files).
24
35
  # --help, -h Show this help
36
+ #
37
+ # Examples:
38
+ # bash scripts/install # everything (default)
39
+ # bash scripts/install --tools=claude-code,cursor # only those two
40
+ # bash scripts/install --tools=cursor --yes # CI-friendly
41
+ # bash scripts/install --list-tools # show catalog
25
42
 
26
43
  set -uo pipefail
27
44
 
@@ -32,19 +49,67 @@ INSTALL_PY="$SCRIPT_DIR/install.py"
32
49
  SOURCE_DIR=""
33
50
  TARGET_DIR=""
34
51
  PROFILE=""
52
+ TOOLS=""
53
+ TOOLS_EXPLICIT=false
54
+ YES=false
35
55
  FORCE=false
36
56
  DRY_RUN=false
37
57
  VERBOSE=false
38
58
  QUIET=false
39
59
  SKIP_SYNC=false
40
60
  SKIP_BRIDGES=false
61
+ LIST_TOOLS=false
62
+ GLOBAL_INSTALL=false
63
+ UNINSTALL=false
64
+
65
+ # Single source of truth for valid tool IDs (also referenced by install.sh / install.py).
66
+ VALID_TOOLS="claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex all"
41
67
 
42
68
  show_help() {
43
- sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
69
+ sed -n '3,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
70
+ }
71
+
72
+ list_tools() {
73
+ cat <<'EOF'
74
+ Supported --tools IDs (default: all):
75
+
76
+ claude-code .claude/rules, .claude/skills, .claude/commands, .claude/settings.json
77
+ claude-desktop ~/Library/Application Support/Claude/ (global, see Phase 4 docs)
78
+ cursor .cursor/rules, .cursor/commands (legacy .cursorrules also written)
79
+ windsurf .windsurf/rules, .windsurf/workflows (legacy .windsurfrules also written)
80
+ cline .clinerules/ symlinks
81
+ gemini-cli GEMINI.md, .gemini/settings.json
82
+ copilot .github/copilot-instructions.md, .vscode/settings.json
83
+ augment .augment/ payload + settings (substrate — recommended for every install)
84
+ aider AGENTS.md (Linux Foundation cross-tool contract; always written)
85
+ codex AGENTS.md (same as aider — no extra action)
86
+ all every ID above (the default; backward-compatible)
87
+
88
+ Examples:
89
+ --tools=claude-code,cursor project-local install for those two surfaces
90
+ --tools=all equivalent to omitting the flag
91
+ EOF
44
92
  }
45
93
 
46
94
  err() { echo " ❌ $*" >&2; }
47
95
 
96
+ # Validate a comma-separated tool list against $VALID_TOOLS. Empty input is
97
+ # rejected so a stray --tools= does not silently behave like --tools=all.
98
+ validate_tools() {
99
+ local raw="$1"
100
+ [[ -z "$raw" ]] && { err "--tools requires a non-empty value (use --list-tools to see options)"; return 1; }
101
+ local IFS=','
102
+ local item
103
+ for item in $raw; do
104
+ [[ -z "$item" ]] && { err "--tools contains an empty entry"; return 1; }
105
+ if [[ " $VALID_TOOLS " != *" $item "* ]]; then
106
+ err "Unknown tool ID: $item (run --list-tools for the catalog)"
107
+ return 1
108
+ fi
109
+ done
110
+ return 0
111
+ }
112
+
48
113
  while [[ $# -gt 0 ]]; do
49
114
  case "$1" in
50
115
  --source) SOURCE_DIR="$2"; shift 2 ;;
@@ -53,17 +118,84 @@ while [[ $# -gt 0 ]]; do
53
118
  --target=*) TARGET_DIR="${1#*=}"; shift ;;
54
119
  --profile) PROFILE="$2"; shift 2 ;;
55
120
  --profile=*) PROFILE="${1#*=}"; shift ;;
121
+ --tools) TOOLS="$2"; TOOLS_EXPLICIT=true; shift 2 ;;
122
+ --tools=*) TOOLS="${1#*=}"; TOOLS_EXPLICIT=true; shift ;;
123
+ --list-tools) LIST_TOOLS=true; shift ;;
124
+ --yes|-y) YES=true; shift ;;
56
125
  --force) FORCE=true; shift ;;
57
126
  --dry-run) DRY_RUN=true; shift ;;
58
127
  --verbose) VERBOSE=true; shift ;;
59
128
  --quiet) QUIET=true; shift ;;
60
129
  --skip-sync) SKIP_SYNC=true; shift ;;
61
130
  --skip-bridges) SKIP_BRIDGES=true; shift ;;
131
+ --global) GLOBAL_INSTALL=true; shift ;;
132
+ --uninstall) UNINSTALL=true; shift ;;
62
133
  --help|-h) show_help; exit 0 ;;
63
134
  *) err "Unknown argument: $1"; show_help >&2; exit 1 ;;
64
135
  esac
65
136
  done
66
137
 
138
+ if $LIST_TOOLS; then
139
+ list_tools
140
+ exit 0
141
+ fi
142
+
143
+ # Interactive --tools picker (S9). Fires only when:
144
+ # - --tools was not explicitly passed
145
+ # - --yes / -y was not passed (CI / non-interactive opt-out)
146
+ # - stdin AND stdout are both TTYs (so we're not in a pipe / curl|bash flow)
147
+ # - --dry-run, --quiet, --skip-sync, --skip-bridges did not opt out of UX
148
+ # Otherwise we fall through to the backward-compatible "all" default.
149
+ prompt_tools() {
150
+ local choice picked tool i=0
151
+ local -a menu=(claude-code claude-desktop cursor windsurf cline gemini-cli copilot augment aider codex)
152
+ echo ""
153
+ echo " 📦 Pick the tools to install (comma-separated numbers, blank = all):"
154
+ for tool in "${menu[@]}"; do
155
+ i=$((i+1))
156
+ printf " %2d) %s\n" "$i" "$tool"
157
+ done
158
+ echo " a) all (default)"
159
+ echo ""
160
+ printf " Selection: "
161
+ IFS= read -r choice || choice=""
162
+ choice="${choice// /}"
163
+ if [[ -z "$choice" || "$choice" == "a" || "$choice" == "all" ]]; then
164
+ TOOLS="all"; return
165
+ fi
166
+ picked=""
167
+ local IFS=','
168
+ for n in $choice; do
169
+ if ! [[ "$n" =~ ^[0-9]+$ ]] || (( n < 1 || n > ${#menu[@]} )); then
170
+ err "Invalid selection: $n (expected 1-${#menu[@]} or 'a')"; exit 1
171
+ fi
172
+ picked+="${menu[$((n-1))]},"
173
+ done
174
+ TOOLS="${picked%,}"
175
+ echo " ✅ Selected: $TOOLS"
176
+ }
177
+
178
+ if ! $TOOLS_EXPLICIT && ! $YES && ! $QUIET && ! $LIST_TOOLS && ! $GLOBAL_INSTALL && [[ -t 0 && -t 1 ]]; then
179
+ prompt_tools
180
+ TOOLS_EXPLICIT=true
181
+ fi
182
+
183
+ if $UNINSTALL && ! $GLOBAL_INSTALL; then
184
+ err "--uninstall is only valid combined with --global"
185
+ exit 1
186
+ fi
187
+
188
+ # Default = "all": backward compatible with pre-Phase-1 invocations. An
189
+ # explicit --tools= (empty value) is rejected by validate_tools — only an
190
+ # absent flag falls through to "all".
191
+ if ! $TOOLS_EXPLICIT && [[ -z "$TOOLS" ]]; then
192
+ TOOLS="all"
193
+ fi
194
+
195
+ if ! validate_tools "$TOOLS"; then
196
+ exit 1
197
+ fi
198
+
67
199
  # Auto-detect source: directory above this script
68
200
  if [[ -z "$SOURCE_DIR" ]]; then
69
201
  SOURCE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -103,6 +235,7 @@ run_sync() {
103
235
  $DRY_RUN && args+=(--dry-run)
104
236
  $VERBOSE && args+=(--verbose)
105
237
  $QUIET && args+=(--quiet)
238
+ args+=(--tools="$TOOLS")
106
239
  bash "$INSTALL_SH" "${args[@]}"
107
240
  }
108
241
 
@@ -123,10 +256,32 @@ run_bridges() {
123
256
  [[ -n "$PROFILE" ]] && args+=(--profile="$PROFILE")
124
257
  $FORCE && args+=(--force)
125
258
  $QUIET && args+=(--quiet)
259
+ args+=(--tools="$TOOLS")
126
260
  "$python_bin" "$INSTALL_PY" "${args[@]}"
127
261
  }
128
262
 
129
263
  RC=0
264
+
265
+ # --global: dedicated user-scope path. Skips the project-bridge sync entirely
266
+ # and forwards to install.py --global (Phase 3 / S13). --uninstall pairs with
267
+ # --global to wipe the event4u/ namespace dir under each surface.
268
+ if $GLOBAL_INSTALL; then
269
+ if ! python_bin="$(find_python)"; then
270
+ err "Python 3 not found — required for --global install"
271
+ exit 1
272
+ fi
273
+ if [[ ! -f "$INSTALL_PY" ]]; then
274
+ err "Missing $INSTALL_PY"
275
+ exit 1
276
+ fi
277
+ args=(--package "$SOURCE_DIR" --global --tools="$TOOLS")
278
+ $FORCE && args+=(--force)
279
+ $QUIET && args+=(--quiet)
280
+ $UNINSTALL && args+=(--uninstall)
281
+ "$python_bin" "$INSTALL_PY" "${args[@]}"
282
+ exit $?
283
+ fi
284
+
130
285
  if ! $SKIP_SYNC; then
131
286
  if [[ ! -f "$INSTALL_SH" ]]; then
132
287
  err "Missing $INSTALL_SH"