@vigolium/piolium 0.0.1

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 (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/agents/access-auditor.md +300 -0
  4. package/agents/assumption-breaker.md +154 -0
  5. package/agents/attack-designer.md +116 -0
  6. package/agents/code-scanner.md +139 -0
  7. package/agents/concurrency-auditor.md +238 -0
  8. package/agents/confirm-writer.md +257 -0
  9. package/agents/context-reviewer.md +274 -0
  10. package/agents/cross-verifier.md +165 -0
  11. package/agents/cve-scout.md +381 -0
  12. package/agents/env-builder.md +282 -0
  13. package/agents/env-profiler.md +205 -0
  14. package/agents/evidence-collector.md +140 -0
  15. package/agents/finding-grader.md +142 -0
  16. package/agents/finding-writer.md +148 -0
  17. package/agents/flow-tracer.md +106 -0
  18. package/agents/goal-backtracer.md +146 -0
  19. package/agents/history-miner.md +467 -0
  20. package/agents/independent-verifier.md +118 -0
  21. package/agents/intent-mapper.md +183 -0
  22. package/agents/longshot-collector.md +128 -0
  23. package/agents/longshot-prober.md +126 -0
  24. package/agents/patch-auditor.md +73 -0
  25. package/agents/poc-author.md +124 -0
  26. package/agents/poc-runner.md +194 -0
  27. package/agents/probe-lead.md +269 -0
  28. package/agents/red-challenger.md +101 -0
  29. package/agents/report-composer.md +208 -0
  30. package/agents/review-adjudicator.md +216 -0
  31. package/agents/spec-auditor.md +155 -0
  32. package/agents/taint-tracer.md +265 -0
  33. package/agents/test-locator.md +209 -0
  34. package/agents/threat-modeler.md +132 -0
  35. package/agents/variant-scanner.md +108 -0
  36. package/agents/variant-spotter.md +110 -0
  37. package/bin/piolium.mjs +376 -0
  38. package/extensions/piolium/_vendor/yaml.bundle.d.mts +6 -0
  39. package/extensions/piolium/_vendor/yaml.bundle.mjs +139 -0
  40. package/extensions/piolium/agent-runner.ts +322 -0
  41. package/extensions/piolium/agents.ts +266 -0
  42. package/extensions/piolium/audit-state.ts +522 -0
  43. package/extensions/piolium/bundled-resources.ts +97 -0
  44. package/extensions/piolium/candidate-scan.ts +966 -0
  45. package/extensions/piolium/command-target.ts +177 -0
  46. package/extensions/piolium/console-stream.ts +57 -0
  47. package/extensions/piolium/export-results.ts +380 -0
  48. package/extensions/piolium/findings.ts +448 -0
  49. package/extensions/piolium/heartbeat.ts +182 -0
  50. package/extensions/piolium/help.ts +234 -0
  51. package/extensions/piolium/index.ts +1865 -0
  52. package/extensions/piolium/longshot.ts +530 -0
  53. package/extensions/piolium/matcher-suggestions.ts +196 -0
  54. package/extensions/piolium/matcher-utils.ts +83 -0
  55. package/extensions/piolium/modes/balanced.ts +750 -0
  56. package/extensions/piolium/modes/confirm-bootstrap.ts +186 -0
  57. package/extensions/piolium/modes/confirm.ts +697 -0
  58. package/extensions/piolium/modes/deep.ts +917 -0
  59. package/extensions/piolium/modes/diff.ts +177 -0
  60. package/extensions/piolium/modes/lite.ts +540 -0
  61. package/extensions/piolium/modes/longshot.ts +595 -0
  62. package/extensions/piolium/modes/merge.ts +204 -0
  63. package/extensions/piolium/modes/phase-runner.ts +267 -0
  64. package/extensions/piolium/modes/reinvest.ts +546 -0
  65. package/extensions/piolium/modes/revisit.ts +279 -0
  66. package/extensions/piolium/modes.ts +48 -0
  67. package/extensions/piolium/phase-labels.ts +123 -0
  68. package/extensions/piolium/phase-status-strip.ts +92 -0
  69. package/extensions/piolium/prompt-prefix-editor.ts +39 -0
  70. package/extensions/piolium/providers/anthropic-vertex.ts +836 -0
  71. package/extensions/piolium/recon.ts +409 -0
  72. package/extensions/piolium/result-stats.ts +105 -0
  73. package/extensions/piolium/retry.ts +120 -0
  74. package/extensions/piolium/scheduler.ts +212 -0
  75. package/extensions/piolium/secrets.ts +368 -0
  76. package/extensions/piolium/tools/web-tools.ts +148 -0
  77. package/package.json +77 -0
  78. package/skills/agentic-actions-auditor/SKILL.md +327 -0
  79. package/skills/agentic-actions-auditor/references/action-profiles.md +186 -0
  80. package/skills/agentic-actions-auditor/references/cross-file-resolution.md +209 -0
  81. package/skills/agentic-actions-auditor/references/foundations.md +94 -0
  82. package/skills/agentic-actions-auditor/references/vector-a-env-var-intermediary.md +77 -0
  83. package/skills/agentic-actions-auditor/references/vector-b-direct-expression-injection.md +83 -0
  84. package/skills/agentic-actions-auditor/references/vector-c-cli-data-fetch.md +83 -0
  85. package/skills/agentic-actions-auditor/references/vector-d-pr-target-checkout.md +88 -0
  86. package/skills/agentic-actions-auditor/references/vector-e-error-log-injection.md +88 -0
  87. package/skills/agentic-actions-auditor/references/vector-f-subshell-expansion.md +82 -0
  88. package/skills/agentic-actions-auditor/references/vector-g-eval-of-ai-output.md +91 -0
  89. package/skills/agentic-actions-auditor/references/vector-h-dangerous-sandbox-configs.md +102 -0
  90. package/skills/agentic-actions-auditor/references/vector-i-wildcard-allowlists.md +88 -0
  91. package/skills/audit/SKILL.md +562 -0
  92. package/skills/audit/assets/icon.svg +7 -0
  93. package/skills/audit/hooks/scripts/validate_phase_output.py +550 -0
  94. package/skills/audit/references/adversarial-review.md +148 -0
  95. package/skills/audit/references/architecture-aware-sast.md +306 -0
  96. package/skills/audit/references/audit-workflow.md +737 -0
  97. package/skills/audit/references/chamber-protocol.md +384 -0
  98. package/skills/audit/references/creative-attack-modes.md +221 -0
  99. package/skills/audit/references/deep-analysis.md +273 -0
  100. package/skills/audit/references/domain-attack-playbooks.md +1129 -0
  101. package/skills/audit/references/knowledge-base-template.md +513 -0
  102. package/skills/audit/references/real-env-validation.md +191 -0
  103. package/skills/audit/references/report-templates.md +417 -0
  104. package/skills/audit/references/triage-and-prereqs.md +134 -0
  105. package/skills/audit/scripts/consolidate_drafts.py +554 -0
  106. package/skills/audit/scripts/partition_findings.py +152 -0
  107. package/skills/audit/scripts/rg-hotspots.sh +121 -0
  108. package/skills/audit/scripts/stamp_file_state.py +349 -0
  109. package/skills/code-reviewer/SKILL.md +65 -0
  110. package/skills/codeql/SKILL.md +281 -0
  111. package/skills/codeql/references/build-fixes.md +90 -0
  112. package/skills/codeql/references/diagnostic-query-templates.md +339 -0
  113. package/skills/codeql/references/extension-yaml-format.md +209 -0
  114. package/skills/codeql/references/important-only-suite.md +153 -0
  115. package/skills/codeql/references/language-details.md +207 -0
  116. package/skills/codeql/references/macos-arm64e-workaround.md +179 -0
  117. package/skills/codeql/references/performance-tuning.md +111 -0
  118. package/skills/codeql/references/quality-assessment.md +172 -0
  119. package/skills/codeql/references/ruleset-catalog.md +63 -0
  120. package/skills/codeql/references/run-all-suite.md +92 -0
  121. package/skills/codeql/references/sarif-processing.md +79 -0
  122. package/skills/codeql/references/threat-models.md +51 -0
  123. package/skills/codeql/workflows/build-database.md +280 -0
  124. package/skills/codeql/workflows/create-data-extensions.md +261 -0
  125. package/skills/codeql/workflows/run-analysis.md +301 -0
  126. package/skills/differential-review/SKILL.md +220 -0
  127. package/skills/differential-review/adversarial.md +203 -0
  128. package/skills/differential-review/methodology.md +234 -0
  129. package/skills/differential-review/patterns.md +300 -0
  130. package/skills/differential-review/reporting.md +369 -0
  131. package/skills/fp-check/SKILL.md +125 -0
  132. package/skills/fp-check/references/bug-class-verification.md +114 -0
  133. package/skills/fp-check/references/deep-verification.md +143 -0
  134. package/skills/fp-check/references/evidence-templates.md +91 -0
  135. package/skills/fp-check/references/false-positive-patterns.md +115 -0
  136. package/skills/fp-check/references/gate-reviews.md +27 -0
  137. package/skills/fp-check/references/standard-verification.md +78 -0
  138. package/skills/insecure-defaults/SKILL.md +117 -0
  139. package/skills/insecure-defaults/references/examples.md +409 -0
  140. package/skills/last30days/SKILL.md +444 -0
  141. package/skills/sarif-parsing/SKILL.md +483 -0
  142. package/skills/sarif-parsing/resources/jq-queries.md +162 -0
  143. package/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
  144. package/skills/security-threat-model/LICENSE.txt +201 -0
  145. package/skills/security-threat-model/SKILL.md +81 -0
  146. package/skills/security-threat-model/agents/openai.yaml +4 -0
  147. package/skills/security-threat-model/references/prompt-template.md +255 -0
  148. package/skills/security-threat-model/references/security-controls-and-assets.md +32 -0
  149. package/skills/semgrep/SKILL.md +212 -0
  150. package/skills/semgrep/references/rulesets.md +162 -0
  151. package/skills/semgrep/references/scan-modes.md +110 -0
  152. package/skills/semgrep/references/scanner-task-prompt.md +140 -0
  153. package/skills/semgrep/scripts/merge_sarif.py +203 -0
  154. package/skills/semgrep/workflows/scan-workflow.md +311 -0
  155. package/skills/semgrep-rule-creator/SKILL.md +168 -0
  156. package/skills/semgrep-rule-creator/references/quick-reference.md +202 -0
  157. package/skills/semgrep-rule-creator/references/workflow.md +240 -0
  158. package/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
  159. package/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
  160. package/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
  161. package/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
  162. package/skills/sharp-edges/SKILL.md +292 -0
  163. package/skills/sharp-edges/references/auth-patterns.md +252 -0
  164. package/skills/sharp-edges/references/case-studies.md +274 -0
  165. package/skills/sharp-edges/references/config-patterns.md +333 -0
  166. package/skills/sharp-edges/references/crypto-apis.md +190 -0
  167. package/skills/sharp-edges/references/lang-c.md +205 -0
  168. package/skills/sharp-edges/references/lang-csharp.md +285 -0
  169. package/skills/sharp-edges/references/lang-go.md +270 -0
  170. package/skills/sharp-edges/references/lang-java.md +263 -0
  171. package/skills/sharp-edges/references/lang-javascript.md +269 -0
  172. package/skills/sharp-edges/references/lang-kotlin.md +265 -0
  173. package/skills/sharp-edges/references/lang-php.md +245 -0
  174. package/skills/sharp-edges/references/lang-python.md +274 -0
  175. package/skills/sharp-edges/references/lang-ruby.md +273 -0
  176. package/skills/sharp-edges/references/lang-rust.md +272 -0
  177. package/skills/sharp-edges/references/lang-swift.md +287 -0
  178. package/skills/sharp-edges/references/language-specific.md +588 -0
  179. package/skills/spec-to-code-compliance/SKILL.md +357 -0
  180. package/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
  181. package/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
  182. package/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
  183. package/skills/supply-chain-risk-auditor/SKILL.md +67 -0
  184. package/skills/supply-chain-risk-auditor/resources/results-template.md +41 -0
  185. package/skills/variant-analysis/METHODOLOGY.md +327 -0
  186. package/skills/variant-analysis/SKILL.md +142 -0
  187. package/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
  188. package/skills/variant-analysis/resources/codeql/go.ql +69 -0
  189. package/skills/variant-analysis/resources/codeql/java.ql +71 -0
  190. package/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
  191. package/skills/variant-analysis/resources/codeql/python.ql +80 -0
  192. package/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
  193. package/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
  194. package/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
  195. package/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
  196. package/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
  197. package/skills/variant-analysis/resources/variant-report-template.md +75 -0
  198. package/skills/vuln-report/SKILL.md +137 -0
  199. package/skills/vuln-report/agents/openai.yaml +4 -0
  200. package/skills/vuln-report/references/report-template.md +135 -0
  201. package/skills/wooyun-legacy/SKILL.md +367 -0
  202. package/skills/wooyun-legacy/references/bank-penetration.md +222 -0
  203. package/skills/wooyun-legacy/references/checklists/command-execution-checklist.md +119 -0
  204. package/skills/wooyun-legacy/references/checklists/csrf-checklist.md +74 -0
  205. package/skills/wooyun-legacy/references/checklists/file-upload-checklist.md +108 -0
  206. package/skills/wooyun-legacy/references/checklists/info-disclosure-checklist.md +114 -0
  207. package/skills/wooyun-legacy/references/checklists/logic-flaws-checklist.md +95 -0
  208. package/skills/wooyun-legacy/references/checklists/misconfig-checklist.md +124 -0
  209. package/skills/wooyun-legacy/references/checklists/path-traversal-checklist.md +87 -0
  210. package/skills/wooyun-legacy/references/checklists/rce-checklist.md +93 -0
  211. package/skills/wooyun-legacy/references/checklists/sql-injection-checklist.md +97 -0
  212. package/skills/wooyun-legacy/references/checklists/ssrf-checklist.md +99 -0
  213. package/skills/wooyun-legacy/references/checklists/unauthorized-access-checklist.md +89 -0
  214. package/skills/wooyun-legacy/references/checklists/weak-password-checklist.md +115 -0
  215. package/skills/wooyun-legacy/references/checklists/xss-checklist.md +103 -0
  216. package/skills/wooyun-legacy/references/checklists/xxe-checklist.md +130 -0
  217. package/skills/wooyun-legacy/references/info-disclosure.md +975 -0
  218. package/skills/wooyun-legacy/references/logic-flaws.md +721 -0
  219. package/skills/wooyun-legacy/references/path-traversal.md +1191 -0
  220. package/skills/wooyun-legacy/references/telecom-penetration.md +156 -0
  221. package/skills/wooyun-legacy/references/unauthorized-access.md +980 -0
  222. package/skills/wooyun-legacy/references/xss.md +746 -0
  223. package/skills/zeroize-audit/SKILL.md +371 -0
  224. package/skills/zeroize-audit/configs/c.yaml +21 -0
  225. package/skills/zeroize-audit/configs/default.yaml +128 -0
  226. package/skills/zeroize-audit/configs/rust.yaml +83 -0
  227. package/skills/zeroize-audit/prompts/report_template.md +238 -0
  228. package/skills/zeroize-audit/prompts/system.md +163 -0
  229. package/skills/zeroize-audit/prompts/task.md +97 -0
  230. package/skills/zeroize-audit/references/compile-commands.md +231 -0
  231. package/skills/zeroize-audit/references/detection-strategy.md +191 -0
  232. package/skills/zeroize-audit/references/ir-analysis.md +252 -0
  233. package/skills/zeroize-audit/references/mcp-analysis.md +221 -0
  234. package/skills/zeroize-audit/references/poc-generation.md +470 -0
  235. package/skills/zeroize-audit/references/rust-zeroization-patterns.md +867 -0
  236. package/skills/zeroize-audit/schemas/input.json +83 -0
  237. package/skills/zeroize-audit/schemas/output.json +140 -0
  238. package/skills/zeroize-audit/tools/analyze_asm.sh +202 -0
  239. package/skills/zeroize-audit/tools/analyze_cfg.py +381 -0
  240. package/skills/zeroize-audit/tools/analyze_heap.sh +211 -0
  241. package/skills/zeroize-audit/tools/analyze_ir_semantic.py +429 -0
  242. package/skills/zeroize-audit/tools/diff_ir.sh +135 -0
  243. package/skills/zeroize-audit/tools/diff_rust_mir.sh +189 -0
  244. package/skills/zeroize-audit/tools/emit_asm.sh +67 -0
  245. package/skills/zeroize-audit/tools/emit_ir.sh +77 -0
  246. package/skills/zeroize-audit/tools/emit_rust_asm.sh +178 -0
  247. package/skills/zeroize-audit/tools/emit_rust_ir.sh +150 -0
  248. package/skills/zeroize-audit/tools/emit_rust_mir.sh +158 -0
  249. package/skills/zeroize-audit/tools/extract_compile_flags.py +284 -0
  250. package/skills/zeroize-audit/tools/generate_poc.py +1329 -0
  251. package/skills/zeroize-audit/tools/mcp/apply_confidence_gates.py +113 -0
  252. package/skills/zeroize-audit/tools/mcp/check_mcp.sh +68 -0
  253. package/skills/zeroize-audit/tools/mcp/normalize_mcp_evidence.py +125 -0
  254. package/skills/zeroize-audit/tools/scripts/check_llvm_patterns.py +481 -0
  255. package/skills/zeroize-audit/tools/scripts/check_mir_patterns.py +554 -0
  256. package/skills/zeroize-audit/tools/scripts/check_rust_asm.py +424 -0
  257. package/skills/zeroize-audit/tools/scripts/check_rust_asm_aarch64.py +300 -0
  258. package/skills/zeroize-audit/tools/scripts/check_rust_asm_x86.py +283 -0
  259. package/skills/zeroize-audit/tools/scripts/find_dangerous_apis.py +375 -0
  260. package/skills/zeroize-audit/tools/scripts/semantic_audit.py +923 -0
  261. package/skills/zeroize-audit/tools/track_dataflow.sh +196 -0
  262. package/skills/zeroize-audit/tools/validate_rust_toolchain.sh +298 -0
  263. package/skills/zeroize-audit/workflows/phase-0-preflight.md +150 -0
  264. package/skills/zeroize-audit/workflows/phase-1-source-analysis.md +144 -0
  265. package/skills/zeroize-audit/workflows/phase-2-compiler-analysis.md +139 -0
  266. package/skills/zeroize-audit/workflows/phase-3-interim-report.md +46 -0
  267. package/skills/zeroize-audit/workflows/phase-4-poc-generation.md +46 -0
  268. package/skills/zeroize-audit/workflows/phase-5-poc-validation.md +136 -0
  269. package/skills/zeroize-audit/workflows/phase-6-final-report.md +44 -0
  270. package/skills/zeroize-audit/workflows/phase-7-test-generation.md +42 -0
  271. package/themes/piolium-srcery.json +94 -0
@@ -0,0 +1,554 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Consolidate finding drafts into per-finding directories under archon/findings/.
4
+
5
+ Reads every *.md file in <archon_dir>/findings-draft/, parses its frontmatter,
6
+ keeps only Verdict: VALID drafts with Severity-Original in {CRITICAL, HIGH,
7
+ MEDIUM}, and assigns deterministic severity-prefixed IDs (C1, C2..., H1,
8
+ H2..., M1, M2...) from one global per-severity counter so IDs are unique and
9
+ stable across both buckets.
10
+
11
+ Every promoted draft becomes a directory `<ID>-<slug>/` containing the draft
12
+ plus any adversarial review, chamber debate transcript, and variant
13
+ metadata.json. The destination bucket depends on the triager's verdict:
14
+
15
+ - drafts NOT marked `Triage-Priority: skip` -> `<archon_dir>/findings/`
16
+ (the actionable bucket; a poc-author is dispatched per entry, and a
17
+ post-PoC routing step later demotes any that did not reach
18
+ `PoC-Status: executed` into findings-theoretical/).
19
+ - drafts marked `Triage-Priority: skip` -> `<archon_dir>/findings-theoretical/`
20
+ directly (no PoC build; finding-writer still authors report.md).
21
+
22
+ The manifest emitted to stdout and
23
+ <archon_dir>/findings-draft/consolidation-manifest.json carries `findings`
24
+ (actionable, for poc-author fan-out), `theoretical` (reporter-only, no
25
+ PoC), and `dropped`. There is no longer a `findings-deferred/` folder —
26
+ triage-skipped findings are folded into the theoretical bucket.
27
+
28
+ Revisit mode: pass --continue-ids to seed the severity counters from the
29
+ max existing ID already present in <archon_dir>/findings/. New finding
30
+ directories created in this mode also receive a metadata.json stamped with
31
+ round / revisit_id / model / agent_sdk (pulled from env vars the
32
+ orchestrator sets) so future revisits can attribute each finding to the
33
+ pass that produced it.
34
+
35
+ Env vars read in continuation mode:
36
+ ARCHON_REVISIT_ROUND integer round number (2 = first revisit)
37
+ ARCHON_REVISIT_ID ISO timestamp identifying the revisit
38
+ ARCHON_REVISIT_MODEL model string (e.g. opus-4.7)
39
+ ARCHON_REVISIT_AGENT_SDK platform string (e.g. claude-code)
40
+
41
+ Usage:
42
+ consolidate_drafts.py [archon_dir] [--continue-ids]
43
+
44
+ archon_dir defaults to "archon". Exit codes:
45
+ 0 success
46
+ 1 no VALID Medium-or-higher drafts to consolidate
47
+ 2 usage error / archon_dir missing
48
+ 3 I/O error during consolidation
49
+ """
50
+
51
+ import json
52
+ import os
53
+ import re
54
+ import shutil
55
+ import sys
56
+ from dataclasses import dataclass, field
57
+ from pathlib import Path
58
+ from typing import Optional
59
+
60
+ SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM"]
61
+ SEVERITY_PREFIX = {"CRITICAL": "C", "HIGH": "H", "MEDIUM": "M"}
62
+
63
+ FILENAME_RE = re.compile(r"^([a-z]+\d*)-(\d+)(?:-(.+))?\.md$")
64
+ KV_RE = re.compile(r"^([A-Za-z][A-Za-z0-9 _-]*):\s*(.*)$")
65
+ EXISTING_FOLDER_RE = re.compile(r"^([CHM])(\d+)-")
66
+
67
+
68
+ @dataclass
69
+ class Draft:
70
+ source_path: Path
71
+ filename: str
72
+ phase: str = ""
73
+ sequence: str = ""
74
+ slug: str = ""
75
+ verdict: str = ""
76
+ severity: str = ""
77
+ debate_path: str = ""
78
+ origin_finding: str = ""
79
+ origin_pattern: str = ""
80
+ assigned_id: str = ""
81
+ origin_resolved_id: str = ""
82
+ triage_priority: str = ""
83
+ is_theoretical: bool = False
84
+ folder: Optional[Path] = field(default=None)
85
+
86
+ @property
87
+ def is_variant(self) -> bool:
88
+ return bool(self.origin_finding)
89
+
90
+
91
+ def parse_frontmatter(path: Path) -> dict:
92
+ """Parse the draft's Key: value header.
93
+
94
+ The finding-draft template begins with '# [Title]' followed by a blank
95
+ line, then Key: value lines, then a blank line, then '## Summary'. We
96
+ skip leading blanks and the '#' title line, collect Key: value pairs
97
+ until either a blank line or a '##' section heading appears.
98
+ """
99
+ out: dict = {}
100
+ try:
101
+ with path.open() as f:
102
+ in_fm = False
103
+ for line in f:
104
+ s = line.rstrip("\n")
105
+ if not in_fm:
106
+ if not s.strip():
107
+ continue # leading blank lines
108
+ if s.startswith("# ") and not s.startswith("## "):
109
+ continue # title line
110
+ if s.startswith("## "):
111
+ break # no frontmatter at all
112
+ m = KV_RE.match(s)
113
+ if m:
114
+ out[m.group(1).strip()] = m.group(2).strip()
115
+ in_fm = True
116
+ continue
117
+ # inside frontmatter
118
+ if not s.strip():
119
+ break
120
+ if s.startswith("## "):
121
+ break
122
+ m = KV_RE.match(s)
123
+ if m:
124
+ out[m.group(1).strip()] = m.group(2).strip()
125
+ except OSError:
126
+ pass
127
+ return out
128
+
129
+
130
+ def parse_filename(filename: str) -> tuple[str, str, str]:
131
+ m = FILENAME_RE.match(filename)
132
+ if not m:
133
+ base = filename[:-3] if filename.endswith(".md") else filename
134
+ return "", "", base
135
+ return m.group(1), m.group(2), m.group(3) or ""
136
+
137
+
138
+ def slugify(text: str) -> str:
139
+ s = (text or "").lower().strip()
140
+ s = re.sub(r"[^\w\s-]", "", s)
141
+ s = re.sub(r"[\s_]+", "-", s)
142
+ s = re.sub(r"-+", "-", s).strip("-")
143
+ return s[:60] or "unknown"
144
+
145
+
146
+ def load_drafts(draft_dir: Path) -> list[Draft]:
147
+ drafts: list[Draft] = []
148
+ if not draft_dir.is_dir():
149
+ return drafts
150
+ for entry in sorted(os.listdir(draft_dir)):
151
+ if not entry.endswith(".md"):
152
+ continue
153
+ if entry == "consolidation-manifest.json":
154
+ continue
155
+ path = draft_dir / entry
156
+ if not path.is_file():
157
+ continue
158
+ fm = parse_frontmatter(path)
159
+ phase_prefix, seq_from_name, slug_from_name = parse_filename(entry)
160
+ d = Draft(source_path=path, filename=entry)
161
+ d.phase = (fm.get("Phase") or phase_prefix or "").strip()
162
+ d.sequence = (fm.get("Sequence") or seq_from_name or "").strip()
163
+ slug_source = fm.get("Slug") or slug_from_name or path.stem
164
+ d.slug = slugify(slug_source)
165
+ d.verdict = (fm.get("Verdict") or "").strip().upper()
166
+ d.severity = (fm.get("Severity-Original") or "").strip().upper()
167
+ d.debate_path = (fm.get("Debate") or "").strip()
168
+ d.origin_finding = (fm.get("Origin-Finding") or "").strip()
169
+ d.origin_pattern = (fm.get("Origin-Pattern") or "").strip()
170
+ d.triage_priority = (fm.get("Triage-Priority") or "").strip().lower()
171
+ drafts.append(d)
172
+ return drafts
173
+
174
+
175
+ def scan_existing_ids(*findings_dirs: Path) -> dict[str, int]:
176
+ """Return the max existing ID number per severity prefix across the given
177
+ finding buckets.
178
+
179
+ Scans directory names matching `<C|H|M><number>-...` under every passed
180
+ directory (e.g. findings/ AND findings-theoretical/) and returns a dict
181
+ like {"C": 2, "H": 4, "M": 0} so a revisit run can seed its counters
182
+ from that floor without colliding with IDs already used in either bucket.
183
+ """
184
+ maxes = {"C": 0, "H": 0, "M": 0}
185
+ for findings_dir in findings_dirs:
186
+ if not findings_dir.is_dir():
187
+ continue
188
+ for entry in os.listdir(findings_dir):
189
+ m = EXISTING_FOLDER_RE.match(entry)
190
+ if not m:
191
+ continue
192
+ prefix = m.group(1)
193
+ try:
194
+ num = int(m.group(2))
195
+ except ValueError:
196
+ continue
197
+ if num > maxes.get(prefix, 0):
198
+ maxes[prefix] = num
199
+ return maxes
200
+
201
+
202
+ def assign_ids(
203
+ drafts: list[Draft],
204
+ seed_counters: Optional[dict[str, int]] = None,
205
+ ) -> tuple[list[Draft], list[dict]]:
206
+ """Partition drafts into (promoted, dropped) and assign global IDs.
207
+
208
+ `promoted` is every draft that passed the verdict + severity gates. Each
209
+ promoted draft receives a deterministic severity-prefixed ID from ONE
210
+ per-severity counter, regardless of bucket, so IDs stay unique and stable
211
+ even if a finding is later moved from findings/ to findings-theoretical/
212
+ by the post-PoC routing step.
213
+
214
+ A promoted draft tagged `Triage-Priority: skip` by the finding-grader is
215
+ flagged `is_theoretical=True`: it still gets an ID and a directory, but
216
+ `consolidate()` writes it straight into findings-theoretical/ (no PoC
217
+ build). `dropped` drafts failed verdict/severity and are discarded.
218
+ """
219
+ promoted: list[Draft] = []
220
+ dropped: list[dict] = []
221
+ for d in drafts:
222
+ if d.verdict != "VALID":
223
+ dropped.append(
224
+ {"file": d.filename, "reason": f"verdict={d.verdict or 'MISSING'}"}
225
+ )
226
+ continue
227
+ if d.severity not in SEVERITY_PREFIX:
228
+ dropped.append(
229
+ {"file": d.filename, "reason": f"severity={d.severity or 'MISSING'}"}
230
+ )
231
+ continue
232
+ d.is_theoretical = d.triage_priority == "skip"
233
+ promoted.append(d)
234
+
235
+ def sort_key(d: Draft):
236
+ sev_rank = SEVERITY_ORDER.index(d.severity)
237
+ # variants sort after non-variants of the same severity so the parent
238
+ # exists in the id map by the time variant resolution runs.
239
+ variant_rank = 1 if d.is_variant else 0
240
+ try:
241
+ seq_num = int(d.sequence)
242
+ except (TypeError, ValueError):
243
+ seq_num = 0
244
+ return (sev_rank, variant_rank, d.phase, seq_num, d.filename)
245
+
246
+ promoted.sort(key=sort_key)
247
+
248
+ # Seed counters from existing findings/ + findings-theoretical/ when
249
+ # running in revisit continuation mode so new IDs don't collide with
250
+ # round-1 folders in either bucket.
251
+ counters = {sev: 0 for sev in SEVERITY_PREFIX}
252
+ if seed_counters:
253
+ for sev, prefix in SEVERITY_PREFIX.items():
254
+ counters[sev] = seed_counters.get(prefix, 0)
255
+ for d in promoted:
256
+ counters[d.severity] += 1
257
+ d.assigned_id = f"{SEVERITY_PREFIX[d.severity]}{counters[d.severity]}"
258
+ return promoted, dropped
259
+
260
+
261
+ def resolve_variants(promoted: list[Draft]) -> None:
262
+ # Build the parent path->ID map across BOTH buckets: a variant in
263
+ # findings/ may point at a parent that was triage-skipped into
264
+ # findings-theoretical/ (or vice versa), and IDs share one namespace.
265
+ path_to_id: dict[str, str] = {}
266
+ for d in promoted:
267
+ if d.is_variant:
268
+ continue
269
+ path_to_id[str(d.source_path)] = d.assigned_id
270
+ path_to_id[d.source_path.name] = d.assigned_id
271
+ path_to_id[f"archon/findings-draft/{d.source_path.name}"] = d.assigned_id
272
+ path_to_id[f"findings-draft/{d.source_path.name}"] = d.assigned_id
273
+
274
+ for d in promoted:
275
+ if not d.is_variant:
276
+ continue
277
+ origin = d.origin_finding.strip()
278
+ if not origin:
279
+ continue
280
+ if origin in path_to_id:
281
+ d.origin_resolved_id = path_to_id[origin]
282
+ continue
283
+ basename = os.path.basename(origin)
284
+ if basename in path_to_id:
285
+ d.origin_resolved_id = path_to_id[basename]
286
+
287
+
288
+ def copy_if_exists(src: Path, dest: Path) -> bool:
289
+ if src.is_file():
290
+ shutil.copy2(src, dest)
291
+ return True
292
+ return False
293
+
294
+
295
+ def resolve_debate_path(raw: str, archon_dir: Path) -> Optional[Path]:
296
+ if not raw:
297
+ return None
298
+ p = Path(raw)
299
+ candidates = [p]
300
+ if not p.is_absolute():
301
+ candidates.append(Path.cwd() / p)
302
+ # Tolerate drafts that stored an archon-relative path.
303
+ if raw.startswith("archon/"):
304
+ candidates.append(archon_dir.parent / p)
305
+ else:
306
+ candidates.append(archon_dir / p)
307
+ for c in candidates:
308
+ if c.is_file():
309
+ return c
310
+ return None
311
+
312
+
313
+ def _materialize_finding(
314
+ d: Draft,
315
+ dest_dir: Path,
316
+ archon_dir: Path,
317
+ adv_dir: Path,
318
+ revisit_meta: Optional[dict],
319
+ ) -> dict:
320
+ """Create `<dest_dir>/<ID>-<slug>/`, copy the draft + sibling artefacts +
321
+ metadata.json into it, and return the manifest entry. Shared by both the
322
+ actionable (findings/) and theoretical (findings-theoretical/) buckets so
323
+ a triage-skipped finding has the exact same on-disk shape as an
324
+ actionable one — finding-writer can author report.md for either.
325
+ """
326
+ folder = dest_dir / f"{d.assigned_id}-{d.slug}"
327
+ (folder / "evidence").mkdir(parents=True, exist_ok=True)
328
+ d.folder = folder
329
+
330
+ shutil.copy2(d.source_path, folder / "draft.md")
331
+
332
+ if adv_dir.is_dir():
333
+ for candidate in (
334
+ adv_dir / f"{d.slug}-review.md",
335
+ adv_dir / f"{d.source_path.stem}-review.md",
336
+ ):
337
+ if copy_if_exists(candidate, folder / "adversarial-review.md"):
338
+ break
339
+
340
+ debate = resolve_debate_path(d.debate_path, archon_dir)
341
+ if debate is not None:
342
+ shutil.copy2(debate, folder / "debate.md")
343
+
344
+ is_revisit = bool(revisit_meta and revisit_meta.get("round"))
345
+ meta: dict = {}
346
+ if d.is_variant:
347
+ meta.update(
348
+ {
349
+ "is_variant": True,
350
+ "origin_finding_id": d.origin_resolved_id,
351
+ "origin_finding_draft": d.origin_finding,
352
+ "origin_pattern": d.origin_pattern,
353
+ }
354
+ )
355
+ elif is_revisit:
356
+ # Non-revisit non-variants emit no metadata.json (the report-composer
357
+ # treats its absence as "round 1"); a revisit round still needs the
358
+ # is_variant: False marker so the round stamp below has a home.
359
+ meta["is_variant"] = False
360
+ if is_revisit:
361
+ meta.update(
362
+ {
363
+ "round": revisit_meta["round"],
364
+ "revisit_id": revisit_meta.get("revisit_id"),
365
+ "model": revisit_meta.get("model"),
366
+ "agent_sdk": revisit_meta.get("agent_sdk"),
367
+ }
368
+ )
369
+ if meta:
370
+ (folder / "metadata.json").write_text(json.dumps(meta, indent=2) + "\n")
371
+
372
+ return {
373
+ "id": d.assigned_id,
374
+ "slug": d.slug,
375
+ "severity": d.severity,
376
+ "folder": str(folder),
377
+ "draft_path": str(d.source_path),
378
+ "is_variant": d.is_variant,
379
+ "origin_finding_id": d.origin_resolved_id if d.is_variant else "",
380
+ }
381
+
382
+
383
+ _SEVERITY_RANK = {sev: i for i, sev in enumerate(SEVERITY_ORDER)}
384
+
385
+
386
+ def consolidate(archon_dir: Path, continue_ids: bool = False) -> int:
387
+ draft_dir = archon_dir / "findings-draft"
388
+ findings_dir = archon_dir / "findings"
389
+ theoretical_dir = archon_dir / "findings-theoretical"
390
+ adv_dir = archon_dir / "adversarial-reviews"
391
+
392
+ drafts = load_drafts(draft_dir)
393
+ if not drafts:
394
+ print(f"error: no draft files found in {draft_dir}", file=sys.stderr)
395
+ return 1
396
+
397
+ seed_counters: Optional[dict[str, int]] = None
398
+ if continue_ids:
399
+ seed_counters = scan_existing_ids(findings_dir, theoretical_dir)
400
+ print(
401
+ f"continue-ids: seeding counters from existing findings/ + "
402
+ f"findings-theoretical/: C={seed_counters.get('C', 0)} "
403
+ f"H={seed_counters.get('H', 0)} M={seed_counters.get('M', 0)}",
404
+ file=sys.stderr,
405
+ )
406
+
407
+ revisit_meta: Optional[dict] = None
408
+ if continue_ids:
409
+ round_raw = os.environ.get("ARCHON_REVISIT_ROUND", "").strip()
410
+ try:
411
+ round_int = int(round_raw) if round_raw else 0
412
+ except ValueError:
413
+ round_int = 0
414
+ revisit_meta = {
415
+ "round": round_int or None,
416
+ "revisit_id": os.environ.get("ARCHON_REVISIT_ID", "") or None,
417
+ "model": os.environ.get("ARCHON_REVISIT_MODEL", "") or None,
418
+ "agent_sdk": os.environ.get("ARCHON_REVISIT_AGENT_SDK", "") or None,
419
+ }
420
+
421
+ promoted, dropped = assign_ids(drafts, seed_counters=seed_counters)
422
+ if not promoted:
423
+ manifest = {
424
+ "archon_dir": str(archon_dir),
425
+ "findings": [],
426
+ "theoretical": [],
427
+ "dropped": dropped,
428
+ "counts": {
429
+ "critical": 0,
430
+ "high": 0,
431
+ "medium": 0,
432
+ "total": 0,
433
+ "dropped": len(dropped),
434
+ "theoretical": 0,
435
+ },
436
+ }
437
+ _write_manifest(draft_dir, manifest)
438
+ print(json.dumps(manifest, indent=2))
439
+ print(
440
+ "warning: no VALID Medium-or-higher drafts to consolidate",
441
+ file=sys.stderr,
442
+ )
443
+ return 1
444
+
445
+ resolve_variants(promoted)
446
+
447
+ findings_out: list[dict] = []
448
+ theoretical_out: list[dict] = []
449
+ for d in promoted:
450
+ dest = theoretical_dir if d.is_theoretical else findings_dir
451
+ entry = _materialize_finding(d, dest, archon_dir, adv_dir, revisit_meta)
452
+ (theoretical_out if d.is_theoretical else findings_out).append(entry)
453
+
454
+ counts = {
455
+ "critical": sum(1 for d in promoted if d.severity == "CRITICAL"),
456
+ "high": sum(1 for d in promoted if d.severity == "HIGH"),
457
+ "medium": sum(1 for d in promoted if d.severity == "MEDIUM"),
458
+ "total": len(promoted),
459
+ "dropped": len(dropped),
460
+ "theoretical": len(theoretical_out),
461
+ }
462
+ # findings/ feeds the poc-author fan-out so it sorts P0-first; theoretical/
463
+ # is reporter-only (no PoC budget to spend) so plain severity order suffices.
464
+ findings_out = _sort_by_triage_priority(findings_out, promoted)
465
+ theoretical_out.sort(
466
+ key=lambda e: (_SEVERITY_RANK.get(e.get("severity", ""), 9), e.get("id", ""))
467
+ )
468
+ manifest = {
469
+ "archon_dir": str(archon_dir),
470
+ "findings": findings_out,
471
+ "theoretical": theoretical_out,
472
+ "dropped": dropped,
473
+ "counts": counts,
474
+ }
475
+ _write_manifest(draft_dir, manifest)
476
+ print(json.dumps(manifest, indent=2))
477
+ msg = (
478
+ f"consolidated {counts['total']} findings "
479
+ f"(C:{counts['critical']} H:{counts['high']} M:{counts['medium']}); "
480
+ f"{len(findings_out)} actionable -> findings/, "
481
+ f"{len(theoretical_out)} triage-skip -> findings-theoretical/"
482
+ )
483
+ msg += f", dropped {counts['dropped']}"
484
+ print(msg, file=sys.stderr)
485
+ return 0
486
+
487
+
488
+ # Order in which P-priorities are processed by downstream poc-author fan-out.
489
+ # Lower index = higher priority; unknown / missing priorities sort after P2 so
490
+ # the orchestrator still builds them but only after the explicitly-prioritized
491
+ # set has consumed its budget.
492
+ TRIAGE_PRIORITY_RANK = {"p0": 0, "p1": 1, "p2": 2, "": 3}
493
+
494
+
495
+ def _sort_by_triage_priority(
496
+ findings_out: list[dict], promoted: list[Draft]
497
+ ) -> list[dict]:
498
+ """Return findings_out sorted so triage P0 entries come first, then P1,
499
+ then P2, then anything without a triage marker. Within each priority
500
+ bucket the existing severity ordering is preserved (CRITICAL → HIGH →
501
+ MEDIUM as already established by `assign_ids`).
502
+ """
503
+ by_id: dict[str, str] = {}
504
+ for d in promoted:
505
+ by_id[d.assigned_id] = (d.triage_priority or "").lower()
506
+
507
+ def key(entry: dict):
508
+ prio = by_id.get(entry.get("id", ""), "")
509
+ prio_rank = TRIAGE_PRIORITY_RANK.get(prio, 3)
510
+ sev_rank = _SEVERITY_RANK.get(entry.get("severity", ""), 9)
511
+ return (prio_rank, sev_rank, entry.get("id", ""))
512
+
513
+ return sorted(findings_out, key=key)
514
+
515
+
516
+ def _write_manifest(draft_dir: Path, manifest: dict) -> None:
517
+ draft_dir.mkdir(parents=True, exist_ok=True)
518
+ path = draft_dir / "consolidation-manifest.json"
519
+ path.write_text(json.dumps(manifest, indent=2) + "\n")
520
+
521
+
522
+ def main() -> None:
523
+ argv = sys.argv[1:]
524
+ if argv and argv[0] in ("-h", "--help"):
525
+ print(__doc__)
526
+ sys.exit(0)
527
+
528
+ continue_ids = False
529
+ positional: list[str] = []
530
+ for arg in argv:
531
+ if arg == "--continue-ids":
532
+ continue_ids = True
533
+ else:
534
+ positional.append(arg)
535
+ if len(positional) > 1:
536
+ print(
537
+ "usage: consolidate_drafts.py [archon_dir] [--continue-ids]",
538
+ file=sys.stderr,
539
+ )
540
+ sys.exit(2)
541
+
542
+ archon_dir = Path(positional[0]) if positional else Path("archon")
543
+ if not archon_dir.is_dir():
544
+ print(f"error: archon dir not found: {archon_dir}", file=sys.stderr)
545
+ sys.exit(2)
546
+ try:
547
+ sys.exit(consolidate(archon_dir, continue_ids=continue_ids))
548
+ except OSError as e:
549
+ print(f"error: I/O failure during consolidation: {e}", file=sys.stderr)
550
+ sys.exit(3)
551
+
552
+
553
+ if __name__ == "__main__":
554
+ main()