solidity-argus 0.3.4 → 0.3.6

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 (36) hide show
  1. package/package.json +4 -4
  2. package/src/agents/argus-prompt.ts +56 -2
  3. package/src/agents/pythia-prompt.ts +11 -0
  4. package/src/agents/scribe-prompt.ts +9 -4
  5. package/src/agents/sentinel-prompt.ts +10 -0
  6. package/src/cli/commands/init.ts +1 -1
  7. package/src/config/schema.ts +2 -2
  8. package/src/create-hooks.ts +95 -12
  9. package/src/create-tools.ts +2 -0
  10. package/src/features/audit-enforcer/audit-enforcer.ts +30 -2
  11. package/src/features/persistent-state/audit-state-manager.ts +180 -10
  12. package/src/features/persistent-state/event-sink.ts +15 -6
  13. package/src/features/persistent-state/findings-materializer.ts +52 -0
  14. package/src/features/persistent-state/index.ts +1 -1
  15. package/src/features/persistent-state/run-finalizer.ts +26 -7
  16. package/src/features/persistent-state/run-journal.ts +12 -4
  17. package/src/hooks/event-hook.ts +4 -1
  18. package/src/hooks/system-prompt-hook.ts +15 -0
  19. package/src/hooks/tool-tracking-hook.ts +168 -10
  20. package/src/shared/audit-artifact-resolver.ts +13 -12
  21. package/src/shared/file-utils.ts +7 -2
  22. package/src/shared/index.ts +8 -8
  23. package/src/shared/path-root-resolver.ts +34 -0
  24. package/src/shared/plugin-metadata.ts +23 -0
  25. package/src/shared/report-path-resolver.ts +3 -3
  26. package/src/state/adapters.ts +99 -5
  27. package/src/state/finding-aggregation.ts +100 -0
  28. package/src/state/finding-fingerprint.ts +47 -0
  29. package/src/state/finding-store.ts +19 -29
  30. package/src/state/projectors.ts +18 -4
  31. package/src/state/schemas.ts +145 -1
  32. package/src/state/types.ts +17 -1
  33. package/src/tools/record-finding-tool.ts +125 -0
  34. package/src/tools/report-generator-tool.ts +116 -7
  35. package/src/tools/report-preflight.ts +79 -0
  36. package/src/tools/solodit-search-tool.ts +6 -2
@@ -3,15 +3,21 @@ import path from "node:path"
3
3
  import { type ToolContext, tool } from "@opencode-ai/plugin"
4
4
  import { loadArgusConfig } from "../config/loader"
5
5
  import type { ArgusConfig } from "../config/types"
6
+ import { readEvents } from "../features/persistent-state/event-sink"
6
7
  import type { DropDiagnostic, DropPolicy } from "../shared/drop-diagnostics"
7
8
  import { createDropDiagnosticsCollector } from "../shared/drop-diagnostics"
8
9
  import { createLogger } from "../shared/logger"
9
10
  import { resolveProjectDir } from "../shared/project-utils"
10
11
  import { resolveReportPath } from "../shared/report-path-resolver"
11
12
  import { normalizeToCanonicalFinding } from "../state/adapters"
12
- import { stableHash } from "../state/projectors"
13
+ import {
14
+ compareIssueFingerprintSets,
15
+ dedupeFindingsForFinalOutput,
16
+ } from "../state/finding-aggregation"
17
+ import { projectFindings, stableHash } from "../state/projectors"
13
18
  import { type ReportInput, SCHEMA_VERSION, validateReportInput } from "../state/schemas"
14
19
  import type { AuditState, Finding, FindingSeverity } from "../state/types"
20
+ import { checkReportPreflight } from "./report-preflight"
15
21
 
16
22
  type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
17
23
 
@@ -23,6 +29,7 @@ type ReportGeneratorArgs = {
23
29
  quality_gate_policy?: QualityGatePolicy
24
30
  report_input?: string
25
31
  audit_state?: string
32
+ preflight_policy?: PreflightPolicy
26
33
  }
27
34
 
