@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.
- package/AGENTS.md +16 -0
- package/Makefile +40 -0
- package/README.md +3 -0
- package/UPGRADE.md +61 -0
- package/agentic +1236 -13
- package/areas/software/full-stack/AGENTS.md +1 -4
- package/areas/software/full-stack/workflows/debug-issue.md +2 -2
- package/docs/agentic-lifecycle.md +114 -0
- package/docs/agentic-token-minimization/README.md +81 -0
- package/docs/agentic-usage.md +157 -0
- package/docs/catalog.schema.json +203 -0
- package/docs/guidance-updates/2026-04-10-software-devops-best-practices.md +26 -0
- package/docs/opencode_prepare_agents.md +40 -0
- package/docs/opencode_setup.md +48 -0
- package/docs/prompt-format.md +80 -0
- package/docs/site/README.md +44 -0
- package/docs/site/app.js +127 -0
- package/docs/site/catalog.json +5002 -0
- package/docs/site/index.html +52 -0
- package/docs/site/styles.css +177 -0
- package/extensions/codex/agents/developer.toml +1 -1
- package/extensions/codex/agents/devops-engineer.toml +1 -1
- package/extensions/codex/agents/product-owner.toml +1 -1
- package/extensions/codex/agents/team-lead.toml +1 -1
- package/extensions/opencode/plugins/model-checker.json +2 -3
- package/extensions/opencode/plugins/model-checker.ts +23 -0
- package/extensions/opencode/plugins/telegram-notification.ts +33 -5
- package/package.json +6 -2
- package/scripts/assess_area_quality.py +216 -0
- package/scripts/build_docs_catalog.py +283 -0
- package/scripts/lint_prompts.py +113 -0
- package/areas/software/full-stack/skills/bash-pro/SKILL.md +0 -310
- package/areas/software/full-stack/skills/python-pro/SKILL.md +0 -158
- package/areas/software/full-stack/skills/skill-creator/LICENSE.txt +0 -202
- package/areas/software/full-stack/skills/skill-creator/SKILL.md +0 -356
- package/areas/software/full-stack/skills/skill-creator/references/output-patterns.md +0 -82
- package/areas/software/full-stack/skills/skill-creator/references/workflows.md +0 -28
- package/areas/software/full-stack/skills/skill-creator/scripts/init_skill.py +0 -303
- package/areas/software/full-stack/skills/skill-creator/scripts/package_skill.py +0 -110
- package/areas/software/full-stack/skills/skill-creator/scripts/quick_validate.py +0 -95
- package/extensions/codex/skills/babysit-pr/SKILL.md +0 -187
- package/extensions/codex/skills/babysit-pr/agents/openai.yaml +0 -4
- package/extensions/codex/skills/babysit-pr/references/github-api-notes.md +0 -72
- package/extensions/codex/skills/babysit-pr/references/heuristics.md +0 -58
- package/extensions/codex/skills/babysit-pr/scripts/gh_pr_watch.py +0 -806
- package/extensions/codex/skills/babysit-pr/scripts/test_gh_pr_watch.py +0 -155
- package/extensions/opencode/skills/code_review_expert/SKILL.md +0 -144
- package/extensions/opencode/skills/design_expert/SKILL.md +0 -42
- 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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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":
|
|
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
|
|
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
|
-
"
|
|
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())
|