@jetrabbits/agentic 0.0.4 → 0.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 (49) hide show
  1. package/AGENTS.md +16 -0
  2. package/Makefile +40 -0
  3. package/README.md +3 -0
  4. package/UPGRADE.md +61 -0
  5. package/agentic +1236 -13
  6. package/areas/software/full-stack/AGENTS.md +1 -4
  7. package/areas/software/full-stack/workflows/debug-issue.md +2 -2
  8. package/docs/agentic-lifecycle.md +114 -0
  9. package/docs/agentic-token-minimization/README.md +81 -0
  10. package/docs/agentic-usage.md +157 -0
  11. package/docs/catalog.schema.json +203 -0
  12. package/docs/guidance-updates/2026-04-10-software-devops-best-practices.md +26 -0
  13. package/docs/opencode_prepare_agents.md +40 -0
  14. package/docs/opencode_setup.md +48 -0
  15. package/docs/prompt-format.md +80 -0
  16. package/docs/site/README.md +44 -0
  17. package/docs/site/app.js +127 -0
  18. package/docs/site/catalog.json +5002 -0
  19. package/docs/site/index.html +52 -0
  20. package/docs/site/styles.css +177 -0
  21. package/extensions/codex/agents/developer.toml +1 -1
  22. package/extensions/codex/agents/devops-engineer.toml +1 -1
  23. package/extensions/codex/agents/product-owner.toml +1 -1
  24. package/extensions/codex/agents/team-lead.toml +1 -1
  25. package/extensions/opencode/plugins/model-checker.json +2 -3
  26. package/extensions/opencode/plugins/model-checker.ts +23 -0
  27. package/extensions/opencode/plugins/telegram-notification.ts +33 -5
  28. package/package.json +6 -2
  29. package/scripts/assess_area_quality.py +216 -0
  30. package/scripts/build_docs_catalog.py +283 -0
  31. package/scripts/lint_prompts.py +113 -0
  32. package/areas/software/full-stack/skills/bash-pro/SKILL.md +0 -310
  33. package/areas/software/full-stack/skills/python-pro/SKILL.md +0 -158
  34. package/areas/software/full-stack/skills/skill-creator/LICENSE.txt +0 -202
  35. package/areas/software/full-stack/skills/skill-creator/SKILL.md +0 -356
  36. package/areas/software/full-stack/skills/skill-creator/references/output-patterns.md +0 -82
  37. package/areas/software/full-stack/skills/skill-creator/references/workflows.md +0 -28
  38. package/areas/software/full-stack/skills/skill-creator/scripts/init_skill.py +0 -303
  39. package/areas/software/full-stack/skills/skill-creator/scripts/package_skill.py +0 -110
  40. package/areas/software/full-stack/skills/skill-creator/scripts/quick_validate.py +0 -95
  41. package/extensions/codex/skills/babysit-pr/SKILL.md +0 -187
  42. package/extensions/codex/skills/babysit-pr/agents/openai.yaml +0 -4
  43. package/extensions/codex/skills/babysit-pr/references/github-api-notes.md +0 -72
  44. package/extensions/codex/skills/babysit-pr/references/heuristics.md +0 -58
  45. package/extensions/codex/skills/babysit-pr/scripts/gh_pr_watch.py +0 -806
  46. package/extensions/codex/skills/babysit-pr/scripts/test_gh_pr_watch.py +0 -155
  47. package/extensions/opencode/skills/code_review_expert/SKILL.md +0 -144
  48. package/extensions/opencode/skills/design_expert/SKILL.md +0 -42
  49. package/extensions/opencode/skills/qa_expert/SKILL.md +0 -116
