@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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/agents/access-auditor.md +300 -0
- package/agents/assumption-breaker.md +154 -0
- package/agents/attack-designer.md +116 -0
- package/agents/code-scanner.md +139 -0
- package/agents/concurrency-auditor.md +238 -0
- package/agents/confirm-writer.md +257 -0
- package/agents/context-reviewer.md +274 -0
- package/agents/cross-verifier.md +165 -0
- package/agents/cve-scout.md +381 -0
- package/agents/env-builder.md +282 -0
- package/agents/env-profiler.md +205 -0
- package/agents/evidence-collector.md +140 -0
- package/agents/finding-grader.md +142 -0
- package/agents/finding-writer.md +148 -0
- package/agents/flow-tracer.md +106 -0
- package/agents/goal-backtracer.md +146 -0
- package/agents/history-miner.md +467 -0
- package/agents/independent-verifier.md +118 -0
- package/agents/intent-mapper.md +183 -0
- package/agents/longshot-collector.md +128 -0
- package/agents/longshot-prober.md +126 -0
- package/agents/patch-auditor.md +73 -0
- package/agents/poc-author.md +124 -0
- package/agents/poc-runner.md +194 -0
- package/agents/probe-lead.md +269 -0
- package/agents/red-challenger.md +101 -0
- package/agents/report-composer.md +208 -0
- package/agents/review-adjudicator.md +216 -0
- package/agents/spec-auditor.md +155 -0
- package/agents/taint-tracer.md +265 -0
- package/agents/test-locator.md +209 -0
- package/agents/threat-modeler.md +132 -0
- package/agents/variant-scanner.md +108 -0
- package/agents/variant-spotter.md +110 -0
- package/bin/piolium.mjs +376 -0
- package/extensions/piolium/_vendor/yaml.bundle.d.mts +6 -0
- package/extensions/piolium/_vendor/yaml.bundle.mjs +139 -0
- package/extensions/piolium/agent-runner.ts +322 -0
- package/extensions/piolium/agents.ts +266 -0
- package/extensions/piolium/audit-state.ts +522 -0
- package/extensions/piolium/bundled-resources.ts +97 -0
- package/extensions/piolium/candidate-scan.ts +966 -0
- package/extensions/piolium/command-target.ts +177 -0
- package/extensions/piolium/console-stream.ts +57 -0
- package/extensions/piolium/export-results.ts +380 -0
- package/extensions/piolium/findings.ts +448 -0
- package/extensions/piolium/heartbeat.ts +182 -0
- package/extensions/piolium/help.ts +234 -0
- package/extensions/piolium/index.ts +1865 -0
- package/extensions/piolium/longshot.ts +530 -0
- package/extensions/piolium/matcher-suggestions.ts +196 -0
- package/extensions/piolium/matcher-utils.ts +83 -0
- package/extensions/piolium/modes/balanced.ts +750 -0
- package/extensions/piolium/modes/confirm-bootstrap.ts +186 -0
- package/extensions/piolium/modes/confirm.ts +697 -0
- package/extensions/piolium/modes/deep.ts +917 -0
- package/extensions/piolium/modes/diff.ts +177 -0
- package/extensions/piolium/modes/lite.ts +540 -0
- package/extensions/piolium/modes/longshot.ts +595 -0
- package/extensions/piolium/modes/merge.ts +204 -0
- package/extensions/piolium/modes/phase-runner.ts +267 -0
- package/extensions/piolium/modes/reinvest.ts +546 -0
- package/extensions/piolium/modes/revisit.ts +279 -0
- package/extensions/piolium/modes.ts +48 -0
- package/extensions/piolium/phase-labels.ts +123 -0
- package/extensions/piolium/phase-status-strip.ts +92 -0
- package/extensions/piolium/prompt-prefix-editor.ts +39 -0
- package/extensions/piolium/providers/anthropic-vertex.ts +836 -0
- package/extensions/piolium/recon.ts +409 -0
- package/extensions/piolium/result-stats.ts +105 -0
- package/extensions/piolium/retry.ts +120 -0
- package/extensions/piolium/scheduler.ts +212 -0
- package/extensions/piolium/secrets.ts +368 -0
- package/extensions/piolium/tools/web-tools.ts +148 -0
- package/package.json +77 -0
- package/skills/agentic-actions-auditor/SKILL.md +327 -0
- package/skills/agentic-actions-auditor/references/action-profiles.md +186 -0
- package/skills/agentic-actions-auditor/references/cross-file-resolution.md +209 -0
- package/skills/agentic-actions-auditor/references/foundations.md +94 -0
- package/skills/agentic-actions-auditor/references/vector-a-env-var-intermediary.md +77 -0
- package/skills/agentic-actions-auditor/references/vector-b-direct-expression-injection.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-c-cli-data-fetch.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-d-pr-target-checkout.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-e-error-log-injection.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-f-subshell-expansion.md +82 -0
- package/skills/agentic-actions-auditor/references/vector-g-eval-of-ai-output.md +91 -0
- package/skills/agentic-actions-auditor/references/vector-h-dangerous-sandbox-configs.md +102 -0
- package/skills/agentic-actions-auditor/references/vector-i-wildcard-allowlists.md +88 -0
- package/skills/audit/SKILL.md +562 -0
- package/skills/audit/assets/icon.svg +7 -0
- package/skills/audit/hooks/scripts/validate_phase_output.py +550 -0
- package/skills/audit/references/adversarial-review.md +148 -0
- package/skills/audit/references/architecture-aware-sast.md +306 -0
- package/skills/audit/references/audit-workflow.md +737 -0
- package/skills/audit/references/chamber-protocol.md +384 -0
- package/skills/audit/references/creative-attack-modes.md +221 -0
- package/skills/audit/references/deep-analysis.md +273 -0
- package/skills/audit/references/domain-attack-playbooks.md +1129 -0
- package/skills/audit/references/knowledge-base-template.md +513 -0
- package/skills/audit/references/real-env-validation.md +191 -0
- package/skills/audit/references/report-templates.md +417 -0
- package/skills/audit/references/triage-and-prereqs.md +134 -0
- package/skills/audit/scripts/consolidate_drafts.py +554 -0
- package/skills/audit/scripts/partition_findings.py +152 -0
- package/skills/audit/scripts/rg-hotspots.sh +121 -0
- package/skills/audit/scripts/stamp_file_state.py +349 -0
- package/skills/code-reviewer/SKILL.md +65 -0
- package/skills/codeql/SKILL.md +281 -0
- package/skills/codeql/references/build-fixes.md +90 -0
- package/skills/codeql/references/diagnostic-query-templates.md +339 -0
- package/skills/codeql/references/extension-yaml-format.md +209 -0
- package/skills/codeql/references/important-only-suite.md +153 -0
- package/skills/codeql/references/language-details.md +207 -0
- package/skills/codeql/references/macos-arm64e-workaround.md +179 -0
- package/skills/codeql/references/performance-tuning.md +111 -0
- package/skills/codeql/references/quality-assessment.md +172 -0
- package/skills/codeql/references/ruleset-catalog.md +63 -0
- package/skills/codeql/references/run-all-suite.md +92 -0
- package/skills/codeql/references/sarif-processing.md +79 -0
- package/skills/codeql/references/threat-models.md +51 -0
- package/skills/codeql/workflows/build-database.md +280 -0
- package/skills/codeql/workflows/create-data-extensions.md +261 -0
- package/skills/codeql/workflows/run-analysis.md +301 -0
- package/skills/differential-review/SKILL.md +220 -0
- package/skills/differential-review/adversarial.md +203 -0
- package/skills/differential-review/methodology.md +234 -0
- package/skills/differential-review/patterns.md +300 -0
- package/skills/differential-review/reporting.md +369 -0
- package/skills/fp-check/SKILL.md +125 -0
- package/skills/fp-check/references/bug-class-verification.md +114 -0
- package/skills/fp-check/references/deep-verification.md +143 -0
- package/skills/fp-check/references/evidence-templates.md +91 -0
- package/skills/fp-check/references/false-positive-patterns.md +115 -0
- package/skills/fp-check/references/gate-reviews.md +27 -0
- package/skills/fp-check/references/standard-verification.md +78 -0
- package/skills/insecure-defaults/SKILL.md +117 -0
- package/skills/insecure-defaults/references/examples.md +409 -0
- package/skills/last30days/SKILL.md +444 -0
- package/skills/sarif-parsing/SKILL.md +483 -0
- package/skills/sarif-parsing/resources/jq-queries.md +162 -0
- package/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
- package/skills/security-threat-model/LICENSE.txt +201 -0
- package/skills/security-threat-model/SKILL.md +81 -0
- package/skills/security-threat-model/agents/openai.yaml +4 -0
- package/skills/security-threat-model/references/prompt-template.md +255 -0
- package/skills/security-threat-model/references/security-controls-and-assets.md +32 -0
- package/skills/semgrep/SKILL.md +212 -0
- package/skills/semgrep/references/rulesets.md +162 -0
- package/skills/semgrep/references/scan-modes.md +110 -0
- package/skills/semgrep/references/scanner-task-prompt.md +140 -0
- package/skills/semgrep/scripts/merge_sarif.py +203 -0
- package/skills/semgrep/workflows/scan-workflow.md +311 -0
- package/skills/semgrep-rule-creator/SKILL.md +168 -0
- package/skills/semgrep-rule-creator/references/quick-reference.md +202 -0
- package/skills/semgrep-rule-creator/references/workflow.md +240 -0
- package/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
- package/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
- package/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
- package/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
- package/skills/sharp-edges/SKILL.md +292 -0
- package/skills/sharp-edges/references/auth-patterns.md +252 -0
- package/skills/sharp-edges/references/case-studies.md +274 -0
- package/skills/sharp-edges/references/config-patterns.md +333 -0
- package/skills/sharp-edges/references/crypto-apis.md +190 -0
- package/skills/sharp-edges/references/lang-c.md +205 -0
- package/skills/sharp-edges/references/lang-csharp.md +285 -0
- package/skills/sharp-edges/references/lang-go.md +270 -0
- package/skills/sharp-edges/references/lang-java.md +263 -0
- package/skills/sharp-edges/references/lang-javascript.md +269 -0
- package/skills/sharp-edges/references/lang-kotlin.md +265 -0
- package/skills/sharp-edges/references/lang-php.md +245 -0
- package/skills/sharp-edges/references/lang-python.md +274 -0
- package/skills/sharp-edges/references/lang-ruby.md +273 -0
- package/skills/sharp-edges/references/lang-rust.md +272 -0
- package/skills/sharp-edges/references/lang-swift.md +287 -0
- package/skills/sharp-edges/references/language-specific.md +588 -0
- package/skills/spec-to-code-compliance/SKILL.md +357 -0
- package/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
- package/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
- package/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
- package/skills/supply-chain-risk-auditor/SKILL.md +67 -0
- package/skills/supply-chain-risk-auditor/resources/results-template.md +41 -0
- package/skills/variant-analysis/METHODOLOGY.md +327 -0
- package/skills/variant-analysis/SKILL.md +142 -0
- package/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
- package/skills/variant-analysis/resources/codeql/go.ql +69 -0
- package/skills/variant-analysis/resources/codeql/java.ql +71 -0
- package/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
- package/skills/variant-analysis/resources/codeql/python.ql +80 -0
- package/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
- package/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
- package/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
- package/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
- package/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
- package/skills/variant-analysis/resources/variant-report-template.md +75 -0
- package/skills/vuln-report/SKILL.md +137 -0
- package/skills/vuln-report/agents/openai.yaml +4 -0
- package/skills/vuln-report/references/report-template.md +135 -0
- package/skills/wooyun-legacy/SKILL.md +367 -0
- package/skills/wooyun-legacy/references/bank-penetration.md +222 -0
- package/skills/wooyun-legacy/references/checklists/command-execution-checklist.md +119 -0
- package/skills/wooyun-legacy/references/checklists/csrf-checklist.md +74 -0
- package/skills/wooyun-legacy/references/checklists/file-upload-checklist.md +108 -0
- package/skills/wooyun-legacy/references/checklists/info-disclosure-checklist.md +114 -0
- package/skills/wooyun-legacy/references/checklists/logic-flaws-checklist.md +95 -0
- package/skills/wooyun-legacy/references/checklists/misconfig-checklist.md +124 -0
- package/skills/wooyun-legacy/references/checklists/path-traversal-checklist.md +87 -0
- package/skills/wooyun-legacy/references/checklists/rce-checklist.md +93 -0
- package/skills/wooyun-legacy/references/checklists/sql-injection-checklist.md +97 -0
- package/skills/wooyun-legacy/references/checklists/ssrf-checklist.md +99 -0
- package/skills/wooyun-legacy/references/checklists/unauthorized-access-checklist.md +89 -0
- package/skills/wooyun-legacy/references/checklists/weak-password-checklist.md +115 -0
- package/skills/wooyun-legacy/references/checklists/xss-checklist.md +103 -0
- package/skills/wooyun-legacy/references/checklists/xxe-checklist.md +130 -0
- package/skills/wooyun-legacy/references/info-disclosure.md +975 -0
- package/skills/wooyun-legacy/references/logic-flaws.md +721 -0
- package/skills/wooyun-legacy/references/path-traversal.md +1191 -0
- package/skills/wooyun-legacy/references/telecom-penetration.md +156 -0
- package/skills/wooyun-legacy/references/unauthorized-access.md +980 -0
- package/skills/wooyun-legacy/references/xss.md +746 -0
- package/skills/zeroize-audit/SKILL.md +371 -0
- package/skills/zeroize-audit/configs/c.yaml +21 -0
- package/skills/zeroize-audit/configs/default.yaml +128 -0
- package/skills/zeroize-audit/configs/rust.yaml +83 -0
- package/skills/zeroize-audit/prompts/report_template.md +238 -0
- package/skills/zeroize-audit/prompts/system.md +163 -0
- package/skills/zeroize-audit/prompts/task.md +97 -0
- package/skills/zeroize-audit/references/compile-commands.md +231 -0
- package/skills/zeroize-audit/references/detection-strategy.md +191 -0
- package/skills/zeroize-audit/references/ir-analysis.md +252 -0
- package/skills/zeroize-audit/references/mcp-analysis.md +221 -0
- package/skills/zeroize-audit/references/poc-generation.md +470 -0
- package/skills/zeroize-audit/references/rust-zeroization-patterns.md +867 -0
- package/skills/zeroize-audit/schemas/input.json +83 -0
- package/skills/zeroize-audit/schemas/output.json +140 -0
- package/skills/zeroize-audit/tools/analyze_asm.sh +202 -0
- package/skills/zeroize-audit/tools/analyze_cfg.py +381 -0
- package/skills/zeroize-audit/tools/analyze_heap.sh +211 -0
- package/skills/zeroize-audit/tools/analyze_ir_semantic.py +429 -0
- package/skills/zeroize-audit/tools/diff_ir.sh +135 -0
- package/skills/zeroize-audit/tools/diff_rust_mir.sh +189 -0
- package/skills/zeroize-audit/tools/emit_asm.sh +67 -0
- package/skills/zeroize-audit/tools/emit_ir.sh +77 -0
- package/skills/zeroize-audit/tools/emit_rust_asm.sh +178 -0
- package/skills/zeroize-audit/tools/emit_rust_ir.sh +150 -0
- package/skills/zeroize-audit/tools/emit_rust_mir.sh +158 -0
- package/skills/zeroize-audit/tools/extract_compile_flags.py +284 -0
- package/skills/zeroize-audit/tools/generate_poc.py +1329 -0
- package/skills/zeroize-audit/tools/mcp/apply_confidence_gates.py +113 -0
- package/skills/zeroize-audit/tools/mcp/check_mcp.sh +68 -0
- package/skills/zeroize-audit/tools/mcp/normalize_mcp_evidence.py +125 -0
- package/skills/zeroize-audit/tools/scripts/check_llvm_patterns.py +481 -0
- package/skills/zeroize-audit/tools/scripts/check_mir_patterns.py +554 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm.py +424 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_aarch64.py +300 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_x86.py +283 -0
- package/skills/zeroize-audit/tools/scripts/find_dangerous_apis.py +375 -0
- package/skills/zeroize-audit/tools/scripts/semantic_audit.py +923 -0
- package/skills/zeroize-audit/tools/track_dataflow.sh +196 -0
- package/skills/zeroize-audit/tools/validate_rust_toolchain.sh +298 -0
- package/skills/zeroize-audit/workflows/phase-0-preflight.md +150 -0
- package/skills/zeroize-audit/workflows/phase-1-source-analysis.md +144 -0
- package/skills/zeroize-audit/workflows/phase-2-compiler-analysis.md +139 -0
- package/skills/zeroize-audit/workflows/phase-3-interim-report.md +46 -0
- package/skills/zeroize-audit/workflows/phase-4-poc-generation.md +46 -0
- package/skills/zeroize-audit/workflows/phase-5-poc-validation.md +136 -0
- package/skills/zeroize-audit/workflows/phase-6-final-report.md +44 -0
- package/skills/zeroize-audit/workflows/phase-7-test-generation.md +42 -0
- 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()
|