@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,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk model for `piolium/audit-state.json`.
|
|
3
|
+
*
|
|
4
|
+
* Snake-case keys preserve compatibility with archon-audit's existing state
|
|
5
|
+
* files — a piolium run on a directory previously audited by Claude/Codex
|
|
6
|
+
* Archon should be able to read prior state and resume or report it.
|
|
7
|
+
*
|
|
8
|
+
* Writes go through `withFileMutationQueue` (process-local serialization) +
|
|
9
|
+
* temp-file-rename (atomic on POSIX). The combination prevents both
|
|
10
|
+
* intra-process write-write races and partially-written files on crash.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { phasesFor } from "./modes.ts";
|
|
17
|
+
import { formatPhaseDetailLabel } from "./phase-labels.ts";
|
|
18
|
+
|
|
19
|
+
export type AuditMode =
|
|
20
|
+
| "lite"
|
|
21
|
+
| "balanced"
|
|
22
|
+
| "deep"
|
|
23
|
+
| "diff"
|
|
24
|
+
| "confirm"
|
|
25
|
+
| "revisit"
|
|
26
|
+
| "merge"
|
|
27
|
+
| "longshot"
|
|
28
|
+
| "reinvest";
|
|
29
|
+
export type RunStatus = "pending" | "in_progress" | "complete" | "failed";
|
|
30
|
+
export type PhaseStatus = "pending" | "in_progress" | "complete" | "failed" | "skipped";
|
|
31
|
+
|
|
32
|
+
export interface PhaseState {
|
|
33
|
+
status: PhaseStatus;
|
|
34
|
+
started_at?: string;
|
|
35
|
+
completed_at?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
artifacts?: string[];
|
|
38
|
+
attempt?: number;
|
|
39
|
+
max_attempts?: number;
|
|
40
|
+
retry_backoff_ms?: number;
|
|
41
|
+
next_retry_at?: string;
|
|
42
|
+
last_error?: string;
|
|
43
|
+
heartbeat_at?: string;
|
|
44
|
+
last_event_at?: string;
|
|
45
|
+
last_tool?: string;
|
|
46
|
+
last_tool_summary?: string;
|
|
47
|
+
run_id?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AuditRunState {
|
|
51
|
+
audit_id: string;
|
|
52
|
+
commit?: string | null;
|
|
53
|
+
branch?: string;
|
|
54
|
+
repository?: string;
|
|
55
|
+
history_available?: boolean;
|
|
56
|
+
mode: AuditMode;
|
|
57
|
+
model?: string;
|
|
58
|
+
agent_sdk?: string;
|
|
59
|
+
started_at: string;
|
|
60
|
+
completed_at?: string | null;
|
|
61
|
+
status: RunStatus;
|
|
62
|
+
phases: Record<string, PhaseState>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AuditStateFile {
|
|
66
|
+
audits: AuditRunState[];
|
|
67
|
+
merge_metadata?: Record<string, unknown>;
|
|
68
|
+
confirmation?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReadAuditStateResult {
|
|
72
|
+
path: string;
|
|
73
|
+
exists: boolean;
|
|
74
|
+
state?: AuditStateFile;
|
|
75
|
+
parseError?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getAuditStatePath(cwd: string): string {
|
|
79
|
+
return join(cwd, "piolium", "audit-state.json");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readAuditState(cwd: string): ReadAuditStateResult {
|
|
83
|
+
const path = getAuditStatePath(cwd);
|
|
84
|
+
if (!existsSync(path)) {
|
|
85
|
+
return { path, exists: false };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const raw = readFileSync(path, "utf8");
|
|
89
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
90
|
+
if (!isAuditStateFile(parsed)) {
|
|
91
|
+
return {
|
|
92
|
+
path,
|
|
93
|
+
exists: true,
|
|
94
|
+
parseError:
|
|
95
|
+
"File is valid JSON but does not match expected audit-state shape (missing `audits` array).",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { path, exists: true, state: parsed };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
path,
|
|
102
|
+
exists: true,
|
|
103
|
+
parseError: err instanceof Error ? err.message : String(err),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAuditStateFile(value: unknown): value is AuditStateFile {
|
|
109
|
+
if (typeof value !== "object" || value === null) return false;
|
|
110
|
+
const v = value as Record<string, unknown>;
|
|
111
|
+
return Array.isArray(v.audits);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Atomically replace the state file. Callers should always go through
|
|
116
|
+
* `mutateAuditState` rather than calling this directly so concurrent
|
|
117
|
+
* mutations within the same process serialize correctly.
|
|
118
|
+
*/
|
|
119
|
+
function writeAuditStateRaw(path: string, state: AuditStateFile): void {
|
|
120
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
121
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
122
|
+
const json = `${JSON.stringify(state, null, "\t")}\n`;
|
|
123
|
+
writeFileSync(tmp, json);
|
|
124
|
+
renameSync(tmp, path);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read-modify-write the audit-state file under the file mutation queue.
|
|
129
|
+
* The transformer receives the current state (or a fresh empty file if none
|
|
130
|
+
* exists) and returns the new state. Returning `undefined` aborts the write
|
|
131
|
+
* (no-op transformer).
|
|
132
|
+
*/
|
|
133
|
+
export async function mutateAuditState(
|
|
134
|
+
cwd: string,
|
|
135
|
+
transform: (state: AuditStateFile) => AuditStateFile | undefined,
|
|
136
|
+
): Promise<AuditStateFile> {
|
|
137
|
+
const path = getAuditStatePath(cwd);
|
|
138
|
+
return withFileMutationQueue(path, async () => {
|
|
139
|
+
const current = readAuditStateOrEmpty(path);
|
|
140
|
+
const next = transform(current);
|
|
141
|
+
if (!next) return current;
|
|
142
|
+
writeAuditStateRaw(path, next);
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readAuditStateOrEmpty(path: string): AuditStateFile {
|
|
148
|
+
if (!existsSync(path)) return { audits: [] };
|
|
149
|
+
const raw = readFileSync(path, "utf8");
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
152
|
+
if (isAuditStateFile(parsed)) return parsed;
|
|
153
|
+
} catch {
|
|
154
|
+
// fall through to empty — caller sees fresh state and overwrites
|
|
155
|
+
}
|
|
156
|
+
return { audits: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Most recent audit by `started_at` (ISO timestamps sort lexically). */
|
|
160
|
+
export function latestAudit(state: AuditStateFile): AuditRunState | undefined {
|
|
161
|
+
if (state.audits.length === 0) return undefined;
|
|
162
|
+
return [...state.audits].sort((a, b) => (a.started_at < b.started_at ? 1 : -1))[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Most recent resumable audit across all modes. Preference order matches
|
|
167
|
+
* archon's resume picker: an `in_progress` run (process killed mid-phase)
|
|
168
|
+
* outranks a `failed` run (orderly terminal state) because the former is
|
|
169
|
+
* more likely a transient outage. `complete` audits are never returned.
|
|
170
|
+
*
|
|
171
|
+
* Ties are broken by `started_at` (most recent first).
|
|
172
|
+
*/
|
|
173
|
+
export function latestResumableAudit(state: AuditStateFile): AuditRunState | undefined {
|
|
174
|
+
const sorted = [...state.audits].sort((a, b) => (a.started_at < b.started_at ? 1 : -1));
|
|
175
|
+
return (
|
|
176
|
+
sorted.find((a) => a.status === "in_progress") ??
|
|
177
|
+
sorted.find((a) => a.status === "failed") ??
|
|
178
|
+
undefined
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface InitAuditOptions {
|
|
183
|
+
mode: AuditMode;
|
|
184
|
+
model?: string;
|
|
185
|
+
agent_sdk?: string;
|
|
186
|
+
commit?: string | null;
|
|
187
|
+
branch?: string;
|
|
188
|
+
repository?: string;
|
|
189
|
+
history_available?: boolean;
|
|
190
|
+
/** Override the phase list (otherwise derived from mode). */
|
|
191
|
+
phases?: readonly string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Append a new audit run to the state file. Returns the appended run with a
|
|
196
|
+
* fresh ISO timestamp `audit_id`.
|
|
197
|
+
*/
|
|
198
|
+
export async function initAudit(cwd: string, options: InitAuditOptions): Promise<AuditRunState> {
|
|
199
|
+
const startedAt = new Date().toISOString();
|
|
200
|
+
const phases: Record<string, PhaseState> = {};
|
|
201
|
+
const phaseList = options.phases ?? phasesFor(options.mode);
|
|
202
|
+
for (const name of phaseList) phases[name] = { status: "pending" };
|
|
203
|
+
|
|
204
|
+
const run: AuditRunState = {
|
|
205
|
+
audit_id: startedAt,
|
|
206
|
+
mode: options.mode,
|
|
207
|
+
started_at: startedAt,
|
|
208
|
+
completed_at: null,
|
|
209
|
+
status: "in_progress",
|
|
210
|
+
phases,
|
|
211
|
+
...(options.model !== undefined && { model: options.model }),
|
|
212
|
+
...(options.agent_sdk !== undefined && { agent_sdk: options.agent_sdk }),
|
|
213
|
+
...(options.commit !== undefined && { commit: options.commit }),
|
|
214
|
+
...(options.branch !== undefined && { branch: options.branch }),
|
|
215
|
+
...(options.repository !== undefined && { repository: options.repository }),
|
|
216
|
+
...(options.history_available !== undefined && { history_available: options.history_available }),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
await mutateAuditState(cwd, (state) => ({ ...state, audits: [...state.audits, run] }));
|
|
220
|
+
return run;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface PhaseUpdate {
|
|
224
|
+
status: PhaseStatus;
|
|
225
|
+
error?: string;
|
|
226
|
+
artifacts?: string[];
|
|
227
|
+
attempt?: number;
|
|
228
|
+
max_attempts?: number;
|
|
229
|
+
retry_backoff_ms?: number | null;
|
|
230
|
+
next_retry_at?: string | null;
|
|
231
|
+
last_error?: string | null;
|
|
232
|
+
heartbeat_at?: string | null;
|
|
233
|
+
last_event_at?: string | null;
|
|
234
|
+
last_tool?: string | null;
|
|
235
|
+
last_tool_summary?: string | null;
|
|
236
|
+
run_id?: string | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Update a single phase on the named audit. Auto-stamps `started_at` on
|
|
241
|
+
* transitions into `in_progress` and `completed_at` on terminal states.
|
|
242
|
+
* Returns the updated audit, or `undefined` if the audit_id wasn't found.
|
|
243
|
+
*/
|
|
244
|
+
export async function setPhaseStatus(
|
|
245
|
+
cwd: string,
|
|
246
|
+
auditId: string,
|
|
247
|
+
phase: string,
|
|
248
|
+
update: PhaseUpdate,
|
|
249
|
+
): Promise<AuditRunState | undefined> {
|
|
250
|
+
let updated: AuditRunState | undefined;
|
|
251
|
+
await mutateAuditState(cwd, (state) => {
|
|
252
|
+
const idx = state.audits.findIndex((a) => a.audit_id === auditId);
|
|
253
|
+
if (idx < 0) return undefined;
|
|
254
|
+
const audit = state.audits[idx];
|
|
255
|
+
if (!audit) return undefined;
|
|
256
|
+
const prev = audit.phases[phase] ?? { status: "pending" as const };
|
|
257
|
+
const now = new Date().toISOString();
|
|
258
|
+
const next: PhaseState = {
|
|
259
|
+
...prev,
|
|
260
|
+
status: update.status,
|
|
261
|
+
...(update.error !== undefined && { error: update.error }),
|
|
262
|
+
...(update.artifacts !== undefined && { artifacts: update.artifacts }),
|
|
263
|
+
...(update.attempt !== undefined && { attempt: update.attempt }),
|
|
264
|
+
...(update.max_attempts !== undefined && { max_attempts: update.max_attempts }),
|
|
265
|
+
};
|
|
266
|
+
if (update.retry_backoff_ms !== undefined) {
|
|
267
|
+
if (update.retry_backoff_ms === null) next.retry_backoff_ms = undefined;
|
|
268
|
+
else next.retry_backoff_ms = update.retry_backoff_ms;
|
|
269
|
+
}
|
|
270
|
+
if (update.next_retry_at !== undefined) {
|
|
271
|
+
if (update.next_retry_at === null) next.next_retry_at = undefined;
|
|
272
|
+
else next.next_retry_at = update.next_retry_at;
|
|
273
|
+
}
|
|
274
|
+
if (update.last_error !== undefined) {
|
|
275
|
+
if (update.last_error === null) next.last_error = undefined;
|
|
276
|
+
else next.last_error = update.last_error;
|
|
277
|
+
}
|
|
278
|
+
if (update.heartbeat_at !== undefined) {
|
|
279
|
+
if (update.heartbeat_at === null) next.heartbeat_at = undefined;
|
|
280
|
+
else next.heartbeat_at = update.heartbeat_at;
|
|
281
|
+
}
|
|
282
|
+
if (update.last_event_at !== undefined) {
|
|
283
|
+
if (update.last_event_at === null) next.last_event_at = undefined;
|
|
284
|
+
else next.last_event_at = update.last_event_at;
|
|
285
|
+
}
|
|
286
|
+
if (update.last_tool !== undefined) {
|
|
287
|
+
if (update.last_tool === null) next.last_tool = undefined;
|
|
288
|
+
else next.last_tool = update.last_tool;
|
|
289
|
+
}
|
|
290
|
+
if (update.last_tool_summary !== undefined) {
|
|
291
|
+
if (update.last_tool_summary === null) next.last_tool_summary = undefined;
|
|
292
|
+
else next.last_tool_summary = update.last_tool_summary;
|
|
293
|
+
}
|
|
294
|
+
if (update.run_id !== undefined) {
|
|
295
|
+
if (update.run_id === null) next.run_id = undefined;
|
|
296
|
+
else next.run_id = update.run_id;
|
|
297
|
+
}
|
|
298
|
+
if (update.status === "in_progress" && !next.started_at) next.started_at = now;
|
|
299
|
+
if (update.status === "in_progress") next.completed_at = undefined;
|
|
300
|
+
if (update.status === "complete") {
|
|
301
|
+
next.error = undefined;
|
|
302
|
+
next.artifacts = undefined;
|
|
303
|
+
next.retry_backoff_ms = undefined;
|
|
304
|
+
next.next_retry_at = undefined;
|
|
305
|
+
next.last_error = undefined;
|
|
306
|
+
next.heartbeat_at = undefined;
|
|
307
|
+
next.last_event_at = undefined;
|
|
308
|
+
next.last_tool = undefined;
|
|
309
|
+
next.last_tool_summary = undefined;
|
|
310
|
+
next.run_id = undefined;
|
|
311
|
+
}
|
|
312
|
+
if (update.status === "complete" || update.status === "failed" || update.status === "skipped") {
|
|
313
|
+
if (!next.started_at) next.started_at = now;
|
|
314
|
+
next.completed_at = now;
|
|
315
|
+
}
|
|
316
|
+
const phases = { ...audit.phases, [phase]: next };
|
|
317
|
+
const newAudit: AuditRunState = { ...audit, phases };
|
|
318
|
+
updated = newAudit;
|
|
319
|
+
const audits = [...state.audits];
|
|
320
|
+
audits[idx] = newAudit;
|
|
321
|
+
return { ...state, audits };
|
|
322
|
+
});
|
|
323
|
+
return updated;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Wrapper around `setPhaseStatus` that also mirrors the disk write onto the
|
|
328
|
+
* caller's in-memory `AuditRunState`. Use this from orchestrators that hold
|
|
329
|
+
* an `audit` object across multiple phase transitions — otherwise their copy
|
|
330
|
+
* goes stale the moment any phase completes, and downstream prerequisite
|
|
331
|
+
* checks (e.g. `ensurePrereqs`) read "pending" for already-completed phases.
|
|
332
|
+
*/
|
|
333
|
+
export async function applyPhaseStatus(
|
|
334
|
+
cwd: string,
|
|
335
|
+
audit: AuditRunState,
|
|
336
|
+
phase: string,
|
|
337
|
+
update: PhaseUpdate,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
const updated = await setPhaseStatus(cwd, audit.audit_id, phase, update);
|
|
340
|
+
if (!updated) return;
|
|
341
|
+
const fresh = updated.phases[phase];
|
|
342
|
+
if (fresh) audit.phases[phase] = fresh;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Mark an audit run as complete or failed. */
|
|
346
|
+
export async function markAuditStatus(
|
|
347
|
+
cwd: string,
|
|
348
|
+
auditId: string,
|
|
349
|
+
status: RunStatus,
|
|
350
|
+
): Promise<AuditRunState | undefined> {
|
|
351
|
+
let updated: AuditRunState | undefined;
|
|
352
|
+
await mutateAuditState(cwd, (state) => {
|
|
353
|
+
const idx = state.audits.findIndex((a) => a.audit_id === auditId);
|
|
354
|
+
if (idx < 0) return undefined;
|
|
355
|
+
const audit = state.audits[idx];
|
|
356
|
+
if (!audit) return undefined;
|
|
357
|
+
const completedAt =
|
|
358
|
+
status === "complete" || status === "failed" ? new Date().toISOString() : audit.completed_at;
|
|
359
|
+
const newAudit: AuditRunState = {
|
|
360
|
+
...audit,
|
|
361
|
+
status,
|
|
362
|
+
completed_at: completedAt ?? null,
|
|
363
|
+
};
|
|
364
|
+
updated = newAudit;
|
|
365
|
+
const audits = [...state.audits];
|
|
366
|
+
audits[idx] = newAudit;
|
|
367
|
+
return { ...state, audits };
|
|
368
|
+
});
|
|
369
|
+
return updated;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface PhaseTally {
|
|
373
|
+
total: number;
|
|
374
|
+
complete: number;
|
|
375
|
+
in_progress: number;
|
|
376
|
+
pending: number;
|
|
377
|
+
failed: number;
|
|
378
|
+
skipped: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function tallyPhases(audit: AuditRunState): PhaseTally {
|
|
382
|
+
const tally: PhaseTally = {
|
|
383
|
+
total: 0,
|
|
384
|
+
complete: 0,
|
|
385
|
+
in_progress: 0,
|
|
386
|
+
pending: 0,
|
|
387
|
+
failed: 0,
|
|
388
|
+
skipped: 0,
|
|
389
|
+
};
|
|
390
|
+
for (const phase of Object.values(audit.phases)) {
|
|
391
|
+
tally.total++;
|
|
392
|
+
tally[phase.status]++;
|
|
393
|
+
}
|
|
394
|
+
return tally;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function compactDetail(text: string, max = 220): string {
|
|
398
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
399
|
+
if (collapsed.length <= max) return collapsed;
|
|
400
|
+
return `${collapsed.slice(0, max - 1)}…`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function equivalentDetail(left: string | undefined, right: string | undefined): boolean {
|
|
404
|
+
if (!left || !right) return false;
|
|
405
|
+
return left.replace(/\s+/g, " ").trim() === right.replace(/\s+/g, " ").trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function formatPhaseDetailLines(
|
|
409
|
+
phaseEntries: Array<[string, PhaseState]>,
|
|
410
|
+
options: {
|
|
411
|
+
markers?: boolean;
|
|
412
|
+
labelFor?: (name: string, index: number, total: number) => string;
|
|
413
|
+
} = {},
|
|
414
|
+
): string[] {
|
|
415
|
+
const lines: string[] = [];
|
|
416
|
+
const displayNames = phaseEntries.map(([name], index) =>
|
|
417
|
+
options.labelFor ? options.labelFor(name, index, phaseEntries.length) : name,
|
|
418
|
+
);
|
|
419
|
+
const displayWidth = Math.max(8, ...displayNames.map((name) => name.length));
|
|
420
|
+
for (const [index, [name, phase]] of phaseEntries.entries()) {
|
|
421
|
+
const displayName = displayNames[index] ?? name;
|
|
422
|
+
const marker = options.markers ? `${phaseMarker(phase.status)} ` : "";
|
|
423
|
+
const primaryError = phase.error ?? phase.last_error;
|
|
424
|
+
let line = ` ${marker}${displayName.padEnd(displayWidth)} ${phase.status}`;
|
|
425
|
+
if (primaryError) line += ` — ${compactDetail(primaryError)}`;
|
|
426
|
+
lines.push(line);
|
|
427
|
+
|
|
428
|
+
if (phase.status === "failed") {
|
|
429
|
+
if (phase.attempt !== undefined && phase.max_attempts !== undefined) {
|
|
430
|
+
lines.push(` attempts: ${phase.attempt}/${phase.max_attempts}`);
|
|
431
|
+
}
|
|
432
|
+
if (phase.last_error && !equivalentDetail(phase.last_error, primaryError)) {
|
|
433
|
+
lines.push(` last error: ${compactDetail(phase.last_error)}`);
|
|
434
|
+
}
|
|
435
|
+
if (phase.artifacts?.length) {
|
|
436
|
+
lines.push(` artifacts: ${phase.artifacts.map((a) => compactDetail(a, 120)).join(", ")}`);
|
|
437
|
+
}
|
|
438
|
+
} else if (phase.status === "in_progress" && phase.next_retry_at) {
|
|
439
|
+
lines.push(` next retry: ${phase.next_retry_at}`);
|
|
440
|
+
if (phase.last_error) lines.push(` last error: ${compactDetail(phase.last_error)}`);
|
|
441
|
+
} else if (phase.status === "in_progress" && phase.heartbeat_at) {
|
|
442
|
+
lines.push(` heartbeat: ${phase.heartbeat_at}`);
|
|
443
|
+
if (phase.last_event_at) lines.push(` last event: ${phase.last_event_at}`);
|
|
444
|
+
if (phase.last_tool) {
|
|
445
|
+
lines.push(
|
|
446
|
+
` last tool: ${compactDetail(
|
|
447
|
+
`${phase.last_tool}${phase.last_tool_summary ? ` ${phase.last_tool_summary}` : ""}`,
|
|
448
|
+
160,
|
|
449
|
+
)}`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return lines;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Format a run + phases as displayable lines for `/piolium-status`.
|
|
459
|
+
* Pure function — no UI deps so it's easy to unit-test later.
|
|
460
|
+
*/
|
|
461
|
+
export function formatAuditStatus(state: AuditStateFile): string[] {
|
|
462
|
+
const lines: string[] = [];
|
|
463
|
+
if (state.audits.length === 0) {
|
|
464
|
+
lines.push("No audits recorded yet in piolium/audit-state.json.");
|
|
465
|
+
return lines;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const audit = latestAudit(state);
|
|
469
|
+
if (!audit) {
|
|
470
|
+
lines.push("No audits recorded yet in piolium/audit-state.json.");
|
|
471
|
+
return lines;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const tally = tallyPhases(audit);
|
|
475
|
+
lines.push(`Audit: ${audit.audit_id}`);
|
|
476
|
+
lines.push(`Mode: ${audit.mode}`);
|
|
477
|
+
lines.push(`Status: ${audit.status}`);
|
|
478
|
+
lines.push(`Started: ${audit.started_at}`);
|
|
479
|
+
if (audit.completed_at) lines.push(`Completed: ${audit.completed_at}`);
|
|
480
|
+
if (audit.commit) lines.push(`Commit: ${audit.commit}`);
|
|
481
|
+
if (audit.branch) lines.push(`Branch: ${audit.branch}`);
|
|
482
|
+
if (audit.repository) lines.push(`Repo: ${audit.repository}`);
|
|
483
|
+
if (audit.model) lines.push(`Model: ${audit.model}`);
|
|
484
|
+
if (audit.agent_sdk) lines.push(`SDK: ${audit.agent_sdk}`);
|
|
485
|
+
lines.push("");
|
|
486
|
+
lines.push(
|
|
487
|
+
`Phases: ${tally.complete}/${tally.total} complete · ${tally.in_progress} running · ${tally.pending} pending · ${tally.failed} failed · ${tally.skipped} skipped`,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const phaseEntries = Object.entries(audit.phases);
|
|
491
|
+
if (phaseEntries.length > 0) {
|
|
492
|
+
lines.push("");
|
|
493
|
+
lines.push("Phase detail:");
|
|
494
|
+
lines.push(
|
|
495
|
+
...formatPhaseDetailLines(phaseEntries, {
|
|
496
|
+
markers: true,
|
|
497
|
+
labelFor: (name, index, total) => formatPhaseDetailLabel(audit.mode, name, index, total),
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (state.audits.length > 1) {
|
|
503
|
+
lines.push("");
|
|
504
|
+
lines.push(`(${state.audits.length} audits in history; showing latest only.)`);
|
|
505
|
+
}
|
|
506
|
+
return lines;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function phaseMarker(status: PhaseStatus): string {
|
|
510
|
+
switch (status) {
|
|
511
|
+
case "complete":
|
|
512
|
+
return "✓";
|
|
513
|
+
case "in_progress":
|
|
514
|
+
return "…";
|
|
515
|
+
case "failed":
|
|
516
|
+
return "✗";
|
|
517
|
+
case "skipped":
|
|
518
|
+
return "↷";
|
|
519
|
+
default:
|
|
520
|
+
return "·";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve directories that ship with the piolium package.
|
|
3
|
+
*
|
|
4
|
+
* The package layout is:
|
|
5
|
+
*
|
|
6
|
+
* piolium/
|
|
7
|
+
* ├── extensions/piolium/<this-file>
|
|
8
|
+
* ├── agents/
|
|
9
|
+
* ├── skills/
|
|
10
|
+
* └── prompts/
|
|
11
|
+
*
|
|
12
|
+
* From any file under `extensions/piolium/`, `import.meta.dirname` resolves
|
|
13
|
+
* to `<package>/extensions/piolium/`. The bundled resource roots sit two
|
|
14
|
+
* levels up.
|
|
15
|
+
*
|
|
16
|
+
* We also expose user and project override directories so operators can
|
|
17
|
+
* tweak agent prompts without forking the package. Project overrides are
|
|
18
|
+
* opt-in (the runner gates them behind a setting) because a repo-controlled
|
|
19
|
+
* agent file can instruct the model to run shell commands.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
|
|
26
|
+
const HERE = import.meta.dirname ?? process.cwd();
|
|
27
|
+
const PACKAGE_ROOT = resolve(HERE, "..", "..");
|
|
28
|
+
|
|
29
|
+
export function getPackageRoot(): string {
|
|
30
|
+
return PACKAGE_ROOT;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getBundledAgentsDir(): string {
|
|
34
|
+
return join(PACKAGE_ROOT, "agents");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getBundledSkillsDir(): string {
|
|
38
|
+
return join(PACKAGE_ROOT, "skills");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Agent discovery search paths in precedence order:
|
|
43
|
+
* 1. project <cwd>/.pi/piolium/agents/ (only if `allowProjectAgents`)
|
|
44
|
+
* 2. user ~/.pi/agent/piolium/agents/
|
|
45
|
+
* 3. bundled <package>/agents/
|
|
46
|
+
*
|
|
47
|
+
* Earlier paths win on collision so an operator can override individual
|
|
48
|
+
* agents without copying the entire bundle.
|
|
49
|
+
*/
|
|
50
|
+
export function getAgentSearchPaths(cwd: string, allowProjectAgents: boolean): string[] {
|
|
51
|
+
const paths: string[] = [];
|
|
52
|
+
if (allowProjectAgents) paths.push(join(cwd, ".pi", "piolium", "agents"));
|
|
53
|
+
paths.push(join(homedir(), ".pi", "agent", "piolium", "agents"));
|
|
54
|
+
paths.push(getBundledAgentsDir());
|
|
55
|
+
return dedupePaths(paths);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dedupePaths(paths: string[]): string[] {
|
|
59
|
+
const seen = new Set<string>();
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
for (const p of paths) {
|
|
62
|
+
let key: string;
|
|
63
|
+
try {
|
|
64
|
+
key = realpathSync(p);
|
|
65
|
+
} catch {
|
|
66
|
+
key = resolve(p);
|
|
67
|
+
}
|
|
68
|
+
if (seen.has(key)) continue;
|
|
69
|
+
seen.add(key);
|
|
70
|
+
out.push(p);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Test helper — exposes the resolved layout for diagnostics. */
|
|
76
|
+
export function describeLayout(): { here: string; root: string; agents: string; skills: string } {
|
|
77
|
+
return {
|
|
78
|
+
here: HERE,
|
|
79
|
+
root: PACKAGE_ROOT,
|
|
80
|
+
agents: getBundledAgentsDir(),
|
|
81
|
+
skills: getBundledSkillsDir(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Helpful in case anyone wants to know whether the bundled tree shipped:
|
|
86
|
+
export function bundledAgentsExist(): boolean {
|
|
87
|
+
return existsSync(getBundledAgentsDir());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function bundledSkillsExist(): boolean {
|
|
91
|
+
return existsSync(getBundledSkillsDir());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** kept for symmetry with piolium's signature; may need real impl when packaged */
|
|
95
|
+
export function _internal_packageRoot_for_tests(): string {
|
|
96
|
+
return dirname(getBundledAgentsDir());
|
|
97
|
+
}
|