@event4u/agent-config 2.2.2 → 2.4.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 +14 -9
- package/.agent-src/rules/external-reference-deep-dive.md +69 -0
- package/.agent-src/skills/ai-council/SKILL.md +5 -3
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +4 -3
- package/.agent-src/templates/copilot-instructions.md +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +29 -7
- package/.agent-src/templates/scripts/work_engine/_lib/user_global_paths.py +249 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +8 -5
- package/.claude-plugin/marketplace.json +27 -1
- package/CHANGELOG.md +79 -0
- package/README.md +1 -8
- package/config/agent-settings.template.yml +5 -3
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -3
- package/docs/contracts/installed-tools-lockfile.md +142 -0
- package/docs/customization.md +23 -17
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +6 -0
- package/docs/decisions/ADR-009-event4u-namespace.md +188 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/development.md +37 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +6 -4
- package/docs/installation.md +17 -2
- package/docs/migration/v1-to-v2.md +45 -0
- package/docs/setup/per-ide/antigravity.md +63 -0
- package/docs/setup/per-ide/augment.md +77 -0
- package/docs/setup/per-ide/claude-desktop.md +107 -65
- package/docs/setup/per-ide/codebuddy.md +63 -0
- package/docs/setup/per-ide/continue.md +68 -0
- package/docs/setup/per-ide/droid.md +65 -0
- package/docs/setup/per-ide/jetbrains.md +76 -0
- package/docs/setup/per-ide/kilocode.md +66 -0
- package/docs/setup/per-ide/kiro.md +72 -0
- package/docs/setup/per-ide/opencode.md +62 -0
- package/docs/setup/per-ide/qoder.md +63 -0
- package/docs/setup/per-ide/roocode.md +68 -0
- package/docs/setup/per-ide/trae.md +63 -0
- package/docs/setup/per-ide/warp.md +63 -0
- package/docs/setup/per-ide/zed.md +73 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +351 -0
- package/scripts/_cli/cmd_prune.py +317 -0
- package/scripts/_cli/cmd_uninstall.py +465 -0
- package/scripts/_cli/cmd_update.py +30 -4
- package/scripts/_cli/cmd_versions.py +147 -0
- package/scripts/_lib/agent_settings.py +29 -7
- package/scripts/_lib/agents_overlay.py +15 -4
- package/scripts/_lib/claude_desktop_bundler.py +150 -0
- package/scripts/_lib/fs_atomic.py +116 -0
- package/scripts/_lib/installed_lock.py +37 -4
- package/scripts/_lib/installed_tools.py +189 -45
- package/scripts/_lib/json_pointers.py +260 -0
- package/scripts/_lib/update_check.py +29 -5
- package/scripts/_lib/user_global_paths.py +249 -0
- package/scripts/agent-config +69 -0
- package/scripts/ai_council/__init__.py +4 -3
- package/scripts/ai_council/budget_guard.py +34 -4
- package/scripts/ai_council/bundler.py +2 -0
- package/scripts/ai_council/clients.py +28 -7
- package/scripts/compress.py +78 -15
- package/scripts/install +8 -0
- package/scripts/install-hooks.sh +54 -1
- package/scripts/install.py +1149 -53
- package/scripts/install_anthropic_key.sh +5 -3
- package/scripts/install_openai_key.sh +5 -3
- package/scripts/skill_trigger_eval.py +13 -2
|
@@ -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
|
@@ -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())
|