@event4u/agent-config 1.9.1 → 1.13.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 (85) hide show
  1. package/.agent-src/commands/agent-handoff.md +15 -0
  2. package/.agent-src/commands/chat-history-clear.md +98 -0
  3. package/.agent-src/commands/chat-history-resume.md +178 -0
  4. package/.agent-src/commands/chat-history.md +102 -0
  5. package/.agent-src/commands/compress.md +9 -9
  6. package/.agent-src/commands/copilot-agents-init.md +1 -1
  7. package/.agent-src/commands/fix-portability.md +2 -2
  8. package/.agent-src/commands/fix-pr-bot-comments.md +1 -1
  9. package/.agent-src/commands/fix-pr-developer-comments.md +1 -1
  10. package/.agent-src/commands/fix-references.md +2 -2
  11. package/.agent-src/commands/mode.md +5 -5
  12. package/.agent-src/commands/onboard.md +171 -0
  13. package/.agent-src/commands/roadmap-create.md +7 -2
  14. package/.agent-src/commands/roadmap-execute.md +2 -2
  15. package/.agent-src/commands/set-cost-profile.md +101 -0
  16. package/.agent-src/commands/sync-agent-settings.md +122 -0
  17. package/.agent-src/commands/sync-gitignore.md +104 -0
  18. package/.agent-src/commands/tests-execute.md +6 -6
  19. package/.agent-src/commands/upstream-contribute.md +5 -4
  20. package/.agent-src/contexts/augment-infrastructure.md +2 -2
  21. package/.agent-src/contexts/override-system.md +1 -1
  22. package/.agent-src/contexts/subagent-configuration.md +3 -3
  23. package/.agent-src/guidelines/agent-infra/layered-settings.md +48 -5
  24. package/.agent-src/rules/ask-when-uncertain.md +56 -3
  25. package/.agent-src/rules/augment-portability.md +52 -1
  26. package/.agent-src/rules/augment-source-of-truth.md +10 -10
  27. package/.agent-src/rules/chat-history.md +171 -0
  28. package/.agent-src/rules/docker-commands.md +5 -7
  29. package/.agent-src/rules/docs-sync.md +13 -9
  30. package/.agent-src/rules/improve-before-implement.md +2 -0
  31. package/.agent-src/rules/onboarding-gate.md +94 -0
  32. package/.agent-src/rules/package-ci-checks.md +6 -5
  33. package/.agent-src/rules/roadmap-progress-sync.md +24 -13
  34. package/.agent-src/rules/size-enforcement.md +1 -1
  35. package/.agent-src/rules/skill-quality.md +1 -1
  36. package/.agent-src/rules/think-before-action.md +1 -0
  37. package/.agent-src/rules/user-interaction.md +53 -7
  38. package/.agent-src/scripts/update_roadmap_progress.py +57 -10
  39. package/.agent-src/skills/check-refs/SKILL.md +1 -1
  40. package/.agent-src/skills/command-routing/SKILL.md +1 -1
  41. package/.agent-src/skills/command-writing/SKILL.md +4 -3
  42. package/.agent-src/skills/file-editor/SKILL.md +2 -2
  43. package/.agent-src/skills/guideline-writing/SKILL.md +4 -3
  44. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +2 -2
  45. package/.agent-src/skills/lint-skills/SKILL.md +1 -1
  46. package/.agent-src/skills/roadmap-management/SKILL.md +13 -10
  47. package/.agent-src/skills/rtk-output-filtering/SKILL.md +20 -30
  48. package/.agent-src/skills/rule-writing/SKILL.md +5 -5
  49. package/.agent-src/skills/terragrunt/SKILL.md +0 -8
  50. package/.agent-src/skills/upstream-contribute/SKILL.md +5 -4
  51. package/.agent-src/templates/agent-settings.md +86 -34
  52. package/.agent-src/templates/github-workflows/roadmap-progress-check.yml +63 -0
  53. package/.agent-src/templates/hooks/pre-commit-roadmap-progress +60 -0
  54. package/.agent-src/templates/scripts/memory_lookup.py +382 -21
  55. package/.agent-src/templates/scripts/memory_status.py +110 -9
  56. package/.claude-plugin/marketplace.json +1 -1
  57. package/AGENTS.md +2 -2
  58. package/CHANGELOG.md +320 -0
  59. package/CONTRIBUTING.md +89 -40
  60. package/README.md +24 -3
  61. package/composer.json +5 -1
  62. package/config/agent-settings.template.yml +45 -6
  63. package/config/gitignore-block.txt +24 -0
  64. package/config/profiles/balanced.ini +5 -0
  65. package/config/profiles/full.ini +5 -0
  66. package/config/profiles/minimal.ini +5 -0
  67. package/docs/customization.md +30 -4
  68. package/docs/getting-started.md +53 -3
  69. package/docs/mcp.md +15 -4
  70. package/package.json +21 -2
  71. package/scripts/agent-config +230 -0
  72. package/scripts/chat_history.py +519 -0
  73. package/scripts/check_portability.py +151 -1
  74. package/scripts/install.py +55 -3
  75. package/scripts/install.sh +50 -21
  76. package/scripts/mcp_render.py +30 -16
  77. package/scripts/memory_lookup.py +143 -7
  78. package/scripts/memory_status.py +76 -14
  79. package/scripts/postinstall.sh +16 -0
  80. package/scripts/release.py +588 -0
  81. package/scripts/sync_agent_settings.py +211 -0
  82. package/scripts/sync_gitignore.py +226 -0
  83. package/templates/agent-config-wrapper.sh +47 -0
  84. package/.agent-src/commands/config-agent-settings.md +0 -126
  85. package/.agent-src/skills/eloquent/evals/last-run.json +0 -99
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ """Sync `.agent-settings.yml` against the template + profile.
3
+
4
+ Applies the section-aware merge rules documented in
5
+ `.agent-src.uncompressed/guidelines/agent-infra/layered-settings.md`:
6
+
7
+ - Template section order always wins — reorder keys to match.
8
+ - Existing user scalar values are preserved verbatim (as parsed).
9
+ - Missing keys land with their template / profile default.
10
+ - Template comments replace user comments in the same position.
11
+ - Unknown user keys (not in the template) are preserved in a trailing
12
+ `_user:` block so custom additions never get silently dropped.
13
+
14
+ Idempotent — writing a file that is already in sync is a no-op.
15
+
16
+ Usage:
17
+ python3 scripts/sync_agent_settings.py # write (default)
18
+ python3 scripts/sync_agent_settings.py --dry-run # show diff, no write
19
+ python3 scripts/sync_agent_settings.py --check # exit 2 on drift (for CI)
20
+ python3 scripts/sync_agent_settings.py --profile balanced # use a specific profile
21
+ python3 scripts/sync_agent_settings.py --path path/to/.agent-settings.yml
22
+
23
+ Exit codes:
24
+ 0 — already in sync, or changes applied (or --dry-run ran cleanly)
25
+ 2 — drift detected under --check, or invalid arguments / missing files
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import difflib
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
36
+ import install as _install # noqa: E402 — shares _yaml_scalar + _replace_template_value
37
+
38
+ try:
39
+ import yaml # type: ignore
40
+ except ImportError:
41
+ print("error: PyYAML not installed (pip install pyyaml)", file=sys.stderr)
42
+ sys.exit(2)
43
+
44
+ DEFAULT_SETTINGS = ".agent-settings.yml"
45
+ DEFAULT_TEMPLATE = Path(__file__).resolve().parent.parent / "config" / "agent-settings.template.yml"
46
+ DEFAULT_PROFILE_DIR = Path(__file__).resolve().parent.parent / "config" / "profiles"
47
+
48
+
49
+ def _flatten(data: dict, prefix: str = "") -> dict[str, object]:
50
+ """Flatten nested dicts to dotted keys — one level of nesting supported."""
51
+ out: dict[str, object] = {}
52
+ for key, value in data.items():
53
+ path = f"{prefix}{key}"
54
+ if isinstance(value, dict):
55
+ for sub_key, sub_val in value.items():
56
+ out[f"{path}.{sub_key}"] = sub_val
57
+ else:
58
+ out[path] = value
59
+ return out
60
+
61
+
62
+ def _as_scalar_text(value: object) -> str:
63
+ """Normalize a parsed YAML value to the raw string form expected by
64
+ `install._replace_template_value` — which re-quotes via `_yaml_scalar`
65
+ internally, so we must pass the *unquoted* payload here."""
66
+ if isinstance(value, bool):
67
+ return "true" if value else "false"
68
+ if isinstance(value, int):
69
+ return str(value)
70
+ if value is None:
71
+ return ""
72
+ return str(value)
73
+
74
+
75
+ def _template_keys(template_body: str) -> set[str]:
76
+ """Return the set of dotted keys declared by the rendered template."""
77
+ data = yaml.safe_load(template_body) or {}
78
+ if not isinstance(data, dict):
79
+ return set()
80
+ return set(_flatten(data).keys())
81
+
82
+
83
+ def _apply_user_values(template_body: str, user_flat: dict[str, object]) -> str:
84
+ """Overlay every known user value on the rendered template body."""
85
+ body = template_body
86
+ for dotted, value in user_flat.items():
87
+ body = _install._replace_template_value(body, dotted, _as_scalar_text(value))
88
+ return body
89
+
90
+
91
+ def _append_unknown(body: str, user_flat: dict[str, object], known: set[str]) -> str:
92
+ """Emit user keys that have no home in the template under `_user:`."""
93
+ unknown = sorted(k for k in user_flat if k not in known)
94
+ if not unknown:
95
+ return body
96
+ lines = [
97
+ "",
98
+ "# Unknown keys preserved by sync_agent_settings.py — review and move",
99
+ "# them into the template or drop them.",
100
+ "_user:",
101
+ ]
102
+ for key in unknown:
103
+ lines.append(f" {key}: {_install._yaml_scalar(_as_scalar_text(user_flat[key]))}")
104
+ suffix = "\n".join(lines) + "\n"
105
+ return body + (suffix if body.endswith("\n") else "\n" + suffix)
106
+
107
+
108
+ def render_target(template_body: str, user_data: dict) -> str:
109
+ """Return the desired `.agent-settings.yml` body for the given user data."""
110
+ user_flat = _flatten(user_data) if user_data else {}
111
+ known = _template_keys(template_body)
112
+ body = _apply_user_values(template_body, user_flat)
113
+ return _append_unknown(body, user_flat, known)
114
+
115
+
116
+ def load_profile(profile_dir: Path, profile: str) -> dict[str, str]:
117
+ profile_source = profile_dir / f"{profile}.ini"
118
+ if not profile_source.is_file():
119
+ raise FileNotFoundError(f"profile not found: {profile_source}")
120
+ return _install._parse_profile_ini(profile_source)
121
+
122
+
123
+ def load_template(path: Path, profile_values: dict[str, str]) -> str:
124
+ if not path.is_file():
125
+ raise FileNotFoundError(f"template not found: {path}")
126
+ return _install._render_template(path.read_text(encoding="utf-8"), profile_values)
127
+
128
+
129
+ def load_user(path: Path) -> dict:
130
+ if not path.is_file():
131
+ return {}
132
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
133
+ if not isinstance(data, dict):
134
+ return {}
135
+ return data
136
+
137
+
138
+ def render_diff(old_text: str, new_text: str, path: str) -> str:
139
+ return "".join(difflib.unified_diff(
140
+ old_text.splitlines(keepends=True),
141
+ new_text.splitlines(keepends=True),
142
+ fromfile=path, tofile=path, n=3,
143
+ ))
144
+
145
+
146
+
147
+ def main(argv: list[str] | None = None) -> int:
148
+ ap = argparse.ArgumentParser(description=__doc__)
149
+ ap.add_argument("--path", default=DEFAULT_SETTINGS,
150
+ help=f"target settings file (default: ./{DEFAULT_SETTINGS})")
151
+ ap.add_argument("--template", default=str(DEFAULT_TEMPLATE),
152
+ help="path to the settings template")
153
+ ap.add_argument("--profile", default=None,
154
+ help="cost_profile preset (minimal|balanced|full). "
155
+ "Default: inferred from target, else 'minimal'")
156
+ ap.add_argument("--profile-dir", default=str(DEFAULT_PROFILE_DIR),
157
+ help="directory containing profile .ini files")
158
+ ap.add_argument("--dry-run", action="store_true",
159
+ help="print diff; do not modify the file")
160
+ ap.add_argument("--check", action="store_true",
161
+ help="exit 2 if the target is out of sync (no writes)")
162
+ ap.add_argument("--quiet", action="store_true",
163
+ help="suppress summary on success")
164
+ args = ap.parse_args(argv)
165
+
166
+ target = Path(args.path)
167
+ template_path = Path(args.template)
168
+ profile_dir = Path(args.profile_dir)
169
+
170
+ try:
171
+ user_data = load_user(target)
172
+ profile = args.profile or str(user_data.get("cost_profile") or "minimal")
173
+ if profile not in _install.SUPPORTED_PROFILES:
174
+ print(f"error: unsupported profile {profile!r}", file=sys.stderr)
175
+ return 2
176
+ profile_values = load_profile(profile_dir, profile)
177
+ template_body = load_template(template_path, profile_values)
178
+ except FileNotFoundError as exc:
179
+ print(f"error: {exc}", file=sys.stderr)
180
+ return 2
181
+
182
+ new_text = render_target(template_body, user_data)
183
+ existing_text = target.read_text(encoding="utf-8") if target.is_file() else ""
184
+
185
+ if new_text == existing_text:
186
+ if not args.quiet:
187
+ print(f"✅ {target}: already in sync (profile={profile})")
188
+ return 0
189
+
190
+ if args.check:
191
+ diff = render_diff(existing_text, new_text, str(target))
192
+ sys.stdout.write(diff)
193
+ print(f"\n❌ {target}: drift detected (profile={profile})", file=sys.stderr)
194
+ return 2
195
+
196
+ if args.dry_run:
197
+ diff = render_diff(existing_text, new_text, str(target))
198
+ sys.stdout.write(diff)
199
+ if not args.quiet:
200
+ print(f"\n(dry-run) would update {target} (profile={profile})", file=sys.stderr)
201
+ return 0
202
+
203
+ target.parent.mkdir(parents=True, exist_ok=True)
204
+ target.write_text(new_text, encoding="utf-8")
205
+ if not args.quiet:
206
+ print(f"✅ {target}: updated (profile={profile})")
207
+ return 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main())
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ """Sync the `event4u/agent-config` block in a project's `.gitignore`.
3
+
4
+ Reads the canonical block body from `config/gitignore-block.txt` and
5
+ ensures every managed entry is present in `.gitignore` between the
6
+ START and END markers:
7
+
8
+ # event4u/agent-config
9
+ ...managed entries...
10
+ # event4u/agent-config — END
11
+
12
+ Idempotent. Append-only by default (user-added lines inside the block
13
+ are preserved). Call with `--replace` for a destructive full rewrite.
14
+
15
+ Usage:
16
+ python3 scripts/sync_gitignore.py [--path .gitignore] [--template config/gitignore-block.txt]
17
+ [--dry-run] [--replace] [--quiet]
18
+
19
+ Exit codes:
20
+ 0 — no changes needed (or --dry-run ran successfully)
21
+ 0 — changes applied
22
+ 2 — invalid arguments / template missing
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import difflib
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ SECTION_HEADER = "# event4u/agent-config"
33
+ SECTION_FOOTER = "# event4u/agent-config — END"
34
+ DEFAULT_GITIGNORE = ".gitignore"
35
+ DEFAULT_TEMPLATE = Path(__file__).resolve().parent.parent / "config" / "gitignore-block.txt"
36
+
37
+
38
+ def _strip(ln: str) -> str:
39
+ return ln.rstrip("\n").rstrip()
40
+
41
+
42
+ def _is_entry(ln: str) -> bool:
43
+ """Non-empty, non-comment line = a path/pattern entry."""
44
+ s = _strip(ln).lstrip()
45
+ return bool(s) and not s.startswith("#")
46
+
47
+
48
+ def load_template(path: Path) -> list[str]:
49
+ if not path.is_file():
50
+ raise FileNotFoundError(f"template not found: {path}")
51
+ text = path.read_text(encoding="utf-8")
52
+ # Keep trailing newlines stripped; we splice explicit newlines.
53
+ return [_strip(ln) for ln in text.splitlines()]
54
+
55
+
56
+ def find_block(lines: list[str]) -> tuple[int, int] | None:
57
+ """Locate the managed block; return (start_idx, end_idx_exclusive).
58
+
59
+ `start_idx` points at the SECTION_HEADER line.
60
+ `end_idx_exclusive` points one past the last line of the block.
61
+ Honors explicit SECTION_FOOTER when present; otherwise treats the
62
+ block as extending to EOF or to the next non-managed section.
63
+ Returns None if no header is found.
64
+ """
65
+ for i, ln in enumerate(lines):
66
+ if _strip(ln) == SECTION_HEADER:
67
+ start = i
68
+ # Explicit footer?
69
+ for j in range(i + 1, len(lines)):
70
+ if _strip(lines[j]) == SECTION_FOOTER:
71
+ return (start, j + 1)
72
+ # Legacy: extend to EOF or next non-managed section break.
73
+ end = len(lines)
74
+ for j in range(i + 1, len(lines)):
75
+ s = _strip(lines[j]).lstrip()
76
+ if (s.startswith("#")
77
+ and not s.startswith("# Agent config")
78
+ and s != SECTION_HEADER):
79
+ end = j
80
+ while end > i + 1 and _strip(lines[end - 1]) == "":
81
+ end -= 1
82
+ break
83
+ return (start, end)
84
+ return None
85
+
86
+
87
+ def block_entries(block_lines: list[str]) -> list[str]:
88
+ """Return entries (paths/patterns) present in the given block."""
89
+ return [_strip(ln).lstrip() for ln in block_lines if _is_entry(ln)]
90
+
91
+
92
+ def template_entries(template_lines: list[str]) -> list[str]:
93
+ return [ln.lstrip() for ln in template_lines if _is_entry(ln)]
94
+
95
+
96
+ def build_fresh_block(template_lines: list[str]) -> list[str]:
97
+ """Return a fresh, fully-managed block with START + body + END."""
98
+ return [SECTION_HEADER, *template_lines, SECTION_FOOTER]
99
+
100
+
101
+ def sync_block(existing_lines: list[str],
102
+ template_lines: list[str],
103
+ *, replace: bool = False) -> tuple[list[str], list[str]]:
104
+ """Return (new_lines, added_entries).
105
+
106
+ - If block missing: append fresh block (preceded by a blank line if
107
+ the file's last line is not already empty).
108
+ - If block present and replace=True: rewrite block in full.
109
+ - If block present and replace=False: append any missing managed
110
+ entries before the END marker (adding END if absent). User-added
111
+ lines inside the block are preserved.
112
+ """
113
+ loc = find_block(existing_lines)
114
+ fresh = build_fresh_block(template_lines)
115
+
116
+ # Missing block → append with leading blank if needed.
117
+ if loc is None:
118
+ new = list(existing_lines)
119
+ if new and _strip(new[-1]) != "":
120
+ new.append("")
121
+ new.extend(fresh)
122
+ return new, template_entries(template_lines)
123
+
124
+ start, end = loc
125
+ head = existing_lines[:start]
126
+ block = existing_lines[start:end]
127
+ tail = existing_lines[end:]
128
+
129
+ if replace:
130
+ added = [e for e in template_entries(template_lines)
131
+ if e not in block_entries(block)]
132
+ return head + fresh + tail, added
133
+
134
+ # Append-only mode.
135
+ existing_entries = set(block_entries(block))
136
+ missing = [e for e in template_entries(template_lines)
137
+ if e not in existing_entries]
138
+ if not missing:
139
+ return existing_lines, []
140
+
141
+ # Ensure block ends with SECTION_FOOTER; insert missing entries
142
+ # right before it.
143
+ if block and _strip(block[-1]) == SECTION_FOOTER:
144
+ insert_at = len(block) - 1
145
+ else:
146
+ block = [*block, SECTION_FOOTER]
147
+ insert_at = len(block) - 1
148
+ new_block = block[:insert_at] + missing + block[insert_at:]
149
+ return head + new_block + tail, missing
150
+
151
+
152
+ def format_file(lines: list[str]) -> str:
153
+ """Join lines with newlines and enforce exactly one trailing newline."""
154
+ text = "\n".join(lines)
155
+ return text.rstrip("\n") + "\n"
156
+
157
+
158
+ def render_diff(old_text: str, new_text: str, path: str) -> str:
159
+ return "".join(difflib.unified_diff(
160
+ old_text.splitlines(keepends=True),
161
+ new_text.splitlines(keepends=True),
162
+ fromfile=path, tofile=path, n=3,
163
+ ))
164
+
165
+
166
+ def main(argv: list[str] | None = None) -> int:
167
+ ap = argparse.ArgumentParser(description=__doc__)
168
+ ap.add_argument("--path", default=DEFAULT_GITIGNORE,
169
+ help="target .gitignore (default: ./.gitignore)")
170
+ ap.add_argument("--template", default=str(DEFAULT_TEMPLATE),
171
+ help="path to the managed-block template")
172
+ ap.add_argument("--dry-run", action="store_true",
173
+ help="print diff; do not modify the file")
174
+ ap.add_argument("--replace", action="store_true",
175
+ help="rewrite the block in full (discards user-added "
176
+ "lines inside the block)")
177
+ ap.add_argument("--quiet", action="store_true",
178
+ help="suppress summary on success")
179
+ args = ap.parse_args(argv)
180
+
181
+ template_path = Path(args.template)
182
+ try:
183
+ template_lines = load_template(template_path)
184
+ except FileNotFoundError as exc:
185
+ print(f"error: {exc}", file=sys.stderr)
186
+ return 2
187
+
188
+ target = Path(args.path)
189
+ if target.is_file():
190
+ existing_text = target.read_text(encoding="utf-8")
191
+ existing_lines = [_strip(ln) for ln in existing_text.splitlines()]
192
+ else:
193
+ existing_text = ""
194
+ existing_lines = []
195
+
196
+ new_lines, added = sync_block(
197
+ existing_lines, template_lines, replace=args.replace,
198
+ )
199
+ new_text = format_file(new_lines)
200
+
201
+ if new_text == existing_text:
202
+ if not args.quiet:
203
+ print(f"✅ {target}: block already in sync "
204
+ f"({len(template_entries(template_lines))} entries)")
205
+ return 0
206
+
207
+ if args.dry_run:
208
+ diff = render_diff(existing_text, new_text, str(target))
209
+ sys.stdout.write(diff)
210
+ if not args.quiet:
211
+ print(f"\n(dry-run) would add {len(added)} entr"
212
+ f"{'y' if len(added) == 1 else 'ies'} to {target}",
213
+ file=sys.stderr)
214
+ return 0
215
+
216
+ target.parent.mkdir(parents=True, exist_ok=True)
217
+ target.write_text(new_text, encoding="utf-8")
218
+ if not args.quiet:
219
+ action = "replaced" if args.replace else "updated"
220
+ print(f"✅ {target}: {action} block "
221
+ f"({len(added)} entr{'y' if len(added) == 1 else 'ies'} added)")
222
+ return 0
223
+
224
+
225
+ if __name__ == "__main__":
226
+ sys.exit(main())
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # agent-config — thin wrapper for the event4u/agent-config CLI.
3
+ #
4
+ # Auto-generated by the package installer. Delegates to the master CLI
5
+ # shipped inside the installed package (node_modules or vendor). Do NOT
6
+ # edit — this file is gitignored and regenerated on every install.
7
+ #
8
+ # Usage:
9
+ # ./agent-config help
10
+ # ./agent-config mcp:render
11
+ # ./agent-config roadmap:progress
12
+ #
13
+ # See: https://github.com/event4u-app/agent-config
14
+
15
+ set -euo pipefail
16
+
17
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+
19
+ locate_master() {
20
+ local candidates=(
21
+ "$SELF_DIR/node_modules/@event4u/agent-config/scripts/agent-config"
22
+ "$SELF_DIR/vendor/event4u/agent-config/scripts/agent-config"
23
+ )
24
+ local c
25
+ for c in "${candidates[@]}"; do
26
+ if [[ -x "$c" ]]; then
27
+ printf '%s' "$c"
28
+ return 0
29
+ fi
30
+ done
31
+ return 1
32
+ }
33
+
34
+ if ! MASTER="$(locate_master)"; then
35
+ cat >&2 <<EOF
36
+ ❌ agent-config: master CLI not found.
37
+ Tried:
38
+ - node_modules/@event4u/agent-config/scripts/agent-config
39
+ - vendor/event4u/agent-config/scripts/agent-config
40
+ Reinstall the package, then retry:
41
+ npm install # for Node projects
42
+ composer install # for PHP projects
43
+ EOF
44
+ exit 127
45
+ fi
46
+
47
+ exec "$MASTER" "$@"
@@ -1,126 +0,0 @@
1
- ---
2
- name: config-agent-settings
3
- description: Create or update .agent-settings.yml — syncs with template, preserves existing values, adds new defaults
4
- skills: [file-editor]
5
- disable-model-invocation: true
6
- ---
7
-
8
- # /config-agent-settings
9
-
10
- Creates or updates `.agent-settings.yml` in the project root by syncing with the
11
- template in `.augment/templates/agent-settings.md`. Both files are YAML with the
12
- same section layout (`personal`, `project`, `github`, `eloquent`, `pipelines`,
13
- `subagents`).
14
-
15
- If a **legacy** flat `.agent-settings` (key=value) is present, `scripts/install`
16
- migrates it to `.agent-settings.yml` automatically (with backup). This command
17
- assumes migration has already happened.
18
-
19
- ## Steps
20
-
21
- ### 1. Read the template
22
-
23
- Read `.augment/templates/agent-settings.md` and extract the YAML block (between
24
- the ` ```yaml ` markers). Parse it into a nested mapping preserving section
25
- order and comments.
26
-
27
- ### 2. Read existing settings (if any)
28
-
29
- Read `.agent-settings.yml` from the project root. If a legacy `.agent-settings`
30
- (key=value, no `.yml`) is present, stop and tell the user to run
31
- `scripts/install` first — do not re-implement the migration here.
32
-
33
- ### 3. Merge settings (section-aware)
34
-
35
- For each section in the template (in template order):
36
-
37
- - Keep the section header and its comments verbatim from the template.
38
- - For each key under the section:
39
- - **Key exists in user's file** → use the user's current value.
40
- - **Key missing** → use the template default.
41
- - **Unknown sections/keys** the user has added → preserve at the end of the
42
- section (or in a trailing `_user:` block if no matching section exists).
43
-
44
- Invariants:
45
- - Template section **order** always wins.
46
- - Existing scalar values are **never overwritten**.
47
- - New keys added to the template land with their default value.
48
- - Comments from the template always replace user comments in the same position
49
- (comments are documentation, not user data).
50
-
51
- ### 4. Write the file
52
-
53
- Write the merged YAML to `.agent-settings.yml` with 2-space indentation and no
54
- trailing whitespace.
55
-
56
- ### 5. Show diff
57
-
58
- If the file already existed, show what changed in YAML dotted-key notation:
59
-
60
- ```
61
- ✅ .agent-settings.yml updated!
62
-
63
- Added:
64
- + project.pr_comment_bot_icon: true (new setting, default applied)
65
-
66
- Unchanged:
67
- personal.ide: phpstorm
68
- personal.open_edited_files: true
69
- ```
70
-
71
- If the file was created fresh:
72
-
73
- ```
74
- ✅ .agent-settings.yml created!
75
-
76
- Settings (all defaults applied):
77
- personal.ide: ""
78
- personal.open_edited_files: false
79
- project.pr_comment_bot_icon: false
80
-
81
- Run /config-agent-settings again to change values, or edit .agent-settings.yml directly.
82
- ```
83
-
84
- ### 6. Interactive setup for empty values
85
-
86
- If any required setting has an empty value (e.g. `personal.ide: ""`), offer to
87
- configure it:
88
-
89
- ```
90
- > `personal.ide` is empty. Which IDE do you use?
91
- >
92
- > 1. VS Code (code)
93
- > 2. PhpStorm (phpstorm)
94
- > 3. Cursor (cursor)
95
- > 4. Skip — I'll set it later
96
- ```
97
-
98
- For `personal.ide`, also try auto-detection first:
99
-
100
- ```bash
101
- ps aux | grep -iE '(Visual Studio Code|Code Helper|phpstorm|cursor)' | grep -v grep
102
- ```
103
-
104
- - If detected → confirm with the user before setting.
105
- - If not detected → ask.
106
-
107
- ### 7. Verify IDE command
108
-
109
- If `personal.ide` was set and `personal.open_edited_files: true`, verify the CLI
110
- command works:
111
-
112
- ```bash
113
- {ide} --version 2>/dev/null
114
- ```
115
-
116
- Warn if it fails and suggest how to install the CLI.
117
-
118
- ## Rules
119
-
120
- - **Do NOT commit `.agent-settings.yml`** — it's in `.gitignore`.
121
- - **Never overwrite existing values** — only add missing keys with defaults.
122
- - **Always use template section order** — reorder keys to match the template.
123
- - **Template is the source of truth** for which keys exist and their defaults.
124
- - **Legacy migration is `scripts/install`'s job**, not this command's. If a flat
125
- `.agent-settings` file is still present, ask the user to run `scripts/install`
126
- and stop.