@event4u/agent-config 4.9.0 → 5.1.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/implement-ticket.md +5 -4
- package/.agent-src/contexts/execution/roadmap-process-loop.md +30 -4
- package/.agent-src/rules/language-and-tone.md +4 -10
- package/.agent-src/rules/linked-projects-onboarding-gate.md +82 -0
- package/.agent-src/rules/roadmap-progress-sync.md +39 -5
- package/.agent-src/scripts/update_roadmap_progress.py +63 -7
- package/.agent-src/skills/command-routing/SKILL.md +5 -4
- package/.agent-src/skills/roadmap-management/SKILL.md +121 -21
- package/.agent-src/skills/roadmap-writing/SKILL.md +63 -0
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +22 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +20 -3
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +106 -0
- package/CONTRIBUTING.md +19 -0
- package/README.md +12 -1
- package/dist/cli/registry.js +0 -2
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +36 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1671
- package/docs/architecture.md +1 -1
- package/docs/benchmark.md +20 -8
- package/docs/benchmarks.md +11 -0
- package/docs/catalog.md +3 -2
- package/docs/contracts/benchmark-corpus-spec.md +31 -3
- package/docs/contracts/command-surface-tiers.md +1 -1
- package/docs/contracts/hook-architecture-v1.md +33 -0
- package/docs/contracts/migrate-command.md +197 -0
- package/docs/contracts/settings-api.md +2 -1
- package/docs/contracts/value-dashboard-spec.md +374 -0
- package/docs/contracts/value-report-schema.md +150 -0
- package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
- package/docs/decisions/ADR-032-linked-projects-scope.md +118 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
- package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
- package/docs/guides/cross-repo-linked-projects.md +86 -0
- package/docs/migration/v1-to-v2.md +40 -27
- package/docs/value.md +84 -0
- package/package.json +8 -8
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_migrate.py +264 -102
- package/scripts/_cli/cmd_settings_migrate.py +2 -1
- package/scripts/_dispatch.bash +147 -49
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +20 -3
- package/scripts/_lib/install_regenerator.py +129 -0
- package/scripts/_lib/linked_projects.py +238 -0
- package/scripts/_lib/value_ladder.py +599 -0
- package/scripts/_lib/value_report.py +441 -0
- package/scripts/bench_rtk_savings.py +320 -0
- package/scripts/check_no_local_settings_committed.py +51 -0
- package/scripts/compile_router.py +19 -5
- package/scripts/expected_perms.json +1 -1
- package/scripts/first_run_gate_hook.py +178 -0
- package/scripts/hook_manifest.yaml +16 -7
- package/scripts/hooks/dispatch_hook.py +27 -0
- package/scripts/hooks/dispatch_issues.py +136 -0
- package/scripts/hooks_doctor.py +40 -1
- package/scripts/install.py +25 -21
- package/scripts/lint_agents_layout.py +5 -4
- package/scripts/lint_bench_corpus.py +86 -4
- package/scripts/lint_global_paths.py +4 -3
- package/scripts/lint_marketplace_install_completeness.py +188 -0
- package/scripts/lint_value_dashboard.py +218 -0
- package/scripts/render_benchmark_md.py +6 -2
- package/scripts/render_value_md.py +355 -0
- package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
- package/scripts/roadmap_progress_hook.py +23 -0
- package/scripts/router_telemetry.py +470 -0
- package/scripts/validate_frontmatter.py +23 -9
- package/scripts/_cli/cmd_migrate_to_global.py +0 -415
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Detect IDE-attached sibling projects (linked-projects scope, Option A).
|
|
2
|
+
|
|
3
|
+
Pure, dependency-free detector. Reads on-disk IDE config the developer already
|
|
4
|
+
created by attaching a sibling repo, and returns the sibling project roots that
|
|
5
|
+
sit *outside* the current project. Config-driven only — never guesses from
|
|
6
|
+
arbitrary adjacent directories.
|
|
7
|
+
|
|
8
|
+
Sources:
|
|
9
|
+
* PhpStorm / IntelliJ — ``.idea/modules.xml`` (``<module fileurl>``) and
|
|
10
|
+
``.idea/vcs.xml`` (``<mapping directory>``).
|
|
11
|
+
* VS Code — ``*.code-workspace`` (``folders[].path``).
|
|
12
|
+
|
|
13
|
+
Guardrails (per the linked-projects council, Option A):
|
|
14
|
+
* a candidate must resolve OUTSIDE the project root, exist, and contain a
|
|
15
|
+
``.git/`` directory;
|
|
16
|
+
* a candidate whose file count exceeds ``max_files`` (default 20000) is
|
|
17
|
+
**flagged** ``large: true`` — NOT excluded. Under Option A the agent only
|
|
18
|
+
carries a passive awareness note and never bulk-includes sibling files, so
|
|
19
|
+
repo size is cost-irrelevant to detection; a real frontend repo routinely
|
|
20
|
+
exceeds 20000 files (excluding node_modules) and must still be surfaced.
|
|
21
|
+
The flag lets the awareness note say "large repo — check targeted impact,
|
|
22
|
+
do not scan the whole tree";
|
|
23
|
+
* the bloat directories ``node_modules``/``.git``/``dist``/``build``/
|
|
24
|
+
``.venv``/``target`` are never descended into while counting.
|
|
25
|
+
|
|
26
|
+
The detector returns awareness candidates; it does NOT include any sibling
|
|
27
|
+
files in context and does NOT persist anything. Opt-in + persistence is the
|
|
28
|
+
caller's job.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import logging
|
|
35
|
+
import re
|
|
36
|
+
import xml.etree.ElementTree as ET
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
#: File-count ceiling above which a sibling is skipped (token-blowup guard).
|
|
43
|
+
DEFAULT_MAX_FILES = 20000
|
|
44
|
+
|
|
45
|
+
#: Directories never descended into while counting a sibling's size.
|
|
46
|
+
SKIP_DIRS: frozenset[str] = frozenset(
|
|
47
|
+
{"node_modules", ".git", "dist", "build", ".venv", "target", ".idea"}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def detect_linked_projects(
|
|
52
|
+
project_root: Path | str,
|
|
53
|
+
*,
|
|
54
|
+
max_files: int = DEFAULT_MAX_FILES,
|
|
55
|
+
) -> list[dict[str, Any]]:
|
|
56
|
+
"""Return IDE-attached sibling projects outside ``project_root``.
|
|
57
|
+
|
|
58
|
+
Each entry is ``{"path": <absolute str>, "detected_via": <source>,
|
|
59
|
+
"large": <bool>}`` where source is one of ``phpstorm_modules`` /
|
|
60
|
+
``phpstorm_vcs`` / ``vscode_workspace`` and ``large`` is true when the
|
|
61
|
+
sibling's file count (excluding bloat dirs) exceeds ``max_files``. Results
|
|
62
|
+
are de-duplicated by resolved path (first source wins) and sorted by path.
|
|
63
|
+
Size never excludes — see the module docstring.
|
|
64
|
+
"""
|
|
65
|
+
root = Path(project_root).resolve()
|
|
66
|
+
if not root.is_dir():
|
|
67
|
+
logger.info("linked_projects: project_root %s is not a directory", root)
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
candidates: list[tuple[Path, str]] = []
|
|
71
|
+
candidates.extend((p, "phpstorm_modules") for p in _phpstorm_modules(root))
|
|
72
|
+
candidates.extend((p, "phpstorm_vcs") for p in _phpstorm_vcs(root))
|
|
73
|
+
candidates.extend((p, "vscode_workspace") for p in _vscode_workspace(root))
|
|
74
|
+
|
|
75
|
+
seen: set[Path] = set()
|
|
76
|
+
out: list[dict[str, Any]] = []
|
|
77
|
+
for path, source in candidates:
|
|
78
|
+
try:
|
|
79
|
+
resolved = path.resolve()
|
|
80
|
+
except OSError:
|
|
81
|
+
logger.info("linked_projects: cannot resolve %s", path)
|
|
82
|
+
continue
|
|
83
|
+
if resolved in seen:
|
|
84
|
+
continue
|
|
85
|
+
if not _is_valid_sibling(resolved, root):
|
|
86
|
+
continue
|
|
87
|
+
large = _exceeds_size(resolved, max_files)
|
|
88
|
+
if large:
|
|
89
|
+
logger.info(
|
|
90
|
+
"linked_projects: %s exceeds %d files — flagged large (awareness only)",
|
|
91
|
+
resolved,
|
|
92
|
+
max_files,
|
|
93
|
+
)
|
|
94
|
+
seen.add(resolved)
|
|
95
|
+
out.append(
|
|
96
|
+
{"path": str(resolved), "detected_via": source, "large": large}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
out.sort(key=lambda e: e["path"])
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_valid_sibling(candidate: Path, root: Path) -> bool:
|
|
104
|
+
"""A sibling must be outside the project root, exist, and be a git repo."""
|
|
105
|
+
try:
|
|
106
|
+
if candidate == root or root in candidate.parents:
|
|
107
|
+
return False # inside the project — that's the module system's job
|
|
108
|
+
if candidate in root.parents:
|
|
109
|
+
return False # an ancestor of the project, not a sibling
|
|
110
|
+
if not candidate.is_dir():
|
|
111
|
+
logger.info("linked_projects: candidate missing/not-a-dir %s", candidate)
|
|
112
|
+
return False
|
|
113
|
+
if not (candidate / ".git").exists():
|
|
114
|
+
logger.info("linked_projects: candidate not a git repo %s", candidate)
|
|
115
|
+
return False
|
|
116
|
+
except OSError:
|
|
117
|
+
logger.info("linked_projects: unreadable candidate %s", candidate)
|
|
118
|
+
return False
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _exceeds_size(candidate: Path, max_files: int) -> bool:
|
|
123
|
+
"""True if the tree (minus SKIP_DIRS) holds more than ``max_files`` files."""
|
|
124
|
+
import os
|
|
125
|
+
|
|
126
|
+
count = 0
|
|
127
|
+
for dirpath, dirnames, filenames in os.walk(candidate):
|
|
128
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
129
|
+
count += len(filenames)
|
|
130
|
+
if count > max_files:
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _phpstorm_modules(root: Path) -> list[Path]:
|
|
136
|
+
"""Sibling roots from ``.idea/modules.xml`` ``<module fileurl>`` entries."""
|
|
137
|
+
path = root / ".idea" / "modules.xml"
|
|
138
|
+
elems = _iter_xml_attrs(path, "module", ("fileurl", "filepath"))
|
|
139
|
+
out: list[Path] = []
|
|
140
|
+
for attrs in elems:
|
|
141
|
+
raw = attrs.get("fileurl") or attrs.get("filepath")
|
|
142
|
+
if not raw:
|
|
143
|
+
continue
|
|
144
|
+
resolved = _resolve_idea_url(raw, root)
|
|
145
|
+
if resolved is None:
|
|
146
|
+
continue
|
|
147
|
+
# raw points at <sibling>/.idea/<name>.iml → sibling is .idea's parent.
|
|
148
|
+
if resolved.parent.name == ".idea":
|
|
149
|
+
out.append(resolved.parent.parent)
|
|
150
|
+
else:
|
|
151
|
+
out.append(resolved)
|
|
152
|
+
return out
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _phpstorm_vcs(root: Path) -> list[Path]:
|
|
156
|
+
"""Sibling roots from ``.idea/vcs.xml`` ``<mapping directory>`` entries."""
|
|
157
|
+
path = root / ".idea" / "vcs.xml"
|
|
158
|
+
out: list[Path] = []
|
|
159
|
+
for attrs in _iter_xml_attrs(path, "mapping", ("directory",)):
|
|
160
|
+
raw = attrs.get("directory")
|
|
161
|
+
if not raw:
|
|
162
|
+
continue
|
|
163
|
+
resolved = _resolve_idea_url(raw, root)
|
|
164
|
+
if resolved is not None:
|
|
165
|
+
out.append(resolved)
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _vscode_workspace(root: Path) -> list[Path]:
|
|
170
|
+
"""Sibling roots from ``*.code-workspace`` ``folders[].path`` entries."""
|
|
171
|
+
out: list[Path] = []
|
|
172
|
+
try:
|
|
173
|
+
workspaces = sorted(root.glob("*.code-workspace"))
|
|
174
|
+
except OSError:
|
|
175
|
+
return out
|
|
176
|
+
for ws in workspaces:
|
|
177
|
+
data = _read_jsonc(ws)
|
|
178
|
+
if not isinstance(data, dict):
|
|
179
|
+
continue
|
|
180
|
+
folders = data.get("folders")
|
|
181
|
+
if not isinstance(folders, list):
|
|
182
|
+
continue
|
|
183
|
+
for folder in folders:
|
|
184
|
+
if not isinstance(folder, dict):
|
|
185
|
+
continue
|
|
186
|
+
rel = folder.get("path")
|
|
187
|
+
if not isinstance(rel, str) or not rel.strip():
|
|
188
|
+
continue
|
|
189
|
+
out.append((root / rel).resolve())
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _resolve_idea_url(raw: str, root: Path) -> Path | None:
|
|
194
|
+
"""Resolve a PhpStorm path token to an absolute Path, or None."""
|
|
195
|
+
value = raw.strip()
|
|
196
|
+
if value.startswith("file://"):
|
|
197
|
+
value = value[len("file://") :]
|
|
198
|
+
value = value.replace("$PROJECT_DIR$", str(root))
|
|
199
|
+
if not value:
|
|
200
|
+
return None
|
|
201
|
+
try:
|
|
202
|
+
return (Path(value) if Path(value).is_absolute() else root / value).resolve()
|
|
203
|
+
except OSError:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _iter_xml_attrs(
|
|
208
|
+
path: Path, tag: str, _attrs: tuple[str, ...]
|
|
209
|
+
) -> list[dict[str, str]]:
|
|
210
|
+
"""Return the attribute dicts of every ``<tag>`` in ``path`` (tolerant)."""
|
|
211
|
+
if not path.is_file():
|
|
212
|
+
return []
|
|
213
|
+
try:
|
|
214
|
+
tree = ET.parse(path)
|
|
215
|
+
except (ET.ParseError, OSError) as exc:
|
|
216
|
+
logger.info("linked_projects: malformed/unreadable %s (%s)", path, exc)
|
|
217
|
+
return []
|
|
218
|
+
return [dict(el.attrib) for el in tree.iter(tag)]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _read_jsonc(path: Path) -> Any:
|
|
222
|
+
"""Parse JSON that may carry ``//`` comments and trailing commas (VS Code)."""
|
|
223
|
+
try:
|
|
224
|
+
text = path.read_text(encoding="utf-8")
|
|
225
|
+
except OSError:
|
|
226
|
+
return None
|
|
227
|
+
try:
|
|
228
|
+
return json.loads(text)
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
pass
|
|
231
|
+
# tolerant fallback: strip line comments + trailing commas, retry once.
|
|
232
|
+
stripped = re.sub(r"^\s*//.*$", "", text, flags=re.MULTILINE)
|
|
233
|
+
stripped = re.sub(r",(\s*[}\]])", r"\1", stripped)
|
|
234
|
+
try:
|
|
235
|
+
return json.loads(stripped)
|
|
236
|
+
except json.JSONDecodeError as exc:
|
|
237
|
+
logger.info("linked_projects: malformed workspace JSON %s (%s)", path, exc)
|
|
238
|
+
return None
|