@jetrabbits/agentic 0.0.4 → 0.0.5
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 +9 -0
- package/Makefile +40 -0
- package/UPGRADE.md +61 -0
- package/agentic +948 -10
- 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 +103 -0
- package/docs/agentic-token-minimization/README.md +79 -0
- package/docs/agentic-usage.md +145 -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 +45 -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,283 @@
|
|
|
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 Dict, List, Tuple
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
12
|
+
AREAS_DIR = ROOT / "areas"
|
|
13
|
+
|
|
14
|
+
WORKFLOW_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
15
|
+
TRIGGER_RE = re.compile(r"^trigger:\s*(.+)$", re.MULTILINE)
|
|
16
|
+
NAME_RE = re.compile(r"^name:\s*(.+)$", re.MULTILINE)
|
|
17
|
+
DESC_RE = re.compile(r"^description:\s*(.+)$", re.MULTILINE)
|
|
18
|
+
PROMPT_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
19
|
+
PROMPT_WORKFLOW_RE = re.compile(r"^workflow:\s*([a-z0-9][a-z0-9-]*)\s*$", re.MULTILINE)
|
|
20
|
+
PROMPT_TITLE_RE = re.compile(r"^#\s*Prompt:\s*`?([^`\n]+)`?", re.MULTILINE)
|
|
21
|
+
USE_WHEN_RE = re.compile(r"^Use when:\s*(.+)$", re.MULTILINE)
|
|
22
|
+
EXAMPLE_RE = re.compile(
|
|
23
|
+
r"##\s*Example\s*(\d+)\s*[—-]\s*(.*?)\n.*?\*\*EN:\*\*\s*\n```\n(.*?)\n```\s*\n\s*\*\*RU:\*\*\s*\n```\n(.*?)\n```",
|
|
24
|
+
re.DOTALL,
|
|
25
|
+
)
|
|
26
|
+
COMMAND_REF_RE = re.compile(r"(?m)^/([a-z0-9][a-z0-9-]*)\b")
|
|
27
|
+
PLACEHOLDER_STRINGS = (
|
|
28
|
+
"<project context>",
|
|
29
|
+
"<контекст проекта>",
|
|
30
|
+
"Objective: clearly state",
|
|
31
|
+
"Goal: execute workflow steps end-to-end",
|
|
32
|
+
"Use when: run workflow",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_yaml_list(frontmatter: str, key: str) -> List[str]:
|
|
37
|
+
m = re.search(rf"^{re.escape(key)}:\s*\n((?:\s*-\s.*\n)+)", frontmatter, re.MULTILINE)
|
|
38
|
+
if not m:
|
|
39
|
+
inline = re.search(rf"^{re.escape(key)}:\s*\[(.*?)\]\s*$", frontmatter, re.MULTILINE)
|
|
40
|
+
if not inline:
|
|
41
|
+
return []
|
|
42
|
+
return [x.strip().strip("'\"") for x in inline.group(1).split(",") if x.strip()]
|
|
43
|
+
return [line.strip()[1:].strip().strip("'\"") for line in m.group(1).splitlines() if line.strip().startswith("-")]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _area_and_stem(path: Path) -> tuple[str, str]:
|
|
47
|
+
rel = path.relative_to(ROOT)
|
|
48
|
+
parts = rel.parts
|
|
49
|
+
return "/".join(parts[1:3]), path.stem
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_skill_paths(area: str, uses_skills: List[str]) -> List[dict]:
|
|
53
|
+
base = ROOT / "areas" / area / "skills"
|
|
54
|
+
resolved: List[dict] = []
|
|
55
|
+
for name in uses_skills:
|
|
56
|
+
c1 = base / name / "SKILL.md"
|
|
57
|
+
c2 = base / f"{name}.md"
|
|
58
|
+
path = c1 if c1.exists() else c2 if c2.exists() else None
|
|
59
|
+
resolved.append({"name": name, "path": str(path.relative_to(ROOT)) if path else None})
|
|
60
|
+
return resolved
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Workflow:
|
|
65
|
+
key: str
|
|
66
|
+
area: str
|
|
67
|
+
stem: str
|
|
68
|
+
trigger: str
|
|
69
|
+
name: str
|
|
70
|
+
description: str
|
|
71
|
+
path: str
|
|
72
|
+
inputs: List[str]
|
|
73
|
+
outputs: List[str]
|
|
74
|
+
roles: List[str]
|
|
75
|
+
related_rules: List[str]
|
|
76
|
+
uses_skills: List[str]
|
|
77
|
+
quality_gates: List[str]
|
|
78
|
+
skill_refs: List[dict]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class Example:
|
|
83
|
+
number: int
|
|
84
|
+
title: str
|
|
85
|
+
en: str
|
|
86
|
+
ru: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Prompt:
|
|
91
|
+
key: str
|
|
92
|
+
area: str
|
|
93
|
+
stem: str
|
|
94
|
+
workflow: str
|
|
95
|
+
trigger: str
|
|
96
|
+
path: str
|
|
97
|
+
use_when: str
|
|
98
|
+
examples: List[Example]
|
|
99
|
+
command_refs: List[str]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_workflow(path: Path) -> Workflow:
|
|
103
|
+
text = path.read_text(encoding="utf-8")
|
|
104
|
+
m = WORKFLOW_RE.search(text)
|
|
105
|
+
if not m:
|
|
106
|
+
raise ValueError(f"No YAML frontmatter: {path}")
|
|
107
|
+
front = m.group(1)
|
|
108
|
+
trig = TRIGGER_RE.search(front)
|
|
109
|
+
name = NAME_RE.search(front)
|
|
110
|
+
desc = DESC_RE.search(front)
|
|
111
|
+
area, stem = _area_and_stem(path)
|
|
112
|
+
uses_skills = _parse_yaml_list(front, "uses-skills")
|
|
113
|
+
|
|
114
|
+
return Workflow(
|
|
115
|
+
key=f"{area}:{stem}",
|
|
116
|
+
area=area,
|
|
117
|
+
stem=stem,
|
|
118
|
+
trigger=trig.group(1).strip() if trig else "",
|
|
119
|
+
name=name.group(1).strip() if name else path.stem,
|
|
120
|
+
description=desc.group(1).strip() if desc else "",
|
|
121
|
+
path=str(path.relative_to(ROOT)),
|
|
122
|
+
inputs=_parse_yaml_list(front, "inputs"),
|
|
123
|
+
outputs=_parse_yaml_list(front, "outputs"),
|
|
124
|
+
roles=_parse_yaml_list(front, "roles"),
|
|
125
|
+
related_rules=_parse_yaml_list(front, "related-rules"),
|
|
126
|
+
uses_skills=uses_skills,
|
|
127
|
+
quality_gates=_parse_yaml_list(front, "quality-gates"),
|
|
128
|
+
skill_refs=_resolve_skill_paths(area, uses_skills),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def parse_prompt(path: Path) -> Prompt:
|
|
133
|
+
text = path.read_text(encoding="utf-8")
|
|
134
|
+
frontmatter = PROMPT_FRONTMATTER_RE.match(text)
|
|
135
|
+
workflow = ""
|
|
136
|
+
if frontmatter:
|
|
137
|
+
workflow_match = PROMPT_WORKFLOW_RE.search(frontmatter.group(1))
|
|
138
|
+
if workflow_match:
|
|
139
|
+
workflow = workflow_match.group(1).strip()
|
|
140
|
+
t = PROMPT_TITLE_RE.search(text)
|
|
141
|
+
uw = USE_WHEN_RE.search(text)
|
|
142
|
+
examples = [
|
|
143
|
+
Example(number=int(m.group(1)), title=m.group(2).strip(), en=m.group(3).strip(), ru=m.group(4).strip())
|
|
144
|
+
for m in EXAMPLE_RE.finditer(text)
|
|
145
|
+
]
|
|
146
|
+
area, stem = _area_and_stem(path)
|
|
147
|
+
refs = sorted({m.group(1) for m in COMMAND_REF_RE.finditer(text)})
|
|
148
|
+
return Prompt(
|
|
149
|
+
key=f"{area}:{stem}",
|
|
150
|
+
area=area,
|
|
151
|
+
stem=stem,
|
|
152
|
+
workflow=workflow,
|
|
153
|
+
trigger=t.group(1).strip() if t else "",
|
|
154
|
+
path=str(path.relative_to(ROOT)),
|
|
155
|
+
use_when=uw.group(1).strip() if uw else "",
|
|
156
|
+
examples=examples,
|
|
157
|
+
command_refs=refs,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _match_prompt_to_workflow(prompt: Prompt, workflows_by_area: Dict[str, Dict[str, Workflow]]) -> Workflow | None:
|
|
162
|
+
area_wf = workflows_by_area.get(prompt.area, {})
|
|
163
|
+
if prompt.workflow:
|
|
164
|
+
return area_wf.get(prompt.workflow)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def build_catalog(validate: bool = False) -> Tuple[dict, List[str]]:
|
|
169
|
+
workflows_by_area: Dict[str, Dict[str, Workflow]] = {}
|
|
170
|
+
prompts: List[Prompt] = []
|
|
171
|
+
problems: List[str] = []
|
|
172
|
+
prompts_by_workflow: Dict[tuple[str, str], Prompt] = {}
|
|
173
|
+
|
|
174
|
+
for path in sorted(AREAS_DIR.glob("**/workflows/*.md")):
|
|
175
|
+
wf = parse_workflow(path)
|
|
176
|
+
workflows_by_area.setdefault(wf.area, {})[wf.stem] = wf
|
|
177
|
+
if not wf.trigger:
|
|
178
|
+
problems.append(f"workflow missing trigger: {wf.path}")
|
|
179
|
+
|
|
180
|
+
for path in sorted(AREAS_DIR.glob("**/prompts/*.md")):
|
|
181
|
+
pr = parse_prompt(path)
|
|
182
|
+
prompts.append(pr)
|
|
183
|
+
if not pr.trigger:
|
|
184
|
+
problems.append(f"prompt missing trigger: {pr.path}")
|
|
185
|
+
if not pr.workflow:
|
|
186
|
+
problems.append(f"prompt missing front matter workflow key: {pr.path}")
|
|
187
|
+
if pr.workflow and pr.stem != pr.workflow:
|
|
188
|
+
problems.append(f"prompt filename must match workflow stem: {pr.path}")
|
|
189
|
+
if pr.workflow and pr.trigger != f"/{pr.workflow}":
|
|
190
|
+
problems.append(f"prompt header must match workflow stem: {pr.path}")
|
|
191
|
+
if not pr.examples:
|
|
192
|
+
problems.append(f"prompt has no EN/RU examples: {pr.path}")
|
|
193
|
+
if len(pr.examples) < 2 or len(pr.examples) > 3:
|
|
194
|
+
problems.append(f"prompt example count must be 2 or 3: {pr.path}")
|
|
195
|
+
if "Workflow link command:" in path.read_text(encoding="utf-8"):
|
|
196
|
+
problems.append(f"legacy workflow link block not allowed: {pr.path}")
|
|
197
|
+
if any(token in path.read_text(encoding="utf-8") for token in PLACEHOLDER_STRINGS):
|
|
198
|
+
problems.append(f"placeholder scaffold text not allowed: {pr.path}")
|
|
199
|
+
if pr.workflow and pr.command_refs and set(pr.command_refs) != {pr.workflow}:
|
|
200
|
+
problems.append(f"prompt contains non-canonical slash commands: {pr.path}")
|
|
201
|
+
if pr.workflow:
|
|
202
|
+
key = (pr.area, pr.workflow)
|
|
203
|
+
if key in prompts_by_workflow:
|
|
204
|
+
problems.append(f"multiple prompts mapped to workflow {pr.area}/{pr.workflow}: {pr.path}")
|
|
205
|
+
else:
|
|
206
|
+
prompts_by_workflow[key] = pr
|
|
207
|
+
|
|
208
|
+
matched_prompt_keys: set[str] = set()
|
|
209
|
+
prompt_by_workflow_key: Dict[str, Prompt] = {}
|
|
210
|
+
for pr in prompts:
|
|
211
|
+
wf = _match_prompt_to_workflow(pr, workflows_by_area)
|
|
212
|
+
if wf:
|
|
213
|
+
prompt_by_workflow_key[wf.key] = pr
|
|
214
|
+
matched_prompt_keys.add(pr.key)
|
|
215
|
+
elif validate:
|
|
216
|
+
problems.append(f"prompt not matched to workflow via front matter: {pr.path}")
|
|
217
|
+
|
|
218
|
+
if validate:
|
|
219
|
+
all_workflows = [wf for area in workflows_by_area.values() for wf in area.values()]
|
|
220
|
+
for wf in all_workflows:
|
|
221
|
+
if wf.key not in prompt_by_workflow_key:
|
|
222
|
+
problems.append(f"workflow has no matched prompt: {wf.path}")
|
|
223
|
+
|
|
224
|
+
areas_out: Dict[str, dict] = {}
|
|
225
|
+
all_workflows = [wf for area in workflows_by_area.values() for wf in area.values()]
|
|
226
|
+
for wf in sorted(all_workflows, key=lambda x: (x.area, x.stem)):
|
|
227
|
+
pr = prompt_by_workflow_key.get(wf.key)
|
|
228
|
+
bucket = areas_out.setdefault(wf.area, {"area": wf.area, "workflows": []})
|
|
229
|
+
bucket["workflows"].append(
|
|
230
|
+
{
|
|
231
|
+
"trigger": wf.trigger,
|
|
232
|
+
"name": wf.name,
|
|
233
|
+
"description": wf.description,
|
|
234
|
+
"workflow_path": wf.path,
|
|
235
|
+
"prompt_path": pr.path if pr else None,
|
|
236
|
+
"prompt_command_refs": pr.command_refs if pr else [],
|
|
237
|
+
"use_when": pr.use_when if pr else "",
|
|
238
|
+
"inputs": wf.inputs,
|
|
239
|
+
"outputs": wf.outputs,
|
|
240
|
+
"roles": wf.roles,
|
|
241
|
+
"related_rules": wf.related_rules,
|
|
242
|
+
"uses_skills": wf.uses_skills,
|
|
243
|
+
"skill_refs": wf.skill_refs,
|
|
244
|
+
"quality_gates": wf.quality_gates,
|
|
245
|
+
"examples": {"both": [asdict(e) for e in (pr.examples if pr else [])]},
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
catalog = {
|
|
250
|
+
"version": "1.1.0",
|
|
251
|
+
"generated_from": "areas/**/{workflows,prompts}",
|
|
252
|
+
"areas": list(areas_out.values()),
|
|
253
|
+
"stats": {
|
|
254
|
+
"workflows": len(all_workflows),
|
|
255
|
+
"prompts": len(prompts),
|
|
256
|
+
"matched_prompts": len(matched_prompt_keys),
|
|
257
|
+
"problems": len(problems),
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
return catalog, problems
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main() -> int:
|
|
264
|
+
parser = argparse.ArgumentParser()
|
|
265
|
+
parser.add_argument("--output", default=str(ROOT / "docs/site/catalog.json"))
|
|
266
|
+
parser.add_argument("--validate", action="store_true")
|
|
267
|
+
args = parser.parse_args()
|
|
268
|
+
|
|
269
|
+
catalog, problems = build_catalog(validate=args.validate)
|
|
270
|
+
out = Path(args.output)
|
|
271
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
out.write_text(json.dumps(catalog, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
273
|
+
|
|
274
|
+
if problems:
|
|
275
|
+
print("Catalog validation issues:")
|
|
276
|
+
for issue in problems:
|
|
277
|
+
print(f"- {issue}")
|
|
278
|
+
|
|
279
|
+
return 1 if args.validate and problems else 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
AREAS = ROOT / "areas"
|
|
10
|
+
|
|
11
|
+
FRONTMATTER = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
12
|
+
WORKFLOW_KEY = re.compile(r"^workflow:\s*([a-z0-9][a-z0-9-]*)\s*$", re.MULTILINE)
|
|
13
|
+
PROMPT_HEADER = re.compile(r"^#\s*Prompt:\s*`?([^`\n]+)`?", re.MULTILINE)
|
|
14
|
+
USE_WHEN = re.compile(r"^Use when:\s*.+$", re.MULTILINE)
|
|
15
|
+
EXAMPLE_BLOCK = re.compile(r"##\s*Example\s*\d+\s*[—-]\s*.+?", re.MULTILINE)
|
|
16
|
+
EN_BLOCK = re.compile(r"\*\*EN:\*\*\s*\n```\n.+?\n```", re.DOTALL)
|
|
17
|
+
RU_BLOCK = re.compile(r"\*\*RU:\*\*\s*\n```\n.+?\n```", re.DOTALL)
|
|
18
|
+
COMMAND_REF_RE = re.compile(r"(?m)^/([a-z0-9][a-z0-9-]*)\b")
|
|
19
|
+
PLACEHOLDER_STRINGS = (
|
|
20
|
+
"<project context>",
|
|
21
|
+
"<контекст проекта>",
|
|
22
|
+
"Objective: clearly state",
|
|
23
|
+
"Goal: execute workflow steps end-to-end",
|
|
24
|
+
"Use when: run workflow",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def workflow_stems_for_prompt(prompt_path: Path) -> set[str]:
|
|
29
|
+
wf_dir = prompt_path.parent.parent / "workflows"
|
|
30
|
+
if not wf_dir.exists():
|
|
31
|
+
return set()
|
|
32
|
+
return {p.stem for p in wf_dir.glob("*.md")}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_workflow_key(text: str) -> str | None:
|
|
36
|
+
frontmatter = FRONTMATTER.match(text)
|
|
37
|
+
if not frontmatter:
|
|
38
|
+
return None
|
|
39
|
+
match = WORKFLOW_KEY.search(frontmatter.group(1))
|
|
40
|
+
if not match:
|
|
41
|
+
return None
|
|
42
|
+
return match.group(1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> int:
|
|
46
|
+
ap = argparse.ArgumentParser()
|
|
47
|
+
ap.add_argument("--strict", action="store_true", help="Exit 1 on any issue")
|
|
48
|
+
args = ap.parse_args()
|
|
49
|
+
|
|
50
|
+
issues: list[str] = []
|
|
51
|
+
for path in sorted(AREAS.glob("**/prompts/*.md")):
|
|
52
|
+
text = path.read_text(encoding="utf-8")
|
|
53
|
+
rel = path.relative_to(ROOT)
|
|
54
|
+
|
|
55
|
+
workflow = parse_workflow_key(text)
|
|
56
|
+
if workflow is None:
|
|
57
|
+
issues.append(f"{rel}: missing prompt front matter with `workflow: <workflow-stem>`")
|
|
58
|
+
|
|
59
|
+
header = PROMPT_HEADER.search(text)
|
|
60
|
+
if not header:
|
|
61
|
+
issues.append(f"{rel}: missing `# Prompt:` header")
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
header_command = header.group(1).strip()
|
|
65
|
+
if workflow and header_command != f"/{workflow}":
|
|
66
|
+
issues.append(f"{rel}: prompt header must equal `/{workflow}`")
|
|
67
|
+
if not USE_WHEN.search(text):
|
|
68
|
+
issues.append(f"{rel}: missing `Use when:` section")
|
|
69
|
+
example_count = len(EXAMPLE_BLOCK.findall(text))
|
|
70
|
+
if not example_count:
|
|
71
|
+
issues.append(f"{rel}: missing `## Example N — ...` block")
|
|
72
|
+
elif example_count < 2 or example_count > 3:
|
|
73
|
+
issues.append(f"{rel}: example count must be 2 or 3 (found {example_count})")
|
|
74
|
+
if not EN_BLOCK.search(text):
|
|
75
|
+
issues.append(f"{rel}: missing EN fenced block")
|
|
76
|
+
if not RU_BLOCK.search(text):
|
|
77
|
+
issues.append(f"{rel}: missing RU fenced block")
|
|
78
|
+
if len(EN_BLOCK.findall(text)) != example_count:
|
|
79
|
+
issues.append(f"{rel}: EN block count must match example count")
|
|
80
|
+
if len(RU_BLOCK.findall(text)) != example_count:
|
|
81
|
+
issues.append(f"{rel}: RU block count must match example count")
|
|
82
|
+
if "Workflow link command:" in text:
|
|
83
|
+
issues.append(f"{rel}: legacy `Workflow link command:` block is not allowed")
|
|
84
|
+
if any(token in text for token in PLACEHOLDER_STRINGS):
|
|
85
|
+
issues.append(f"{rel}: placeholder or generic scaffold text remains")
|
|
86
|
+
if workflow and path.stem != workflow:
|
|
87
|
+
issues.append(f"{rel}: prompt filename must match workflow stem `{workflow}`")
|
|
88
|
+
|
|
89
|
+
refs = {m.group(1) for m in COMMAND_REF_RE.finditer(text)}
|
|
90
|
+
area_stems = workflow_stems_for_prompt(path)
|
|
91
|
+
if workflow:
|
|
92
|
+
if workflow not in area_stems:
|
|
93
|
+
issues.append(f"{rel}: workflow `{workflow}` not found in sibling workflows/")
|
|
94
|
+
if not refs:
|
|
95
|
+
issues.append(f"{rel}: no /<workflow-file-name> command reference found")
|
|
96
|
+
elif refs != {workflow}:
|
|
97
|
+
issues.append(f"{rel}: all slash commands must be canonical `/{workflow}` (found: {', '.join(sorted(refs))})")
|
|
98
|
+
elif area_stems and not (refs & area_stems):
|
|
99
|
+
issues.append(f"{rel}: no /<workflow-file-name> command reference found")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if issues:
|
|
103
|
+
print("Prompt lint issues:")
|
|
104
|
+
for issue in issues:
|
|
105
|
+
print(f"- {issue}")
|
|
106
|
+
return 1 if args.strict else 0
|
|
107
|
+
|
|
108
|
+
print("All prompts pass format checks.")
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
raise SystemExit(main())
|