agent-engineering-skills 1.0.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 (48) hide show
  1. package/AGENTS.md +249 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.js +223 -0
  5. package/docs/agent-integration.md +200 -0
  6. package/docs/philosophy.md +131 -0
  7. package/docs/reference-authoring.md +117 -0
  8. package/docs/skill-authoring.md +126 -0
  9. package/examples/authorization-bypass.md +191 -0
  10. package/examples/frontend-review.md +244 -0
  11. package/examples/race-condition.md +128 -0
  12. package/examples/xp-reward-loop.md +123 -0
  13. package/package.json +45 -0
  14. package/references/engineering.yaml +88 -0
  15. package/references/frontend.yaml +139 -0
  16. package/references/product.yaml +54 -0
  17. package/references/research.yaml +88 -0
  18. package/references/security.yaml +85 -0
  19. package/references/ux.yaml +37 -0
  20. package/scripts/validate.py +454 -0
  21. package/skills/audit/adversarial-review/SKILL.md +190 -0
  22. package/skills/audit/business-logic-audit/SKILL.md +182 -0
  23. package/skills/audit/edge-case-hunter/SKILL.md +159 -0
  24. package/skills/audit/error-flow-audit/SKILL.md +184 -0
  25. package/skills/audit/state-consistency-audit/SKILL.md +174 -0
  26. package/skills/audit/user-flow-audit/SKILL.md +161 -0
  27. package/skills/frontend/accessibility-review/SKILL.md +186 -0
  28. package/skills/frontend/animation-review/SKILL.md +171 -0
  29. package/skills/frontend/interaction-design/SKILL.md +162 -0
  30. package/skills/frontend/ux-review/SKILL.md +172 -0
  31. package/skills/frontend/visual-quality-review/SKILL.md +160 -0
  32. package/skills/meta/research-router/SKILL.md +184 -0
  33. package/skills/meta/skill-router/SKILL.md +206 -0
  34. package/skills/product/gamification-audit/SKILL.md +213 -0
  35. package/skills/reliability/data-integrity-audit/SKILL.md +187 -0
  36. package/skills/reliability/idempotency-audit/SKILL.md +191 -0
  37. package/skills/reliability/race-condition-hunter/SKILL.md +181 -0
  38. package/skills/research/github-reference-research/SKILL.md +197 -0
  39. package/skills/research/implementation-research/SKILL.md +181 -0
  40. package/skills/research/market-research/SKILL.md +202 -0
  41. package/skills/research/reference-research/SKILL.md +186 -0
  42. package/skills/security/api-abuse-audit/SKILL.md +178 -0
  43. package/skills/security/authorization-audit/SKILL.md +176 -0
  44. package/skills/security/input-trust-audit/SKILL.md +178 -0
  45. package/templates/audit-report.md +89 -0
  46. package/templates/bug-report.md +107 -0
  47. package/templates/design-review.md +122 -0
  48. package/templates/research-report.md +96 -0
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env python3
2
+ """Validate the agent-engineering-skills repository against its own contracts.
3
+
4
+ Checks:
5
+ 1. Every skills/**/SKILL.md has valid frontmatter (name, description, category,
6
+ triggers, priority) and all 9 mandated body sections in order.
7
+ 2. name matches the directory; category is valid; priority is low|medium|high.
8
+ 3. Every references/*.yaml parses, and each entry has the 7 required fields with
9
+ valid type/authority enums and matching category.
10
+ 4. Cross-reference integrity: every skill name referenced in
11
+ skills/meta/skill-router/SKILL.md resolves to an existing SKILL.md.
12
+
13
+ Exit code 0 if all pass, 1 otherwise. Prints a summary.
14
+
15
+ No third-party dependencies — uses only the standard library.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import re
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Configuration
27
+ # ---------------------------------------------------------------------------
28
+
29
+ ROOT = Path(__file__).resolve().parent.parent
30
+
31
+ SKILLS_DIR = ROOT / "skills"
32
+ REFERENCES_DIR = ROOT / "references"
33
+ ROUTER = SKILLS_DIR / "meta" / "skill-router" / "SKILL.md"
34
+
35
+ VALID_CATEGORIES = {
36
+ "audit", "security", "reliability", "product",
37
+ "frontend", "research", "meta",
38
+ }
39
+ VALID_PRIORITIES = {"low", "medium", "high"}
40
+
41
+ VALID_REF_TYPES = {
42
+ "methodology", "heuristic", "inspiration", "implementation", "discovery",
43
+ }
44
+ VALID_AUTHORITIES = {"established", "community", "vendor", "curated"}
45
+
46
+ # All skill names defined by plan.md (the known universe). The router may reference
47
+ # skills from later milestones before they are implemented; references to names in
48
+ # this set that are not yet on disk are WARNINGS, not errors. A reference to a name
49
+ # in NEITHER this set NOR on disk is a real error (typo / unknown skill).
50
+ PLAN_SKILLS = {
51
+ # audit (core + deferred dead-end-flow-audit)
52
+ "adversarial-review", "user-flow-audit", "business-logic-audit",
53
+ "edge-case-hunter", "state-consistency-audit", "error-flow-audit",
54
+ "dead-end-flow-audit",
55
+ # security
56
+ "authorization-audit", "api-abuse-audit", "input-trust-audit",
57
+ # reliability
58
+ "race-condition-hunter", "idempotency-audit", "data-integrity-audit",
59
+ # product
60
+ "gamification-audit",
61
+ # frontend
62
+ "ux-review", "visual-quality-review", "interaction-design",
63
+ "animation-review", "accessibility-review",
64
+ # research
65
+ "reference-research", "github-reference-research", "market-research",
66
+ "implementation-research",
67
+ # meta
68
+ "skill-router", "research-router",
69
+ }
70
+
71
+ REQUIRED_FRONTMATTER = ["name", "description", "category", "triggers", "priority"]
72
+
73
+ # The 9 mandated body sections, in order (docs/skill-authoring.md).
74
+ REQUIRED_SECTIONS = [
75
+ "Objective",
76
+ "When to Use",
77
+ "Mental Model",
78
+ "Investigation Procedure",
79
+ "Questions to Ask",
80
+ "Attack Patterns",
81
+ "Evidence Requirements",
82
+ "False Positives",
83
+ "Output Format",
84
+ ]
85
+
86
+ REQUIRED_REF_FIELDS = [
87
+ "name", "url", "type", "category",
88
+ "authority", "use_when", "avoid_when", "search_queries",
89
+ ]
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Minimal YAML subset parser
93
+ # ---------------------------------------------------------------------------
94
+ # We avoid a PyYAML dependency. We only need: a top-level list of mappings where
95
+ # values are scalars or lists of scalars. Field order in a mapping is preserved by
96
+ # reading sequentially. This is intentionally narrow and strict — it rejects anything
97
+ # it cannot fully understand so it never silently passes malformed input.
98
+
99
+
100
+ class YAMLError(ValueError):
101
+ pass
102
+
103
+
104
+ def _parse_scalar(token: str):
105
+ token = token.strip()
106
+ if len(token) >= 2 and token[0] == token[-1] and token[0] in ("'", '"'):
107
+ return token[1:-1]
108
+ return token
109
+
110
+
111
+ def parse_yaml_list(text: str) -> list[dict]:
112
+ """Parse a YAML file that is a list of mappings (scalar / list-of-scalar values).
113
+
114
+ Handles two indentation shapes:
115
+ A) mapping item with nested list field:
116
+ - name: x
117
+ use_when:
118
+ - a
119
+ - b
120
+ B) list field at the same indent as its items (frontmatter uses this):
121
+ triggers:
122
+ - a
123
+ - b
124
+ """
125
+ # Drop comments and blank lines, but keep indentation.
126
+ lines = []
127
+ for raw in text.splitlines():
128
+ stripped = raw.rstrip()
129
+ if not stripped.strip():
130
+ continue
131
+ if stripped.lstrip().startswith("#"):
132
+ continue
133
+ lines.append(stripped)
134
+
135
+ entries: list[dict] = []
136
+ current: dict | None = None
137
+ # The field currently collecting list items, and the indent at which those items
138
+ # are expected. None when not inside a list field.
139
+ list_field: str | None = None
140
+ list_indent: int | None = None
141
+
142
+ for line in lines:
143
+ indent = len(line) - len(line.lstrip())
144
+ body = line.strip()
145
+
146
+ is_list_marker = body == "-" or body.startswith("- ")
147
+ marker_payload = body[2:].strip() if body.startswith("- ") else ""
148
+
149
+ # Decide whether this line is a nested list item (belongs to list_field) or a
150
+ # new mapping field/item. A nested list item must be indented strictly deeper
151
+ # than the field's own line (shape A) OR at a deeper indent than the current
152
+ # mapping item while list_field is open (shape B for frontmatter).
153
+ nested_item = (
154
+ is_list_marker
155
+ and list_field is not None
156
+ and (list_indent is None or indent > list_indent)
157
+ )
158
+
159
+ if nested_item:
160
+ # "list_field" stays open; append the item. If the marker carries a
161
+ # "key: value", that's a mapping inside the list — not supported here.
162
+ if marker_payload and ":" in marker_payload:
163
+ raise YAMLError(
164
+ f"Mapping list items not supported: {body!r}"
165
+ )
166
+ if marker_payload:
167
+ current[list_field].append(_parse_scalar(marker_payload))
168
+ # bare "-" with payload on next line is unsupported; ignore empties.
169
+ continue
170
+
171
+ # Not a nested list item → this line either opens a list field, sets a scalar,
172
+ # or starts a new top-level mapping item. Close any open list field.
173
+ list_field = None
174
+ list_indent = None
175
+
176
+ if body.startswith("- "):
177
+ current = {}
178
+ entries.append(current)
179
+ rest = marker_payload
180
+ if ":" in rest:
181
+ key, _, val = rest.partition(":")
182
+ key = key.strip()
183
+ val = val.strip()
184
+ if val == "":
185
+ current[key] = []
186
+ list_field = key
187
+ list_indent = indent
188
+ else:
189
+ current[key] = _parse_scalar(val)
190
+ else:
191
+ raise YAMLError(f"Unexpected bare item at top level: {body!r}")
192
+ elif body == "-":
193
+ current = {}
194
+ entries.append(current)
195
+ elif ":" in body:
196
+ key, _, val = body.partition(":")
197
+ key = key.strip()
198
+ val = val.strip()
199
+ if current is None:
200
+ raise YAMLError(f"Key outside any item: {body!r}")
201
+ if val == "":
202
+ current[key] = []
203
+ list_field = key
204
+ list_indent = indent
205
+ else:
206
+ current[key] = _parse_scalar(val)
207
+ else:
208
+ raise YAMLError(f"Unexpected line: {body!r}")
209
+
210
+ return entries
211
+
212
+
213
+ def parse_frontmatter(text: str) -> tuple[dict, str]:
214
+ """Parse leading '---' YAML frontmatter. Returns (metadata, body)."""
215
+ if not text.startswith("---"):
216
+ raise YAMLError("Missing opening '---' frontmatter delimiter")
217
+ end = text.find("\n---", 3)
218
+ if end == -1:
219
+ raise YAMLError("Missing closing '---' frontmatter delimiter")
220
+ fm_text = text[3:end].strip()
221
+ body = text[end + 4:].lstrip("\n")
222
+
223
+ # Frontmatter is a single mapping (not a list). Parse as one item.
224
+ entries = parse_yaml_list("- \n" + fm_text) if fm_text else []
225
+ if not entries:
226
+ return {}, body
227
+ return entries[0], body
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Checks
232
+ # ---------------------------------------------------------------------------
233
+
234
+
235
+ def find_skills() -> list[Path]:
236
+ if not SKILLS_DIR.is_dir():
237
+ return []
238
+ return sorted(SKILLS_DIR.rglob("SKILL.md"))
239
+
240
+
241
+ def check_skill(path: Path) -> list[str]:
242
+ """Return a list of error strings for this SKILL.md (empty = ok)."""
243
+ errors: list[str] = []
244
+ rel = path.relative_to(ROOT)
245
+ text = path.read_text(encoding="utf-8")
246
+
247
+ try:
248
+ meta, body = parse_frontmatter(text)
249
+ except YAMLError as e:
250
+ return [f"{rel}: frontmatter invalid: {e}"]
251
+
252
+ for field in REQUIRED_FRONTMATTER:
253
+ if field not in meta:
254
+ errors.append(f"{rel}: missing frontmatter field '{field}'")
255
+
256
+ # name == directory name (the skill dir, two levels up from SKILL.md)
257
+ skill_dir = path.parent.name
258
+ if "name" in meta and meta["name"] != skill_dir:
259
+ errors.append(
260
+ f"{rel}: frontmatter name {meta['name']!r} != directory {skill_dir!r}"
261
+ )
262
+
263
+ if "category" in meta and meta["category"] not in VALID_CATEGORIES:
264
+ errors.append(
265
+ f"{rel}: invalid category {meta['category']!r} "
266
+ f"(valid: {sorted(VALID_CATEGORIES)})"
267
+ )
268
+
269
+ if "priority" in meta and meta["priority"] not in VALID_PRIORITIES:
270
+ errors.append(
271
+ f"{rel}: invalid priority {meta['priority']!r} "
272
+ f"(valid: {sorted(VALID_PRIORITIES)})"
273
+ )
274
+
275
+ if "triggers" in meta and not isinstance(meta["triggers"], list):
276
+ errors.append(f"{rel}: 'triggers' must be a list")
277
+
278
+ # 9 sections, in order, as '## Heading' (allow trailing text after the heading).
279
+ headings = re.findall(r"^##\s+(.+?)\s*$", body, flags=re.MULTILINE)
280
+ heading_names = [h.strip() for h in headings]
281
+ # The first '## Objective' etc. must appear in order. Allow extra '##' sections
282
+ # after, but the 9 required ones must be present in order.
283
+ idx = 0
284
+ for required in REQUIRED_SECTIONS:
285
+ # find the next heading equal to the required one at or after idx
286
+ found = False
287
+ for j in range(idx, len(heading_names)):
288
+ if heading_names[j] == required:
289
+ idx = j + 1
290
+ found = True
291
+ break
292
+ if not found:
293
+ errors.append(
294
+ f"{rel}: missing or out-of-order section '## {required}'"
295
+ )
296
+
297
+ return errors
298
+
299
+
300
+ def check_references() -> list[str]:
301
+ errors: list[str] = []
302
+ if not REFERENCES_DIR.is_dir():
303
+ return [f"{REFERENCES_DIR.relative_to(ROOT)}: references/ directory missing"]
304
+
305
+ for yml in sorted(REFERENCES_DIR.glob("*.yaml")):
306
+ rel = yml.relative_to(ROOT)
307
+ expected_category = yml.stem # frontend.yaml → "frontend"
308
+ text = yml.read_text(encoding="utf-8")
309
+ try:
310
+ entries = parse_yaml_list(text)
311
+ except YAMLError as e:
312
+ errors.append(f"{rel}: YAML parse error: {e}")
313
+ continue
314
+
315
+ if not entries:
316
+ errors.append(f"{rel}: no entries (empty catalog)")
317
+ continue
318
+
319
+ for n, entry in enumerate(entries, 1):
320
+ for field in REQUIRED_REF_FIELDS:
321
+ if field not in entry:
322
+ errors.append(f"{rel}: entry #{n} missing field '{field}'")
323
+
324
+ if "type" in entry and entry["type"] not in VALID_REF_TYPES:
325
+ errors.append(
326
+ f"{rel}: entry #{n} invalid type {entry['type']!r} "
327
+ f"(valid: {sorted(VALID_REF_TYPES)})"
328
+ )
329
+ if "authority" in entry and entry["authority"] not in VALID_AUTHORITIES:
330
+ errors.append(
331
+ f"{rel}: entry #{n} invalid authority {entry['authority']!r} "
332
+ f"(valid: {sorted(VALID_AUTHORITIES)})"
333
+ )
334
+ if "category" in entry and entry["category"] != expected_category:
335
+ errors.append(
336
+ f"{rel}: entry #{n} category {entry['category']!r} != "
337
+ f"file category {expected_category!r}"
338
+ )
339
+ if "url" in entry:
340
+ url = str(entry["url"])
341
+ if not re.match(r"^https?://", url):
342
+ errors.append(f"{rel}: entry #{n} url not absolute: {url!r}")
343
+ for list_field in ("use_when", "avoid_when", "search_queries"):
344
+ if list_field in entry:
345
+ val = entry[list_field]
346
+ if not isinstance(val, list) or len(val) == 0:
347
+ errors.append(
348
+ f"{rel}: entry #{n} '{list_field}' must be a non-empty list"
349
+ )
350
+
351
+ return errors
352
+
353
+
354
+ def collect_skill_names() -> set[str]:
355
+ names = set()
356
+ for path in find_skills():
357
+ names.add(path.parent.name)
358
+ return names
359
+
360
+
361
+ def check_router_integrity() -> tuple[list[str], list[str]]:
362
+ """Check skill references in skill-router/SKILL.md.
363
+
364
+ Returns (errors, warnings). A reference to a name that is neither on disk nor in
365
+ PLAN_SKILLS is an error (typo / unknown skill). A reference to a name in PLAN_SKILLS
366
+ but not yet implemented is a warning (later-milestone skill).
367
+ """
368
+ errors: list[str] = []
369
+ warnings: list[str] = []
370
+ if not ROUTER.is_file():
371
+ return errors, warnings # router not built yet; skip
372
+ existing = collect_skill_names()
373
+ text = ROUTER.read_text(encoding="utf-8")
374
+ pattern = re.compile(r"\b([a-z]+-[a-z]+(?:-[a-z]+)*)\b")
375
+ candidates = set(pattern.findall(text))
376
+ known_suffixes = ("-audit", "-hunter", "-review", "-research", "-router",
377
+ "-design")
378
+ for cand in candidates:
379
+ # Only consider structural references: inside a code block, or a table cell.
380
+ structural = False
381
+ for block in re.findall(r"```.*?```", text, flags=re.DOTALL):
382
+ if cand in block:
383
+ structural = True
384
+ break
385
+ if re.search(rf"\|\s*{re.escape(cand)}\s*\|", text):
386
+ structural = True
387
+ if not structural:
388
+ continue
389
+ if not cand.endswith(known_suffixes):
390
+ continue
391
+ if cand in existing:
392
+ continue
393
+ if cand in PLAN_SKILLS:
394
+ warnings.append(
395
+ f"skill-router references '{cand}' — planned but not yet "
396
+ f"implemented (later milestone)"
397
+ )
398
+ else:
399
+ errors.append(
400
+ f"skill-router references unknown skill '{cand}' "
401
+ f"(not in plan.md and not found under skills/*/)"
402
+ )
403
+ return errors, warnings
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Main
408
+ # ---------------------------------------------------------------------------
409
+
410
+
411
+ def main() -> int:
412
+ errors: list[str] = []
413
+ warnings: list[str] = []
414
+
415
+ skills = find_skills()
416
+ if not skills:
417
+ errors.append("No skills/**/SKILL.md files found")
418
+ for path in skills:
419
+ errors.extend(check_skill(path))
420
+
421
+ errors.extend(check_references())
422
+ e, w = check_router_integrity()
423
+ errors.extend(e)
424
+ warnings.extend(w)
425
+
426
+ # Summary
427
+ print(f"Skills found: {len(skills)}")
428
+ ref_count = 0
429
+ if REFERENCES_DIR.is_dir():
430
+ ref_count = len(list(REFERENCES_DIR.glob("*.yaml")))
431
+ print(f"Reference catalogs: {ref_count}")
432
+ print(f"Errors: {len(errors)}")
433
+ print(f"Warnings: {len(warnings)}")
434
+ print()
435
+
436
+ if warnings:
437
+ for msg in warnings:
438
+ print(f" ⚠ {msg}")
439
+ print()
440
+
441
+ if errors:
442
+ for e in errors:
443
+ print(f" ✗ {e}")
444
+ return 1
445
+
446
+ if warnings:
447
+ print("✓ All contracts satisfied (with planned-skill warnings above).")
448
+ else:
449
+ print("✓ All contracts satisfied.")
450
+ return 0
451
+
452
+
453
+ if __name__ == "__main__":
454
+ sys.exit(main())
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: adversarial-review
3
+ description: Teaches the agent to attack the system as curious, malicious, power, careless, competitor, and stale-state users using repeat, reverse, reorder, skip, replay, concurrent, and manipulate operations.
4
+ category: audit
5
+ triggers:
6
+ - "audit a feature"
7
+ - "test a flow adversarially"
8
+ - "find bugs by attacking assumptions"
9
+ - "review before shipping"
10
+ - "what could a malicious user do"
11
+ priority: high
12
+ ---
13
+
14
+ # Adversarial Review
15
+
16
+ ## Objective
17
+
18
+ Ensinar o agente a **atacar as suposições por trás do sistema** em vez de apenas ler o
19
+ código. A skill não procura o que está escrito; ela procura o que o código *assume* que
20
+ é verdade — e testa se essa suposição sobrevive a um usuário que não coopera.
21
+
22
+ > Don't just review the code. Attack the assumptions behind the system.
23
+
24
+ ## When to Use
25
+
26
+ * Antes de lançar ou refatorar um fluxo não-trivial.
27
+ * Quando uma tarefa envolve estado, permissões, recompensas, dinheiro, ou contadores.
28
+ * Como a skill "guarda-chuva" que abre uma auditoria: ela gera hipóteses que as skills
29
+ especializadas (`business-logic-audit`, `race-condition-hunter`, etc.) confirmam.
30
+ * Quando o pedido inclui "audit", "attack", "stress test", "what could go wrong".
31
+ * **Composição:** raramente atua sozinha. Abre o leque: gera hipóteses que despacham
32
+ para `business-logic-audit`, `edge-case-hunter`, `race-condition-hunter`,
33
+ `state-consistency-audit`, `error-flow-audit`, `authorization-audit`,
34
+ `idempotency-audit`, `api-abuse-audit`.
35
+
36
+ ## Mental Model
37
+
38
+ Existem dois erros simétricos ao revisar código:
39
+
40
+ 1. **Ler como o autor esperava** — segue o happy path, vê o que *deveria* acontecer,
41
+ aprova. Falso negativo.
42
+ 2. **Procurar bugs sem método** — acha coisas estranhas aleatórias, reporta ruído.
43
+ Falso positivo.
44
+
45
+ O modelo adversarial é uma terceira via: **adotar uma persona e aplicar um conjunto
46
+ finito de operações canônicas**. Cada (persona × operação) é uma hipótese testável.
47
+ Isto é sistemático, não aleatório, e gera hipóteses que se convertem em findings apenas
48
+ com evidência.
49
+
50
+ As personas são **modelos de uso**, não perfis de marketing. Cada uma representa uma
51
+ classe de pressão sobre uma suposição diferente:
52
+
53
+ | Persona | Qual suposição ela ataca |
54
+ |---|---|
55
+ | usuário curioso | "campos hidden / IDs / params são só display" |
56
+ | usuário malicioso | "o sistema confia que ninguém vai tentar X" |
57
+ | power user | "ninguém usa atalhos, reorder, ou bypass" |
58
+ | usuário descuidado | "todo mundo completa o fluxo na ordem certa" |
59
+ | usuário concorrente | "dois usuários não agirão sobre o mesmo recurso ao mesmo tempo" |
60
+ | usuário com estado antigo | "o estado do cliente/sessão está sempre sincronizado" |
61
+
62
+ As operações são verbos canônicos que transformam uma execução "normal" em um caso de
63
+ pressão:
64
+
65
+ ```text
66
+ repeat — fazer a mesma ação N vezes
67
+ reverse — desfazer e refazer
68
+ reorder — executar passos fora da ordem esperada
69
+ skip — pular um passo que deveria ser obrigatório
70
+ replay — repetir um request idempotente-deveria-ser
71
+ concurrent — duas execuções sobre o mesmo estado ao mesmo tempo
72
+ manipulate — alterar IDs, campos, roles, timestamps direto no payload
73
+ ```
74
+
75
+ ## Investigation Procedure
76
+
77
+ 1. **Mapear o fluxo nominal.** Liste os passos como o sistema *espera* que ocorram.
78
+ 2. **Listar as suposições.** Para cada passo, escreva o que ele assume sobre o input,
79
+ o estado, e o usuário.
80
+ 3. **Gerar hipóteses (persona × operação).** Para cada persona, aplique cada operação
81
+ aos passos. Não tente confirmar ainda — só gere a hipótese "se eu fizer X, e a
82
+ suposição Y for falsa, então Z".
83
+ 4. **Triar por plausibilidade.** Descarte as obviamente impossíveis (sem caminho no
84
+ código). Priorize as que atacam uma suposição de servidor ou estado compartilhado.
85
+ 5. **Confirmar com evidência.** Para cada hipótese sobrevivente, reproduza ou encontre
86
+ o mecanismo no código. Suba o nível de confiança só com evidência.
87
+ 6. **Classificar falso positivo.** Para cada finding, verifique se o comportamento é
88
+ intencional/aceitável antes de reportar.
89
+ 7. **Reportar** no formato de saída, apontando para `templates/audit-report.md`.
90
+
91
+ ## Questions to Ask
92
+
93
+ * Quem é o público deste fluxo? Qual deles NÃO coopera com a UI?
94
+ * Quais campos o servidor aceita que a UI nem mostra? (`manipulate`)
95
+ * Se eu repetir essa ação 100 vezes, o que cresce que não deveria? (`repeat`)
96
+ * Se eu desfizer e refazer, ganho algo de volta que não devia? (`reverse`)
97
+ * Posso pular o passo de pré-condição e ir direto ao efeito? (`skip`)
98
+ * Posso chamar os passos fora de ordem? (`reorder`)
99
+ * Se dois requests simultâneos passam pela mesma checagem, ambos efetivam? (`concurrent`)
100
+ * O que acontece se eu refizer um request cuja resposta se perdeu? (`replay`)
101
+ * Qual estado o cliente guarda que pode ficar desatualizado vs o servidor? (`stale state`)
102
+
103
+ ## Attack Patterns
104
+
105
+ ```text
106
+ repeat
107
+ request → 200 OK
108
+ request → 200 OK (deveria ser 409/idempotente?)
109
+ ...
110
+ contador/reward inflado
111
+
112
+ reverse
113
+ action → reward granted
114
+ undo action (reward é removida?)
115
+ redo action → reward granted again
116
+ → farming infinito se reward não foi removida OU se foi re-concedida
117
+
118
+ reorder
119
+ step C (efeito) antes do step A (pré-condição)
120
+ → o efeito ocorre sem a pré-condição?
121
+
122
+ skip
123
+ POST /grant-directly (pulando o fluxo que valida)
124
+ → a validação server-side cobre o caminho direto?
125
+
126
+ replay
127
+ request → 200 (resposta perdida na rede)
128
+ retry request
129
+ → efeito duplicado?
130
+
131
+ concurrent
132
+ request A: read balance (ok)
133
+ request B: read balance (ok) ← mesmo estado lido
134
+ request A: write (deduct)
135
+ request B: write (deduct) ← double-spend
136
+
137
+ manipulate
138
+ GET /resource/123 → 200 (é meu? sou só autenticado, não autorizado)
139
+ PUT /resource/123 {role:"admin"} (campo extra aceito?)
140
+ POST /reward {xp: 99999} (valor confiável no payload?)
141
+ ```
142
+
143
+ ## Evidence Requirements
144
+
145
+ Um finding de `adversarial-review` deve, no mínimo:
146
+
147
+ * **Nomear a suposição atacada** — qual invariant/assumption foi violada.
148
+ * **Nomear a persona e a operação** — qual combinação gerou a hipótese.
149
+ * **Mostrar o mecanismo** — onde no código a suposição é feita e onde falha. Ou
150
+ reprodução concreta (sequência de requests, passos).
151
+ * **Escalar confiança:**
152
+ * `CONFIRMED` — reproduzido (request sequência + resposta, ou teste).
153
+ * `HIGH CONFIDENCE` — mecanismo identificado no código, sem reprodução executada.
154
+ * `POSSIBLE` — hipótese plausível, caminho existe, mecanismo não confirmado.
155
+ * `SPECULATIVE` — "parece que poderia" sem caminho no código; reportar como risco.
156
+
157
+ Sem mecanismo e sem reprodução, no máximo `POSSIBLE`.
158
+
159
+ ## False Positives
160
+
161
+ * **Rate limiting já protege** — repetição é bloqueada antes de efeito. Verificar se o
162
+ limite está em vigor antes de reportar `repeat` como defeito.
163
+ * **Idempotência real** — se o servidor usa idempotency key / unique constraint, `replay`
164
+ e `repeat` não duplicam. Confirmar a ausência da proteção antes de reportar.
165
+ * **Autorização server-side presente** — se o servidor valida ownership no handler,
166
+ `manipulate` de ID não funciona. Não reportar bypass sem confirmar que a checagem
167
+ falta.
168
+ * **Comportamento intencional** — algumas ações são *desenhadas* para serem repetíveis
169
+ ou reversíveis. Se o produto exige isso, não é bug. Quando em dúvida, marque como
170
+ `POSSIBLE` e levante na seção "Out of scope / precisa decisão de produto".
171
+ * **Ambiente de teste** — repetir em staging pode não refletir produção (locks, limits).
172
+ Reportar confiança reduzida se não puder testar em condições reais.
173
+
174
+ ## Output Format
175
+
176
+ Para cada hipótese sobrevivente, produza um finding seguindo
177
+ `templates/audit-report.md`. Campos obrigatórios: Severity, Confidence, Affected
178
+ component, Affected flow, Reproduction, Expected behavior, Actual behavior, Root cause,
179
+ Impact, Recommendation.
180
+
181
+ Inclua adicionalmente em **Evidence**:
182
+ * a persona + operação que gerou a hipótese;
183
+ * a suposição exata que foi atacada;
184
+ * o mecanismo ou reprodução.
185
+
186
+ Findings `SPECULATIVE` vão em uma seção separada "Riscos a verificar", não na lista
187
+ principal de bugs.
188
+
189
+ Ao final, liste quais hipóteses foram **descartadas** e por quê — isto mostra a
190
+ cobertura e previne retrabalho.