@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,1865 @@
1
+ /**
2
+ * piolium extension entry point.
3
+ *
4
+ * Registers the `/piolium-*` slash commands. M0/M1/M2 ship:
5
+ * - /piolium-status — read-only state inspector
6
+ * - /piolium-smoke — runs a tiny inline agent end-to-end via the runner
7
+ * so operators can verify their Pi setup before kicking
8
+ * off a real audit
9
+ * - /piolium-lite — stub; real impl in M3
10
+ *
11
+ * Pi loads this file via jiti at runtime (see `pi.extensions` in
12
+ * package.json). The default export runs once per session.
13
+ */
14
+
15
+ import { randomUUID } from "node:crypto";
16
+ import type {
17
+ AgentSessionEvent,
18
+ ExtensionAPI,
19
+ ExtensionCommandContext,
20
+ ExtensionUIContext,
21
+ } from "@earendil-works/pi-coding-agent";
22
+ import { Container, Text } from "@earendil-works/pi-tui";
23
+ import { AgentRunError, type AgentRuntimeModel, runAgent } from "./agent-runner.ts";
24
+ import type { AgentDefinition } from "./agents.ts";
25
+ import {
26
+ type AuditMode,
27
+ type PhaseState,
28
+ formatAuditStatus,
29
+ formatPhaseDetailLines,
30
+ getAuditStatePath,
31
+ latestAudit,
32
+ latestResumableAudit,
33
+ readAuditState,
34
+ tallyPhases,
35
+ } from "./audit-state.ts";
36
+ import {
37
+ parseArchonCommandArgs,
38
+ readOptionValue,
39
+ readRepeatedOptionValues,
40
+ } from "./command-target.ts";
41
+ import { type ArchonConsoleStream, createArchonConsoleStream } from "./console-stream.ts";
42
+ import { type ExportFormat, normalizeExportSeverity, runExport } from "./export-results.ts";
43
+ import {
44
+ PHASE_HEARTBEAT_UI_COLOR,
45
+ type PhaseHeartbeat,
46
+ formatPhaseHeartbeat,
47
+ formatPhaseHeartbeatStatusLine,
48
+ } from "./heartbeat.ts";
49
+ import { PIOLIUM_STARTUP_HINT, buildPioliumHelpLines } from "./help.ts";
50
+ import { runMatcherLearn } from "./matcher-suggestions.ts";
51
+ import { phasesFor } from "./modes.ts";
52
+ import { runBalancedAudit } from "./modes/balanced.ts";
53
+ import { bootstrapResultsOnlyConfirm } from "./modes/confirm-bootstrap.ts";
54
+ import { runConfirmAudit } from "./modes/confirm.ts";
55
+ import { runDeepAudit } from "./modes/deep.ts";
56
+ import { runDiffAudit } from "./modes/diff.ts";
57
+ import { runLiteAudit } from "./modes/lite.ts";
58
+ import { runLongshotAudit } from "./modes/longshot.ts";
59
+ import { runMergeAudit } from "./modes/merge.ts";
60
+ import { runReinvestAudit } from "./modes/reinvest.ts";
61
+ import { runRevisitAudit } from "./modes/revisit.ts";
62
+ import { formatPhaseDetailLabel } from "./phase-labels.ts";
63
+ import { extractStatusPhase, renderPhaseStatusList } from "./phase-status-strip.ts";
64
+ import { ArchonPromptPrefixEditor, shouldUseArchonPromptPrefix } from "./prompt-prefix-editor.ts";
65
+ import { registerAnthropicVertex } from "./providers/anthropic-vertex.ts";
66
+ import { buildAuditResultStatsLines } from "./result-stats.ts";
67
+ import { readNonNegativeIntEnv, readPositiveIntEnv, runWithRetry } from "./retry.ts";
68
+
69
+ const ARCHON_STREAM = "archon-stream";
70
+ const FLAG_DIR = "plm-dir";
71
+ const FLAG_SINCE = "plm-since";
72
+ const FLAG_COMMIT_MAX = "plm-scan-limit";
73
+ const FLAG_COMMIT_SINCE = "plm-scan-since";
74
+ const COMMAND_RESULT_DIALOG_HINT = "Esc returns to chat";
75
+ const COMMAND_RESULT_DISMISS_HINT = "Press Esc to return to chat.";
76
+ const AUDIT_STATE_PROGRESS_POLL_MS = readPositiveIntEnv("PIOLIUM_AUDIT_STATE_POLL_MS", 5000);
77
+ const AUDIT_STATE_PROGRESS_REPEAT_MS = readPositiveIntEnv("PIOLIUM_AUDIT_STATE_REPEAT_MS", 30_000);
78
+ const INITIAL_UI_PAINT_DELAY_MS = 160;
79
+ const PREPARING_SPINNER_INTERVAL_MS = readPositiveIntEnv(
80
+ "PIOLIUM_PREPARING_SPINNER_INTERVAL_MS",
81
+ 80,
82
+ );
83
+ const PREPARING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
84
+
85
+ const FLAG_ENV_MAPPINGS = [
86
+ {
87
+ flag: FLAG_COMMIT_MAX,
88
+ env: "PIOLIUM_COMMIT_SCAN_LIMIT",
89
+ description: "Commit archaeology max commits",
90
+ },
91
+ {
92
+ flag: FLAG_COMMIT_SINCE,
93
+ env: "PIOLIUM_COMMIT_SCAN_SINCE",
94
+ description: "Commit archaeology git --since window",
95
+ },
96
+ {
97
+ flag: "plm-file-records",
98
+ env: "PIOLIUM_FILE_RECORDS",
99
+ description: "Set 1/true to write piolium/file-records per-file scan records (default off)",
100
+ },
101
+ {
102
+ flag: "plm-phase-retries",
103
+ env: "PIOLIUM_PHASE_MAX_RETRIES",
104
+ description: "Agent phase retries after the first attempt (default: 5)",
105
+ },
106
+ {
107
+ flag: "plm-phase-backoff",
108
+ env: "PIOLIUM_PHASE_BACKOFF_BASE_MS",
109
+ description: "Agent phase retry base backoff in ms (default: 5000)",
110
+ },
111
+ {
112
+ flag: "plm-phase-backoff-max",
113
+ env: "PIOLIUM_PHASE_BACKOFF_MAX_MS",
114
+ description: "Agent phase retry max backoff in ms (default: 120000)",
115
+ },
116
+ {
117
+ flag: "plm-lite-retries",
118
+ env: "PIOLIUM_LITE_PHASE_MAX_RETRIES",
119
+ description: "Lite deterministic phase retries after the first attempt (default: phase retries)",
120
+ },
121
+ {
122
+ flag: "plm-lite-backoff",
123
+ env: "PIOLIUM_LITE_PHASE_BACKOFF_BASE_MS",
124
+ description: "Lite deterministic phase retry base backoff in ms (default: phase backoff)",
125
+ },
126
+ {
127
+ flag: "plm-lite-backoff-max",
128
+ env: "PIOLIUM_LITE_PHASE_BACKOFF_MAX_MS",
129
+ description: "Lite deterministic phase retry max backoff in ms (default: phase max backoff)",
130
+ },
131
+ {
132
+ flag: "plm-command-retries",
133
+ env: "PIOLIUM_COMMAND_MAX_RETRIES",
134
+ description: "Top-level /piolium-* command retries after the first attempt (default: 3)",
135
+ },
136
+ {
137
+ flag: "plm-command-backoff",
138
+ env: "PIOLIUM_COMMAND_BACKOFF_BASE_MS",
139
+ description: "Top-level command retry base backoff in ms (default: 5000)",
140
+ },
141
+ {
142
+ flag: "plm-command-backoff-max",
143
+ env: "PIOLIUM_COMMAND_BACKOFF_MAX_MS",
144
+ description: "Top-level command retry max backoff in ms (default: 120000)",
145
+ },
146
+ {
147
+ flag: "plm-longshot-limit",
148
+ env: "PIOLIUM_LONGSHOT_LIMIT",
149
+ description: "Longshot max files to hunt (default: 1000)",
150
+ },
151
+ {
152
+ flag: "plm-longshot-timeout",
153
+ env: "PIOLIUM_LONGSHOT_TIMEOUT_MS",
154
+ description: "Longshot per-file kill timer in ms (default: 21600000 / 6h)",
155
+ },
156
+ {
157
+ flag: "plm-longshot-langs",
158
+ env: "PIOLIUM_LONGSHOT_LANGS",
159
+ description:
160
+ "Longshot language allowlist (comma-list, e.g. python,go); default auto-detects dominant",
161
+ },
162
+ {
163
+ flag: "plm-longshot-include-tests",
164
+ env: "PIOLIUM_LONGSHOT_INCLUDE_TESTS",
165
+ description: "Set 1/true to include test files in longshot enumeration (default off)",
166
+ },
167
+ ] as const;
168
+
169
+ function agentRuntimeFromCommandContext(
170
+ pi: ExtensionAPI,
171
+ ctx: ExtensionCommandContext,
172
+ ): AgentRuntimeModel {
173
+ const thinkingLevel = readParentThinkingLevel(pi);
174
+ return {
175
+ ...(ctx.model ? { model: ctx.model } : {}),
176
+ modelRegistry: ctx.modelRegistry,
177
+ ...(thinkingLevel ? { thinkingLevel } : {}),
178
+ };
179
+ }
180
+
181
+ function readParentThinkingLevel(
182
+ pi: ExtensionAPI,
183
+ ): ReturnType<ExtensionAPI["getThinkingLevel"]> | undefined {
184
+ try {
185
+ return pi.getThinkingLevel();
186
+ } catch {
187
+ // Extension actions throw before runner.initialize(); guard so
188
+ // non-interactive flows that call commands very early still work.
189
+ return undefined;
190
+ }
191
+ }
192
+
193
+ interface CommandUi {
194
+ notify: (text: string, level: "info" | "warning" | "error") => void;
195
+ setStatus: (key: string, text?: string) => void;
196
+ }
197
+
198
+ interface PhaseStripContext {
199
+ cwd: string;
200
+ ui: ExtensionUIContext;
201
+ }
202
+
203
+ interface PhaseStripCommandUi extends CommandUi {
204
+ onPhaseHeartbeat: (phase: string, heartbeat?: PhaseHeartbeat) => void;
205
+ clearStatus: () => void;
206
+ }
207
+
208
+ class CommandFailedStatus<T> extends Error {
209
+ constructor(
210
+ label: string,
211
+ public readonly result: T,
212
+ ) {
213
+ super(`${label} returned failed status`);
214
+ this.name = "CommandFailedStatus";
215
+ }
216
+ }
217
+
218
+ function hasFailedStatus(value: unknown): value is { status: "failed" } {
219
+ return (
220
+ !!value &&
221
+ typeof value === "object" &&
222
+ "status" in value &&
223
+ (value as { status?: unknown }).status === "failed"
224
+ );
225
+ }
226
+
227
+ type CommandPhaseStatus = "complete" | "failed" | "skipped";
228
+
229
+ interface AuditStateConsoleProgress {
230
+ line: string;
231
+ signature: string;
232
+ }
233
+
234
+ function formatCommandPhaseLines(
235
+ cwd: string,
236
+ auditId: string,
237
+ phases: Record<string, CommandPhaseStatus>,
238
+ ): string[] {
239
+ const state = readAuditState(cwd).state;
240
+ const audit = state?.audits.find((a) => a.audit_id === auditId);
241
+ const entries: Array<[string, PhaseState]> = Object.entries(phases).map(([name, status]) => [
242
+ name,
243
+ audit?.phases[name] ?? { status },
244
+ ]);
245
+ const mode = audit?.mode ?? "deep";
246
+ return formatPhaseDetailLines(entries, {
247
+ labelFor: (name, index, total) => formatPhaseDetailLabel(mode, name, index, total),
248
+ });
249
+ }
250
+
251
+ function formatConsolePhaseLabel(
252
+ mode: AuditMode,
253
+ phase: string,
254
+ phases: readonly string[],
255
+ ): string {
256
+ const index = phases.indexOf(phase);
257
+ if (index < 0) return phase;
258
+ return formatPhaseDetailLabel(mode, phase, index, phases.length);
259
+ }
260
+
261
+ const AUDIT_MODES: readonly AuditMode[] = [
262
+ "lite",
263
+ "balanced",
264
+ "deep",
265
+ "diff",
266
+ "confirm",
267
+ "revisit",
268
+ "merge",
269
+ "longshot",
270
+ "reinvest",
271
+ ];
272
+
273
+ /**
274
+ * Best-effort mode resolution from a phase-strip status key
275
+ * (`piolium-<mode>`), used only for label rendering before an audit entry
276
+ * exists on disk. Falls back to `deep` so numeric-id labels stay sensible.
277
+ */
278
+ function auditModeFromStatusKey(statusKey: string): AuditMode {
279
+ const candidate = statusKey.replace(/^piolium-/, "");
280
+ return (AUDIT_MODES as readonly string[]).includes(candidate) ? (candidate as AuditMode) : "deep";
281
+ }
282
+
283
+ function formatAuditStateConsoleActivity(
284
+ phaseStates: Record<string, PhaseState>,
285
+ activePhases: readonly string[],
286
+ ): string | undefined {
287
+ for (const phase of activePhases) {
288
+ const state = phaseStates[phase];
289
+ if (!state) continue;
290
+ if (state.next_retry_at) return `${phase} retrying at ${state.next_retry_at}`;
291
+ if (state.last_tool) {
292
+ return `${phase} last tool ${compactLine(
293
+ `${state.last_tool}${state.last_tool_summary ? ` ${state.last_tool_summary}` : ""}`,
294
+ 120,
295
+ )}`;
296
+ }
297
+ if (state.last_event_at) return `${phase} last event ${state.last_event_at}`;
298
+ if (state.heartbeat_at) return `${phase} heartbeat ${state.heartbeat_at}`;
299
+ if (state.error) return `${phase} ${compactLine(state.error, 120)}`;
300
+ }
301
+ return undefined;
302
+ }
303
+
304
+ function formatAuditStateConsoleProgress(
305
+ cwd: string,
306
+ phases: readonly string[],
307
+ currentPhase: string | undefined,
308
+ ): AuditStateConsoleProgress {
309
+ const relPath = "./piolium/audit-state.json";
310
+ const result = readAuditState(cwd);
311
+ if (!result.exists) {
312
+ return {
313
+ line: `audit-state: waiting for ${relPath}`,
314
+ signature: "missing",
315
+ };
316
+ }
317
+ if (result.parseError) {
318
+ return {
319
+ line: `audit-state: cannot parse ${relPath}: ${compactLine(result.parseError, 140)}`,
320
+ signature: `parse:${result.parseError}`,
321
+ };
322
+ }
323
+ const audit = result.state ? latestAudit(result.state) : undefined;
324
+ if (!audit) {
325
+ return {
326
+ line: `audit-state: ${relPath} has no audits yet`,
327
+ signature: "empty",
328
+ };
329
+ }
330
+
331
+ const tally = tallyPhases(audit);
332
+ const activePhases = phases.filter((phase) => audit.phases[phase]?.status === "in_progress");
333
+ const fallbackCurrent =
334
+ currentPhase && phases.includes(currentPhase) && audit.status === "in_progress"
335
+ ? [currentPhase]
336
+ : [];
337
+ const displayActivePhases = activePhases.length > 0 ? activePhases : fallbackCurrent;
338
+ const activeText =
339
+ displayActivePhases.length > 0
340
+ ? `running ${displayActivePhases.map((phase) => formatConsolePhaseLabel(audit.mode, phase, phases)).join(", ")}`
341
+ : audit.status;
342
+ const activity = formatAuditStateConsoleActivity(audit.phases, displayActivePhases);
343
+ const counts = `${tally.complete}/${tally.total} complete, ${tally.pending} pending, ${tally.failed} failed, ${tally.skipped} skipped`;
344
+ const line = `audit-state: ${audit.mode} ${audit.status}; ${activeText}; ${counts}${activity ? `; ${activity}` : ""}`;
345
+ return {
346
+ line,
347
+ signature: line,
348
+ };
349
+ }
350
+
351
+ function createPhaseStripCommandUi(
352
+ ctx: PhaseStripContext,
353
+ statusKey: string,
354
+ phases: readonly string[],
355
+ options: { initialPhase?: string; consoleStream?: ArchonConsoleStream } = {},
356
+ ): PhaseStripCommandUi {
357
+ let currentPhase = options.initialPhase;
358
+ let hasPhaseActivity = false;
359
+ let preparingMessage: string | undefined;
360
+ const heartbeats = new Map<string, PhaseHeartbeat>();
361
+ const lastConsoleHeartbeatAt = new Map<string, number>();
362
+ const widgetKey = `${statusKey}-phase-list`;
363
+ let lastConsoleStatusText: string | undefined;
364
+ let lastAuditProgressSignature: string | undefined;
365
+ let lastAuditProgressAt = 0;
366
+ let auditProgressTimer: ReturnType<typeof setInterval> | undefined;
367
+ let preparingSpinnerTimer: ReturnType<typeof setInterval> | undefined;
368
+ let preparingSpinnerFrame = 0;
369
+
370
+ const emitAuditStateProgress = (force = false): void => {
371
+ if (!options.consoleStream?.enabled) return;
372
+ const progress = formatAuditStateConsoleProgress(ctx.cwd, phases, currentPhase);
373
+ const now = Date.now();
374
+ if (
375
+ force ||
376
+ progress.signature !== lastAuditProgressSignature ||
377
+ now - lastAuditProgressAt >= AUDIT_STATE_PROGRESS_REPEAT_MS
378
+ ) {
379
+ options.consoleStream.writeLine(`[${statusKey}] ${progress.line}`);
380
+ lastAuditProgressSignature = progress.signature;
381
+ lastAuditProgressAt = now;
382
+ }
383
+ };
384
+
385
+ if (options.consoleStream?.enabled) {
386
+ options.consoleStream.writeLine(`[${statusKey}] target: ${ctx.cwd}`);
387
+ options.consoleStream.writeLine(
388
+ `[${statusKey}] watching ./piolium/audit-state.json (${getAuditStatePath(ctx.cwd)})`,
389
+ );
390
+ emitAuditStateProgress(true);
391
+ auditProgressTimer = setInterval(emitAuditStateProgress, AUDIT_STATE_PROGRESS_POLL_MS);
392
+ (auditProgressTimer as { unref?: () => void }).unref?.();
393
+ }
394
+
395
+ const emitConsoleStatus = (text?: string): void => {
396
+ if (!text || !options.consoleStream?.enabled || text === lastConsoleStatusText) return;
397
+ lastConsoleStatusText = text;
398
+ options.consoleStream.writeLine(`[${statusKey}] ${compactLine(text, 160)}`);
399
+ };
400
+
401
+ const render = (text?: string): void => {
402
+ const phaseFromText = extractStatusPhase(text, phases);
403
+ if (phaseFromText) {
404
+ currentPhase = phaseFromText;
405
+ hasPhaseActivity = true;
406
+ preparingMessage = undefined;
407
+ } else if (text && !hasPhaseActivity) {
408
+ preparingMessage = formatPreparingStatusMessage(text);
409
+ } else if (hasPhaseActivity) {
410
+ preparingMessage = undefined;
411
+ }
412
+
413
+ if (preparingMessage && !preparingSpinnerTimer) {
414
+ preparingSpinnerTimer = setInterval(() => {
415
+ preparingSpinnerFrame = (preparingSpinnerFrame + 1) % PREPARING_SPINNER_FRAMES.length;
416
+ render();
417
+ }, PREPARING_SPINNER_INTERVAL_MS);
418
+ (preparingSpinnerTimer as { unref?: () => void }).unref?.();
419
+ } else if (!preparingMessage && preparingSpinnerTimer) {
420
+ clearInterval(preparingSpinnerTimer);
421
+ preparingSpinnerTimer = undefined;
422
+ preparingSpinnerFrame = 0;
423
+ }
424
+
425
+ const state = readAuditState(ctx.cwd).state;
426
+ const audit = state ? latestAudit(state) : undefined;
427
+ const phaseStates =
428
+ hasPhaseActivity || audit?.status === "in_progress" ? (audit?.phases ?? {}) : {};
429
+ const mode = audit?.mode ?? auditModeFromStatusKey(statusKey);
430
+ const lines = renderPhaseStatusList(mode, phases, phaseStates, currentPhase, ctx.ui.theme);
431
+ const heartbeatLines = phases
432
+ .map((phase) => heartbeats.get(phase))
433
+ .filter((heartbeat): heartbeat is PhaseHeartbeat => !!heartbeat)
434
+ .map((heartbeat) =>
435
+ ctx.ui.theme.fg(PHASE_HEARTBEAT_UI_COLOR, ` ${formatPhaseHeartbeatStatusLine(heartbeat)}`),
436
+ );
437
+ ctx.ui.setStatus(statusKey, undefined);
438
+ ctx.ui.setWidget(
439
+ widgetKey,
440
+ (_tui, theme) => {
441
+ const container = new Container();
442
+ if (preparingMessage) {
443
+ const frame =
444
+ PREPARING_SPINNER_FRAMES[preparingSpinnerFrame % PREPARING_SPINNER_FRAMES.length] ?? "●";
445
+ container.addChild(
446
+ new Text(`${theme.fg("accent", frame)} ${theme.fg("muted", preparingMessage)}`, 1, 0),
447
+ );
448
+ }
449
+ for (const line of lines) {
450
+ container.addChild(new Text(line, 1, 0));
451
+ }
452
+ for (const line of heartbeatLines) {
453
+ container.addChild(new Text(line, 1, 0));
454
+ }
455
+ return container;
456
+ },
457
+ { placement: "belowEditor" },
458
+ );
459
+ };
460
+
461
+ return {
462
+ notify: (text, level) => {
463
+ options.consoleStream?.writeLine(`${level.toUpperCase()}: ${text}`);
464
+ ctx.ui.notify(text, level);
465
+ },
466
+ setStatus: (key, text) => {
467
+ if (key !== statusKey) {
468
+ ctx.ui.setStatus(key, text);
469
+ return;
470
+ }
471
+ // Phase runners clear their status after every phase. Keep the full
472
+ // phase strip mounted until the command handler itself finishes.
473
+ emitConsoleStatus(text);
474
+ render(text);
475
+ },
476
+ onPhaseHeartbeat: (phase, heartbeat) => {
477
+ if (heartbeat) {
478
+ heartbeats.set(phase, heartbeat);
479
+ const lastConsoleAt = lastConsoleHeartbeatAt.get(phase) ?? 0;
480
+ if (lastConsoleAt === 0 || heartbeat.nowMs - lastConsoleAt >= 30_000) {
481
+ lastConsoleHeartbeatAt.set(phase, heartbeat.nowMs);
482
+ options.consoleStream?.writeLine(
483
+ `[${phase}] ... ${formatPhaseHeartbeat(heartbeat, {
484
+ includePhase: false,
485
+ })}`,
486
+ );
487
+ }
488
+ } else {
489
+ heartbeats.delete(phase);
490
+ lastConsoleHeartbeatAt.delete(phase);
491
+ }
492
+ if (heartbeat) {
493
+ currentPhase = phase;
494
+ hasPhaseActivity = true;
495
+ }
496
+ render();
497
+ },
498
+ clearStatus: () => {
499
+ if (auditProgressTimer) clearInterval(auditProgressTimer);
500
+ auditProgressTimer = undefined;
501
+ if (preparingSpinnerTimer) clearInterval(preparingSpinnerTimer);
502
+ preparingSpinnerTimer = undefined;
503
+ ctx.ui.setStatus(statusKey, undefined);
504
+ ctx.ui.setWidget(widgetKey, undefined);
505
+ },
506
+ };
507
+ }
508
+
509
+ async function runCommandWithRetry<T>(
510
+ label: string,
511
+ statusKey: string,
512
+ ui: CommandUi,
513
+ operation: (attempt: number, maxAttempts: number) => Promise<T>,
514
+ options: { retryFailedStatus?: boolean } = {},
515
+ ): Promise<T> {
516
+ let lastFailedResult: T | undefined;
517
+ try {
518
+ return await runWithRetry(
519
+ async (attempt, maxAttempts) => {
520
+ if (maxAttempts > 1) {
521
+ ui.setStatus(statusKey, `● ${label} (${attempt}/${maxAttempts})`);
522
+ }
523
+ const result = await operation(attempt, maxAttempts);
524
+ if ((options.retryFailedStatus ?? true) && hasFailedStatus(result)) {
525
+ lastFailedResult = result;
526
+ throw new CommandFailedStatus(label, result);
527
+ }
528
+ return result;
529
+ },
530
+ {
531
+ maxRetries: readNonNegativeIntEnv("PIOLIUM_COMMAND_MAX_RETRIES", 3),
532
+ backoffBaseMs: readPositiveIntEnv("PIOLIUM_COMMAND_BACKOFF_BASE_MS", 5000),
533
+ backoffMaxMs: readPositiveIntEnv("PIOLIUM_COMMAND_BACKOFF_MAX_MS", 120_000),
534
+ onRetry: (info) => {
535
+ ui.notify(
536
+ `${label} attempt ${info.attempt}/${info.maxAttempts} failed; retrying in ${Math.ceil(info.backoffMs / 1000)}s.`,
537
+ "warning",
538
+ );
539
+ ui.setStatus(statusKey, `● ${label} retrying in ${Math.ceil(info.backoffMs / 1000)}s`);
540
+ },
541
+ },
542
+ );
543
+ } catch (err) {
544
+ if (err instanceof CommandFailedStatus && lastFailedResult !== undefined) {
545
+ return lastFailedResult;
546
+ }
547
+ throw err;
548
+ }
549
+ }
550
+
551
+ function summarizeArgs(args: unknown): string {
552
+ if (!args || typeof args !== "object") return "";
553
+ const obj = args as Record<string, unknown>;
554
+ const pickKey = ["file_path", "path", "command", "pattern", "query", "url"].find(
555
+ (k) => typeof obj[k] === "string",
556
+ );
557
+ if (pickKey) {
558
+ const value = String(obj[pickKey]);
559
+ return value.length > 120 ? `${value.slice(0, 117)}…` : value;
560
+ }
561
+ const json = JSON.stringify(obj);
562
+ return json.length > 120 ? `${json.slice(0, 117)}…` : json;
563
+ }
564
+
565
+ function extractAssistantText(content: unknown): string {
566
+ if (typeof content === "string") return content;
567
+ if (!Array.isArray(content)) return "";
568
+ return content
569
+ .filter((c) => c && typeof c === "object" && (c as { type?: string }).type === "text")
570
+ .map((c) => (c as { text?: string }).text ?? "")
571
+ .join("");
572
+ }
573
+
574
+ function summarizeToolResult(result: unknown): string {
575
+ if (result == null) return "";
576
+ if (typeof result === "string") return result;
577
+ if (typeof result === "number" || typeof result === "boolean") return String(result);
578
+ if (Array.isArray(result)) {
579
+ return result
580
+ .map((item) => {
581
+ if (typeof item === "string") return item;
582
+ if (item && typeof item === "object" && "text" in (item as Record<string, unknown>)) {
583
+ return String((item as { text?: unknown }).text ?? "");
584
+ }
585
+ return JSON.stringify(item);
586
+ })
587
+ .join("\n");
588
+ }
589
+ if (typeof result !== "object") return "";
590
+ const obj = result as Record<string, unknown>;
591
+ // MCP CallToolResult shape: { content: [{ type: "text", text: "..." }, ...] }
592
+ if (Array.isArray(obj.content)) {
593
+ const unwrapped = summarizeToolResult(obj.content);
594
+ if (unwrapped) return unwrapped;
595
+ }
596
+ const preferKey = ["stdout", "output", "text", "content", "result"].find(
597
+ (k) => typeof obj[k] === "string" && (obj[k] as string).length > 0,
598
+ );
599
+ if (preferKey) return obj[preferKey] as string;
600
+ try {
601
+ return JSON.stringify(obj);
602
+ } catch {
603
+ return "";
604
+ }
605
+ }
606
+
607
+ function compactLine(text: string, max: number): string {
608
+ const collapsed = text.replace(/\s+/g, " ").trim();
609
+ if (collapsed.length <= max) return collapsed;
610
+ return `${collapsed.slice(0, max - 1)}…`;
611
+ }
612
+
613
+ function formatPreparingStatusMessage(text: string): string {
614
+ const stripped = text.replace(/^●\s*/, "").trim();
615
+ return stripped || "preparing";
616
+ }
617
+
618
+ async function allowInitialUiPaint(): Promise<void> {
619
+ await new Promise<void>((resolve) => setTimeout(resolve, INITIAL_UI_PAINT_DELAY_MS));
620
+ }
621
+
622
+ async function showCommandResult(
623
+ ctx: { ui: { select: (title: string, lines: string[]) => Promise<unknown> } },
624
+ consoleStream: ArchonConsoleStream,
625
+ title: string,
626
+ lines: string[],
627
+ options: { footerLines?: string[] } = {},
628
+ ): Promise<void> {
629
+ const displayLines = withCommandResultDismissHint(lines, options.footerLines);
630
+ consoleStream.writeBlock(title, displayLines);
631
+ await ctx.ui.select(`${title} (${COMMAND_RESULT_DIALOG_HINT})`, displayLines);
632
+ }
633
+
634
+ function withCommandResultDismissHint(
635
+ lines: readonly string[],
636
+ footerLines: readonly string[] = [],
637
+ ): string[] {
638
+ if (lines.includes(COMMAND_RESULT_DISMISS_HINT)) return [...lines];
639
+ const displayLines = [...lines];
640
+ if (footerLines.length > 0) {
641
+ if (displayLines.length > 0 && displayLines.at(-1) !== "") displayLines.push("");
642
+ displayLines.push(...footerLines);
643
+ }
644
+ if (displayLines.length > 0 && displayLines.at(-1) !== "") displayLines.push("");
645
+ displayLines.push(COMMAND_RESULT_DISMISS_HINT);
646
+ return displayLines;
647
+ }
648
+
649
+ function notifyCommandError(
650
+ ctx: { ui: { notify: (text: string, level: "error") => void } },
651
+ consoleStream: ArchonConsoleStream,
652
+ message: string,
653
+ ): void {
654
+ consoleStream.writeLine(`ERROR: ${message}`);
655
+ ctx.ui.notify(message, "error");
656
+ }
657
+
658
+ function parseCommandTargetOrNotify(
659
+ args: string,
660
+ ctx: { cwd: string; ui: { notify: (text: string, level: "error") => void } },
661
+ consoleStream: ArchonConsoleStream,
662
+ pi: ExtensionAPI,
663
+ ): ReturnType<typeof parseArchonCommandArgs> | undefined {
664
+ const parsed = parseArchonCommandArgs(args, ctx.cwd, {
665
+ defaultTarget: readPiFlagString(pi, FLAG_DIR),
666
+ });
667
+ if (parsed.error) {
668
+ notifyCommandError(ctx, consoleStream, parsed.error);
669
+ return undefined;
670
+ }
671
+ applyPioliumProcessFlagEnv(pi);
672
+ return parsed;
673
+ }
674
+
675
+ function registerPioliumFlags(pi: ExtensionAPI): void {
676
+ pi.registerFlag(FLAG_DIR, {
677
+ description: "Default target directory for /piolium-* commands",
678
+ type: "string",
679
+ });
680
+ pi.registerFlag(FLAG_SINCE, {
681
+ description: "Default base commit for /piolium-diff",
682
+ type: "string",
683
+ });
684
+ for (const spec of FLAG_ENV_MAPPINGS) {
685
+ pi.registerFlag(spec.flag, {
686
+ description: spec.description,
687
+ type: "string",
688
+ });
689
+ }
690
+ }
691
+
692
+ function readPiFlagString(pi: ExtensionAPI, name: string): string | undefined {
693
+ const value = pi.getFlag(name);
694
+ if (typeof value !== "string") return undefined;
695
+ const trimmed = value.trim();
696
+ return trimmed.length > 0 ? trimmed : undefined;
697
+ }
698
+
699
+ function applyPioliumProcessFlagEnv(pi: ExtensionAPI): void {
700
+ for (const spec of FLAG_ENV_MAPPINGS) {
701
+ const value = readPiFlagString(pi, spec.flag);
702
+ if (value) process.env[spec.env] = value;
703
+ }
704
+ }
705
+
706
+ function normalizeExportFormat(value: string | undefined): ExportFormat | undefined {
707
+ if (!value || value === "json") return "json";
708
+ if (value === "md-dir") return "md-dir";
709
+ return undefined;
710
+ }
711
+
712
+ function parseSeverityList(
713
+ value: string | undefined,
714
+ ): Array<NonNullable<ReturnType<typeof normalizeExportSeverity>>> | undefined {
715
+ if (!value) return [];
716
+ const severities: Array<NonNullable<ReturnType<typeof normalizeExportSeverity>>> = [];
717
+ for (const part of value.split(/[,\s]+/).filter(Boolean)) {
718
+ const severity = normalizeExportSeverity(part);
719
+ if (!severity) return undefined;
720
+ severities.push(severity);
721
+ }
722
+ return severities;
723
+ }
724
+
725
+ type StreamLineKind = "tool-start" | "tool-end" | "tool-error" | "assistant";
726
+
727
+ interface StreamLineDetails {
728
+ kind: StreamLineKind;
729
+ phase: string;
730
+ toolName?: string;
731
+ body?: string;
732
+ }
733
+
734
+ function makeAgentEventForwarder(pi: ExtensionAPI, consoleStream: ArchonConsoleStream) {
735
+ const send = (details: StreamLineDetails, fallback: string) => {
736
+ consoleStream.writeLine(fallback);
737
+ pi.sendMessage<StreamLineDetails>({
738
+ customType: ARCHON_STREAM,
739
+ content: fallback,
740
+ display: true,
741
+ details,
742
+ });
743
+ };
744
+
745
+ return (phase: string, event: AgentSessionEvent): void => {
746
+ switch (event.type) {
747
+ case "tool_execution_start": {
748
+ const body = summarizeArgs(event.args);
749
+ send(
750
+ { kind: "tool-start", phase, toolName: event.toolName, body },
751
+ `[${phase}] → ${event.toolName}${body ? ` ${body}` : ""}`,
752
+ );
753
+ return;
754
+ }
755
+ case "tool_execution_end": {
756
+ const body = compactLine(summarizeToolResult(event.result), 200);
757
+ const kind: StreamLineKind = event.isError ? "tool-error" : "tool-end";
758
+ const marker = event.isError ? "✗" : "←";
759
+ send(
760
+ { kind, phase, toolName: event.toolName, body },
761
+ `[${phase}] ${marker} ${event.toolName}${body ? ` ${body}` : ""}`,
762
+ );
763
+ return;
764
+ }
765
+ case "message_end": {
766
+ const message = event.message as { role?: string; content?: unknown };
767
+ if (message.role !== "assistant") return;
768
+ const text = extractAssistantText(message.content).trim();
769
+ if (!text) return;
770
+ const head = compactLine(text, 240);
771
+ send({ kind: "assistant", phase, body: head }, `[${phase}] ${head}`);
772
+ return;
773
+ }
774
+ }
775
+ };
776
+ }
777
+
778
+ /** Inline smoke-test agent. Hardcoded so the importer can wipe `agents/` without breaking it. */
779
+ const SMOKE_AGENT: AgentDefinition = {
780
+ name: "piolium-smoke",
781
+ description: "Piolium smoke test agent. Confirms the runner harness boots end-to-end.",
782
+ systemPrompt: [
783
+ "You are the piolium smoke test agent.",
784
+ "When the user gives you a task, reply with a single short paragraph that:",
785
+ " 1. confirms you received the task,",
786
+ " 2. echoes the first 60 characters of the task verbatim,",
787
+ " 3. announces 'piolium runner OK'.",
788
+ "Do not call any tools. Do not perform any work beyond replying.",
789
+ ].join("\n"),
790
+ allowedTools: [],
791
+ skills: [],
792
+ sourcePath: "<inline:piolium-smoke>",
793
+ };
794
+
795
+ export default function archonExtension(pi: ExtensionAPI) {
796
+ registerPioliumFlags(pi);
797
+ registerAnthropicVertex(pi);
798
+
799
+ pi.on("session_start", async (_event, ctx) => {
800
+ if (!ctx.hasUI) return;
801
+ ctx.ui.notify(PIOLIUM_STARTUP_HINT, "info");
802
+ if (shouldUseArchonPromptPrefix(ctx.ui.theme)) {
803
+ ctx.ui.setEditorComponent(
804
+ (tui, theme, keybindings) => new ArchonPromptPrefixEditor(tui, theme, keybindings),
805
+ );
806
+ }
807
+ });
808
+
809
+ pi.registerMessageRenderer<StreamLineDetails>(ARCHON_STREAM, (message, _options, theme) => {
810
+ const details = message.details;
811
+ if (!details || typeof details !== "object") {
812
+ const fallback = typeof message.content === "string" ? message.content : "";
813
+ return new Text(theme.fg("muted", fallback), 0, 0);
814
+ }
815
+ const { kind, phase, toolName, body } = details;
816
+ // Indent end/error lines so they visually nest under the matching start line.
817
+ // The pad width matches the "[phase] " prefix on start lines.
818
+ const phaseTag = theme.fg("accent", `[${phase}]`);
819
+ const indent = " ".repeat(phase.length + 3);
820
+ let line: string;
821
+ switch (kind) {
822
+ case "tool-start": {
823
+ const arrow = theme.fg("muted", "→");
824
+ const name = theme.fg("toolTitle", theme.bold(toolName ?? ""));
825
+ const args = body ? ` ${theme.fg("muted", body)}` : "";
826
+ line = `${phaseTag} ${arrow} ${name}${args}`;
827
+ break;
828
+ }
829
+ case "tool-end": {
830
+ const arrow = theme.fg("success", "←");
831
+ const result = body ? ` ${theme.fg("dim", body)}` : ` ${theme.fg("dim", "(ok)")}`;
832
+ line = `${indent}${arrow}${result}`;
833
+ break;
834
+ }
835
+ case "tool-error": {
836
+ const marker = theme.fg("error", "✗");
837
+ const result = body ? ` ${theme.fg("error", body)}` : ` ${theme.fg("error", "failed")}`;
838
+ line = `${indent}${marker}${result}`;
839
+ break;
840
+ }
841
+ case "assistant":
842
+ line = `${phaseTag} ${theme.fg("muted", body ?? "")}`;
843
+ break;
844
+ default:
845
+ line = typeof message.content === "string" ? theme.fg("muted", message.content) : "";
846
+ }
847
+ return new Text(line, 0, 0);
848
+ });
849
+
850
+ const consoleStream = createArchonConsoleStream();
851
+ const onAgentEvent = makeAgentEventForwarder(pi, consoleStream);
852
+
853
+ pi.registerCommand("piolium-help", {
854
+ description: "Show piolium commands, CLI flags, and examples.",
855
+ handler: async (_args, ctx) => {
856
+ await showCommandResult(ctx, consoleStream, "Piolium — Help", buildPioliumHelpLines());
857
+ },
858
+ });
859
+
860
+ pi.registerCommand("piolium-status", {
861
+ description: "Show piolium audit progress for the current directory.",
862
+ handler: async (args, ctx) => {
863
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
864
+ if (!command) return;
865
+ await runCommandWithRetry(
866
+ "piolium-status",
867
+ "piolium-status",
868
+ ctx.ui,
869
+ async () => {
870
+ const result = readAuditState(command.cwd);
871
+ if (!result.exists) {
872
+ ctx.ui.notify(
873
+ `No audit state at ${result.path}. Run /piolium-lite, /piolium-balanced, or /piolium-deep to start.`,
874
+ "info",
875
+ );
876
+ return;
877
+ }
878
+ if (result.parseError) {
879
+ ctx.ui.notify(`Failed to parse ${result.path}: ${result.parseError}`, "error");
880
+ return;
881
+ }
882
+ if (!result.state) {
883
+ ctx.ui.notify(`No state returned from ${result.path}.`, "warning");
884
+ return;
885
+ }
886
+ const lines = formatAuditStatus(result.state);
887
+ await showCommandResult(ctx, consoleStream, "Piolium — Audit Status", lines);
888
+ },
889
+ { retryFailedStatus: false },
890
+ );
891
+ },
892
+ });
893
+
894
+ pi.registerCommand("piolium-resume", {
895
+ description:
896
+ "Resume the most recent non-complete audit (in_progress > failed) without re-specifying the mode.",
897
+ handler: async (args, ctx) => {
898
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
899
+ if (!command) return;
900
+
901
+ const stateResult = readAuditState(command.cwd);
902
+ if (!stateResult.exists) {
903
+ ctx.ui.notify(
904
+ `No audit state at ${stateResult.path}. Run /piolium-lite, /piolium-balanced, or /piolium-deep to start.`,
905
+ "info",
906
+ );
907
+ return;
908
+ }
909
+ if (stateResult.parseError || !stateResult.state) {
910
+ notifyCommandError(
911
+ ctx,
912
+ consoleStream,
913
+ `Failed to parse ${stateResult.path}: ${stateResult.parseError ?? "no state returned"}`,
914
+ );
915
+ return;
916
+ }
917
+
918
+ const audit = latestResumableAudit(stateResult.state);
919
+ if (!audit) {
920
+ ctx.ui.notify(
921
+ stateResult.state.audits.length === 0
922
+ ? `No audits recorded in ${stateResult.path} yet — start one with /piolium-lite, /piolium-balanced, or /piolium-deep.`
923
+ : `Every audit in ${stateResult.path} is already complete; nothing to resume.`,
924
+ "info",
925
+ );
926
+ return;
927
+ }
928
+
929
+ const mode = audit.mode;
930
+ if (mode === "diff" || mode === "merge") {
931
+ notifyCommandError(
932
+ ctx,
933
+ consoleStream,
934
+ `Mode ${mode} doesn't support /piolium-resume — re-run /piolium-${mode} directly with its required arguments.`,
935
+ );
936
+ return;
937
+ }
938
+
939
+ const completed = Object.values(audit.phases).filter((p) => p.status === "complete").length;
940
+ const total = Object.keys(audit.phases).length;
941
+ ctx.ui.notify(
942
+ `[resume] audit ${audit.audit_id} mode=${mode} status=${audit.status} (${completed}/${total} phases complete)`,
943
+ "info",
944
+ );
945
+
946
+ const phases = phasesFor(mode);
947
+ const initialPhase = phases[0];
948
+ if (!initialPhase) {
949
+ notifyCommandError(ctx, consoleStream, `Mode ${mode} has no phases; cannot resume.`);
950
+ return;
951
+ }
952
+ const commandKey = `piolium-${mode}`;
953
+ const phaseUi = createPhaseStripCommandUi({ cwd: command.cwd, ui: ctx.ui }, commandKey, phases, {
954
+ initialPhase,
955
+ consoleStream,
956
+ });
957
+ phaseUi.setStatus(commandKey, `● resuming ${mode} audit`);
958
+ await allowInitialUiPaint();
959
+
960
+ const runnerUi = {
961
+ notify: phaseUi.notify,
962
+ setStatus: phaseUi.setStatus,
963
+ onAgentEvent,
964
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
965
+ };
966
+ const baseOpts = {
967
+ cwd: command.cwd,
968
+ forceFresh: false as const,
969
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
970
+ ui: runnerUi,
971
+ };
972
+
973
+ try {
974
+ const dispatched = await runCommandWithRetry(commandKey, commandKey, phaseUi, async () => {
975
+ switch (mode) {
976
+ case "lite": {
977
+ const r = await runLiteAudit(baseOpts);
978
+ return {
979
+ auditId: r.auditId,
980
+ extraLines: [`Recon report: ${r.summaryPath}`],
981
+ phases: r.phases,
982
+ status: r.status,
983
+ };
984
+ }
985
+ case "balanced": {
986
+ const r = await runBalancedAudit(baseOpts);
987
+ return {
988
+ auditId: r.auditId,
989
+ extraLines: [],
990
+ phases: r.phases,
991
+ status: r.status,
992
+ };
993
+ }
994
+ case "deep": {
995
+ const r = await runDeepAudit(baseOpts);
996
+ return {
997
+ auditId: r.auditId,
998
+ extraLines: [],
999
+ phases: r.phases,
1000
+ status: r.status,
1001
+ };
1002
+ }
1003
+ case "confirm": {
1004
+ const r = await runConfirmAudit(baseOpts);
1005
+ return {
1006
+ auditId: r.auditId,
1007
+ extraLines: [],
1008
+ phases: r.phases,
1009
+ status: r.status,
1010
+ };
1011
+ }
1012
+ case "revisit": {
1013
+ const r = await runRevisitAudit(baseOpts);
1014
+ return {
1015
+ auditId: r.auditId,
1016
+ extraLines: [],
1017
+ phases: r.phases,
1018
+ status: r.status,
1019
+ };
1020
+ }
1021
+ case "longshot": {
1022
+ const r = await runLongshotAudit(baseOpts);
1023
+ return {
1024
+ auditId: r.auditId,
1025
+ extraLines: [
1026
+ `Files hunted: ${r.targetsCompleted}/${r.targetsTotal}`,
1027
+ `Files failed: ${r.targetsFailed}`,
1028
+ ],
1029
+ phases: r.phases,
1030
+ status: r.status,
1031
+ footerExtras: [`Targets file: ${r.targetsPath}`, `Summary: ${r.summaryPath}`],
1032
+ };
1033
+ }
1034
+ case "reinvest": {
1035
+ const r = await runReinvestAudit(baseOpts);
1036
+ return {
1037
+ auditId: r.auditId,
1038
+ extraLines: [
1039
+ `Findings reinvested: ${r.reinvestedCount}`,
1040
+ `Flipped to DISPROVED: ${r.flippedCount}`,
1041
+ `Mixed / uncertain: ${r.uncertainCount}`,
1042
+ ],
1043
+ phases: r.phases,
1044
+ status: r.status,
1045
+ footerExtras: [`Report: ${r.reportPath}`],
1046
+ };
1047
+ }
1048
+ }
1049
+ });
1050
+
1051
+ if (!dispatched) return;
1052
+ const phaseLines = formatCommandPhaseLines(command.cwd, dispatched.auditId, dispatched.phases);
1053
+ await showCommandResult(
1054
+ ctx,
1055
+ consoleStream,
1056
+ `Piolium — Resume (${mode})`,
1057
+ [
1058
+ `Directory: ${command.cwd}`,
1059
+ `Audit: ${dispatched.auditId}`,
1060
+ `Status: ${dispatched.status}`,
1061
+ ...dispatched.extraLines,
1062
+ "",
1063
+ "Phases:",
1064
+ ...phaseLines,
1065
+ ...(dispatched.footerExtras ? ["", ...dispatched.footerExtras] : []),
1066
+ ],
1067
+ {
1068
+ footerLines: buildAuditResultStatsLines(command.cwd, dispatched.auditId),
1069
+ },
1070
+ );
1071
+ } catch (err) {
1072
+ notifyCommandError(
1073
+ ctx,
1074
+ consoleStream,
1075
+ `Resume threw: ${err instanceof Error ? err.message : String(err)}`,
1076
+ );
1077
+ } finally {
1078
+ phaseUi.clearStatus();
1079
+ }
1080
+ },
1081
+ });
1082
+
1083
+ pi.registerCommand("piolium-export", {
1084
+ description:
1085
+ "Export finalized findings with severity, confirmation, false-positive, since, and owner filters.",
1086
+ handler: async (args, ctx) => {
1087
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1088
+ if (!command) return;
1089
+ await runCommandWithRetry(
1090
+ "piolium-export",
1091
+ "piolium-export",
1092
+ ctx.ui,
1093
+ async () => {
1094
+ const formatToken = readOptionValue(command.tokens, "--format");
1095
+ const format = normalizeExportFormat(formatToken);
1096
+ if (!format) {
1097
+ notifyCommandError(ctx, consoleStream, "Invalid --format. Use json or md-dir.");
1098
+ return;
1099
+ }
1100
+ const minSeverityToken = readOptionValue(command.tokens, "--min-severity");
1101
+ const minSeverity = normalizeExportSeverity(minSeverityToken);
1102
+ if (minSeverityToken && !minSeverity) {
1103
+ notifyCommandError(ctx, consoleStream, "Invalid --min-severity.");
1104
+ return;
1105
+ }
1106
+ const onlySeverityToken = readOptionValue(command.tokens, "--only-severity");
1107
+ const onlySeverity = parseSeverityList(onlySeverityToken);
1108
+ if (onlySeverityToken && !onlySeverity) {
1109
+ notifyCommandError(ctx, consoleStream, "Invalid --only-severity.");
1110
+ return;
1111
+ }
1112
+ const result = runExport(command.cwd, {
1113
+ format,
1114
+ ...(readOptionValue(command.tokens, "--out")
1115
+ ? { outPath: readOptionValue(command.tokens, "--out") }
1116
+ : {}),
1117
+ ...(minSeverity ? { minSeverity } : {}),
1118
+ ...(onlySeverity && onlySeverity.length > 0 ? { onlySeverity } : {}),
1119
+ confirmedOnly: command.tokens.includes("--confirmed-only"),
1120
+ excludeFp: command.tokens.includes("--exclude-fp"),
1121
+ ...(readOptionValue(command.tokens, "--since")
1122
+ ? { since: readOptionValue(command.tokens, "--since") }
1123
+ : {}),
1124
+ requireOwner: command.tokens.includes("--require-owner"),
1125
+ });
1126
+ await showCommandResult(ctx, consoleStream, "Piolium — Export", result.lines);
1127
+ },
1128
+ { retryFailedStatus: false },
1129
+ );
1130
+ },
1131
+ });
1132
+
1133
+ pi.registerCommand("piolium-learn", {
1134
+ description:
1135
+ "Generate project-local candidate matcher suggestions from finalized findings. Pass --apply to merge them into piolium/matchers.json.",
1136
+ handler: async (args, ctx) => {
1137
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1138
+ if (!command) return;
1139
+ await runCommandWithRetry(
1140
+ "piolium-learn",
1141
+ "piolium-learn",
1142
+ ctx.ui,
1143
+ async () => {
1144
+ const result = runMatcherLearn(command.cwd, { apply: command.tokens.includes("--apply") });
1145
+ await showCommandResult(ctx, consoleStream, "Piolium — Learn Matchers", result.lines);
1146
+ },
1147
+ { retryFailedStatus: false },
1148
+ );
1149
+ },
1150
+ });
1151
+
1152
+ pi.registerCommand("piolium-smoke", {
1153
+ description: "Run a tiny inline agent end-to-end to verify the piolium runner works.",
1154
+ handler: async (args, ctx) => {
1155
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1156
+ if (!command) return;
1157
+ const task =
1158
+ command.args.trim().length > 0
1159
+ ? command.args.trim()
1160
+ : "Hello from piolium. Please confirm the runner is working.";
1161
+ ctx.ui.setStatus("piolium-smoke", "● running smoke agent");
1162
+ try {
1163
+ await runCommandWithRetry(
1164
+ "piolium-smoke",
1165
+ "piolium-smoke",
1166
+ ctx.ui,
1167
+ async (attempt) => {
1168
+ const runId = `smoke-${new Date().toISOString().replace(/[:.]/g, "-")}-a${attempt}-${randomUUID().slice(0, 8)}`;
1169
+ const result = await runAgent({
1170
+ agent: SMOKE_AGENT,
1171
+ task,
1172
+ runId,
1173
+ runtime: { cwd: command.cwd, mode: "lite", phase: "smoke" },
1174
+ ...agentRuntimeFromCommandContext(pi, ctx),
1175
+ });
1176
+ ctx.ui.setStatus("piolium-smoke", undefined);
1177
+ const lines = [
1178
+ `Run id: ${runId}`,
1179
+ `Duration: ${result.durationMs}ms`,
1180
+ `Stop: ${result.stopReason ?? "(none)"}`,
1181
+ `Transcript: ${result.transcriptPath}`,
1182
+ `Result: ${result.resultPath}`,
1183
+ "",
1184
+ "--- final text ---",
1185
+ result.text || "(no output)",
1186
+ ];
1187
+ await showCommandResult(ctx, consoleStream, "Piolium — Smoke", lines);
1188
+ },
1189
+ { retryFailedStatus: false },
1190
+ );
1191
+ } catch (err) {
1192
+ ctx.ui.setStatus("piolium-smoke", undefined);
1193
+ if (err instanceof AgentRunError) {
1194
+ notifyCommandError(
1195
+ ctx,
1196
+ consoleStream,
1197
+ `Smoke agent failed: ${err.message}. Transcript: ${err.result.transcriptPath}`,
1198
+ );
1199
+ } else {
1200
+ notifyCommandError(
1201
+ ctx,
1202
+ consoleStream,
1203
+ `Smoke agent failed: ${err instanceof Error ? err.message : String(err)}`,
1204
+ );
1205
+ }
1206
+ }
1207
+ },
1208
+ });
1209
+
1210
+ pi.registerCommand("piolium-lite", {
1211
+ description: "Run the lite audit pipeline (L1 recon → L2 secrets + L3 fast SAST).",
1212
+ handler: async (args, ctx) => {
1213
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1214
+ if (!command) return;
1215
+ const fresh = command.tokens.includes("--fresh");
1216
+ const phaseUi = createPhaseStripCommandUi(
1217
+ { cwd: command.cwd, ui: ctx.ui },
1218
+ "piolium-lite",
1219
+ phasesFor("lite"),
1220
+ {
1221
+ initialPhase: "L1",
1222
+ consoleStream,
1223
+ },
1224
+ );
1225
+ phaseUi.setStatus("piolium-lite", "● starting lite audit");
1226
+ await allowInitialUiPaint();
1227
+ try {
1228
+ const result = await runCommandWithRetry("piolium-lite", "piolium-lite", phaseUi, async () =>
1229
+ runLiteAudit({
1230
+ cwd: command.cwd,
1231
+ forceFresh: fresh,
1232
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1233
+ ui: {
1234
+ notify: phaseUi.notify,
1235
+ setStatus: phaseUi.setStatus,
1236
+ onAgentEvent,
1237
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1238
+ },
1239
+ }),
1240
+ );
1241
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1242
+ await showCommandResult(
1243
+ ctx,
1244
+ consoleStream,
1245
+ "Piolium — Lite Audit",
1246
+ [
1247
+ `Directory: ${command.cwd}`,
1248
+ `Audit: ${result.auditId}`,
1249
+ `Status: ${result.status}`,
1250
+ "",
1251
+ "Phases:",
1252
+ ...phaseLines,
1253
+ "",
1254
+ `Recon report: ${result.summaryPath}`,
1255
+ ],
1256
+ {
1257
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1258
+ },
1259
+ );
1260
+ } catch (err) {
1261
+ notifyCommandError(
1262
+ ctx,
1263
+ consoleStream,
1264
+ `Lite audit threw: ${err instanceof Error ? err.message : String(err)}`,
1265
+ );
1266
+ } finally {
1267
+ phaseUi.clearStatus();
1268
+ }
1269
+ },
1270
+ });
1271
+
1272
+ pi.registerCommand("piolium-deep", {
1273
+ description:
1274
+ "Run the deep audit pipeline as 14 ordered stages with clear names. Pass an internal phase id (e.g. D6) to rerun a single stage.",
1275
+ handler: async (args, ctx) => {
1276
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1277
+ if (!command) return;
1278
+ const tokens = command.tokens;
1279
+ const fresh = tokens.includes("--fresh");
1280
+ const deepPhases = phasesFor("deep");
1281
+ const phaseTokens = tokens.filter((t) => /^D[0-9]+$/.test(t));
1282
+ const invalidPhaseTokens = phaseTokens.filter((t) => !deepPhases.includes(t));
1283
+ if (invalidPhaseTokens.length > 0) {
1284
+ notifyCommandError(
1285
+ ctx,
1286
+ consoleStream,
1287
+ `Unknown deep phase ${invalidPhaseTokens.join(", ")}. Valid phases: ${deepPhases.join(", ")}`,
1288
+ );
1289
+ return;
1290
+ }
1291
+ const only = phaseTokens;
1292
+ const phaseUi = createPhaseStripCommandUi(
1293
+ { cwd: command.cwd, ui: ctx.ui },
1294
+ "piolium-deep",
1295
+ deepPhases,
1296
+ {
1297
+ initialPhase: only[0] ?? "D1",
1298
+ consoleStream,
1299
+ },
1300
+ );
1301
+ phaseUi.setStatus("piolium-deep", "● starting deep audit");
1302
+ await allowInitialUiPaint();
1303
+ try {
1304
+ const result = await runCommandWithRetry("piolium-deep", "piolium-deep", phaseUi, async () =>
1305
+ runDeepAudit({
1306
+ cwd: command.cwd,
1307
+ forceFresh: fresh,
1308
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1309
+ ...(only.length > 0 ? { only } : {}),
1310
+ ui: {
1311
+ notify: phaseUi.notify,
1312
+ setStatus: phaseUi.setStatus,
1313
+ onAgentEvent,
1314
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1315
+ },
1316
+ }),
1317
+ );
1318
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1319
+ await showCommandResult(
1320
+ ctx,
1321
+ consoleStream,
1322
+ "Piolium — Deep Audit",
1323
+ [
1324
+ `Directory: ${command.cwd}`,
1325
+ `Audit: ${result.auditId}`,
1326
+ `Status: ${result.status}`,
1327
+ ...(only.length > 0 ? [`Phases requested: ${only.join(", ")}`] : []),
1328
+ "",
1329
+ "Phases:",
1330
+ ...phaseLines,
1331
+ ],
1332
+ {
1333
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1334
+ },
1335
+ );
1336
+ } catch (err) {
1337
+ notifyCommandError(
1338
+ ctx,
1339
+ consoleStream,
1340
+ `Deep audit threw: ${err instanceof Error ? err.message : String(err)}`,
1341
+ );
1342
+ } finally {
1343
+ phaseUi.clearStatus();
1344
+ }
1345
+ },
1346
+ });
1347
+
1348
+ pi.registerCommand("piolium-confirm", {
1349
+ description:
1350
+ "Run the confirmation pass over an existing audit (V1-V7). Pass a remote URL to skip local provisioning. If cwd has only the piolium/ results folder, the source repo is auto-cloned into a sibling <repo>-confirm/ directory; pass --repo <url> to override the inferred clone URL.",
1351
+ handler: async (args, ctx) => {
1352
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1353
+ if (!command) return;
1354
+ const tokens = command.tokens;
1355
+ const fresh = tokens.includes("--fresh");
1356
+ const repoOverride = readOptionValue(tokens, "--repo");
1357
+ const target = tokens.find((t) => /^https?:\/\//.test(t) && t !== repoOverride);
1358
+
1359
+ let effectiveCwd = command.cwd;
1360
+ try {
1361
+ const bootstrap = bootstrapResultsOnlyConfirm({
1362
+ cwd: command.cwd,
1363
+ ...(repoOverride ? { repoOverride } : {}),
1364
+ notify: (text, level) => ctx.ui.notify(text, level ?? "info"),
1365
+ });
1366
+ effectiveCwd = bootstrap.cwd;
1367
+ if (bootstrap.bootstrapped && bootstrap.cloneDir) {
1368
+ consoleStream.writeLine(
1369
+ `Confirm bootstrap: cloned ${bootstrap.cloneUrl} → ${bootstrap.cloneDir}`,
1370
+ );
1371
+ }
1372
+ } catch (err) {
1373
+ notifyCommandError(
1374
+ ctx,
1375
+ consoleStream,
1376
+ `Confirm bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
1377
+ );
1378
+ return;
1379
+ }
1380
+
1381
+ const phaseUi = createPhaseStripCommandUi(
1382
+ { cwd: effectiveCwd, ui: ctx.ui },
1383
+ "piolium-confirm",
1384
+ phasesFor("confirm"),
1385
+ {
1386
+ initialPhase: "V1",
1387
+ consoleStream,
1388
+ },
1389
+ );
1390
+ phaseUi.setStatus("piolium-confirm", "● starting confirm pass");
1391
+ await allowInitialUiPaint();
1392
+ try {
1393
+ const result = await runCommandWithRetry(
1394
+ "piolium-confirm",
1395
+ "piolium-confirm",
1396
+ phaseUi,
1397
+ async () =>
1398
+ runConfirmAudit({
1399
+ cwd: effectiveCwd,
1400
+ forceFresh: fresh,
1401
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1402
+ ...(target ? { target } : {}),
1403
+ ui: {
1404
+ notify: phaseUi.notify,
1405
+ setStatus: phaseUi.setStatus,
1406
+ onAgentEvent,
1407
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1408
+ },
1409
+ }),
1410
+ );
1411
+ const phaseLines = formatCommandPhaseLines(effectiveCwd, result.auditId, result.phases);
1412
+ await showCommandResult(
1413
+ ctx,
1414
+ consoleStream,
1415
+ "Piolium — Confirm",
1416
+ [
1417
+ `Directory: ${effectiveCwd}`,
1418
+ `Audit: ${result.auditId}`,
1419
+ `Status: ${result.status}`,
1420
+ "",
1421
+ "Phases:",
1422
+ ...phaseLines,
1423
+ ],
1424
+ {
1425
+ footerLines: buildAuditResultStatsLines(effectiveCwd, result.auditId),
1426
+ },
1427
+ );
1428
+ } catch (err) {
1429
+ notifyCommandError(
1430
+ ctx,
1431
+ consoleStream,
1432
+ `Confirm threw: ${err instanceof Error ? err.message : String(err)}`,
1433
+ );
1434
+ } finally {
1435
+ phaseUi.clearStatus();
1436
+ }
1437
+ },
1438
+ });
1439
+
1440
+ pi.registerCommand("piolium-diff", {
1441
+ description:
1442
+ "Re-audit only files changed since the last completed audit. Pass `--since=<sha>` to override the prior commit.",
1443
+ handler: async (args, ctx) => {
1444
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1445
+ if (!command) return;
1446
+ const since =
1447
+ readOptionValue(command.tokens, "--since") ??
1448
+ readOptionValue(command.tokens, "--plm-since") ??
1449
+ readPiFlagString(pi, FLAG_SINCE);
1450
+ ctx.ui.setStatus("piolium-diff", "● starting diff");
1451
+ await allowInitialUiPaint();
1452
+ try {
1453
+ const result = await runCommandWithRetry("piolium-diff", "piolium-diff", ctx.ui, async () =>
1454
+ runDiffAudit({
1455
+ cwd: command.cwd,
1456
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1457
+ ...(since ? { since } : {}),
1458
+ ui: {
1459
+ notify: (text, level) => ctx.ui.notify(text, level),
1460
+ setStatus: (key, text) => ctx.ui.setStatus(key, text),
1461
+ onAgentEvent,
1462
+ },
1463
+ }),
1464
+ );
1465
+ await showCommandResult(
1466
+ ctx,
1467
+ consoleStream,
1468
+ "Piolium — Diff",
1469
+ [
1470
+ `Directory: ${command.cwd}`,
1471
+ `Status: ${result.status}`,
1472
+ `Prior commit: ${result.priorCommit ?? "(none)"}`,
1473
+ `Changed files: ${result.changedFiles.length}`,
1474
+ ...result.changedFiles.slice(0, 30).map((f) => ` - ${f}`),
1475
+ ...(result.changedFiles.length > 30 ? [` ... ${result.changedFiles.length - 30} more`] : []),
1476
+ ],
1477
+ {
1478
+ footerLines: result.auditId
1479
+ ? buildAuditResultStatsLines(command.cwd, result.auditId)
1480
+ : undefined,
1481
+ },
1482
+ );
1483
+ } catch (err) {
1484
+ ctx.ui.setStatus("piolium-diff", undefined);
1485
+ notifyCommandError(
1486
+ ctx,
1487
+ consoleStream,
1488
+ `Diff threw: ${err instanceof Error ? err.message : String(err)}`,
1489
+ );
1490
+ }
1491
+ },
1492
+ });
1493
+
1494
+ pi.registerCommand("piolium-revisit", {
1495
+ description:
1496
+ "Second-pass re-audit with anti-anchoring prompts (R0 intent corpus → R5 → ... → R11c).",
1497
+ handler: async (args, ctx) => {
1498
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1499
+ if (!command) return;
1500
+ const fresh = command.tokens.includes("--fresh");
1501
+ const phaseUi = createPhaseStripCommandUi(
1502
+ { cwd: command.cwd, ui: ctx.ui },
1503
+ "piolium-revisit",
1504
+ phasesFor("revisit"),
1505
+ {
1506
+ initialPhase: "0",
1507
+ consoleStream,
1508
+ },
1509
+ );
1510
+ phaseUi.setStatus("piolium-revisit", "● starting revisit");
1511
+ await allowInitialUiPaint();
1512
+ try {
1513
+ const result = await runCommandWithRetry(
1514
+ "piolium-revisit",
1515
+ "piolium-revisit",
1516
+ phaseUi,
1517
+ async () =>
1518
+ runRevisitAudit({
1519
+ cwd: command.cwd,
1520
+ forceFresh: fresh,
1521
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1522
+ ui: {
1523
+ notify: phaseUi.notify,
1524
+ setStatus: phaseUi.setStatus,
1525
+ onAgentEvent,
1526
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1527
+ },
1528
+ }),
1529
+ );
1530
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1531
+ await showCommandResult(
1532
+ ctx,
1533
+ consoleStream,
1534
+ "Piolium — Revisit",
1535
+ [
1536
+ `Directory: ${command.cwd}`,
1537
+ `Audit: ${result.auditId}`,
1538
+ `Status: ${result.status}`,
1539
+ "",
1540
+ "Phases:",
1541
+ ...phaseLines,
1542
+ ],
1543
+ {
1544
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1545
+ },
1546
+ );
1547
+ } catch (err) {
1548
+ notifyCommandError(
1549
+ ctx,
1550
+ consoleStream,
1551
+ `Revisit threw: ${err instanceof Error ? err.message : String(err)}`,
1552
+ );
1553
+ } finally {
1554
+ phaseUi.clearStatus();
1555
+ }
1556
+ },
1557
+ });
1558
+
1559
+ pi.registerCommand("piolium-merge", {
1560
+ description:
1561
+ "Merge multiple piolium/ result trees into one canonical output. Usage: /piolium-merge --dir=<path> --dir=<path> [...]",
1562
+ handler: async (args, ctx) => {
1563
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1564
+ if (!command) return;
1565
+ const dirs = readRepeatedOptionValues(command.tokens, "--dir");
1566
+ if (dirs.length < 2) {
1567
+ ctx.ui.notify("Need at least two --dir arguments pointing at piolium/ trees.", "warning");
1568
+ return;
1569
+ }
1570
+ const phaseUi = createPhaseStripCommandUi(
1571
+ { cwd: command.cwd, ui: ctx.ui },
1572
+ "piolium-merge",
1573
+ phasesFor("merge"),
1574
+ {
1575
+ initialPhase: "M1",
1576
+ consoleStream,
1577
+ },
1578
+ );
1579
+ phaseUi.setStatus("piolium-merge", "● starting merge");
1580
+ await allowInitialUiPaint();
1581
+ try {
1582
+ const result = await runCommandWithRetry("piolium-merge", "piolium-merge", phaseUi, async () =>
1583
+ runMergeAudit({
1584
+ cwd: command.cwd,
1585
+ sources: dirs,
1586
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1587
+ ui: {
1588
+ notify: phaseUi.notify,
1589
+ setStatus: phaseUi.setStatus,
1590
+ onAgentEvent,
1591
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1592
+ },
1593
+ }),
1594
+ );
1595
+ await showCommandResult(
1596
+ ctx,
1597
+ consoleStream,
1598
+ "Piolium — Merge",
1599
+ [
1600
+ `Directory: ${command.cwd}`,
1601
+ `Audit: ${result.auditId}`,
1602
+ `Status: ${result.status}`,
1603
+ `Merged finding dirs: ${result.mergedFindings.length}`,
1604
+ ],
1605
+ {
1606
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1607
+ },
1608
+ );
1609
+ } catch (err) {
1610
+ notifyCommandError(
1611
+ ctx,
1612
+ consoleStream,
1613
+ `Merge threw: ${err instanceof Error ? err.message : String(err)}`,
1614
+ );
1615
+ } finally {
1616
+ phaseUi.clearStatus();
1617
+ }
1618
+ },
1619
+ });
1620
+
1621
+ pi.registerCommand("piolium-balanced", {
1622
+ description:
1623
+ "Run the balanced audit pipeline (L1 → L2 → [L3 + L4] → L5 → L6 → L6b → L6c → L7 cleanup).",
1624
+ handler: async (args, ctx) => {
1625
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1626
+ if (!command) return;
1627
+ const fresh = command.tokens.includes("--fresh");
1628
+ const phaseUi = createPhaseStripCommandUi(
1629
+ { cwd: command.cwd, ui: ctx.ui },
1630
+ "piolium-balanced",
1631
+ phasesFor("balanced"),
1632
+ {
1633
+ initialPhase: "B1",
1634
+ consoleStream,
1635
+ },
1636
+ );
1637
+ phaseUi.setStatus("piolium-balanced", "● starting balanced audit");
1638
+ await allowInitialUiPaint();
1639
+ try {
1640
+ const result = await runCommandWithRetry(
1641
+ "piolium-balanced",
1642
+ "piolium-balanced",
1643
+ phaseUi,
1644
+ async () =>
1645
+ runBalancedAudit({
1646
+ cwd: command.cwd,
1647
+ forceFresh: fresh,
1648
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1649
+ ui: {
1650
+ notify: phaseUi.notify,
1651
+ setStatus: phaseUi.setStatus,
1652
+ onAgentEvent,
1653
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1654
+ },
1655
+ }),
1656
+ );
1657
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1658
+ await showCommandResult(
1659
+ ctx,
1660
+ consoleStream,
1661
+ "Piolium — Balanced Audit",
1662
+ [
1663
+ `Directory: ${command.cwd}`,
1664
+ `Audit: ${result.auditId}`,
1665
+ `Status: ${result.status}`,
1666
+ "",
1667
+ "Phases:",
1668
+ ...phaseLines,
1669
+ ],
1670
+ {
1671
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1672
+ },
1673
+ );
1674
+ } catch (err) {
1675
+ notifyCommandError(
1676
+ ctx,
1677
+ consoleStream,
1678
+ `Balanced audit threw: ${err instanceof Error ? err.message : String(err)}`,
1679
+ );
1680
+ } finally {
1681
+ phaseUi.clearStatus();
1682
+ }
1683
+ },
1684
+ });
1685
+
1686
+ pi.registerCommand("piolium-longshot", {
1687
+ description:
1688
+ "Hail-mary scan: enumerate every interesting source file, hunt each one in parallel, then aggregate (X1 → X2 → X3).",
1689
+ handler: async (args, ctx) => {
1690
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1691
+ if (!command) return;
1692
+ const tokens = command.tokens;
1693
+ const fresh = tokens.includes("--fresh");
1694
+ const includeTests = tokens.includes("--include-tests");
1695
+ const limitToken =
1696
+ readOptionValue(tokens, "--limit") ??
1697
+ readOptionValue(tokens, "--plm-longshot-limit") ??
1698
+ readPiFlagString(pi, "plm-longshot-limit");
1699
+ const timeoutToken =
1700
+ readOptionValue(tokens, "--timeout") ??
1701
+ readOptionValue(tokens, "--plm-longshot-timeout") ??
1702
+ readPiFlagString(pi, "plm-longshot-timeout");
1703
+ const langsToken =
1704
+ readOptionValue(tokens, "--langs") ??
1705
+ readOptionValue(tokens, "--plm-longshot-langs") ??
1706
+ readPiFlagString(pi, "plm-longshot-langs");
1707
+ const limit = limitToken ? Number.parseInt(limitToken, 10) : undefined;
1708
+ const perFileTimeoutMs = timeoutToken ? Number.parseInt(timeoutToken, 10) : undefined;
1709
+ const languages = langsToken
1710
+ ? langsToken
1711
+ .split(/[,\s]+/)
1712
+ .map((s) => s.trim())
1713
+ .filter(Boolean)
1714
+ : undefined;
1715
+ const phaseUi = createPhaseStripCommandUi(
1716
+ { cwd: command.cwd, ui: ctx.ui },
1717
+ "piolium-longshot",
1718
+ phasesFor("longshot"),
1719
+ {
1720
+ initialPhase: "1",
1721
+ consoleStream,
1722
+ },
1723
+ );
1724
+ phaseUi.setStatus("piolium-longshot", "● starting longshot");
1725
+ await allowInitialUiPaint();
1726
+ try {
1727
+ const result = await runCommandWithRetry(
1728
+ "piolium-longshot",
1729
+ "piolium-longshot",
1730
+ phaseUi,
1731
+ async () =>
1732
+ runLongshotAudit({
1733
+ cwd: command.cwd,
1734
+ forceFresh: fresh,
1735
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1736
+ ...(limit && Number.isFinite(limit) && limit > 0 ? { limit } : {}),
1737
+ ...(perFileTimeoutMs && Number.isFinite(perFileTimeoutMs) && perFileTimeoutMs > 0
1738
+ ? { perFileTimeoutMs }
1739
+ : {}),
1740
+ ...(languages && languages.length > 0 ? { languages } : {}),
1741
+ includeTests,
1742
+ ui: {
1743
+ notify: phaseUi.notify,
1744
+ setStatus: phaseUi.setStatus,
1745
+ onAgentEvent,
1746
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1747
+ },
1748
+ }),
1749
+ );
1750
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1751
+ await showCommandResult(
1752
+ ctx,
1753
+ consoleStream,
1754
+ "Piolium — Longshot",
1755
+ [
1756
+ `Directory: ${command.cwd}`,
1757
+ `Audit: ${result.auditId}`,
1758
+ `Status: ${result.status}`,
1759
+ `Files hunted: ${result.targetsCompleted}/${result.targetsTotal}`,
1760
+ `Files failed: ${result.targetsFailed}`,
1761
+ "",
1762
+ "Phases:",
1763
+ ...phaseLines,
1764
+ "",
1765
+ `Targets file: ${result.targetsPath}`,
1766
+ `Summary: ${result.summaryPath}`,
1767
+ ],
1768
+ {
1769
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1770
+ },
1771
+ );
1772
+ } catch (err) {
1773
+ notifyCommandError(
1774
+ ctx,
1775
+ consoleStream,
1776
+ `Longshot threw: ${err instanceof Error ? err.message : String(err)}`,
1777
+ );
1778
+ } finally {
1779
+ phaseUi.clearStatus();
1780
+ }
1781
+ },
1782
+ });
1783
+
1784
+ pi.registerCommand("piolium-reinvest", {
1785
+ description:
1786
+ "Cross-agent re-verification of CRIT/HIGH findings (1 enumerate → 2 cross-verifier fan-out → 3 consensus). Optional comma-separated finding-id list to scope the run.",
1787
+ handler: async (args, ctx) => {
1788
+ const command = parseCommandTargetOrNotify(args, ctx, consoleStream, pi);
1789
+ if (!command) return;
1790
+ const tokens = command.tokens;
1791
+ const fresh = tokens.includes("--fresh");
1792
+ const scopeToken = readOptionValue(tokens, "--scope") ?? readOptionValue(tokens, "--ids");
1793
+ const positionalScope = tokens.find((t) => /^[CH][0-9]+(,[CH][0-9]+)*$/i.test(t));
1794
+ const scopeRaw = scopeToken ?? positionalScope;
1795
+ const scope = scopeRaw
1796
+ ? scopeRaw
1797
+ .split(",")
1798
+ .map((s) => s.trim())
1799
+ .filter(Boolean)
1800
+ : undefined;
1801
+
1802
+ const phaseUi = createPhaseStripCommandUi(
1803
+ { cwd: command.cwd, ui: ctx.ui },
1804
+ "piolium-reinvest",
1805
+ phasesFor("reinvest"),
1806
+ {
1807
+ initialPhase: "1",
1808
+ consoleStream,
1809
+ },
1810
+ );
1811
+ phaseUi.setStatus("piolium-reinvest", "● starting reinvest");
1812
+ await allowInitialUiPaint();
1813
+ try {
1814
+ const result = await runCommandWithRetry(
1815
+ "piolium-reinvest",
1816
+ "piolium-reinvest",
1817
+ phaseUi,
1818
+ async () =>
1819
+ runReinvestAudit({
1820
+ cwd: command.cwd,
1821
+ forceFresh: fresh,
1822
+ agentRuntime: agentRuntimeFromCommandContext(pi, ctx),
1823
+ ...(scope && scope.length > 0 ? { scope } : {}),
1824
+ ui: {
1825
+ notify: phaseUi.notify,
1826
+ setStatus: phaseUi.setStatus,
1827
+ onAgentEvent,
1828
+ onPhaseHeartbeat: phaseUi.onPhaseHeartbeat,
1829
+ },
1830
+ }),
1831
+ );
1832
+ const phaseLines = formatCommandPhaseLines(command.cwd, result.auditId, result.phases);
1833
+ await showCommandResult(
1834
+ ctx,
1835
+ consoleStream,
1836
+ "Piolium — Reinvest",
1837
+ [
1838
+ `Directory: ${command.cwd}`,
1839
+ `Audit: ${result.auditId}`,
1840
+ `Status: ${result.status}`,
1841
+ `Findings reinvested: ${result.reinvestedCount}`,
1842
+ `Flipped to DISPROVED: ${result.flippedCount}`,
1843
+ `Mixed / uncertain: ${result.uncertainCount}`,
1844
+ "",
1845
+ "Phases:",
1846
+ ...phaseLines,
1847
+ "",
1848
+ `Report: ${result.reportPath}`,
1849
+ ],
1850
+ {
1851
+ footerLines: buildAuditResultStatsLines(command.cwd, result.auditId),
1852
+ },
1853
+ );
1854
+ } catch (err) {
1855
+ notifyCommandError(
1856
+ ctx,
1857
+ consoleStream,
1858
+ `Reinvest threw: ${err instanceof Error ? err.message : String(err)}`,
1859
+ );
1860
+ } finally {
1861
+ phaseUi.clearStatus();
1862
+ }
1863
+ },
1864
+ });
1865
+ }