@event4u/agent-config 1.38.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 (56) hide show
  1. package/.agent-src/commands/onboard.md +131 -50
  2. package/.agent-src/commands/orchestrate.md +123 -0
  3. package/.agent-src/commands/sync-gitignore/fix.md +135 -0
  4. package/.agent-src/commands/sync-gitignore.md +31 -5
  5. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
  6. package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
  7. package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
  8. package/.agent-src/templates/agent-settings.md +9 -0
  9. package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
  10. package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
  11. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
  12. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
  13. package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
  14. package/.claude-plugin/marketplace.json +3 -1
  15. package/AGENTS.md +4 -4
  16. package/CHANGELOG.md +76 -0
  17. package/README.md +17 -6
  18. package/bin/install.php +13 -6
  19. package/config/agent-settings.template.yml +21 -0
  20. package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
  21. package/docs/architecture.md +1 -1
  22. package/docs/catalog.md +3 -2
  23. package/docs/contracts/audit-log-v1.md +142 -0
  24. package/docs/contracts/command-clusters.md +2 -0
  25. package/docs/contracts/file-ownership-matrix.json +20 -0
  26. package/docs/contracts/orchestration-dsl-v1.md +152 -0
  27. package/docs/customization.md +45 -0
  28. package/docs/getting-started.md +1 -1
  29. package/docs/guidelines/agent-infra/layered-settings.md +54 -17
  30. package/docs/installation.md +132 -0
  31. package/docs/setup/mcp-client-config.md +152 -0
  32. package/docs/setup/mcp-cloud-endpoints.md +16 -0
  33. package/docs/setup/per-ide/aider.md +48 -0
  34. package/docs/setup/per-ide/claude-code.md +108 -0
  35. package/docs/setup/per-ide/claude-desktop.md +148 -0
  36. package/docs/setup/per-ide/cline.md +43 -0
  37. package/docs/setup/per-ide/codex.md +46 -0
  38. package/docs/setup/per-ide/copilot.md +80 -0
  39. package/docs/setup/per-ide/cursor.md +125 -0
  40. package/docs/setup/per-ide/gemini-cli.md +45 -0
  41. package/docs/setup/per-ide/windsurf.md +120 -0
  42. package/package.json +1 -1
  43. package/scripts/_lib/agent_settings.py +168 -0
  44. package/scripts/compress.py +153 -1
  45. package/scripts/extract_audit_patterns.py +202 -0
  46. package/scripts/install +156 -1
  47. package/scripts/install.py +270 -10
  48. package/scripts/install.sh +52 -7
  49. package/scripts/lint_orchestration_dsl.py +214 -0
  50. package/scripts/skill_linter.py +9 -0
  51. package/scripts/sync_gitignore.py +56 -1
  52. package/templates/claude_desktop_config.json.template +21 -0
  53. package/templates/cursor-rule.mdc.j2 +7 -0
  54. package/templates/global-install-manifest.yml +91 -0
  55. package/templates/marketing-copy.yml +64 -0
  56. package/templates/windsurf-rule.md.j2 +7 -0
@@ -37,10 +37,26 @@ DRY_RUN=false
37
37
  VERBOSE=false
38
38
  QUIET=false
39
39
  SKIP_GITIGNORE=false
40
+ # Comma-separated tool IDs (default: all). Set by --tools or the
41
+ # orchestrator (scripts/install). The .augment/ substrate is always
42
+ # synced because every other tool symlinks back into it.
43
+ TOOLS="all"
40
44
  # Resolved from <TARGET>/.agent-settings.yml in resolve_settings(); when
41
45
  # true, .augment/rules/ files are symlinked instead of copied.
42
46
  USE_RULES_SYMLINKS=false
43
47
 
48
+ # Return 0 if a tool ID is enabled by the current --tools selection.
49
+ # "all" matches everything; otherwise match the comma list exactly.
50
+ is_tool_enabled() {
51
+ local id="$1"
52
+ [[ "$TOOLS" == "all" ]] && return 0
53
+ case ",$TOOLS," in
54
+ *,"$id",*) return 0 ;;
55
+ *,all,*) return 0 ;;
56
+ esac
57
+ return 1
58
+ }
59
+
44
60
  # --- Logging ---
45
61
  log_info() { $QUIET || echo " ✅ $*"; }
46
62
  log_warn() { $QUIET || echo " ⚠️ $*" >&2; }
@@ -57,11 +73,15 @@ parse_args() {
57
73
  --verbose) VERBOSE=true; shift ;;
