self-evolve-framework 1.0.7 → 1.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 (126) hide show
  1. package/README.md +12 -8
  2. package/bin/cli.js +63 -35
  3. package/package.json +1 -1
  4. package/template/rules/CodeGraph.mdc +23 -0
  5. package/template/rules/Svelte_5.mdc +167 -0
  6. package/template/rules/Svelte_Flow.mdc +176 -0
  7. package/template/rules/Tailwind_CSS_v4.mdc +187 -0
  8. package/template/rules/Tauri.mdc +145 -0
  9. package/template/rules/app-error-pattern.mdc +65 -0
  10. package/template/rules/invoke-safe-pattern.mdc +53 -0
  11. package/template/rules/js.mdc +10 -0
  12. package/template/rules/powershell.mdc +9 -0
  13. package/template/rules/self-evolve.mdc +4 -4
  14. package/template/rules//346/227/245/345/277/227.mdc +15 -0
  15. package/template/rules//350/257/267/346/261/202.mdc +49 -0
  16. package/template/skills/caveman/SKILL.md +49 -0
  17. package/template/skills/check/SKILL.md +393 -0
  18. package/template/skills/check/agents/reviewer-architecture.md +39 -0
  19. package/template/skills/check/agents/reviewer-security.md +39 -0
  20. package/template/skills/check/references/persona-catalog.md +56 -0
  21. package/template/skills/check/references/project-context.md +120 -0
  22. package/template/skills/check/references/public-reply.md +14 -0
  23. package/template/skills/check/scripts/audit_signals.py +666 -0
  24. package/template/skills/check/scripts/run-tests.sh +19 -0
  25. package/template/skills/design/SKILL.md +173 -0
  26. package/template/skills/design/references/design-aesthetic-quality.md +67 -0
  27. package/template/skills/design/references/design-data-viz.md +34 -0
  28. package/template/skills/design/references/design-reference.md +295 -0
  29. package/template/skills/design/references/design-tokens.md +45 -0
  30. package/template/skills/design/references/design-traps.md +43 -0
  31. package/template/skills/design-an-interface/SKILL.md +94 -0
  32. package/template/skills/diagnose/SKILL.md +117 -0
  33. package/template/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  34. package/template/skills/edit-article/SKILL.md +14 -0
  35. package/template/skills/git-guardrails-claude-code/SKILL.md +95 -0
  36. package/template/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +25 -0
  37. package/template/skills/grill-me/SKILL.md +10 -0
  38. package/template/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  39. package/template/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  40. package/template/skills/grill-with-docs/SKILL.md +88 -0
  41. package/template/skills/handoff/SKILL.md +15 -0
  42. package/template/skills/health/SKILL.md +260 -0
  43. package/template/skills/health/agents/inspector-context.md +119 -0
  44. package/template/skills/health/agents/inspector-control.md +84 -0
  45. package/template/skills/health/agents/inspector-maintainability.md +55 -0
  46. package/template/skills/health/scripts/check-agent-context.sh +5 -0
  47. package/template/skills/health/scripts/check-doc-refs.sh +8 -0
  48. package/template/skills/health/scripts/check-maintainability.sh +8 -0
  49. package/template/skills/health/scripts/check-verifier-output.sh +5 -0
  50. package/template/skills/health/scripts/check_agent_context.py +444 -0
  51. package/template/skills/health/scripts/check_doc_refs.py +110 -0
  52. package/template/skills/health/scripts/check_maintainability.py +635 -0
  53. package/template/skills/health/scripts/check_verifier_output.py +116 -0
  54. package/template/skills/health/scripts/collect-data.sh +751 -0
  55. package/template/skills/hunt/SKILL.md +232 -0
  56. package/template/skills/hunt/references/failure-patterns.md +138 -0
  57. package/template/skills/hunt/references/ime-unicode.md +58 -0
  58. package/template/skills/hunt/references/logging-techniques.md +72 -0
  59. package/template/skills/hunt/references/rendering-debug.md +34 -0
  60. package/template/skills/impeccable/SKILL.md +47 -0
  61. package/template/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  62. package/template/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  63. package/template/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  64. package/template/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  65. package/template/skills/improve-codebase-architecture/SKILL.md +81 -0
  66. package/template/skills/learn/SKILL.md +140 -0
  67. package/template/skills/migrate-to-shoehorn/SKILL.md +118 -0
  68. package/template/skills/obsidian-vault/SKILL.md +59 -0
  69. package/template/skills/prototype/LOGIC.md +79 -0
  70. package/template/skills/prototype/SKILL.md +30 -0
  71. package/template/skills/prototype/UI.md +112 -0
  72. package/template/skills/qa/SKILL.md +130 -0
  73. package/template/skills/read/SKILL.md +141 -0
  74. package/template/skills/read/references/read-methods.md +129 -0
  75. package/template/skills/read/scripts/fetch.sh +106 -0
  76. package/template/skills/read/scripts/fetch_feishu.py +251 -0
  77. package/template/skills/read/scripts/fetch_local.py +218 -0
  78. package/template/skills/read/scripts/fetch_weixin.py +107 -0
  79. package/template/skills/request-refactor-plan/SKILL.md +68 -0
  80. package/template/skills/review/SKILL.md +78 -0
  81. package/template/skills/rust-auto-fix/SKILL.md +94 -0
  82. package/template/skills/scaffold-exercises/SKILL.md +106 -0
  83. package/template/skills/sdd-dev/SKILL.md +114 -0
  84. package/template/skills/setup-matt-pocock-skills/SKILL.md +121 -0
  85. package/template/skills/setup-matt-pocock-skills/domain.md +51 -0
  86. package/template/skills/setup-matt-pocock-skills/issue-tracker-github.md +22 -0
  87. package/template/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md +23 -0
  88. package/template/skills/setup-matt-pocock-skills/issue-tracker-local.md +19 -0
  89. package/template/skills/setup-matt-pocock-skills/triage-labels.md +15 -0
  90. package/template/skills/setup-pre-commit/SKILL.md +91 -0
  91. package/template/skills/skillopt-sleep/SKILL.md +1 -1
  92. package/template/skills/svelte-warnings-fix/SKILL.md +94 -0
  93. package/template/skills/tauri-nsis-installer-icon/SKILL.md +92 -0
  94. package/template/skills/tauri-nsis-installer-icon/references/tauri-nsis-schema.md +71 -0
  95. package/template/skills/tb/SKILL.md +62 -0
  96. package/template/skills/tdd/SKILL.md +109 -0
  97. package/template/skills/tdd/deep-modules.md +33 -0
  98. package/template/skills/tdd/interface-design.md +31 -0
  99. package/template/skills/tdd/mocking.md +59 -0
  100. package/template/skills/tdd/refactoring.md +10 -0
  101. package/template/skills/tdd/tests.md +61 -0
  102. package/template/skills/teach/GLOSSARY-FORMAT.md +35 -0
  103. package/template/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  104. package/template/skills/teach/MISSION-FORMAT.md +31 -0
  105. package/template/skills/teach/RESOURCES-FORMAT.md +32 -0
  106. package/template/skills/teach/SKILL.md +91 -0
  107. package/template/skills/think/SKILL.md +184 -0
  108. package/template/skills/to-issues/SKILL.md +83 -0
  109. package/template/skills/to-prd/SKILL.md +74 -0
  110. package/template/skills/triage/AGENT-BRIEF.md +168 -0
  111. package/template/skills/triage/OUT-OF-SCOPE.md +101 -0
  112. package/template/skills/triage/SKILL.md +103 -0
  113. package/template/skills/ubiquitous-language/SKILL.md +93 -0
  114. package/template/skills/ver/SKILL.md +62 -0
  115. package/template/skills/write/SKILL.md +209 -0
  116. package/template/skills/write/references/write-en.md +199 -0
  117. package/template/skills/write/references/write-product-localization.md +43 -0
  118. package/template/skills/write/references/write-zh-bilingual.md +59 -0
  119. package/template/skills/write/references/write-zh-prose.md +50 -0
  120. package/template/skills/write/references/write-zh-release-notes.md +40 -0
  121. package/template/skills/write/references/write-zh.md +721 -0
  122. package/template/skills/write-a-skill/SKILL.md +117 -0
  123. package/template/skills/writing-beats/SKILL.md +52 -0
  124. package/template/skills/writing-fragments/SKILL.md +75 -0
  125. package/template/skills/writing-shape/SKILL.md +64 -0
  126. package/template/skills/zoom-out/SKILL.md +7 -0
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env python3
2
+ """Project audit signals (Phase 1) for /check audit mode.
3
+
4
+ Walks a project root and emits structured signal blocks to stdout.
5
+ Each block ends with `status: PASS|WARN|FAIL|N/A` so the LLM driving the
6
+ 4-axis Linus-style scorecard can skim quickly.
7
+
8
+ Pure stdlib. Read-only. Exits 0 even on WARN/FAIL so the harness does
9
+ not confuse "finding surfaced" with "script broken".
10
+
11
+ Run as: python3 skills/check/scripts/audit_signals.py --root <path>
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+
24
+
25
+ EXCLUDED_DIRS = {
26
+ ".git", ".hg", ".svn", "node_modules", "dist", "build", ".next",
27
+ "__pycache__", ".turbo", "target", ".venv", "venv", "vendor",
28
+ "coverage", ".cache", ".parcel-cache", ".pytest_cache", ".mypy_cache",
29
+ ".ruff_cache", "Pods", "Carthage", ".swiftpm", ".gradle",
30
+ }
31
+
32
+ SOURCE_EXTS = {
33
+ ".py", ".swift", ".rs", ".go", ".ts", ".tsx", ".js", ".jsx", ".sh",
34
+ ".bash", ".zsh", ".rb", ".java", ".kt", ".m", ".mm", ".vue", ".c",
35
+ ".cc", ".cpp", ".h", ".hpp", ".cs",
36
+ }
37
+
38
+ HOTSPOT_LINES = 500
39
+ HOTSPOT_FAIL = 1500
40
+ HEREDOC_LINES = 100
41
+ DRIFT_WARN = 50
42
+ DRIFT_FAIL = 150
43
+ DUP_JACCARD = 0.70
44
+
45
+ MARKER_RE = re.compile(r"\b(TODO|FIXME|HACK|XXX)\b")
46
+ HEREDOC_OPEN_RE = re.compile(
47
+ r"(python3?|node|ruby|perl|php)\b[^|\n]*?<<-?\s*['\"]?(\w+)['\"]?"
48
+ )
49
+ INSTALL_URL_RE = re.compile(
50
+ r"raw\.githubusercontent\.com/[^/\s]+/[^/\s]+/([^/\s]+)/"
51
+ )
52
+ # --exclude requires = or trailing value to avoid matching git's --exclude-standard
53
+ DENYLIST_HINT_RE = re.compile(
54
+ r"(^\s*(skip|exclude)\s*=|\s--exclude=|!\*\.\w+|grep\s+-v\b|--ignore=)",
55
+ re.IGNORECASE,
56
+ )
57
+ MINIFIED_RE = re.compile(r"\.min\.[a-z]+$", re.IGNORECASE)
58
+ CLI_CONTRACT_BUCKETS: tuple[tuple[str, re.Pattern[str]], ...] = (
59
+ ("help_or_usage", re.compile(r"(--help|\busage\b|\bhelp output\b)", re.IGNORECASE)),
60
+ ("version", re.compile(r"(--version|\bversion output\b)", re.IGNORECASE)),
61
+ ("exit_code", re.compile(r"\b(exit code|exit status|return code|exit_code|\$\?)\b", re.IGNORECASE)),
62
+ ("stdout", re.compile(r"\b(stdout|standard output)\b|>\s*\"\$?[A-Za-z0-9_./-]*stdout", re.IGNORECASE)),
63
+ ("stderr", re.compile(r"\b(stderr|standard error)\b|2>\s*\"\$?[A-Za-z0-9_./-]*stderr", re.IGNORECASE)),
64
+ ("non_interactive_or_tty", re.compile(r"\b(non-interactive|noninteractive|tty|isatty|/dev/null|CI=1)\b", re.IGNORECASE)),
65
+ (
66
+ "install_run",
67
+ re.compile(
68
+ r"(\binstall\s+-m\b|\binstalled command\b|\binstalled-runtime\b|"
69
+ r"\binstall/run\b|\binstall run\b|\btemp prefix\b|\bPATH shim\b|"
70
+ r"\bpackage-manager path\b|\bnpm link\b|\bpipx install\b|"
71
+ r"\bcargo install\b|\bbrew install\b|\bmake install\b)",
72
+ re.IGNORECASE,
73
+ ),
74
+ ),
75
+ ("json_or_schema", re.compile(r"\b(json|schema)\b", re.IGNORECASE)),
76
+ ("completion", re.compile(r"\bcompletion\b", re.IGNORECASE)),
77
+ )
78
+ CLI_CORE_BUCKETS = (
79
+ "help_or_usage",
80
+ "version",
81
+ "exit_code",
82
+ "stdout",
83
+ "stderr",
84
+ "install_run",
85
+ )
86
+
87
+
88
+ # The file-walk helpers below are deliberately duplicated in
89
+ # skills/health/scripts/check_maintainability.py. Both scripts ship
90
+ # standalone (see packaging.allowlist) and run inside an arbitrary target
91
+ # project, so they import only stdlib. Do not hoist them into a shared
92
+ # scripts/ module: it is dev-only, not on the ship allowlist, and would
93
+ # couple a standalone tool to the install layout.
94
+ def is_excluded(path: Path, root: Path) -> bool:
95
+ try:
96
+ parts = path.relative_to(root).parts
97
+ except ValueError:
98
+ parts = path.parts
99
+ if any(p in EXCLUDED_DIRS for p in parts):
100
+ return True
101
+ return bool(MINIFIED_RE.search(path.name))
102
+
103
+
104
+ def iter_files(root: Path) -> list[Path]:
105
+ try:
106
+ proc = subprocess.run(
107
+ ["git", "-C", str(root), "ls-files",
108
+ "--cached", "--others", "--exclude-standard"],
109
+ text=True, stdout=subprocess.PIPE,
110
+ stderr=subprocess.DEVNULL, check=False,
111
+ )
112
+ if proc.returncode == 0 and proc.stdout.strip():
113
+ out = []
114
+ for line in proc.stdout.splitlines():
115
+ p = root / line
116
+ if p.is_file() and not is_excluded(p, root):
117
+ out.append(p)
118
+ return out
119
+ except OSError:
120
+ pass
121
+ out = []
122
+ for dirpath, dirnames, filenames in os.walk(root):
123
+ current = Path(dirpath)
124
+ dirnames[:] = [d for d in dirnames if d not in EXCLUDED_DIRS]
125
+ if is_excluded(current, root):
126
+ continue
127
+ for fname in filenames:
128
+ p = current / fname
129
+ if p.is_file() and not is_excluded(p, root):
130
+ out.append(p)
131
+ return out
132
+
133
+
134
+ def line_count(path: Path) -> int:
135
+ try:
136
+ with path.open("rb") as fh:
137
+ return sum(1 for _ in fh)
138
+ except OSError:
139
+ return 0
140
+
141
+
142
+ def read_text(path: Path, limit: int = 0) -> str:
143
+ try:
144
+ data = path.read_text(encoding="utf-8", errors="replace")
145
+ except OSError:
146
+ return ""
147
+ return data[:limit] if limit else data
148
+
149
+
150
+ def rel(path: Path, root: Path) -> str:
151
+ try:
152
+ return path.relative_to(root).as_posix()
153
+ except ValueError:
154
+ return path.as_posix()
155
+
156
+
157
+ def header(name: str) -> None:
158
+ print(f"=== {name} ===")
159
+
160
+
161
+ def status(label: str) -> None:
162
+ print(f"status: {label}")
163
+
164
+
165
+ def block_hotspots(files: list[Path], root: Path) -> None:
166
+ header("FILE SIZE HOTSPOTS")
167
+ sized = ((p, line_count(p)) for p in files if p.suffix in SOURCE_EXTS)
168
+ big = sorted(
169
+ (item for item in sized if item[1] >= HOTSPOT_LINES),
170
+ key=lambda x: -x[1],
171
+ )[:10]
172
+ if not big:
173
+ print(f"(no source files >= {HOTSPOT_LINES} lines)")
174
+ status("PASS")
175
+ return
176
+ for path, n in big:
177
+ print(f" {n:>5} {rel(path, root)}")
178
+ status("FAIL" if any(n >= HOTSPOT_FAIL for _, n in big) else "WARN")
179
+
180
+
181
+ def block_heredoc(files: list[Path], root: Path) -> None:
182
+ header("HEREDOC BLOAT")
183
+ hits: list[tuple[str, int, str, int]] = []
184
+ for path in files:
185
+ if path.suffix not in {".sh", ".bash", ".zsh"}:
186
+ continue
187
+ text = read_text(path)
188
+ if not text:
189
+ continue
190
+ lines = text.splitlines()
191
+ i = 0
192
+ while i < len(lines):
193
+ m = HEREDOC_OPEN_RE.search(lines[i])
194
+ if not m:
195
+ i += 1
196
+ continue
197
+ lang, marker = m.group(1), m.group(2)
198
+ j = i + 1
199
+ close = re.compile(r"^\s*" + re.escape(marker) + r"\s*$")
200
+ while j < len(lines) and not close.match(lines[j]):
201
+ j += 1
202
+ size = j - i
203
+ if size >= HEREDOC_LINES:
204
+ hits.append((rel(path, root), i + 1, lang, size))
205
+ i = j + 1
206
+ if not hits:
207
+ print("(no python/node/ruby/perl/php heredocs >= 100 lines)")
208
+ status("PASS")
209
+ return
210
+ for f, ln, lang, sz in hits:
211
+ print(f" {f}:{ln} lang={lang} block_lines={sz}")
212
+ status("WARN")
213
+
214
+
215
+ def block_test_ci(files: list[Path], root: Path) -> None:
216
+ header("TEST AND CI SURFACE")
217
+ test_files = [
218
+ p for p in files
219
+ if p.suffix in SOURCE_EXTS
220
+ and (("test" in p.name.lower()) or ("spec" in p.name.lower()))
221
+ ]
222
+ src_files = [p for p in files if p.suffix in SOURCE_EXTS]
223
+ wf_dir = root / ".github" / "workflows"
224
+ workflows = []
225
+ if wf_dir.is_dir():
226
+ workflows = sorted(list(wf_dir.glob("*.yml")) + list(wf_dir.glob("*.yaml")))
227
+ job_names: list[str] = []
228
+ for wf in workflows:
229
+ text = read_text(wf, 50_000)
230
+ for m in re.finditer(r"^name:\s*(.+?)\s*$", text, re.MULTILINE):
231
+ job_names.append(f"{wf.name}: {m.group(1)[:60]}")
232
+ break
233
+ ratio = len(test_files) / max(len(src_files), 1)
234
+ print(f"tests_count={len(test_files)} source_count={len(src_files)} "
235
+ f"ratio={ratio:.1%}")
236
+ print(f"ci_workflow_files={len(workflows)}")
237
+ for j in job_names[:10]:
238
+ print(f" workflow: {j}")
239
+ if not test_files and not workflows:
240
+ status("FAIL")
241
+ elif not test_files or not workflows:
242
+ status("WARN")
243
+ else:
244
+ status("PASS")
245
+
246
+
247
+ def _package_bin_entrypoints(root: Path) -> list[str]:
248
+ path = root / "package.json"
249
+ if not path.is_file():
250
+ return []
251
+ text = read_text(path, 200_000)
252
+ try:
253
+ data = json.loads(text)
254
+ except json.JSONDecodeError:
255
+ return []
256
+ bin_field = data.get("bin")
257
+ name = str(data.get("name") or "package")
258
+ if isinstance(bin_field, str):
259
+ return [f"package.json bin:{name} -> {bin_field}"]
260
+ if isinstance(bin_field, dict):
261
+ return [
262
+ f"package.json bin:{cmd} -> {target}"
263
+ for cmd, target in sorted(bin_field.items())
264
+ if isinstance(cmd, str) and isinstance(target, str)
265
+ ]
266
+ return []
267
+
268
+
269
+ def _pyproject_script_entrypoints(root: Path) -> list[str]:
270
+ path = root / "pyproject.toml"
271
+ if not path.is_file():
272
+ return []
273
+ text = read_text(path, 200_000)
274
+ entries: list[str] = []
275
+ in_scripts = False
276
+ for line in text.splitlines():
277
+ stripped = line.strip()
278
+ if stripped.startswith("[") and stripped.endswith("]"):
279
+ in_scripts = stripped in {
280
+ "[project.scripts]",
281
+ "[tool.poetry.scripts]",
282
+ }
283
+ continue
284
+ if not in_scripts or not stripped or stripped.startswith("#"):
285
+ continue
286
+ m = re.match(r'([A-Za-z0-9_.-]+)\s*=\s*["\']([^"\']+)["\']', stripped)
287
+ if m:
288
+ entries.append(f"pyproject.toml script:{m.group(1)} -> {m.group(2)}")
289
+ return entries
290
+
291
+
292
+ def _cargo_entrypoints(root: Path) -> list[str]:
293
+ entries: list[str] = []
294
+ cargo = root / "Cargo.toml"
295
+ if cargo.is_file():
296
+ text = read_text(cargo, 200_000)
297
+ if "[[bin]]" in text:
298
+ names = re.findall(r'(?m)^\s*name\s*=\s*["\']([^"\']+)["\']', text)
299
+ if names:
300
+ entries.extend(f"Cargo.toml bin:{name}" for name in sorted(set(names)))
301
+ else:
302
+ entries.append("Cargo.toml [[bin]]")
303
+ if (root / "src" / "main.rs").is_file():
304
+ entries.append("src/main.rs")
305
+ return entries
306
+
307
+
308
+ def cli_entrypoints(files: list[Path], root: Path) -> list[str]:
309
+ entries: set[str] = set()
310
+ entries.update(_package_bin_entrypoints(root))
311
+ entries.update(_pyproject_script_entrypoints(root))
312
+ entries.update(_cargo_entrypoints(root))
313
+
314
+ for path in files:
315
+ try:
316
+ parts = path.relative_to(root).parts
317
+ except ValueError:
318
+ continue
319
+ if not parts:
320
+ continue
321
+ if parts[0] == "bin" and len(parts) >= 2:
322
+ entries.add("/".join(parts[:2]))
323
+ if parts[0] == "cmd" and len(parts) >= 3 and path.suffix == ".go":
324
+ entries.add(f"cmd/{parts[1]}")
325
+ return sorted(entries)
326
+
327
+
328
+ def _is_cli_contract_candidate(path: Path, root: Path) -> bool:
329
+ try:
330
+ parts = path.relative_to(root).parts
331
+ except ValueError:
332
+ return False
333
+ if not parts:
334
+ return False
335
+ lower_parts = tuple(p.lower() for p in parts)
336
+ name = lower_parts[-1]
337
+ if name in {"readme.md", "readme.txt", "agents.md", "claude.md"}:
338
+ return True
339
+ if lower_parts[0] in {"tests", "test", "spec", "scripts"}:
340
+ return True
341
+ if "test" in name or "spec" in name:
342
+ return True
343
+ if len(lower_parts) >= 3 and lower_parts[:2] == (".github", "workflows"):
344
+ return True
345
+ return False
346
+
347
+
348
+ def cli_contract_evidence(files: list[Path], root: Path) -> dict[str, list[tuple[str, str]]]:
349
+ hits: dict[str, list[tuple[str, str]]] = {}
350
+ for path in files:
351
+ if not _is_cli_contract_candidate(path, root):
352
+ continue
353
+ text = read_text(path, 200_000)
354
+ if not text:
355
+ continue
356
+ for bucket, pattern in CLI_CONTRACT_BUCKETS:
357
+ m = pattern.search(text)
358
+ if m:
359
+ hits.setdefault(bucket, []).append((rel(path, root), m.group(0)))
360
+ return {bucket: sorted(values) for bucket, values in sorted(hits.items())}
361
+
362
+
363
+ def block_cli_contract_surface(files: list[Path], root: Path) -> None:
364
+ header("CLI CONTRACT SURFACE")
365
+ entries = cli_entrypoints(files, root)
366
+ if not entries:
367
+ print("(no CLI entrypoints detected)")
368
+ status("N/A")
369
+ return
370
+
371
+ print(f"entrypoints={len(entries)}")
372
+ for entry in entries[:12]:
373
+ print(f" entry: {entry}")
374
+ if len(entries) > 12:
375
+ print(f" ... {len(entries) - 12} more")
376
+
377
+ evidence = cli_contract_evidence(files, root)
378
+ covered = tuple(bucket for bucket, _ in CLI_CONTRACT_BUCKETS if bucket in evidence)
379
+ missing = tuple(bucket for bucket in CLI_CORE_BUCKETS if bucket not in evidence)
380
+ print(f"covered={','.join(covered) if covered else 'none'}")
381
+ print(f"missing={','.join(missing) if missing else 'none'}")
382
+ printed = 0
383
+ for bucket in covered:
384
+ for path, signal in evidence[bucket][:3]:
385
+ print(f" evidence: {bucket} {path} signal={signal}")
386
+ printed += 1
387
+ if printed >= 12:
388
+ break
389
+ if printed >= 12:
390
+ break
391
+ if not missing:
392
+ status("PASS")
393
+ else:
394
+ status("WARN")
395
+
396
+
397
+ def _grep_version(path: Path, pattern: str) -> str | None:
398
+ text = read_text(path, 20_000)
399
+ if not text:
400
+ return None
401
+ m = re.search(pattern, text, re.MULTILINE)
402
+ return m.group(1).strip() if m else None
403
+
404
+
405
+ def block_version_sources(root: Path) -> None:
406
+ header("VERSION SOURCE COUNT")
407
+ found: list[tuple[str, str]] = []
408
+ v = root / "VERSION"
409
+ if v.is_file():
410
+ first = read_text(v).strip().splitlines()
411
+ if first:
412
+ found.append(("VERSION", first[0]))
413
+ probes = [
414
+ ("package.json", r'"version"\s*:\s*"([^"]+)"'),
415
+ ("Cargo.toml", r'^\s*version\s*=\s*"([^"]+)"'),
416
+ ("pyproject.toml", r'^\s*version\s*=\s*"([^"]+)"'),
417
+ ("setup.py", r"version\s*=\s*['\"]([^'\"]+)['\"]"),
418
+ ]
419
+ for fname, pat in probes:
420
+ p = root / fname
421
+ if p.is_file():
422
+ v_str = _grep_version(p, pat)
423
+ if v_str:
424
+ found.append((fname, v_str))
425
+ for pat in ("*.podspec", "*.csproj"):
426
+ for path in root.glob(pat):
427
+ v_str = _grep_version(
428
+ path, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
429
+ )
430
+ if v_str:
431
+ found.append((path.name, v_str))
432
+ for path in list(root.glob("build.gradle*")):
433
+ v_str = _grep_version(
434
+ path, r'(?i)version\s*[:=]\s*["\']?(\d+\.\d+\.\d+[\w.-]*)'
435
+ )
436
+ if v_str:
437
+ found.append((path.name, v_str))
438
+ if not found:
439
+ print("(no declared version source found)")
440
+ status("PASS")
441
+ return
442
+ for f, val in found:
443
+ print(f" {f}: {val}")
444
+ distinct = {val for _, val in found if val}
445
+ print(f"sources={len(found)} distinct_values={len(distinct)}")
446
+ if len(found) > 1 and len(distinct) > 1:
447
+ status("WARN")
448
+ else:
449
+ status("PASS")
450
+
451
+
452
+ def block_packaging_posture(root: Path) -> None:
453
+ header("PACKAGING FILTER POSTURE")
454
+ allowlist_files = list(root.glob("*.allowlist")) + list(root.glob("MANIFEST.in"))
455
+ pkg_scripts = (list(root.glob("scripts/package*.sh"))
456
+ + list(root.glob("scripts/release*.sh")))
457
+ denylist_hits = 0
458
+ for sp in pkg_scripts:
459
+ for line in read_text(sp).splitlines():
460
+ if DENYLIST_HINT_RE.search(line):
461
+ denylist_hits += 1
462
+ if allowlist_files:
463
+ for f in allowlist_files:
464
+ print(f" allowlist: {rel(f, root)}")
465
+ print(f"posture=allowlist denylist_hits_in_scripts={denylist_hits}")
466
+ status("PASS")
467
+ return
468
+ if denylist_hits:
469
+ for sp in pkg_scripts:
470
+ print(f" script: {rel(sp, root)}")
471
+ print(f"posture=denylist denylist_hits_in_scripts={denylist_hits}")
472
+ status("WARN")
473
+ return
474
+ print("posture=none (no packaging scripts)")
475
+ status("N/A")
476
+
477
+
478
+ def block_install_url(root: Path) -> None:
479
+ header("INSTALL URL PINNING")
480
+ targets: list[Path] = [root / "README.md"]
481
+ targets += list(root.glob("scripts/setup*.sh"))
482
+ targets += list(root.glob("scripts/install*.sh"))
483
+ findings: list[tuple[str, int, str]] = []
484
+ for path in targets:
485
+ if not path.is_file():
486
+ continue
487
+ text = read_text(path, 200_000)
488
+ for i, line in enumerate(text.splitlines(), start=1):
489
+ for m in INSTALL_URL_RE.finditer(line):
490
+ findings.append((rel(path, root), i, m.group(1)))
491
+ if not findings:
492
+ print("(no raw.githubusercontent.com refs found)")
493
+ status("PASS")
494
+ return
495
+ moving = [f for f in findings if f[2] in ("main", "master", "HEAD")]
496
+ for f, ln, ref in findings[:20]:
497
+ marker = " [MOVING]" if ref in ("main", "master", "HEAD") else ""
498
+ print(f" {f}:{ln} ref={ref}{marker}")
499
+ print(f"total={len(findings)} moving={len(moving)}")
500
+ if moving:
501
+ status("WARN")
502
+ else:
503
+ status("PASS")
504
+
505
+
506
+ def block_agent_doc_dedup(root: Path) -> None:
507
+ header("AGENT DOC DEDUP")
508
+ claude = root / "CLAUDE.md"
509
+ agents = root / "AGENTS.md"
510
+ have_c = claude.exists() or claude.is_symlink()
511
+ have_a = agents.exists() or agents.is_symlink()
512
+ if not have_c and not have_a:
513
+ print("posture=none")
514
+ status("PASS")
515
+ return
516
+ if not (have_c and have_a):
517
+ print(f"posture=single-file ({'CLAUDE.md' if have_c else 'AGENTS.md'} only)")
518
+ status("PASS")
519
+ return
520
+ if claude.is_symlink() and claude.resolve(strict=False).name == "AGENTS.md":
521
+ print("posture=symlink (CLAUDE.md -> AGENTS.md)")
522
+ status("PASS")
523
+ return
524
+ if agents.is_symlink() and agents.resolve(strict=False).name == "CLAUDE.md":
525
+ print("posture=symlink (AGENTS.md -> CLAUDE.md)")
526
+ status("PASS")
527
+ return
528
+ a = read_text(claude)
529
+ b = read_text(agents)
530
+ if a and a == b:
531
+ print("posture=identical (consider symlink to dedup)")
532
+ status("WARN")
533
+ return
534
+ cross = ("AGENTS.md" in a) or ("CLAUDE.md" in b)
535
+ a_set = {ln.strip() for ln in a.splitlines()
536
+ if ln.strip() and not ln.strip().startswith("#")}
537
+ b_set = {ln.strip() for ln in b.splitlines()
538
+ if ln.strip() and not ln.strip().startswith("#")}
539
+ union = a_set | b_set
540
+ jaccard = len(a_set & b_set) / len(union) if union else 0.0
541
+ print(f"jaccard={jaccard:.2f} cross_refs={cross}")
542
+ if jaccard >= 0.20:
543
+ print("posture=divergent-overlap (drift risk; consider symlink)")
544
+ status("WARN")
545
+ return
546
+ if cross:
547
+ print("posture=cross-ref (one references the other)")
548
+ status("WARN")
549
+ return
550
+ print("posture=independent")
551
+ status("PASS")
552
+
553
+
554
+ def block_drift_markers(files: list[Path], root: Path) -> None:
555
+ header("DRIFT MARKERS")
556
+ counts: list[tuple[str, int]] = []
557
+ total = 0
558
+ for path in files:
559
+ if path.suffix not in SOURCE_EXTS:
560
+ continue
561
+ text = read_text(path, 200_000)
562
+ if not text:
563
+ continue
564
+ n = len(MARKER_RE.findall(text))
565
+ if n:
566
+ counts.append((rel(path, root), n))
567
+ total += n
568
+ counts.sort(key=lambda x: -x[1])
569
+ for f, n in counts[:5]:
570
+ print(f" {n:>4} {f}")
571
+ print(f"total={total}")
572
+ if total >= DRIFT_FAIL:
573
+ status("FAIL")
574
+ elif total >= DRIFT_WARN:
575
+ status("WARN")
576
+ else:
577
+ status("PASS")
578
+
579
+
580
+ def block_duplicate_setup(root: Path) -> None:
581
+ header("DUPLICATE SETUP SCRIPTS")
582
+ scripts = (list(root.glob("scripts/setup-*.sh"))
583
+ + list(root.glob("scripts/install-*.sh")))
584
+ if len(scripts) < 2:
585
+ print("(fewer than 2 setup-* scripts to compare)")
586
+ status("N/A")
587
+ return
588
+ sets: dict[Path, set[str]] = {}
589
+ for sp in scripts:
590
+ sets[sp] = {ln.strip() for ln in read_text(sp).splitlines()
591
+ if ln.strip() and not ln.strip().startswith("#")}
592
+ pairs: list[tuple[str, str, float]] = []
593
+ names = list(sets.keys())
594
+ for i, a in enumerate(names):
595
+ for b in names[i + 1:]:
596
+ union = sets[a] | sets[b]
597
+ if not union:
598
+ continue
599
+ j = len(sets[a] & sets[b]) / len(union)
600
+ if j >= DUP_JACCARD:
601
+ pairs.append((rel(a, root), rel(b, root), j))
602
+ if not pairs:
603
+ print("(no setup pairs with jaccard >= 0.70)")
604
+ status("PASS")
605
+ return
606
+ for a, b, j in pairs:
607
+ print(f" {a} vs {b} jaccard={j:.2f}")
608
+ status("WARN")
609
+
610
+
611
+ def block_denylist_in_build(root: Path) -> None:
612
+ header("DENYLIST IN BUILD")
613
+ targets = (list(root.glob("scripts/package*.sh"))
614
+ + list(root.glob("scripts/release*.sh"))
615
+ + [root / "Makefile", root / "Justfile"])
616
+ real_targets = [p for p in targets if p.is_file()]
617
+ if not real_targets:
618
+ print("(no build scripts present)")
619
+ status("N/A")
620
+ return
621
+ hits: list[tuple[str, int, str]] = []
622
+ for path in real_targets:
623
+ text = read_text(path, 100_000)
624
+ for i, line in enumerate(text.splitlines(), start=1):
625
+ if DENYLIST_HINT_RE.search(line):
626
+ hits.append((rel(path, root), i, line.strip()[:80]))
627
+ if not hits:
628
+ print("(no denylist patterns found in build scripts)")
629
+ status("PASS")
630
+ return
631
+ for f, ln, s in hits[:20]:
632
+ print(f" {f}:{ln} {s}")
633
+ status("WARN")
634
+
635
+
636
+ def main() -> int:
637
+ parser = argparse.ArgumentParser(description=__doc__)
638
+ parser.add_argument(
639
+ "--root", type=Path, default=Path.cwd(),
640
+ help="Project root to audit (default: current working directory)",
641
+ )
642
+ args = parser.parse_args()
643
+ root = args.root.resolve()
644
+ if not root.is_dir():
645
+ print(f"audit_signals: not a directory: {root}", file=sys.stderr)
646
+ return 2
647
+ files = iter_files(root)
648
+ print(f"project_root: {root}")
649
+ print(f"files_scanned: {len(files)}")
650
+ print()
651
+ block_hotspots(files, root); print()
652
+ block_heredoc(files, root); print()
653
+ block_test_ci(files, root); print()
654
+ block_cli_contract_surface(files, root); print()
655
+ block_version_sources(root); print()
656
+ block_packaging_posture(root); print()
657
+ block_install_url(root); print()
658
+ block_agent_doc_dedup(root); print()
659
+ block_drift_markers(files, root); print()
660
+ block_duplicate_setup(root); print()
661
+ block_denylist_in_build(root)
662
+ return 0
663
+
664
+
665
+ if __name__ == "__main__":
666
+ sys.exit(main())
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Auto-detect and run project verification (lint + typecheck + tests).
3
+ # Run from the project root. Exits non-zero on failure.
4
+ set -euo pipefail
5
+
6
+ if [ -f Cargo.toml ]; then
7
+ cargo check && cargo test
8
+ elif [ -f tsconfig.json ]; then
9
+ npx tsc --noEmit && npm test
10
+ elif [ -f package.json ] && grep -q '"test"' package.json; then
11
+ npm test
12
+ elif [ -f Makefile ] && grep -q '^test:' Makefile; then
13
+ make test
14
+ elif [ -f pytest.ini ] || [ -f pyproject.toml ] || find . -maxdepth 2 -name "test_*.py" | grep -q .; then
15
+ pytest
16
+ else
17
+ echo "(no test command detected - ask the user for the verification command)"
18
+ exit 1
19
+ fi