@@ -0,0 +1,52 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agent Guides Docs</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="site-header">
11
+ <div class="site-header__inner">
12
+ <div class="site-header__summary">
13
+ <div class="site-header__brand">
14
+ <p class="site-header__eyebrow">Docs</p>
15
+ <div>
16
+ <a class="site-header__title" href="https://github.com/sawrus/agent-guides">Agent Guides Docs</a>
17
+ <a class="site-header__github" href="https://github.com/sawrus/agent-guides">
18
+ Star on GitHub to support the project
19
+ </a>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="install-box" aria-label="Agentic install example">
24
+ <span class="install-box__label">Install agentic</span>
25
+ <code>npx @jetrabbits/agentic@latest</code>
26
+ <code>curl -fsSL https://raw.githubusercontent.com/sawrus/agent-guides/main/install | bash</code>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="toolbar" aria-label="Search docs">
31
+ <input id="search" type="search" placeholder="Search trigger, workflow, examples..." />
32
+ <select id="language" aria-label="Language">
33
+ <option value="both">EN + RU</option>
34
+ <option value="en">EN only</option>
35
+ <option value="ru">RU only</option>
36
+ </select>
37
+ </div>
38
+ </div>
39
+ </header>
40
+
41
+ <main>
42
+ <aside id="menu"></aside>
43
+ <section id="content">
44
+ <p>Loading catalog…</p>
45
+ </section>
46
+ </main>
47
+
48
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
49
+ <script src="https://cdn.jsdelivr.net/npm/lunr/lunr.min.js"></script>
50
+ <script src="./app.js" type="module"></script>
51
+ </body>
52
+ </html>
@@ -0,0 +1,177 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #0f172a;
4
+ --fg: #e2e8f0;
5
+ --muted: #94a3b8;
6
+ --accent: #22c55e;
7
+ --panel: rgba(15, 23, 42, 0.82);
8
+ --panel-strong: #1e293b;
9
+ --border: #334155;
10
+ --code-bg: #020617;
11
+ }
12
+ body {
13
+ margin: 0;
14
+ font-family: Inter, system-ui, sans-serif;
15
+ background: var(--bg);
16
+ color: var(--fg);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+ .site-header {
22
+ position: sticky;
23
+ top: 0;
24
+ z-index: 20;
25
+ padding: 0.75rem 1rem;
26
+ border-bottom: 1px solid var(--border);
27
+ background: var(--panel);
28
+ backdrop-filter: blur(14px);
29
+ }
30
+ .site-header__inner {
31
+ display: grid;
32
+ grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
33
+ gap: 0.9rem;
34
+ align-items: center;
35
+ }
36
+ .site-header__summary {
37
+ display: grid;
38
+ grid-template-columns: minmax(0, 220px) minmax(0, 1fr);
39
+ gap: 0.9rem;
40
+ align-items: center;
41
+ min-width: 0;
42
+ }
43
+ .site-header__brand {
44
+ display: flex;
45
+ gap: 0.75rem;
46
+ align-items: flex-start;
47
+ min-width: 0;
48
+ }
49
+ .site-header__eyebrow {
50
+ margin: 0.2rem 0 0;
51
+ font-size: 0.68rem;
52
+ line-height: 1;
53
+ letter-spacing: 0.12em;
54
+ text-transform: uppercase;
55
+ color: var(--muted);
56
+ }
57
+ .site-header__title,
58
+ .site-header__github {
59
+ display: inline-block;
60
+ text-decoration: none;
61
+ }
62
+ .site-header__title {
63
+ color: var(--fg);
64
+ font-size: 0.95rem;
65
+ font-weight: 700;
66
+ }
67
+ .site-header__title:hover,
68
+ .site-header__github:hover {
69
+ color: var(--accent);
70
+ }
71
+ .site-header__github {
72
+ margin-top: 0.18rem;
73
+ color: var(--muted);
74
+ font-size: 0.78rem;
75
+ }
76
+ .install-box {
77
+ display: grid;
78
+ gap: 0.35rem;
79
+ min-width: 0;
80
+ }
81
+ .install-box__label {
82
+ font-size: 0.7rem;
83
+ line-height: 1;
84
+ letter-spacing: 0.1em;
85
+ text-transform: uppercase;
86
+ color: var(--muted);
87
+ }
88
+ .install-box code {
89
+ display: block;
90
+ padding: 0.45rem 0.65rem;
91
+ border: 1px solid var(--border);
92
+ border-radius: 10px;
93
+ background: rgba(2, 6, 23, 0.75);
94
+ font-size: 0.75rem;
95
+ line-height: 1.35;
96
+ overflow-wrap: anywhere;
97
+ white-space: normal;
98
+ }
99
+ main {
100
+ flex: 1;
101
+ min-height: 0;
102
+ display: grid;
103
+ grid-template-columns: 320px minmax(0, 1fr);
104
+ }
105
+ #menu {
106
+ border-right: 1px solid var(--border);
107
+ overflow: auto;
108
+ padding: 1rem;
109
+ }
110
+ #content {
111
+ padding: 1rem 1.25rem;
112
+ overflow: auto;
113
+ }
114
+ .toolbar {
115
+ display: flex;
116
+ gap: 0.5rem;
117
+ align-items: center;
118
+ justify-content: flex-end;
119
+ }
120
+ .toolbar input {
121
+ flex: 1 1 220px;
122
+ min-width: 0;
123
+ }
124
+ .toolbar select {
125
+ flex: 0 0 auto;
126
+ }
127
+ input, select {
128
+ background: var(--panel-strong);
129
+ color: var(--fg);
130
+ border: 1px solid var(--border);
131
+ border-radius: 10px;
132
+ padding: 0.5rem 0.75rem;
133
+ }
134
+ .area-title { font-size: .85rem; text-transform: uppercase; color: var(--muted); margin: 1rem 0 .4rem; }
135
+ .wf-btn { width: 100%; text-align: left; margin-bottom: .4rem; background: var(--panel-strong); color: var(--fg); border: 1px solid var(--border); border-radius: 8px; padding: .5rem; cursor: pointer; }
136
+ .wf-btn:hover { border-color: var(--accent); }
137
+ code, pre { background: var(--code-bg); border-radius: 8px; }
138
+ .chip { display:inline-block; border:1px solid var(--border); border-radius:999px; padding:.15rem .5rem; margin:.15rem; color:var(--muted); font-size:.82rem; }
139
+ .meta { color: var(--muted); }
140
+
141
+ @media (max-width: 980px) {
142
+ .site-header__inner {
143
+ grid-template-columns: 1fr;
144
+ }
145
+ .site-header__summary {
146
+ grid-template-columns: 1fr;
147
+ align-items: start;
148
+ }
149
+ .toolbar {
150
+ justify-content: stretch;
151
+ }
152
+ main {
153
+ grid-template-columns: 280px minmax(0, 1fr);
154
+ }
155
+ }
156
+
157
+ @media (max-width: 720px) {
158
+ .site-header {
159
+ padding: 0.7rem 0.8rem;
160
+ }
161
+ .toolbar {
162
+ flex-wrap: wrap;
163
+ }
164
+ .toolbar input,
165
+ .toolbar select {
166
+ width: 100%;
167
+ flex-basis: 100%;
168
+ }
169
+ main {
170
+ grid-template-columns: 1fr;
171
+ }
172
+ #menu {
173
+ border-right: 0;
174
+ border-bottom: 1px solid var(--border);
175
+ max-height: 32vh;
176
+ }
177
+ }
@@ -1,6 +1,6 @@
1
1
  name = "developer"