58
74
  --quiet) QUIET=true; shift ;;
59
75
  --skip-gitignore) SKIP_GITIGNORE=true; shift ;;
76
+ --tools) TOOLS="$2"; shift 2 ;;
77
+ --tools=*) TOOLS="${1#*=}"; shift ;;
60
78
  --help|-h) show_help; exit 0 ;;
61
79
  *) log_error "Unknown argument: $1"; show_help; exit 1 ;;
62
80
  esac
63
81
  done
64
82
 
83
+ [[ -z "$TOOLS" ]] && TOOLS="all"
84
+
65
85
  # Auto-detect source: directory where this script lives (../ = package root)
66
86
  if [[ -z "$SOURCE_DIR" ]]; then
67
87
  SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -106,6 +126,10 @@ Syncs agent-config to target project. Copies rules, symlinks everything else.
106
126
  Options:
107
127
  --source <dir> Package source directory (default: auto-detect from script location)
108
128
  --target <dir> Target project root (default: $PROJECT_ROOT or cwd)
129
+ --tools <list> Comma-separated tool IDs (default: all). Filters tool-specific
130
+ payload (.claude/, .cursor/, .clinerules/, .windsurfrules,
131
+ GEMINI.md, copilot-instructions.md). The .augment/ substrate
132
+ and AGENTS.md are always written.
109
133
  --dry-run Show what would happen without making changes
110
134
  --verbose Show detailed output
111
135
  --quiet Suppress all output except errors
@@ -409,15 +433,25 @@ clean_stale() {
409
433
  }
410
434
 
411
435
 
