@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.
Files changed (82) hide show
  1. package/.agent-src/commands/implement-ticket.md +5 -4
  2. package/.agent-src/contexts/execution/roadmap-process-loop.md +30 -4
  3. package/.agent-src/rules/language-and-tone.md +4 -10
  4. package/.agent-src/rules/linked-projects-onboarding-gate.md +82 -0
  5. package/.agent-src/rules/roadmap-progress-sync.md +39 -5
  6. package/.agent-src/scripts/update_roadmap_progress.py +63 -7
  7. package/.agent-src/skills/command-routing/SKILL.md +5 -4
  8. package/.agent-src/skills/roadmap-management/SKILL.md +121 -21
  9. package/.agent-src/skills/roadmap-writing/SKILL.md +63 -0
  10. package/.agent-src/templates/agent-settings.md +16 -0
  11. package/.agent-src/templates/roadmaps.md +22 -1
  12. package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +20 -3
  13. package/.claude-plugin/marketplace.json +1 -1
  14. package/CHANGELOG.md +106 -0
  15. package/CONTRIBUTING.md +19 -0
  16. package/README.md +12 -1
  17. package/dist/cli/registry.js +0 -2
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/discovery/deprecation-report.md +1 -1
  20. package/dist/discovery/discovery-manifest.json +36 -14
  21. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  22. package/dist/discovery/discovery-manifest.summary.md +3 -3
  23. package/dist/discovery/orphan-report.md +1 -1
  24. package/dist/discovery/packs.json +6 -5
  25. package/dist/discovery/trust-report.md +3 -3
  26. package/dist/discovery/workspaces.json +5 -4
  27. package/dist/mcp/registry-manifest.json +3 -3
  28. package/dist/router.json +1 -1671
  29. package/docs/architecture.md +1 -1
  30. package/docs/benchmark.md +20 -8
  31. package/docs/benchmarks.md +11 -0
  32. package/docs/catalog.md +3 -2
  33. package/docs/contracts/benchmark-corpus-spec.md +31 -3
  34. package/docs/contracts/command-surface-tiers.md +1 -1
  35. package/docs/contracts/hook-architecture-v1.md +33 -0
  36. package/docs/contracts/migrate-command.md +197 -0
  37. package/docs/contracts/settings-api.md +2 -1
  38. package/docs/contracts/value-dashboard-spec.md +374 -0
  39. package/docs/contracts/value-report-schema.md +150 -0
  40. package/docs/decisions/ADR-031-validation-severity-tiers-and-projection-roundtrip.md +97 -0
  41. package/docs/decisions/ADR-032-linked-projects-scope.md +118 -0
  42. package/docs/decisions/INDEX.md +2 -0
  43. package/docs/getting-started.md +1 -1
  44. package/docs/guidelines/agent-infra/installed-tools-manifest.md +6 -3
  45. package/docs/guidelines/agent-infra/language-and-tone-examples.md +35 -0
  46. package/docs/guides/cross-repo-linked-projects.md +86 -0
  47. package/docs/migration/v1-to-v2.md +40 -27
  48. package/docs/value.md +84 -0
  49. package/package.json +8 -8
  50. package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  51. package/scripts/_cli/cmd_migrate.py +264 -102
  52. package/scripts/_cli/cmd_settings_migrate.py +2 -1
  53. package/scripts/_dispatch.bash +147 -49
  54. package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  55. package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  56. package/scripts/_lib/agent_settings.py +20 -3
  57. package/scripts/_lib/install_regenerator.py +129 -0
  58. package/scripts/_lib/linked_projects.py +238 -0
  59. package/scripts/_lib/value_ladder.py +599 -0
  60. package/scripts/_lib/value_report.py +441 -0
  61. package/scripts/bench_rtk_savings.py +320 -0
  62. package/scripts/check_no_local_settings_committed.py +51 -0
  63. package/scripts/compile_router.py +19 -5
  64. package/scripts/expected_perms.json +1 -1
  65. package/scripts/first_run_gate_hook.py +178 -0
  66. package/scripts/hook_manifest.yaml +16 -7
  67. package/scripts/hooks/dispatch_hook.py +27 -0
  68. package/scripts/hooks/dispatch_issues.py +136 -0
  69. package/scripts/hooks_doctor.py +40 -1
  70. package/scripts/install.py +25 -21
  71. package/scripts/lint_agents_layout.py +5 -4
  72. package/scripts/lint_bench_corpus.py +86 -4
  73. package/scripts/lint_global_paths.py +4 -3
  74. package/scripts/lint_marketplace_install_completeness.py +188 -0
  75. package/scripts/lint_value_dashboard.py +218 -0
  76. package/scripts/render_benchmark_md.py +6 -2
  77. package/scripts/render_value_md.py +355 -0
  78. package/scripts/repro/repro_marketplace_install_gap.sh +161 -0
  79. package/scripts/roadmap_progress_hook.py +23 -0
  80. package/scripts/router_telemetry.py +470 -0
  81. package/scripts/validate_frontmatter.py +23 -9
  82. 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