2
2
  description = "Use this agent for feature implementation, bug fixes, writing tests, and code delivery after scope and architecture are approved."
3
- model = "gpt-5-codex"
3
+ model = "gpt-5.5"
4
4
  model_reasoning_effort = "high"
5
5
  sandbox_mode = "workspace-write"
6
6
  developer_instructions = """
@@ -1,6 +1,6 @@
1
1
  name = "devops-engineer"
2
2
  description = "Use this agent for CI/CD pipelines, infrastructure-as-code, deployment automation, container configuration, secrets management, and observability setup."
3
- model = "gpt-5-codex"
3
+ model = "gpt-5.5"
4
4
  model_reasoning_effort = "high"
5
5
  sandbox_mode = "workspace-write"
6
6
  developer_instructions = """
@@ -1,6 +1,6 @@
1
1
  name = "product-owner"
2
2
  description = "Use this agent to define scope, write acceptance criteria, orchestrate the SDLC handoff order, and make final acceptance decisions."
3
- model = "gpt-5.4"
3
+ model = "gpt-5.5"
4
4
  model_reasoning_effort = "high"
5
5
  sandbox_mode = "read-only"
6
6
  developer_instructions = """
@@ -1,6 +1,6 @@
1
1
  name = "team-lead"
2
2
  description = "Use this agent for technical strategy, architecture decisions, code review, planning, and pre-release technical sign-off."
3
- model = "gpt-5.4"
3
+ model = "gpt-5.5"
4
4
  model_reasoning_effort = "high"
5
5
  sandbox_mode = "read-only"
6
6
  developer_instructions = """
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "models": [
3
- "openai/gpt-5.3-codex",
4
- "openai/gpt-5.4",
3
+ "openai/gpt-5.5",
5
4
  "opencode/big-pickle",
6
5
  "opencode/minimax-m2.5-free",
7
6
  "google/antigravity-claude-opus-4-6-thinking",
@@ -9,6 +8,6 @@
9
8
  "google/antigravity-gemini-3-flash",
10
9
  "google/antigravity-gemini-3.1-pro"
11
10
  ],