412
- # Create tool-specific rule symlinks
436
+ # Create tool-specific rule symlinks (filtered by --tools selection).
437
+ # Map: tool ID → (target dir, relative prefix from target → .augment/rules).
413
438
  create_tool_symlinks() {
414
439
  local project_root="$1"
415
440
  local rules_dir="$project_root/.augment/rules"
416
441
 
417
442
  [[ -d "$rules_dir" ]] || return
418
443
 
419
- local -a tool_dirs=(".claude/rules" ".cursor/rules" ".clinerules")
420
- local -a rel_prefixes=("../../.augment/rules" "../../.augment/rules" "../.augment/rules")
444
+ local -a tool_ids=()
445
+ local -a tool_dirs=()
446
+ local -a rel_prefixes=()
447
+ is_tool_enabled "claude-code" && { tool_ids+=("claude-code"); tool_dirs+=(".claude/rules"); rel_prefixes+=("../../.augment/rules"); }
448
+ is_tool_enabled "cursor" && { tool_ids+=("cursor"); tool_dirs+=(".cursor/rules"); rel_prefixes+=("../../.augment/rules"); }
449
+ is_tool_enabled "cline" && { tool_ids+=("cline"); tool_dirs+=(".clinerules"); rel_prefixes+=("../.augment/rules"); }
450
+
451
+ if [[ ${#tool_dirs[@]} -eq 0 ]]; then
452
+ log_verbose "no tool rule directories selected (--tools=$TOOLS)"
453
+ return 0
454
+ fi
421
455
 
422
456
  local count=0
423
457
  for i in "${!tool_dirs[@]}"; do
@@ -467,15 +501,16 @@ create_tool_symlinks() {
467
501
  done
468
502
  done
469
503
 
470
- log_info "Created $count rule symlinks across ${#tool_dirs[@]} tool directories"
504
+ log_info "Created $count rule symlinks across ${#tool_dirs[@]} tool directories (${tool_ids[*]})"
471
505
  }
472
506
 
473
- # Create .claude/skills/ directory symlinks
507
+ # Create .claude/skills/ directory symlinks (claude-code only).
474
508
  create_skill_symlinks() {
475
509
  local project_root="$1"
476
510
  local skills_dir="$project_root/.augment/skills"
477
511
 
478
512
  [[ -d "$skills_dir" ]] || return
513
+ is_tool_enabled "claude-code" || { log_verbose "skip .claude/skills/ (claude-code not selected)"; return 0; }
479
514
 
480
515
  local target_dir="$project_root/.claude/skills"
481
516
  mkdir -p "$target_dir"
@@ -528,6 +563,7 @@ generate_windsurfrules() {
528
563
  local rules_dir="$project_root/.augment/rules"
529
564
 
530
565
  [[ -d "$rules_dir" ]] || return
566
+ is_tool_enabled "windsurf" || { log_verbose "skip .windsurfrules (windsurf not selected)"; return 0; }
531
567
 
532
568
  local output="$project_root/.windsurfrules"
533
569
  local count=0
@@ -561,11 +597,13 @@ generate_windsurfrules() {
561
597
  }
562
598
 
563
599
 
564
- # Create GEMINI.md symlink
600
+ # Create GEMINI.md symlink (gemini-cli only).
565
601
  create_gemini_md() {
566
602
  local project_root="$1"
567
603
  local link="$project_root/GEMINI.md"
568
604
 
605
+ is_tool_enabled "gemini-cli" || { log_verbose "skip GEMINI.md (gemini-cli not selected)"; return 0; }
606
+
569
607
  if [[ -L "$link" ]] || [[ -f "$link" ]]; then
570
608
  return # Don't overwrite
571
609
  fi
@@ -726,8 +764,15 @@ main() {
726
764
  # own root AGENTS.md / copilot-instructions.md — those are meta docs
727
765
  # about the package itself and would leak package-specific content
728
766
  # into consumer projects.
767
+ # AGENTS.md is the universal cross-tool contract (aider, codex, claude,
768
+ # etc.) and is always written. copilot-instructions.md is gated on the
769
+ # copilot tool selection.
729
770
  copy_if_missing "$SOURCE_PAYLOAD/templates/AGENTS.md" "$TARGET_DIR/AGENTS.md"
730
- copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-instructions.md" "$TARGET_DIR/.github/copilot-instructions.md"
771
+ if is_tool_enabled "copilot"; then
772
+ copy_if_missing "$SOURCE_PAYLOAD/templates/copilot-instructions.md" "$TARGET_DIR/.github/copilot-instructions.md"
773
+ else
774
+ log_verbose "skip .github/copilot-instructions.md (copilot not selected)"
775
+ fi
731
776
 
732
777
  # 3. Create tool-specific symlinks
733
778
  create_tool_symlinks "$TARGET_DIR"
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """Lint `.agent-config/orchestrations/*.yaml` pipeline files.
3
+
4
+ CI gate for the orchestration DSL contract
5
+ (`docs/contracts/orchestration-dsl-v1.md`). Hard-fails on:
6
+
7
+ - missing or malformed top-level keys
8
+ (`schema_version`, `name`, `description`, `steps`)
9
+ - `schema_version != 1`
10
+ - `name` not matching `[a-z][a-z0-9-]*` or not matching the filename
11
+ - duplicate `steps[].id`
12
+ - `steps[].kind` outside the enum (`skill` / `command` / `persona` /
13
+ `subagent`)
14
+ - `steps[].ref` pointing at a non-existent target on disk
15
+ - `${{ ... }}` reference to an unknown input or step id
16
+ - `steps[]` length outside [1, 32]
17
+ - `outputs` referencing an unknown step
18
+
19
+ Exit codes mirror `lint_hook_manifest.py`:
20
+ 0 — clean
21
+ 1 — at least one hard failure
22
+ 2 — file or schema-load error
23
+
24
+ Invocation:
25
+
26
+ python3 scripts/lint_orchestration_dsl.py [--dir PATH] [--file PATH]
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import re
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ DEFAULT_DIR = REPO_ROOT / ".agent-config" / "orchestrations"
37
+
38
+ NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$")
39
+ STEP_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
40
+ INTERP_RE = re.compile(r"\$\{\{\s*(inputs|steps)\.([a-z0-9_-]+)(?:\.output)?\s*\}\}")
41
+
42
+ VALID_KINDS = {"skill", "command", "persona", "subagent"}
43
+ MAX_STEPS = 32
44
+ MIN_STEPS = 1
45
+
46
+ # Subagent-orchestration modes — kept in lock-step with
47
+ # .agent-src.uncompressed/skills/subagent-orchestration/SKILL.md.
48
+ SUBAGENT_MODES = {
49
+ "do-and-judge", "do-and-judge-two-stage",
50
+ "do-in-steps", "do-in-parallel", "do-in-worktrees",
51
+ "do-competitively", "judge-with-debate",
52
+ }
53
+
54
+
55
+ def _load_yaml(path: Path) -> object:
56
+ """Reuse the dispatcher's loader so the linter sees what the
57
+ runtime sees — fallback parser when PyYAML is not installed."""
58
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
59
+ from hooks.dispatch_hook import _load_yaml as _load # noqa: E402
60
+ return _load(path)
61
+
62
+
63
+ def _ref_exists(kind: str, ref: str) -> bool:
64
+ if kind == "skill":
65
+ return (REPO_ROOT / ".agent-src.uncompressed" / "skills" / ref / "SKILL.md").is_file()
66
+ if kind == "command":
67
+ return (REPO_ROOT / ".agent-src.uncompressed" / "commands" / f"{ref}.md").is_file()
68
+ if kind == "persona":
69
+ return (REPO_ROOT / ".agent-src.uncompressed" / "personas" / f"{ref}.md").is_file()
70
+ if kind == "subagent":
71
+ return ref in SUBAGENT_MODES
72
+ return False
73
+
74
+
75
+ def _walk_interpolations(value: object):
76
+ """Yield (namespace, ident) tuples for every ${{ ns.ident }} in the
77
+ nested value (dict / list / str)."""
78
+ if isinstance(value, str):
79
+ for match in INTERP_RE.finditer(value):
80
+ yield match.group(1), match.group(2)
81
+ elif isinstance(value, dict):
82
+ for v in value.values():
83
+ yield from _walk_interpolations(v)
84
+ elif isinstance(value, list):
85
+ for v in value:
86
+ yield from _walk_interpolations(v)
87
+
88
+
89
+ def _check_unknown_namespaces(value: object, path: str, errors: list[str]) -> None:
90
+ if isinstance(value, str):
91
+ for match in re.finditer(r"\$\{\{\s*([a-z]+)\.", value):
92
+ if match.group(1) not in ("inputs", "steps"):
93
+ errors.append(f"{path}: unknown interpolation namespace '{match.group(1)}'")
94
+ elif isinstance(value, dict):
95
+ for k, v in value.items():
96
+ _check_unknown_namespaces(v, f"{path}.{k}", errors)
97
+ elif isinstance(value, list):
98
+ for i, v in enumerate(value):
99
+ _check_unknown_namespaces(v, f"{path}[{i}]", errors)
100
+
101
+
102
+ def _check_steps(doc: dict, input_ids: set[str], errors: list[str]) -> set[str]:
103
+ steps = doc.get("steps")
104
+ if not isinstance(steps, list) or not (MIN_STEPS <= len(steps) <= MAX_STEPS):
105
+ errors.append(f"steps: must be a list of {MIN_STEPS}–{MAX_STEPS} entries")
106
+ return set()
107
+ step_ids: set[str] = set()
108
+ for i, step in enumerate(steps):
109
+ if not isinstance(step, dict):
110
+ errors.append(f"steps[{i}]: must be a mapping")
111
+ continue
112
+ sid = step.get("id")
113
+ if not isinstance(sid, str) or not STEP_ID_RE.match(sid):
114
+ errors.append(f"steps[{i}].id: must be snake-case identifier")
115
+ continue
116
+ if sid in step_ids:
117
+ errors.append(f"steps[{i}].id: duplicate id '{sid}'")
118
+ continue
119
+ step_ids.add(sid)
120
+ kind = step.get("kind")
121
+ ref = step.get("ref")
122
+ if kind not in VALID_KINDS:
123
+ errors.append(f"steps.{sid}.kind: must be one of {sorted(VALID_KINDS)}")
124
+ continue
125
+ if not isinstance(ref, str) or not _ref_exists(kind, ref):
126
+ errors.append(f"steps.{sid}.ref: {kind} '{ref}' not found on disk")
127
+ _check_unknown_namespaces(step.get("with"), f"steps.{sid}.with", errors)
128
+ for ns, ident in _walk_interpolations(step.get("with") or {}):
129
+ if ns == "inputs" and ident not in input_ids:
130
+ errors.append(f"steps.{sid}.with: unknown input '{ident}'")
131
+ if ns == "steps" and ident not in step_ids - {sid}:
132
+ errors.append(f"steps.{sid}.with: unknown step '{ident}' (forward ref or self)")
133
+ return step_ids
134
+
135
+
136
+
137
+ def _check_outputs(doc: dict, step_ids: set[str], input_ids: set[str], errors: list[str]) -> None:
138
+ outputs = doc.get("outputs")
139
+ if outputs is None:
140
+ return
141
+ if not isinstance(outputs, dict):
142
+ errors.append("outputs: must be a mapping")
143
+ return
144
+ for name, value in outputs.items():
145
+ for ns, ident in _walk_interpolations(value):
146
+ if ns == "steps" and ident not in step_ids:
147
+ errors.append(f"outputs.{name}: unknown step '{ident}'")
148
+ if ns == "inputs" and ident not in input_ids:
149
+ errors.append(f"outputs.{name}: unknown input '{ident}'")
150
+
151
+
152
+ def _check_inputs(doc: dict, errors: list[str]) -> set[str]:
153
+ inputs = doc.get("inputs") or []
154
+ if not isinstance(inputs, list):
155
+ errors.append("inputs: must be a list")
156
+ return set()
157
+ ids: set[str] = set()
158
+ for i, inp in enumerate(inputs):
159
+ if not isinstance(inp, dict) or not isinstance(inp.get("id"), str):
160
+ errors.append(f"inputs[{i}]: must be a mapping with string 'id'")
161
+ continue
162
+ if inp["id"] in ids:
163
+ errors.append(f"inputs[{i}].id: duplicate id '{inp['id']}'")
164
+ ids.add(inp["id"])
165
+ return ids
166
+
167
+
168
+ def lint(path: Path) -> int:
169
+ try:
170
+ doc = _load_yaml(path)
171
+ except Exception as exc:
172
+ sys.stderr.write(f"lint_orchestration_dsl: load error: {exc}\n")
173
+ return 2
174
+ if not isinstance(doc, dict):
175
+ sys.stderr.write(f"{path}: top-level must be a mapping\n")
176
+ return 1
177
+
178
+ errors: list[str] = []
179
+ if doc.get("schema_version") != 1:
180
+ errors.append("schema_version: must be 1")
181
+ name = doc.get("name")
182
+ if not isinstance(name, str) or not NAME_RE.match(name):
183
+ errors.append("name: must be kebab-case starting with a letter")
184
+ elif name != path.stem:
185
+ errors.append(f"name: '{name}' must match filename stem '{path.stem}'")
186
+ if not isinstance(doc.get("description"), str) or not doc["description"].strip():
187
+ errors.append("description: must be a non-empty string")
188
+
189
+ input_ids = _check_inputs(doc, errors)
190
+ step_ids = _check_steps(doc, input_ids, errors)
191
+ _check_outputs(doc, step_ids, input_ids, errors)
192
+
193
+ for e in errors:
194
+ sys.stderr.write(f"error: {path}: {e}\n")
195
+ return 1 if errors else 0
196
+
197
+
198
+ def main(argv: list[str] | None = None) -> int:
199
+ parser = argparse.ArgumentParser(description=__doc__)
200
+ parser.add_argument("--dir", type=Path, default=DEFAULT_DIR)
201
+ parser.add_argument("--file", type=Path, default=None)
202
+ args = parser.parse_args(argv)
203
+ if args.file is not None:
204
+ return lint(args.file)
205
+ if not args.dir.is_dir():
206
+ return 0 # opt-in directory; absence is not a failure
207
+ rc = 0
208
+ for path in sorted(args.dir.glob("*.yaml")):
209
+ rc = max(rc, lint(path))
210
+ return rc
211
+
212
+
213
+ if __name__ == "__main__":
214
+ raise SystemExit(main())
@@ -1605,6 +1605,15 @@ def gather_changed_candidate_files(root: Path) -> list[Path]:
1605
1605
  if path.is_symlink():
1606
1606
  continue
1607
1607
  norm = raw.replace("\\", "/")
1608
+ # Only lint source-of-truth and source-mirror dirs. Projection
1609
+ # dirs (.windsurf/, .cursor/, .clinerules/, .claude/) use
1610
+ # tool-native frontmatter (e.g. Windsurf's trigger/globs) that
1611
+ # the linter does not validate — they regenerate from source.
1612
+ if not (
1613
+ norm.startswith(".agent-src.uncompressed/")
1614
+ or norm.startswith(".agent-src/")
1615
+ ):
1616
+ continue
1608
1617
  if path.name == "SKILL.md" or "/rules/" in norm or "/commands/" in norm:
1609
1618
  files.append(path)
1610
1619
  return sorted(set(files))
@@ -14,7 +14,13 @@ are preserved). Call with `--replace` for a destructive full rewrite.
14
14
 
15
15
  Usage:
16
16
  python3 scripts/sync_gitignore.py [--path .gitignore] [--template config/gitignore-block.txt]
17
- [--dry-run] [--replace] [--quiet]
17
+ [--dry-run] [--replace] [--cleanup-legacy] [--quiet]
18
+
19
+ `--cleanup-legacy` additionally scrubs legacy patterns (pre-/agents/ layout
20
+ runtime artefacts) from anywhere in the target file — inside the managed
21
+ block and outside, where older installers or hand-edits dropped them.
22
+ Runs before the regular sync, so a single invocation removes garbage and
23
+ re-adds the current canonical entries.
18
24
 
19
25
  Exit codes:
20
26
  0 — no changes needed (or --dry-run ran successfully)
@@ -34,6 +40,20 @@ SECTION_FOOTER = "# event4u/agent-config — END"
34
40
  DEFAULT_GITIGNORE = ".gitignore"
35
41
  DEFAULT_TEMPLATE = Path(__file__).resolve().parent.parent / "config" / "gitignore-block.txt"
36
42
 
43
+ # Legacy patterns that lived in older versions of config/gitignore-block.txt
44
+ # before runtime artefacts moved under /agents/ (May 2026). They get stripped
45
+ # wherever they appear in the consumer's .gitignore — inside the managed block
46
+ # or outside (older installers / hand-edits). Current canonical equivalents
47
+ # (e.g. /agents/.agent-chat-history) come from the template and are NOT
48
+ # affected. Leading-slash variants are matched defensively.
49
+ LEGACY_PATTERNS: tuple[str, ...] = (
50
+ ".agent-chat-history",
51
+ ".agent-chat-history.bak",
52
+ ".agent-chat-history.*.bak",
53
+ ".agent-prices.md",
54
+ ".council-tmp/",
55
+ )
56
+
37
57
 
38
58
  def _strip(ln: str) -> str:
39
59
  return ln.rstrip("\n").rstrip()
@@ -149,6 +169,26 @@ def sync_block(existing_lines: list[str],
149
169
  return head + new_block + tail, missing
150
170
 
151
171
 
172
+ def cleanup_legacy(lines: list[str]) -> tuple[list[str], list[str]]:
173
+ """Strip legacy entries from anywhere in the file.
174
+
175
+ A line is legacy when its stripped, leading-whitespace-trimmed content
176
+ matches a `LEGACY_PATTERNS` entry — with or without a leading slash.
177
+ Comments and blank lines are untouched; current managed entries (e.g.
178
+ `/agents/.agent-chat-history`) are not in the legacy set and survive.
179
+ """
180
+ legacy = set(LEGACY_PATTERNS)
181
+ kept: list[str] = []
182
+ removed: list[str] = []
183
+ for ln in lines:
184
+ s = _strip(ln).lstrip()
185
+ if s in legacy or (s.startswith("/") and s[1:] in legacy):
186
+ removed.append(s)
187
+ continue
188
+ kept.append(ln)
189
+ return kept, removed
190
+
191
+
152
192
  def format_file(lines: list[str]) -> str:
153
193
  """Join lines with newlines and enforce exactly one trailing newline."""
154
194
  text = "\n".join(lines)
@@ -174,6 +214,9 @@ def main(argv: list[str] | None = None) -> int:
174
214
  ap.add_argument("--replace", action="store_true",
175
215
  help="rewrite the block in full (discards user-added "
176
216
  "lines inside the block)")
217
+ ap.add_argument("--cleanup-legacy", action="store_true",
218
+ help="strip legacy patterns (pre-/agents/ layout) from "
219
+ "anywhere in the file before syncing the block")
177
220
  ap.add_argument("--quiet", action="store_true",
178
221
  help="suppress summary on success")
179
222
  args = ap.parse_args(argv)
@@ -193,6 +236,10 @@ def main(argv: list[str] | None = None) -> int:
193
236
  existing_text = ""
194
237
  existing_lines = []
195
238
 
239
+ removed_legacy: list[str] = []
240
+ if args.cleanup_legacy:
241
+ existing_lines, removed_legacy = cleanup_legacy(existing_lines)
242
+
196
243
  new_lines, added = sync_block(
197
244
  existing_lines, template_lines, replace=args.replace,
198
245
  )
@@ -211,6 +258,10 @@ def main(argv: list[str] | None = None) -> int:
211
258
  print(f"\n(dry-run) would add {len(added)} entr"
212
259
  f"{'y' if len(added) == 1 else 'ies'} to {target}",
213
260
  file=sys.stderr)
261
+ if removed_legacy:
262
+ print(f"(dry-run) would remove {len(removed_legacy)} legacy "
263
+ f"entr{'y' if len(removed_legacy) == 1 else 'ies'}: "
264
+ f"{', '.join(removed_legacy)}", file=sys.stderr)
214
265
  return 0
215
266
 
216
267
  target.parent.mkdir(parents=True, exist_ok=True)
@@ -219,6 +270,10 @@ def main(argv: list[str] | None = None) -> int:
219
270
  action = "replaced" if args.replace else "updated"
220
271
  print(f"✅ {target}: {action} block "
221
272
  f"({len(added)} entr{'y' if len(added) == 1 else 'ies'} added)")
273
+ if removed_legacy:
274
+ print(f" removed {len(removed_legacy)} legacy "
275
+ f"entr{'y' if len(removed_legacy) == 1 else 'ies'}: "
276
+ f"{', '.join(removed_legacy)}")
222
277
  return 0
223
278
 
224
279
 
@@ -0,0 +1,21 @@
1
+ {
2
+ "//": "Claude Desktop config template — agent-config",
3
+ "//1": "Copy this to one of:",
4
+ "//2": " macOS ~/Library/Application Support/Claude/claude_desktop_config.json",
5
+ "//3": " Windows %APPDATA%\\Claude\\claude_desktop_config.json",
6
+ "//4": " Linux ~/.config/Claude/claude_desktop_config.json",
7
+ "//5": "Strip the leading '_disabled_' prefix on the mcpServers entry below to enable the MCP connector.",
8
+ "//6": "Hosted endpoint is read-only and identity-stable per release; full client setup details in docs/setup/mcp-client-config.md.",
9
+ "//7": "Restart Claude Desktop fully (Cmd+Q on macOS) after editing — the menubar process keeps the old config cached otherwise.",
10
+
11
+ "_disabled_mcpServers": {
12
+ "agent-config": {
13
+ "command": "npx",
14
+ "args": [
15
+ "-y",
16
+ "mcp-remote",
17
+ "https://agent-config-mcp.event4u.workers.dev"
18
+ ]
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: {{ description }}
3
+ globs: {{ globs | default("") }}
4
+ alwaysApply: {{ "true" if always_apply else "false" }}
5
+ ---
6
+
7
+ {{ body }}
@@ -0,0 +1,91 @@
1
+ # Global Install Manifest — road-to-simplicity-and-everywhere Phase 3
2
+ #
3
+ # Curated subset of rules + skills shipped to user-scope directories
4
+ # (~/.claude/, ~/.cursor/, ~/.codeium/windsurf/, ~/.config/agent-config/)
5
+ # by `scripts/install --global`. Without curation we'd dump 200+ skills
6
+ # into user dirs — that is the failure mode anthropics/claude-code#53950
7
+ # flagged.
8
+ #
9
+ # Schema (v1):
10
+ # kernel_rules: list[str] — rule IDs from router.json's `kernel`.
11
+ # Always shipped to every enabled surface.
12
+ # top_skills: list[entry] — curated universal skills.
13
+ # entry.id : skill directory under .agent-src/skills/
14
+ # entry.surfaces : subset of [claude-code, cursor, windsurf, fallback]
15
+ # — fallback is always written.
16
+ #
17
+ # Source: kernel_rules mirrors router.json; top_skills is a 15-pick
18
+ # distilled from .agent-src/skills/ — universal coding-loop helpers
19
+ # (work, commit, PR, review, quality, analyze, handoff). Per-stack
20
+ # skills (laravel, react-shadcn-ui, php-coder) stay project-local.
21
+ #
22
+ # Per-tool target paths (handled by scripts/install.py global subcommand):
23
+ # claude-code : ~/.claude/skills/event4u/<id>/SKILL.md
24
+ # ~/.claude/rules/event4u/<rule>.md
25
+ # cursor : ~/.cursor/rules/imported/event4u/<rule>.mdc
26
+ # ~/.cursor/skills/event4u/<id>/ (skills surface; mirrors agent-os)
27
+ # windsurf : ~/.codeium/windsurf/global_workflows/event4u/<id>.md
28
+ # fallback : ~/.config/agent-config/{rules,skills}/event4u/
29
+ #
30
+ # All writes are namespaced under `event4u/` so `--global --uninstall`
31
+ # can `rm -rf` the namespace dir without touching user-added files.
32
+
33
+ schema_version: 1
34
+
35
+ # Always-loaded rules — mirrors router.json's `kernel` array.
36
+ # Symlinked / copied into every enabled surface's rules dir.
37
+ kernel_rules:
38
+ - agent-authority
39
+ - ask-when-uncertain
40
+ - commit-policy
41
+ - direct-answers
42
+ - language-and-tone
43
+ - no-cheap-questions
44
+ - non-destructive-by-default
45
+ - scope-control
46
+ - verify-before-complete
47
+
48
+ # Top-N curated skills. The 15 picks below are coding-loop universals:
49
+ # the agent reaches for them on almost any task, regardless of stack.
50
+ # Per-stack and domain skills are deliberately omitted — they belong in
51
+ # project-local installs where the stack signal is unambiguous.
52
+ top_skills:
53
+ # --- Workflow drivers (most-touched commands) ---
54
+ - id: work
55
+ surfaces: [claude-code, cursor, windsurf, fallback]
56
+ - id: commit
57
+ surfaces: [claude-code, cursor, windsurf, fallback]
58
+ - id: create-pr
59
+ surfaces: [claude-code, cursor, windsurf, fallback]
60
+ - id: review-changes
61
+ surfaces: [claude-code, cursor, windsurf, fallback]
62
+ - id: quality-fix
63
+ surfaces: [claude-code, cursor, windsurf, fallback]
64
+
65
+ # --- Session / context management ---
66
+ - id: agent-handoff
67
+ surfaces: [claude-code, cursor, windsurf, fallback]
68
+ - id: agent-status
69
+ surfaces: [claude-code, cursor, windsurf, fallback]
70
+ - id: project-analyze
71
+ surfaces: [claude-code, cursor, windsurf, fallback]
72
+ - id: project-health
73
+ surfaces: [claude-code, cursor, windsurf, fallback]
74
+
75
+ # --- Investigation / debugging ---
76
+ - id: bug-investigate
77
+ surfaces: [claude-code, cursor, windsurf, fallback]
78
+ - id: bug-fix
79
+ surfaces: [claude-code, cursor, windsurf, fallback]
80
+ - id: systematic-debugging
81
+ surfaces: [claude-code, cursor, windsurf, fallback]
82
+
83
+ # --- Ticket / PR drivers ---
84
+ - id: implement-ticket
85
+ surfaces: [claude-code, cursor, windsurf, fallback]
86
+ - id: prepare-for-review
87
+ surfaces: [claude-code, cursor, windsurf, fallback]
88
+
89
+ # --- Onboarding (single command, surfaces a project) ---
90
+ - id: onboard
91
+ surfaces: [claude-code, cursor, windsurf, fallback]
@@ -0,0 +1,64 @@
1
+ # marketing-copy.yml — single source of truth for all marketplace
2
+ # listings, GitHub repo metadata, npm package descriptions, and
3
+ # README taglines. Edit here once; downstream surfaces sync on
4
+ # `task generate-tools` (Phase 7 / S38 of road-to-simplicity-and-everywhere).
5
+
6
+ tagline: >-
7
+ Shared agent configuration — skills, rules, and slash commands for
8
+ AI coding tools (Claude Code, Cursor, Windsurf, Cline, Aider,
9
+ Codex, Gemini CLI, Copilot, Augment).
10
+
11
+ short_description: >-
12
+ Project-installable + global agent skill suite — works across
13
+ every major AI coding tool. One install, every editor.
14
+
15
+ # GitHub repo topics (set via `gh repo edit --add-topic ...`).
16
+ # Keep this list short and high-signal; GitHub caps display at ~6.
17
+ github_topics:
18
+ - agent
19
+ - claude-code
20
+ - cursor
21
+ - windsurf
22
+ - cline
23
+ - mcp
24
+ - agents-md
25
+ - skill-files
26
+ - copilot
27
+ - aider
28
+
29
+ # Marketplace listings (Anthropic, Cursor, Smithery) read these.
30
+ listing:
31
+ name: event4u/agent-config
32
+ category: agent-tooling
33
+ primary_surface: claude-code
34
+ also_supports:
35
+ - claude-desktop
36
+ - cursor
37
+ - windsurf
38
+ - cline
39
+ - aider
40
+ - codex
41
+ - gemini-cli
42
+ - copilot
43
+ - augment
44
+
45
+ # npm package metadata (synced into packages/create-agent-config/package.json
46
+ # and any future @event4u/agent-config packages).
47
+ npm:
48
+ package: "@event4u/create-agent-config"
49
+ description: >-
50
+ Zero-config installer for event4u/agent-config — picks tools,
51
+ runs the install, works in any directory.
52
+ keywords:
53
+ - agent
54
+ - claude
55
+ - cursor
56
+ - windsurf
57
+ - skills
58
+ - mcp
59
+
60
+ # Authoritative URLs.
61
+ urls:
62
+ homepage: https://github.com/event4u-app/agent-config
63
+ bugs: https://github.com/event4u-app/agent-config/issues
64
+ docs: https://github.com/event4u-app/agent-config/blob/main/docs/installation.md
@@ -0,0 +1,7 @@
1
+ ---
2
+ trigger: {{ trigger }}
3
+ description: {{ description }}
4
+ globs: {{ globs | default("") }}
5
+ ---
6
+
7
+ {{ body }}