28
35
  type FindingsCount = {
@@ -46,6 +53,8 @@ export type ReportGenerationResult = {
46
53
 
47
54
  type QualityGatePolicy = "warn" | "strict-fail"
48
55
 
56
+ type PreflightPolicy = "warn" | "strict-fail"
57
+
49
58
  type ReportQualityViolation = {
50
59
  findingId: string
51
60
  code: string
@@ -59,6 +68,10 @@ type ReportQualityValidation = {
59
68
 
60
69
  export type ReportGenerationDependencies = {
61
70
  loadConfig?: (projectDir: string) => ArgusConfig
71
+ readEvents?: (
72
+ runId: string,
73
+ projectDir: string,
74
+ ) => Promise<import("../state/schemas").AuditEvent[]>
62
75
  }
63
76
 
64
77
  export const SINGLE_WRITER_POLICY_VERSION = "1.0.0"
@@ -993,9 +1006,25 @@ export function buildProvenanceAppendix(
993
1006
  lines.push("| Tool | Duration | Status | Findings |")
994
1007
  lines.push("| --- | --- | --- | ---: |")
995
1008
  for (const exec of state.toolsExecuted) {
996
- const duration = exec.endTime != null ? formatDuration(exec.endTime - exec.startTime) : ""
997
- const status = exec.success ? "✅ success" : "❌ failure"
998
- lines.push(`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`)
1009
+ const toolName = typeof exec.tool === "string" && exec.tool ? exec.tool : "(unknown tool)"
1010
+ const hasTimes =
1011
+ typeof exec.startTime === "number" &&
1012
+ !Number.isNaN(exec.startTime) &&
1013
+ exec.endTime != null &&
1014
+ typeof exec.endTime === "number" &&
1015
+ !Number.isNaN(exec.endTime)
1016
+ const duration = hasTimes ? formatDuration((exec.endTime as number) - exec.startTime) : "N/A"
1017
+ const status =
1018
+ typeof exec.success === "boolean"
1019
+ ? exec.success
1020
+ ? "\u2705 success"
1021
+ : "\u274C failure"
1022
+ : "\u26A0 malformed"
1023
+ const findings =
1024
+ typeof exec.findingsCount === "number" && !Number.isNaN(exec.findingsCount)
1025
+ ? exec.findingsCount
1026
+ : "N/A"
1027
+ lines.push(`| ${toolName} | ${duration} | ${status} | ${findings} |`)
999
1028
  }
1000
1029
  }
1001
1030
 
@@ -1008,7 +1037,11 @@ export function buildProvenanceAppendix(
1008
1037
  lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
1009
1038
  }
1010
1039
  if (syncExec) {
1011
- lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`)
1040
+ const syncTime =
1041
+ typeof syncExec.startTime === "number" && !Number.isNaN(syncExec.startTime)
1042
+ ? new Date(syncExec.startTime).toISOString()
1043
+ : "N/A"
1044
+ lines.push(`- SCVD last synced: ${syncTime}`)
1012
1045
  }
1013
1046
  }
1014
1047
 
@@ -1066,10 +1099,81 @@ export async function executeReportGeneration(
1066
1099
  const threshold = args.severity_threshold ?? "low"
1067
1100
  const qualityGatePolicy = args.quality_gate_policy ?? "warn"
1068
1101
  const { reportInput, diagnostics } = parseReportInputPayload(args, context)
1102
+ const preflightPolicy = args.preflight_policy ?? "warn"
1103
+ let preflightWarningSection: string | null = null
1104
+ const warningBullets: string[] = []
1105
+ try {
1106
+ const readEventsFn = deps.readEvents ?? readEvents
1107
+ const events = await readEventsFn(reportInput.run_id, reportInput.projectDir)
1108
+ const preflightResult = checkReportPreflight(events)
1109
+ if (!preflightResult.passed) {
1110
+ if (preflightPolicy === "strict-fail") {
1111
+ const parts: string[] = []
1112
+ if (preflightResult.orphanedTools.length > 0)
1113
+ parts.push(`orphaned tools: ${preflightResult.orphanedTools.join(", ")}`)
1114
+ if (preflightResult.missingLifecycle.length > 0)
1115
+ parts.push(`missing lifecycle: ${preflightResult.missingLifecycle.join(", ")}`)
1116
+ if (preflightResult.missingRequiredTools.length > 0)
1117
+ parts.push(`missing required tools: ${preflightResult.missingRequiredTools.join(", ")}`)
1118
+ throw new Error(`Preflight failed (strict-fail): ${parts.join("; ")}`)
1119
+ }
1120
+ if (preflightResult.orphanedTools.length > 0)
1121
+ warningBullets.push(`- Orphaned tools: ${preflightResult.orphanedTools.join(", ")}`)
1122
+ if (preflightResult.missingLifecycle.length > 0)
1123
+ warningBullets.push(`- Missing lifecycle: ${preflightResult.missingLifecycle.join(", ")}`)
1124
+ if (preflightResult.missingRequiredTools.length > 0)
1125
+ warningBullets.push(
1126
+ `- Missing required tools: ${preflightResult.missingRequiredTools.join(", ")}`,
1127
+ )
1128
+ if (preflightResult.warnings.length > 0)
1129
+ warningBullets.push(`- Warnings: ${preflightResult.warnings.join(", ")}`)
1130
+ }
1131
+
1132
+ const eventFindings = dedupeFindingsForFinalOutput(projectFindings(events))
1133
+ const inputFindings = dedupeFindingsForFinalOutput(reportInput.findings)
1134
+ const parity = compareIssueFingerprintSets(eventFindings, inputFindings)
1135
+
1136
+ if (!parity.matches) {
1137
+ const mismatchSummary = `missing=${parity.missing.length}, extra=${parity.extra.length}`
1138
+ if (preflightPolicy === "strict-fail") {
1139
+ throw new Error(
1140
+ `Preflight failed (strict-fail): finding parity mismatch (${mismatchSummary})`,
1141
+ )
1142
+ }
1143
+
1144
+ warningBullets.push(`- Finding parity mismatch: ${mismatchSummary}`)
1145
+ if (parity.missing.length > 0) {
1146
+ warningBullets.push(`- Missing issue fingerprints: ${parity.missing.join(", ")}`)
1147
+ }
1148
+ if (parity.extra.length > 0) {
1149
+ warningBullets.push(`- Extra issue fingerprints: ${parity.extra.join(", ")}`)
1150
+ }
1151
+ }
1152
+ } catch (err) {
1153
+ if (err instanceof Error && err.message.startsWith("Preflight failed (strict-fail)")) {
1154
+ throw err
1155
+ }
1156
+ if (preflightPolicy === "strict-fail") {
1157
+ throw new Error("Preflight failed: unable to read event stream for completeness check")
1158
+ }
1159
+ // warn mode: skip preflight when events cannot be read
1160
+ }
1161
+
1162
+ if (warningBullets.length > 0) {
1163
+ preflightWarningSection = [
1164
+ "## \u26A0 Completeness Warning",
1165
+ "",
1166
+ "This report was generated with incomplete orchestration state.",
1167
+ "",
1168
+ ...warningBullets,
1169
+ ].join("\n")
1170
+ }
1171
+
1069
1172
  const state = reportInputToAuditState(reportInput)
1070
1173
  const scope = args.scope.length > 0 ? args.scope : reportInput.scope
1174
+ const finalFindings = dedupeFindingsForFinalOutput(reportInput.findings)
1071
1175
  const findings = sortFindingsDeterministically(
1072
- state.findings.filter((finding) => shouldIncludeFinding(finding, threshold)),
1176
+ finalFindings.filter((finding) => shouldIncludeFinding(finding, threshold)),
1073
1177
  )
1074
1178
  const qualityGates = validateReportQuality(findings, qualityGatePolicy)
1075
1179
  if (!qualityGates.passed && qualityGatePolicy === "strict-fail") {
@@ -1129,6 +1233,10 @@ export async function executeReportGeneration(
1129
1233
  sections.push(`- ${item}`)
1130
1234
  }
1131
1235
 
1236
+ if (preflightWarningSection) {
1237
+ sections.push(preflightWarningSection)
1238
+ }
1239
+
1132
1240
  sections.push(buildProvenanceAppendix(state, threshold, findings.length))
1133
1241
 
1134
1242
  // Embed report metadata for single-writer policy enforcement
@@ -1159,7 +1267,7 @@ export async function executeReportGeneration(
1159
1267
  const loadConfig = deps.loadConfig ?? loadArgusConfig
1160
1268
  const projectDir = resolveProjectDir(context)
1161
1269
  const config = loadConfig(projectDir)
1162
- const outputDir = config.reporting?.output_dir ?? ".opencode/reports/"
1270
+ const outputDir = config.reporting?.output_dir ?? ".argus/reports/"
1163
1271
  const fullPath = path.join(projectDir, outputDir, canonicalFilename)
1164
1272
 
1165
1273
  // Single-writer policy: check for duplicate writes with same run_id
@@ -1194,6 +1302,7 @@ export const reportGeneratorTool = tool({
1194
1302
  .default("low"),
1195
1303
  report_input: tool.schema.string().optional(),
1196
1304
  audit_state: tool.schema.string().optional(),
1305
+ preflight_policy: tool.schema.enum(["warn", "strict-fail"]).optional(),
1197
1306
  },
1198
1307
  async execute(args, context) {
1199
1308
  const result = await executeReportGeneration(args, context)
@@ -0,0 +1,79 @@
1
+ import {
2
+ collectToolLifecycleIssues,
3
+ hasSessionCreated,
4
+ hasSessionDeleted,
5
+ } from "../features/persistent-state/run-finalizer"
6
+ import type { AuditEvent } from "../state/schemas"
7
+
8
+ export interface PreflightResult {
9
+ passed: boolean
10
+ orphanedTools: string[]
11
+ missingLifecycle: string[]
12
+ missingRequiredTools: string[]
13
+ warnings: string[]
14
+ }
15
+
16
+ export interface PreflightOptions {
17
+ requiredTools?: string[]
18
+ }
19
+
20
+ function asRecord(value: unknown): Record<string, unknown> | null {
21
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
22
+ return value as Record<string, unknown>
23
+ }
24
+ return null
25
+ }
26
+
27
+ function hasCompletedTool(events: AuditEvent[], toolName: string): boolean {
28
+ for (const event of events) {
29
+ if (event.type !== "tool.completed") {
30
+ continue
31
+ }
32
+
33
+ const payload = asRecord(event.payload)
34
+ if (!payload) {
35
+ continue
36
+ }
37
+
38
+ const name = payload.name
39
+ const tool = payload.tool
40
+ if (name === toolName || tool === toolName) {
41
+ return true
42
+ }
43
+ }
44
+
45
+ return false
46
+ }
47
+
48
+ export function checkReportPreflight(
49
+ events: AuditEvent[],
50
+ options: PreflightOptions = {},
51
+ ): PreflightResult {
52
+ const missingLifecycle: string[] = []
53
+ if (!hasSessionCreated(events)) {
54
+ missingLifecycle.push("session.created")
55
+ }
56
+ if (!hasSessionDeleted(events)) {
57
+ missingLifecycle.push("session.deleted")
58
+ }
59
+
60
+ const { orphanedToolCallIds, malformedEvents } = collectToolLifecycleIssues(events)
61
+
62
+ const missingRequiredTools: string[] = []
63
+ for (const requiredTool of options.requiredTools ?? []) {
64
+ if (!hasCompletedTool(events, requiredTool)) {
65
+ missingRequiredTools.push(requiredTool)
66
+ }
67
+ }
68
+
69
+ return {
70
+ passed:
71
+ orphanedToolCallIds.length === 0 &&
72
+ missingLifecycle.length === 0 &&
73
+ missingRequiredTools.length === 0,
74
+ orphanedTools: orphanedToolCallIds,
75
+ missingLifecycle,
76
+ missingRequiredTools,
77
+ warnings: malformedEvents,
78
+ }
79
+ }
@@ -258,7 +258,9 @@ export async function executeSoloditSearch(
258
258
  let hadMcpError = false
259
259
  for (const toolName of SOLODIT_MCP_TOOLS) {
260
260
  try {
261
- logger.debug(`[solodit] Trying MCP tool '${toolName}' on server '${SOLODIT_MCP_SERVER}' for query: ${query}`)
261
+ logger.debug(
262
+ `[solodit] Trying MCP tool '${toolName}' on server '${SOLODIT_MCP_SERVER}' for query: ${query}`,
263
+ )
262
264
  const response = await mcpCaller(
263
265
  SOLODIT_MCP_SERVER,
264
266
  toolName,
@@ -289,7 +291,9 @@ export async function executeSoloditSearch(
289
291
  }
290
292
 
291
293
  // All MCP tools failed — fall back to HTTP
292
- logger.debug(`[solodit] All MCP tools failed (hadMcpError=${hadMcpError}) — falling back to HTTP for query: ${query}`)
294
+ logger.debug(
295
+ `[solodit] All MCP tools failed (hadMcpError=${hadMcpError}) — falling back to HTTP for query: ${query}`,
296
+ )
293
297
  return callSoloditHttp(query, limit, args.severity, port)
294
298
  }
295
299