@event4u/agent-config 2.2.2 → 2.3.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 (38) hide show
  1. package/.agent-src/rules/external-reference-deep-dive.md +69 -0
  2. package/.agent-src/templates/copilot-instructions.md +7 -0
  3. package/.claude-plugin/marketplace.json +27 -1
  4. package/CHANGELOG.md +49 -0
  5. package/README.md +1 -8
  6. package/docs/architecture.md +1 -1
  7. package/docs/contracts/installed-tools-lockfile.md +138 -0
  8. package/docs/development.md +37 -0
  9. package/docs/getting-started.md +1 -1
  10. package/docs/installation.md +14 -0
  11. package/docs/setup/per-ide/antigravity.md +63 -0
  12. package/docs/setup/per-ide/augment.md +77 -0
  13. package/docs/setup/per-ide/codebuddy.md +63 -0
  14. package/docs/setup/per-ide/continue.md +68 -0
  15. package/docs/setup/per-ide/droid.md +65 -0
  16. package/docs/setup/per-ide/jetbrains.md +76 -0
  17. package/docs/setup/per-ide/kilocode.md +66 -0
  18. package/docs/setup/per-ide/kiro.md +72 -0
  19. package/docs/setup/per-ide/opencode.md +62 -0
  20. package/docs/setup/per-ide/qoder.md +63 -0
  21. package/docs/setup/per-ide/roocode.md +68 -0
  22. package/docs/setup/per-ide/trae.md +63 -0
  23. package/docs/setup/per-ide/warp.md +63 -0
  24. package/docs/setup/per-ide/zed.md +73 -0
  25. package/package.json +1 -1
  26. package/scripts/_cli/cmd_doctor.py +351 -0
  27. package/scripts/_cli/cmd_prune.py +317 -0
  28. package/scripts/_cli/cmd_uninstall.py +465 -0
  29. package/scripts/_cli/cmd_update.py +26 -3
  30. package/scripts/_cli/cmd_versions.py +147 -0
  31. package/scripts/_lib/fs_atomic.py +116 -0
  32. package/scripts/_lib/installed_tools.py +188 -44
  33. package/scripts/_lib/json_pointers.py +260 -0
  34. package/scripts/agent-config +69 -0
  35. package/scripts/compress.py +78 -15
  36. package/scripts/install +8 -0
  37. package/scripts/install-hooks.sh +54 -1
  38. package/scripts/install.py +1053 -51
