@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.
- package/.agent-src/commands/onboard.md +131 -50
- package/.agent-src/commands/orchestrate.md +123 -0
- package/.agent-src/commands/sync-gitignore/fix.md +135 -0
- package/.agent-src/commands/sync-gitignore.md +31 -5
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +30 -2
- package/.agent-src/skills/subagent-orchestration/SKILL.md +9 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/.agent-src/templates/agent-settings.md +9 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.agent-src/templates/scripts/work_engine/orchestration.py +168 -0
- package/.claude-plugin/marketplace.json +3 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +76 -0
- package/README.md +17 -6
- package/bin/install.php +13 -6
- package/config/agent-settings.template.yml +21 -0
- package/docs/DISTRIBUTION_CHECKLIST.md +169 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +3 -2
- package/docs/contracts/audit-log-v1.md +142 -0
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/file-ownership-matrix.json +20 -0
- package/docs/contracts/orchestration-dsl-v1.md +152 -0
- package/docs/customization.md +45 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/installation.md +132 -0
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +16 -0
- package/docs/setup/per-ide/aider.md +48 -0
- package/docs/setup/per-ide/claude-code.md +108 -0
- package/docs/setup/per-ide/claude-desktop.md +148 -0
- package/docs/setup/per-ide/cline.md +43 -0
- package/docs/setup/per-ide/codex.md +46 -0
- package/docs/setup/per-ide/copilot.md +80 -0
- package/docs/setup/per-ide/cursor.md +125 -0
- package/docs/setup/per-ide/gemini-cli.md +45 -0
- package/docs/setup/per-ide/windsurf.md +120 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
- package/scripts/compress.py +153 -1
- package/scripts/extract_audit_patterns.py +202 -0
- package/scripts/install +156 -1
- package/scripts/install.py +270 -10
- package/scripts/install.sh +52 -7
- package/scripts/lint_orchestration_dsl.py +214 -0
- package/scripts/skill_linter.py +9 -0
- package/scripts/sync_gitignore.py +56 -1
- package/templates/claude_desktop_config.json.template +21 -0
- package/templates/cursor-rule.mdc.j2 +7 -0
- package/templates/global-install-manifest.yml +91 -0
- package/templates/marketing-copy.yml +64 -0
- package/templates/windsurf-rule.md.j2 +7 -0
package/scripts/install.sh
CHANGED
|
@@ -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
|
|
420
|
-
local -a
|
|
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
|
-
|
|
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())
|
package/scripts/skill_linter.py
CHANGED
|
@@ -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,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
|