12
- "timeoutMs": 10000,
11
+ "timeoutMs": 2000,
13
12
  "concurrency": 10
14
13
  }
@@ -1,5 +1,6 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import { readFileSync } from "node:fs"
3
4
  import { join } from "node:path"
4
5
  import { tmpdir } from "node:os"
5
6
  import { spawn } from "node:child_process"
@@ -25,6 +26,23 @@ type CommandResult = {
25
26
  durationMs: number
26
27
  }
27
28
 
29
+ type AgenticPluginConfig = {
30
+ modelChecker?: {
31
+ enabled?: boolean
32
+ }
33
+ }
34
+
35
+ function readAgenticConfig(): AgenticPluginConfig {
36
+ const configHome = process.env.XDG_CONFIG_HOME || join(process.env.HOME || "", ".config")
37
+ const configPath = join(configHome, "agentic", "opencode-plugins.json")
38
+
39
+ try {
40
+ return JSON.parse(readFileSync(configPath, "utf-8")) as AgenticPluginConfig
41
+ } catch {
42
+ return {}
43
+ }
44
+ }
45
+
28
46
  async function readModelsJson(projectDir: string): Promise<ModelCheckerConfig> {
29
47
  const defaults: ModelCheckerConfig = {
30
48
  models: [],
@@ -231,6 +249,11 @@ async function regenerateOpencodeJson(projectDir: string, selected: string, pass
231
249
  }
232
250
 
233
251
  export const ModelCheckerPlugin: Plugin = async ({ directory }) => {
252
+ const config = readAgenticConfig()
253
+ if (!config.modelChecker?.enabled) {
254
+ return {}
255
+ }
256
+
234
257
  if (process.env.OPENCODE_MODEL_CHECKER_ACTIVE === "1") {
235
258
  return {}
236
259
  }
@@ -1,14 +1,42 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ type AgenticPluginConfig = {
6
+ telegram?: {
7
+ enabled?: boolean
8
+ botToken?: string
9
+ chatId?: string
10
+ }
11
+ }
12
+
13
+ function readAgenticConfig(): AgenticPluginConfig {
14
+ const configHome = process.env.XDG_CONFIG_HOME || join(process.env.HOME || "", ".config")
15
+ const configPath = join(configHome, "agentic", "opencode-plugins.json")
16
+
17
+ try {
18
+ return JSON.parse(readFileSync(configPath, "utf-8")) as AgenticPluginConfig
19
+ } catch {
20
+ return {}
21
+ }
22
+ }
2
23
 
3
24
  export const TelegramNotificationPlugin: Plugin = async ({ $, client, directory }) => {
25
+ const config = readAgenticConfig()
26
+ const telegram = config.telegram
27
+ const botToken = process.env.OPENCODE_TELEGRAM_BOT_TOKEN || telegram?.botToken
28
+ const chatId = process.env.OPENCODE_TELEGRAM_CHAT_ID || telegram?.chatId
29
+
30
+ if (!telegram?.enabled || !botToken || !chatId) {
31
+ return {}
32
+ }
33
+
34
+ process.env.OPENCODE_TELEGRAM_BOT_TOKEN = botToken
35
+ process.env.OPENCODE_TELEGRAM_CHAT_ID = chatId
36
+
4
37
  return {
5
38
  event: async ({ event }) => {
6
39
  if (event.type === "session.idle") {
7
- const botToken = process.env.OPENCODE_TELEGRAM_BOT_TOKEN
8
- const chatId = process.env.OPENCODE_TELEGRAM_CHAT_ID
9
-
10
- if (!botToken || !chatId) return
11
-
12
40
  const sessionID = event.properties.sessionID
13
41
  let messageText = "✅ Задача завершена"
14
42
  let fullText = ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetrabbits/agentic",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Agent Intelligence Configuration CLI",
5
5
  "bin": {
6
6
  "agentic": "bin/agentic.js"
@@ -11,10 +11,14 @@
11
11
  },
12
12
  "files": [
13
13
  "AGENTS.md",
14
+ "UPGRADE.md",
14
15
  "LICENSE",
16
+ "Makefile",
15
17
  "agentic",
16
18
  "areas",
17
- "extensions"
19
+ "docs",
20
+ "extensions",
21
+ "scripts"
18
22
  ],
19
23
  "license": "MIT"
20
24
  }
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ from dataclasses import dataclass, asdict
8
+ from pathlib import Path
9
+ from typing import Iterable
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ AREAS = ROOT / "areas"
13
+
14
+ FRONTMATTER = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
15
+ LIST_ITEM = re.compile(r"^\s*-\s+(.+?)\s*$", re.MULTILINE)
16
+
17
+
18
+ @dataclass
19
+ class Finding:
20
+ severity: str
21
+ message: str
22
+ path: str
23
+
24
+
25
+ @dataclass
26
+ class Score:
27
+ specialization: str
28
+ environment: str
29
+ score: int
30
+ dimensions: dict[str, int]
31
+ findings: list[Finding]
32
+
33
+
34
+ def read(path: Path) -> str:
35
+ return path.read_text(encoding="utf-8")
36
+
37
+
38
+ def frontmatter(path: Path) -> str:
39
+ match = FRONTMATTER.match(read(path))
40
+ return match.group(1) if match else ""
41
+
42
+
43
+ def yaml_list(front: str, key: str) -> list[str]:
44
+ match = re.search(rf"^{re.escape(key)}:\s*\n((?:\s*-\s.*\n?)+)", front, re.MULTILINE)
45
+ if not match:
46
+ return []
47
+ return [x.strip().strip("'\"") for x in LIST_ITEM.findall(match.group(1))]
48
+
49
+
50
+ def has_key(front: str, key: str) -> bool:
51
+ return re.search(rf"^{re.escape(key)}:\s*.+$", front, re.MULTILINE) is not None
52
+
53
+
54
+ def spec_dirs() -> Iterable[Path]:
55
+ for area in sorted(AREAS.iterdir()):
56
+ if not area.is_dir() or area.name == "template":
57
+ continue
58
+ for spec in sorted(area.iterdir()):
59
+ if spec.is_dir():
60
+ yield spec
61
+
62
+
63
+ def bounded(value: int) -> int:
64
+ return max(0, min(100, value))
65
+
66
+
67
+ def assess_spec(spec: Path, environment: str) -> Score:
68
+ rel_spec = spec.relative_to(ROOT).as_posix()
69
+ findings: list[Finding] = []
70
+ dimensions = {
71
+ "structure": 100,
72
+ "reference_integrity": 100,
73
+ "sdlc_coverage": 100,
74
+ "role_quality_gates": 100,
75
+ "prompt_usefulness": 100,
76
+ "environment_compatibility": 100,
77
+ "token_efficiency": 100,
78
+ "documentation_readiness": 100,
79
+ }
80
+
81
+ required_dirs = ["rules", "skills", "workflows", "prompts"]
82
+ if not (spec / "AGENTS.md").exists():
83
+ findings.append(Finding("error", "missing specialization AGENTS.md", rel_spec))
84
+ dimensions["structure"] -= 35
85
+ for name in required_dirs:
86
+ if not (spec / name).is_dir():
87
+ findings.append(Finding("error", f"missing {name}/ directory", rel_spec))
88
+ dimensions["structure"] -= 15
89
+
90
+ skills = {p.parent.name for p in spec.glob("skills/*/SKILL.md")}
91
+ workflows = sorted(spec.glob("workflows/*.md"))
92
+ prompts = sorted(spec.glob("prompts/*.md"))
93
+ rules = sorted(spec.glob("rules/*.md"))
94
+
95
+ if len(skills) > 6:
96
+ findings.append(Finding("warn", f"skill count is {len(skills)}; target is <= 6 for token efficiency", rel_spec))
97
+ dimensions["token_efficiency"] -= min(40, (len(skills) - 6) * 8)
98
+ if len(rules) > 12:
99
+ findings.append(Finding("warn", f"rule count is {len(rules)}; consider consolidating always-loaded guidance", rel_spec))
100
+ dimensions["token_efficiency"] -= min(30, (len(rules) - 12) * 3)
101
+
102
+ workflow_stems = {p.stem for p in workflows}
103
+ prompt_stems = {p.stem for p in prompts}
104
+ missing_prompts = sorted(workflow_stems - prompt_stems)
105
+ if missing_prompts:
106
+ findings.append(Finding("error", f"workflows without matching prompts: {', '.join(missing_prompts)}", rel_spec))
107
+ dimensions["reference_integrity"] -= min(40, len(missing_prompts) * 10)
108
+
109
+ for workflow in workflows:
110
+ rel = workflow.relative_to(ROOT).as_posix()
111
+ front = frontmatter(workflow)
112
+ for key in ["name", "type", "trigger", "description"]:
113
+ if not has_key(front, key):
114
+ findings.append(Finding("error", f"workflow missing `{key}` front matter", rel))
115
+ dimensions["reference_integrity"] -= 5
116
+ if not yaml_list(front, "roles"):
117
+ findings.append(Finding("error", "workflow has no roles", rel))
118
+ dimensions["role_quality_gates"] -= 10
119
+ if not yaml_list(front, "quality-gates"):
120
+ findings.append(Finding("error", "workflow has no quality gates", rel))
121
+ dimensions["role_quality_gates"] -= 15
122
+ for skill in yaml_list(front, "uses-skills"):
123
+ if skill not in skills:
124
+ findings.append(Finding("error", f"uses missing skill `{skill}`", rel))
125
+ dimensions["reference_integrity"] -= 12
126
+
127
+ text = read(workflow).lower()
128
+ for phase in ["input", "actions", "done when"]:
129
+ if phase not in text:
130
+ findings.append(Finding("warn", f"workflow lacks `{phase}` step language", rel))
131
+ dimensions["sdlc_coverage"] -= 5
132
+ if "docs/" not in text and "document" not in text:
133
+ findings.append(Finding("warn", "workflow lacks documentation output or docs reference", rel))
134
+ dimensions["documentation_readiness"] -= 5
135
+
136
+ for prompt in prompts:
137
+ rel = prompt.relative_to(ROOT).as_posix()
138
+ text = read(prompt)
139
+ examples = len(re.findall(r"^##\s*Example\s+\d+", text, re.MULTILINE))
140
+ if examples < 2:
141
+ findings.append(Finding("warn", "prompt has fewer than two examples", rel))
142
+ dimensions["prompt_usefulness"] -= 15
143
+ if "**EN:**" not in text or "**RU:**" not in text:
144
+ findings.append(Finding("warn", "prompt should include EN and RU examples", rel))
145
+ dimensions["prompt_usefulness"] -= 10
146
+
147
+ agent_text = read(spec / "AGENTS.md") if (spec / "AGENTS.md").exists() else ""
148
+ if environment == "opencode" and "skills/*/SKILL.md" not in agent_text:
149
+ findings.append(Finding("warn", "AGENTS.md does not describe skill loading for opencode-compatible layouts", rel_spec))
150
+ dimensions["environment_compatibility"] -= 10
151
+
152
+ dimensions = {k: bounded(v) for k, v in dimensions.items()}
153
+ score = round(sum(dimensions.values()) / len(dimensions))
154
+ return Score(
155
+ specialization=rel_spec,
156
+ environment=environment,
157
+ score=score,
158
+ dimensions=dimensions,
159
+ findings=findings,
160
+ )
161
+
162
+
163
+ def markdown_report(scores: list[Score]) -> str:
164
+ lines = ["# Agentic Area Quality Report", ""]
165
+ for score in scores:
166
+ lines.append(f"## {score.specialization} ({score.environment})")
167
+ lines.append("")
168
+ lines.append(f"Score: **{score.score}/100**")
169
+ lines.append("")
170
+ lines.append("| Dimension | Score |")
171
+ lines.append("|---|---:|")
172
+ for name, value in score.dimensions.items():
173
+ lines.append(f"| {name} | {value} |")
174
+ lines.append("")
175
+ if score.findings:
176
+ lines.append("Findings:")
177
+ for finding in score.findings:
178
+ lines.append(f"- [{finding.severity}] `{finding.path}`: {finding.message}")
179
+ else:
180
+ lines.append("Findings: none")
181
+ lines.append("")
182
+ return "\n".join(lines)
183
+
184
+
185
+ def main() -> int:
186
+ parser = argparse.ArgumentParser()
187
+ parser.add_argument("--environment", default="all", choices=["all", "codex", "opencode", "claude", "gemini", "antigravity", "cursor"])
188
+ parser.add_argument("--json-output", default="reports/area-quality.json")
189
+ parser.add_argument("--markdown-output", default="reports/area-quality.md")
190
+ parser.add_argument("--strict", action="store_true")
191
+ parser.add_argument("--min-score", type=int, default=75)
192
+ args = parser.parse_args()
193
+
194
+ environments = ["codex", "opencode", "claude", "gemini", "antigravity", "cursor"] if args.environment == "all" else [args.environment]
195
+ scores = [assess_spec(spec, env) for spec in spec_dirs() for env in environments]
196
+
197
+ json_path = ROOT / args.json_output
198
+ md_path = ROOT / args.markdown_output
199
+ json_path.parent.mkdir(parents=True, exist_ok=True)
200
+ md_path.parent.mkdir(parents=True, exist_ok=True)
201
+ json_path.write_text(json.dumps([asdict(score) for score in scores], indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
202
+ md_path.write_text(markdown_report(scores), encoding="utf-8")
203
+
204
+ print(f"Wrote {json_path.relative_to(ROOT)}")
205
+ print(f"Wrote {md_path.relative_to(ROOT)}")
206
+
207
+ failing = [score for score in scores if score.score < args.min_score]
208
+ if args.strict and failing:
209
+ for score in failing:
210
+ print(f"{score.specialization} ({score.environment}) scored {score.score}, below {args.min_score}")
211
+ return 1
212
+ return 0
213
+
214
+
215
+ if __name__ == "__main__":
216
+ raise SystemExit(main())