@@ -0,0 +1,73 @@
1
+ # Zed Setup
2
+
3
+ Zed (<https://zed.dev>) reads `.rules` at the project root as
4
+ system-level instructions. The bridge drops a marker under `.zed/`
5
+ and documents the wiring; Zed itself does not auto-discover
6
+ `.zed/agent-config.md`.
7
+
8
+ ## Prerequisites
9
+
10
+ - Zed editor: <https://zed.dev/download>.
11
+ - Node.js ≥ 18 for the install entrypoints.
12
+
13
+ ## Install
14
+
15
+ Project scope (default):
16
+
17
+ ```bash
18
+ npx @event4u/agent-config init --tools=zed
19
+ ```
20
+
21
+ Global scope (cross-project, deploys the universal skill bundle to
22
+ `~/.config/zed/`):
23
+
24
+ ```bash
25
+ npx @event4u/agent-config init --tools=zed --global
26
+ ```
27
+
28
+ Populates (project):
29
+
30
+ - `.zed/agent-config.md` — informational marker
31
+ - `AGENTS.md` — canonical agent self-orientation
32
+ - `.agent-settings.yml` — per-project knobs
33
+
34
+ ## How to use
35
+
36
+ - After install, append the following line to `.rules` at the
37
+ project root (create the file if missing):
38
+
39
+ ```
40
+ @.augment/AGENTS.md
41
+ ```
42
+
43
+ Zed reads `.rules` on session start. The `@`-prefix tells Zed to
44
+ inline the referenced file as part of the system prompt.
45
+ - Zed's Assistant panel honors the inlined rules across all
46
+ conversation modes.
47
+ - Slash commands and skills live under `.augment/commands/` and
48
+ `.augment/skills/`. Zed does not register them natively — invoke
49
+ them by name in chat (e.g. *"run the create-pr command"*).
50
+
51
+ ## Verification
52
+
53
+ ```bash
54
+ test -f .zed/agent-config.md
55
+ test -f AGENTS.md
56
+ grep -q "@.augment/AGENTS.md" .rules
57
+ ```
58
+
59
+ In Zed: open the Assistant panel and ask *"What is this repo?"* —
60
+ the answer should cite the AGENTS.md emergency triage block.
61
+
62
+ ## Troubleshooting
63
+
64
+ | Symptom | Fix |
65
+ |---|---|
66
+ | Assistant ignores rules | Confirm `.rules` exists at project root and contains `@.augment/AGENTS.md`. |
67
+ | `.rules` not auto-loaded | Restart Zed; `.rules` is read on session start. |
68
+ | Inlined skill missing | Replace `@.augment/AGENTS.md` with the specific skill path you need to inline. |
69
+
70
+ ## Cross-references
71
+
72
+ - [`AGENTS.md`](../../../AGENTS.md) — canonical agent self-orientation.
73
+ - [`docs/installation.md`](../../installation.md) — install matrix index.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Shared agent configuration \u2014 skills, rules, commands, guidelines, and templates for AI coding tools",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,351 @@
1
+ """``agent-config doctor`` — manifest ↔ filesystem drift report.
2
+
3
+ Phase 4 of road-to-multi-package-coexistence. Read-only sibling to
4
+ ``prune``/``validate``: walks the project manifest and the on-disk
5
+ deploy roots, then produces four categories:
6
+
7
+ * ``missing`` — manifest entry has a ``path`` that is **not** on disk.
8
+ * ``modified`` — manifest entry records a ``sha256`` that does not
9
+ match the current bytes on disk.
10
+ * ``foreign`` — file present under one of the ``deploy_roots`` that
11
+ no manifest entry claims (potential neighbour-tool drift).
12
+ * ``tag-drift`` — manifest-claimed ``.md`` file carries a frontmatter
13
+ ``package:`` value that disagrees with this package's identifier
14
+ (P5.2). Hand-edited tags or accidental cross-package writes show up
15
+ here; files without frontmatter are skipped (P5.1 contract).
16
+
17
+ Exit codes: ``0`` (clean) · ``1`` (drift) · ``2`` (error such as
18
+ "manifest missing"). Both human and ``--json`` output emit the four
19
+ category lists. Every entry carries a one-line ``fix`` hint (P4.3).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import hashlib
25
+ import json
26
+ import re
27
+ import sys
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from scripts._lib import installed_tools
32
+
33
+
34
+ class _Sentinel:
35
+ """Tiny stand-in for a private sentinel value type."""
36
+
37
+ __slots__ = ()
38
+
39
+
40
+ #: Returned by :func:`_read_inline_package_tag` when the file is out
41
+ #: of scope for tag-drift detection (no ``.md`` suffix, unreadable, or
42
+ #: no leading frontmatter block).
43
+ NO_FRONTMATTER = _Sentinel()
44
+
45
+
46
+ def _resolve_project_root(arg: str | None) -> Path:
47
+ if arg:
48
+ return Path(arg).expanduser().resolve()
49
+ return Path.cwd().resolve()
50
+
51
+
52
+ def _resolve_path(project_root: Path, raw: str) -> Path:
53
+ p = Path(raw).expanduser()
54
+ if not p.is_absolute():
55
+ p = project_root / p
56
+ return p
57
+
58
+
59
+ def _sha256(path: Path) -> str | None:
60
+ try:
61
+ return hashlib.sha256(path.read_bytes()).hexdigest()
62
+ except OSError:
63
+ return None
64
+
65
+
66
+ #: Inline-tag identifier this package writes into deployed Markdown
67
+ #: frontmatter (P5.1). Kept in sync with ``install.PACKAGE_TAG_ID``;
68
+ #: duplicated here to keep ``cmd_doctor`` import-light (no pull on the
69
+ #: installer module from the CLI).
70
+ PACKAGE_TAG_ID = "event4u/agent-config"
71
+
72
+ _FRONTMATTER_PACKAGE_RE = re.compile(
73
+ r"^package:\s*(.+?)\s*$", re.MULTILINE,
74
+ )
75
+
76
+
77
+ def _read_inline_package_tag(path: Path) -> str | None | _Sentinel:
78
+ """Extract the inline ``package:`` value from a Markdown frontmatter.
79
+
80
+ Returns ``NO_FRONTMATTER`` when ``path`` is not a Markdown file or
81
+ has no leading ``---`` block (P5.1: those files are out of scope).
82
+ Returns ``None`` when frontmatter is present but lacks a
83
+ ``package:`` key. Returns the string value otherwise.
84
+ """
85
+ if path.suffix != ".md":
86
+ return NO_FRONTMATTER
87
+ try:
88
+ text = path.read_text(encoding="utf-8")
89
+ except OSError:
90
+ return NO_FRONTMATTER
91
+ if not (text.startswith("---\n") or text.startswith("---\r\n")):
92
+ return NO_FRONTMATTER
93
+ lines = text.splitlines()
94
+ close_idx: int | None = None
95
+ for i in range(1, len(lines)):
96
+ if lines[i].rstrip() == "---":
97
+ close_idx = i
98
+ break
99
+ if close_idx is None:
100
+ return NO_FRONTMATTER
101
+ block = "\n".join(lines[1:close_idx])
102
+ m = _FRONTMATTER_PACKAGE_RE.search(block)
103
+ if not m:
104
+ return None
105
+ return m.group(1).strip().strip("'\"")
106
+
107
+
108
+ def _fix_hint(category: str, kind: str | None) -> str:
109
+ """Return a one-line remediation hint for the surfaced item."""
110
+ if category == "missing":
111
+ return "run `./agent-config sync` to re-install"
112
+ if category == "modified":
113
+ return "commit the local change, or re-install with --force"
114
+ if category == "foreign":
115
+ return (
116
+ "identify owning tool, or run `./agent-config prune` "
117
+ "if confirmed orphan"
118
+ )
119
+ if category == "tag-drift":
120
+ return (
121
+ "re-install with --force to restore the inline tag, "
122
+ "or remove the file if it is no longer ours"
123
+ )
124
+ return ""
125
+
126
+
127
+ def _collect_manifest_entries(
128
+ project_root: Path, manifest: dict[str, Any],
129
+ ) -> tuple[
130
+ list[tuple[str, Path, str, str | None]], # (tool, abs_path, kind, sha)
131
+ set[Path], # resolved-known set
132
+ ]:
133
+ """Flatten v2 ``tools[].files[]`` into per-file records.
134
+
135
+ Returns the records list and a set of resolved absolute paths so
136
+ the foreign-file scan can skip anything the manifest claims.
137
+ """
138
+ records: list[tuple[str, Path, str, str | None]] = []
139
+ known: set[Path] = set()
140
+ for tool in manifest.get("tools") or []:
141
+ if tool.get("scope") != "project":
142
+ continue
143
+ tool_id = str(tool.get("name", ""))
144
+ for entry in tool.get("files") or []:
145
+ raw = entry.get("path") or ""
146
+ if not raw:
147
+ continue
148
+ kind = entry.get("kind") or ""
149
+ target = _resolve_path(project_root, raw)
150
+ try:
151
+ resolved = target.resolve()
152
+ except OSError:
153
+ resolved = target
154
+ records.append((tool_id, target, kind, entry.get("sha256")))
155
+ known.add(resolved)
156
+ return records, known
157
+
158
+
159
+ def _scan_foreign(
160
+ project_root: Path,
161
+ manifest: dict[str, Any],
162
+ known: set[Path],
163
+ ) -> list[Path]:
164
+ """Walk every declared deploy root and surface unclaimed files.
165
+
166
+ Only ``regular files`` under ``deploy_roots`` count; directories and
167
+ symlinks are followed but the bookkeeping is on the resolved final
168
+ path so a manifest claim via either path silences both surfaces.
169
+ Falls back to :data:`installed_tools.DEFAULT_DEPLOY_ROOTS` when the
170
+ manifest lacks an explicit ``deploy_roots`` list.
171
+ """
172
+ roots = manifest.get("deploy_roots") or list(
173
+ installed_tools.DEFAULT_DEPLOY_ROOTS,
174
+ )
175
+ foreign: list[Path] = []
176
+ seen: set[Path] = set()
177
+ for root_rel in roots:
178
+ root = _resolve_path(project_root, str(root_rel))
179
+ if not root.exists() or not root.is_dir():
180
+ continue
181
+ for child in root.rglob("*"):
182
+ if not child.is_file():
183
+ continue
184
+ try:
185
+ resolved = child.resolve()
186
+ except OSError:
187
+ resolved = child
188
+ if resolved in known or resolved in seen:
189
+ continue
190
+ seen.add(resolved)
191
+ foreign.append(child)
192
+ foreign.sort()
193
+ return foreign
194
+
195
+
196
+ def _classify(
197
+ records: list[tuple[str, Path, str, str | None]],
198
+ ) -> tuple[
199
+ list[dict[str, Any]],
200
+ list[dict[str, Any]],
201
+ list[dict[str, Any]],
202
+ ]:
203
+ """Split manifest records into missing / modified / tag-drift lists.
204
+
205
+ Tag-drift inspection (P5.2) is restricted to manifest entries that
206
+ point at a present ``.md`` file with a frontmatter block. A file
207
+ that has frontmatter but whose ``package:`` value disagrees with
208
+ :data:`PACKAGE_TAG_ID` — or that has dropped the key entirely —
209
+ surfaces here. Files without frontmatter are silently ignored per
210
+ the P5.1 contract (we never synthesise frontmatter).
211
+ """
212
+ missing: list[dict[str, Any]] = []
213
+ modified: list[dict[str, Any]] = []
214
+ tag_drift: list[dict[str, Any]] = []
215
+ for tool_id, target, kind, expected in records:
216
+ if not target.exists():
217
+ missing.append({
218
+ "tool": tool_id, "path": str(target), "kind": kind,
219
+ "fix": _fix_hint("missing", kind),
220
+ })
221
+ continue
222
+ tag = _read_inline_package_tag(target)
223
+ if not isinstance(tag, _Sentinel) and tag != PACKAGE_TAG_ID:
224
+ tag_drift.append({
225
+ "tool": tool_id, "path": str(target), "kind": kind,
226
+ "expected": PACKAGE_TAG_ID,
227
+ "found": "" if tag is None else tag,
228
+ "fix": _fix_hint("tag-drift", kind),
229
+ })
230
+ if expected is None:
231
+ continue
232
+ actual = _sha256(target)
233
+ if actual is None or actual == expected:
234
+ continue
235
+ modified.append({
236
+ "tool": tool_id, "path": str(target), "kind": kind,
237
+ "fix": _fix_hint("modified", kind),
238
+ })
239
+ return missing, modified, tag_drift
240
+
241
+
242
+ def _foreign_records(
243
+ project_root: Path, foreign: list[Path],
244
+ ) -> list[dict[str, Any]]:
245
+ out: list[dict[str, Any]] = []
246
+ for p in foreign:
247
+ try:
248
+ rel = p.relative_to(project_root)
249
+ path_str = str(rel)
250
+ except ValueError:
251
+ path_str = str(p)
252
+ out.append({
253
+ "tool": "",
254
+ "path": path_str,
255
+ "kind": "deployed",
256
+ "fix": _fix_hint("foreign", "deployed"),
257
+ })
258
+ return out
259
+
260
+
261
+ def _emit_json(
262
+ project_root: Path,
263
+ missing: list[dict[str, Any]],
264
+ modified: list[dict[str, Any]],
265
+ foreign: list[dict[str, Any]],
266
+ tag_drift: list[dict[str, Any]],
267
+ ) -> None:
268
+ print(json.dumps({
269
+ "project_root": str(project_root),
270
+ "missing": missing,
271
+ "modified": modified,
272
+ "foreign": foreign,
273
+ "tag_drift": tag_drift,
274
+ }, indent=2))
275
+
276
+
277
+ def _emit_text(
278
+ project_root: Path,
279
+ missing: list[dict[str, Any]],
280
+ modified: list[dict[str, Any]],
281
+ foreign: list[dict[str, Any]],
282
+ tag_drift: list[dict[str, Any]],
283
+ ) -> None:
284
+ total = len(missing) + len(modified) + len(foreign) + len(tag_drift)
285
+ if total == 0:
286
+ print(f"✅ doctor: manifest matches filesystem under {project_root}")
287
+ return
288
+ print(f"⚠️ doctor: {total} drift item(s) under {project_root}")
289
+ for label, items in (
290
+ ("missing", missing),
291
+ ("modified", modified),
292
+ ("foreign", foreign),
293
+ ("tag-drift", tag_drift),
294
+ ):
295
+ if not items:
296
+ continue
297
+ print(f"\n {label} ({len(items)}):")
298
+ for it in items:
299
+ tool = it["tool"] or "?"
300
+ print(f" · [{tool}] {it['path']}")
301
+ if label == "tag-drift":
302
+ found = it.get("found") or "(missing)"
303
+ expected = it.get("expected", PACKAGE_TAG_ID)
304
+ print(f" expected: {expected}")
305
+ print(f" found: {found}")
306
+ print(f" fix: {it['fix']}")
307
+
308
+
309
+ def _parse(argv: list[str]) -> argparse.Namespace:
310
+ parser = argparse.ArgumentParser(
311
+ prog="agent-config doctor",
312
+ description=(
313
+ "Read-only manifest ↔ filesystem drift report. Surfaces "
314
+ "missing, modified, foreign, and tag-drift files."
315
+ ),
316
+ )
317
+ parser.add_argument("--project", default=None,
318
+ help="project root (default: cwd)")
319
+ parser.add_argument("--json", action="store_true",
320
+ help="emit a JSON report instead of human text")
321
+ return parser.parse_args(argv)
322
+
323
+
324
+ def main(argv: list[str] | None = None) -> int:
325
+ opts = _parse(list(argv) if argv is not None else sys.argv[1:])
326
+ project_root = _resolve_project_root(opts.project)
327
+ manifest_pth = installed_tools.manifest_path(project_root)
328
+ manifest = installed_tools.read_manifest(manifest_pth)
329
+ if manifest is None:
330
+ print(f"❌ doctor: no project lockfile at {manifest_pth}",
331
+ file=sys.stderr)
332
+ print(" run `./agent-config init` to create one",
333
+ file=sys.stderr)
334
+ return 2
335
+
336
+ records, known = _collect_manifest_entries(project_root, manifest)
337
+ missing, modified, tag_drift = _classify(records)
338
+ foreign = _foreign_records(
339
+ project_root, _scan_foreign(project_root, manifest, known),
340
+ )
341
+
342
+ if opts.json:
343
+ _emit_json(project_root, missing, modified, foreign, tag_drift)
344
+ else:
345
+ _emit_text(project_root, missing, modified, foreign, tag_drift)
346
+
347
+ return 1 if (missing or modified or foreign or tag_drift) else 0
348
+
349
+
350
+ if __name__ == "__main__": # pragma: no cover
351
+ raise SystemExit(main())