solidity-argus 0.5.10 → 0.6.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 (42) hide show
  1. package/AGENTS.md +8 -1
  2. package/README.md +27 -21
  3. package/package.json +2 -2
  4. package/skills/INVENTORY.md +14 -1
  5. package/skills/README.md +4 -2
  6. package/skills/references/attack-vector-deck/SKILL.md +62 -0
  7. package/skills/specialist-profiles/access-control-specialist/SKILL.md +31 -0
  8. package/skills/specialist-profiles/economic-security/SKILL.md +31 -0
  9. package/skills/specialist-profiles/execution-trace/SKILL.md +31 -0
  10. package/skills/specialist-profiles/first-principles/SKILL.md +31 -0
  11. package/skills/specialist-profiles/invariant/SKILL.md +31 -0
  12. package/skills/specialist-profiles/math-precision/SKILL.md +31 -0
  13. package/skills/specialist-profiles/periphery/SKILL.md +31 -0
  14. package/skills/specialist-profiles/vector-scan/SKILL.md +28 -0
  15. package/src/agents/argus-prompt.ts +59 -6
  16. package/src/agents/audit-specialist-prompt.ts +94 -0
  17. package/src/agents/pythia-prompt.ts +7 -4
  18. package/src/agents/scribe-prompt.ts +9 -0
  19. package/src/agents/sentinel-prompt.ts +12 -0
  20. package/src/agents/themis-prompt.ts +4 -0
  21. package/src/config/schema.ts +2 -0
  22. package/src/constants/defaults.ts +1 -0
  23. package/src/create-hooks.ts +9 -1
  24. package/src/features/background-agent/background-manager.ts +85 -2
  25. package/src/features/persistent-state/run-finalizer.ts +37 -3
  26. package/src/hooks/config-handler.ts +23 -0
  27. package/src/hooks/system-prompt-hook.ts +72 -2
  28. package/src/hooks/tool-tracking-hook.ts +50 -6
  29. package/src/managers/types.ts +21 -0
  30. package/src/shared/agent-names.ts +1 -0
  31. package/src/shared/lineage-validator.ts +96 -0
  32. package/src/shared/report-path-resolver.ts +8 -2
  33. package/src/state/adapters.ts +1 -1
  34. package/src/state/projectors.ts +50 -0
  35. package/src/state/schemas.ts +86 -1
  36. package/src/state/types.ts +25 -1
  37. package/src/tools/forge-coverage-tool.ts +41 -5
  38. package/src/tools/persist-deduped-tool.ts +45 -1
  39. package/src/tools/read-findings-tool.ts +46 -5
  40. package/src/tools/record-finding-tool.ts +10 -30
  41. package/src/tools/report-generator-tool.ts +135 -37
  42. package/src/tools/slither-tool.ts +62 -2
@@ -35,6 +35,73 @@ export function buildFallbackDirectives(unavailableTools: string[]): string[] {
35
35
  return directives
36
36
  }
37
37
 
38
+ function formatDuration(startTime: number, endTime?: number): string {
39
+ if (typeof endTime !== "number" || endTime < startTime) return "pending"
40
+ return `${endTime - startTime}ms`
41
+ }
42
+
43
+ function buildToolLedgerLine(auditState: AuditState): string {
44
+ const taskTools = auditState.toolsExecuted.filter((tool) => tool.tool === "task")
45
+ const taskDispatches = taskTools.length
46
+ const argusTools = auditState.toolsExecuted.filter((tool) => tool.tool !== "task").slice(-5)
47
+ const entries = argusTools.map((tool) => {
48
+ const status = tool.success ? "ok" : "failed"
49
+ return `${tool.tool}=${status} findings=${tool.findingsCount} duration=${formatDuration(tool.startTime, tool.endTime)}`
50
+ })
51
+
52
+ if (taskDispatches > 0) {
53
+ const bySubagent = new Map<string, number>()
54
+ for (const tool of taskTools) {
55
+ const subagent = tool.subagent_type ?? "unknown"
56
+ bySubagent.set(subagent, (bySubagent.get(subagent) ?? 0) + 1)
57
+ }
58
+ const subagentSummary = [...bySubagent.entries()]
59
+ .sort(([a], [b]) => a.localeCompare(b))
60
+ .map(([subagent, count]) => `${subagent}=${count}`)
61
+ .join(" ")
62
+ entries.push(
63
+ subagentSummary.length > 0
64
+ ? `task dispatches=${taskDispatches} (${subagentSummary})`
65
+ : `task dispatches=${taskDispatches}`,
66
+ )
67
+ }
68
+ return entries.length > 0 ? entries.join("; ") : "none"
69
+ }
70
+
71
+ function buildToolsLine(auditState: AuditState): string {
72
+ const tools = auditState.toolsExecuted
73
+ .filter((tool) => tool.tool !== "task")
74
+ .map((tool) => tool.tool)
75
+ return tools.length > 0 ? tools.join(", ") : "none"
76
+ }
77
+
78
+ function buildFindingCountsLine(auditState: AuditState): string | null {
79
+ const counts = auditState.findingCounts
80
+ if (!counts) return null
81
+
82
+ return [
83
+ "Finding Counts:",
84
+ `raw_observations=${counts.rawObservations ?? 0}`,
85
+ `recorded=${counts.recordedFindings ?? 0}`,
86
+ `deduped=${counts.dedupedFindings ?? 0}`,
87
+ `actionable=${counts.actionableFindings ?? 0}`,
88
+ `non_actionable=${counts.nonActionableFindings ?? 0}`,
89
+ ].join(" ")
90
+ }
91
+
92
+ function buildCoverageLine(auditState: AuditState): string {
93
+ const attempt = auditState.coverageAttempt
94
+ if (attempt) {
95
+ return attempt.reason
96
+ ? `Coverage: ${attempt.status} — ${attempt.reason}`
97
+ : `Coverage: ${attempt.status}`
98
+ }
99
+ const unavailable = auditState.unavailableTools ?? []
100
+ return unavailable.includes("forge")
101
+ ? "Coverage: skipped — forge unavailable"
102
+ : "Coverage: pending"
103
+ }
104
+
38
105
  export function buildDynamicContext(
39
106
  auditState: AuditState,
40
107
  agent: string,
@@ -45,7 +112,7 @@ export function buildDynamicContext(
45
112
  const executedToolNames = new Set(
46
113
  auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
47
114
  )
48
- const tools = auditState.toolsExecuted.map((tool) => tool.tool).join(", ") || "none"
115
+ const findingCountsLine = buildFindingCountsLine(auditState)
49
116
  const taskStatus = KEY_TOOLS.map(
50
117
  (t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
51
118
  ).join(" ")
@@ -62,7 +129,10 @@ export function buildDynamicContext(
62
129
  `Phase: ${auditState.currentPhase}`,
63
130
  `Contracts: ${auditState.contractsReviewed.length} reviewed`,
64
131
  `Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
65
- `Tools: ${tools}`,
132
+ ...(findingCountsLine ? [findingCountsLine] : []),
133
+ `Tools: ${buildToolsLine(auditState)}`,
134
+ `Tool Ledger: ${buildToolLedgerLine(auditState)}`,
135
+ buildCoverageLine(auditState),
66
136
  `Tasks: ${taskStatus}`,
67
137
  ]
68
138
 
@@ -20,6 +20,7 @@ import type {
20
20
  ArgusAgentName,
21
21
  AuditState,
22
22
  Finding,
23
+ FindingCounts,
23
24
  FindingSeverity,
24
25
  FuzzCounterexample,
25
26
  SoloditResult,
@@ -465,14 +466,38 @@ function processSoloditResult(parsed: Record<string, unknown>, state: AuditState
465
466
  })
466
467
  }
467
468
 
468
- function recordToolExecution(state: AuditState, toolName: string, findingsCount: number): void {
469
+ function buildFindingCounts(state: AuditState, findingsCount: number): FindingCounts {
470
+ return {
471
+ rawObservations: Math.max(0, findingsCount),
472
+ recordedFindings: state.findings.length,
473
+ }
474
+ }
475
+
476
+ function readErrorReason(record: Record<string, unknown>): string | undefined {
477
+ if (typeof record.error === "string" && record.error.trim().length > 0) return record.error
478
+ const errorRecord = toRecord(record.error)
479
+ if (typeof errorRecord?.message === "string" && errorRecord.message.trim().length > 0) {
480
+ return errorRecord.message
481
+ }
482
+ if (typeof record.stderr === "string" && record.stderr.trim().length > 0) return record.stderr
483
+ return undefined
484
+ }
485
+
486
+ function recordToolExecution(
487
+ state: AuditState,
488
+ toolName: string,
489
+ findingsCount: number,
490
+ success: boolean,
491
+ findingCounts?: FindingCounts,
492
+ ): void {
469
493
  const now = Date.now()
470
494
  state.toolsExecuted.push({
471
495
  tool: toolName,
472
496
  startTime: now,
473
497
  endTime: now,
474
- success: true,
498
+ success,
475
499
  findingsCount,
500
+ findingCounts,
476
501
  })
477
502
  }
478
503
 
@@ -616,7 +641,7 @@ export function createToolTrackingHook(
616
641
  }
617
642
 
618
643
  if (resolved) {
619
- recordToolExecution(resolved.state, "task", 0)
644
+ recordToolExecution(resolved.state, "task", 0, true, buildFindingCounts(resolved.state, 0))
620
645
  onStateChanged?.({ tool: "task", findingsCount: 0, sessionId: input.sessionID })
621
646
  }
622
647
 
@@ -875,9 +900,16 @@ export function createToolTrackingHook(
875
900
  break
876
901
  }
877
902
  case "argus_forge_coverage": {
903
+ const now = Date.now()
878
904
  const reportObj = toRecord(record.report)
879
905
  const files = reportObj?.files
880
- if (Array.isArray(files)) {
906
+ if (record.success === false) {
907
+ auditState.coverageAttempt = {
908
+ status: "failed",
909
+ attemptedAt: now,
910
+ reason: readErrorReason(record),
911
+ }
912
+ } else if (Array.isArray(files)) {
881
913
  auditState.coverageReport = {
882
914
  files: files
883
915
  .filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
@@ -889,6 +921,13 @@ export function createToolTrackingHook(
889
921
  functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
890
922
  })),
891
923
  }
924
+ auditState.coverageAttempt = { status: "run", attemptedAt: now }
925
+ } else {
926
+ auditState.coverageAttempt = {
927
+ status: "failed",
928
+ attemptedAt: now,
929
+ reason: "coverage report was missing or invalid",
930
+ }
892
931
  }
893
932
  break
894
933
  }
@@ -963,10 +1002,12 @@ export function createToolTrackingHook(
963
1002
  }
964
1003
  }
965
1004
 
966
- completedSuccess = true
1005
+ completedSuccess = record.success !== false
967
1006
  }
968
1007
 
969
- recordToolExecution(auditState, input.tool, findingsCount)
1008
+ const findingCounts = buildFindingCounts(auditState, findingsCount)
1009
+ auditState.findingCounts = findingCounts
1010
+ recordToolExecution(auditState, input.tool, findingsCount, completedSuccess, findingCounts)
970
1011
 
971
1012
  const nextPhase = inferPhaseAdvancement(auditState, input.tool)
972
1013
  if (nextPhase) {
@@ -1003,6 +1044,8 @@ export function createToolTrackingHook(
1003
1044
  break
1004
1045
  case "argus_forge_coverage":
1005
1046
  if (auditState.coverageReport) enrichment.coverageReport = auditState.coverageReport
1047
+ if (auditState.coverageAttempt)
1048
+ enrichment.coverageAttempt = auditState.coverageAttempt
1006
1049
  break
1007
1050
  case "argus_gas_analysis":
1008
1051
  if (auditState.gasHotspots) enrichment.gasHotspots = auditState.gasHotspots
@@ -1028,6 +1071,7 @@ export function createToolTrackingHook(
1028
1071
  buildEvent("tool.completed", runId, sessionId, toolCallId, {
1029
1072
  tool: input.tool,
1030
1073
  findingsCount,
1074
+ findingCounts: completedSuccess ? auditState.findingCounts : undefined,
1031
1075
  success: completedSuccess,
1032
1076
  ...(completionError ? { error: completionError } : {}),
1033
1077
  ...enrichment,
@@ -1,5 +1,20 @@
1
1
  import type { AuditState } from "../state/types"
2
2
 
3
+ export type BackgroundTaskStatus = "queued" | "running" | "completed" | "failed" | "cancelled"
4
+
5
+ export type BackgroundFailureDiagnostic = {
6
+ category: "model_error" | "tool_error" | "timeout" | "cancelled" | "unknown"
7
+ retry_recommendation: "safe_to_retry" | "retry_with_changes" | "do_not_retry"
8
+ summary: string
9
+ }
10
+
11
+ export type BackgroundTaskDiagnostic = {
12
+ status: BackgroundTaskStatus
13
+ result?: unknown
14
+ error?: unknown
15
+ diagnostic?: BackgroundFailureDiagnostic
16
+ }
17
+
3
18
  /**
4
19
  * BackgroundManager interface
5
20
  * Handles dispatching and managing background agent tasks
@@ -27,6 +42,12 @@ export interface BackgroundManager {
27
42
  */
28
43
  getResult(taskId: string): Promise<unknown>
29
44
 
45
+ /**
46
+ * Get task status and structured diagnostics for completed, failed, queued, and cancelled tasks.
47
+ * Unknown task IDs resolve to undefined.
48
+ */
49
+ getTaskStatus(taskId: string): Promise<BackgroundTaskDiagnostic | undefined>
50
+
30
51
  /**
31
52
  * Register a callback to be invoked when a task completes
32
53
  * @param callback - Function called with (taskId, result) when task finishes
@@ -2,6 +2,7 @@ export const ARGUS_ORCHESTRATOR: ReadonlySet<string> = new Set(["argus"])
2
2
  export const ARGUS_SUBAGENTS: ReadonlySet<string> = new Set([
3
3
  "sentinel",
4
4
  "pythia",
5
+ "audit-specialist",
5
6
  "scribe",
6
7
  "themis",
7
8
  ])
@@ -0,0 +1,96 @@
1
+ import type { CanonicalFinding } from "../state/schemas"
2
+ import type { Finding } from "../state/types"
3
+
4
+ export type LineageCountMismatch = {
5
+ check: string
6
+ observation_count?: number
7
+ observation_ids_length: number
8
+ }
9
+
10
+ export type FindingLineageResult = {
11
+ valid: boolean
12
+ raw_count: number
13
+ mapped_count: number
14
+ duplicate_observation_ids: string[]
15
+ phantom_observation_ids: string[]
16
+ missing_observation_ids: string[]
17
+ count_mismatches: LineageCountMismatch[]
18
+ }
19
+
20
+ type FindingLike = Pick<Finding, "check"> & {
21
+ id?: string
22
+ observation_id?: string
23
+ observation_ids?: unknown
24
+ observation_count?: unknown
25
+ }
26
+
27
+ function sorted(values: Iterable<string>): string[] {
28
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b))
29
+ }
30
+
31
+ function observationIds(value: FindingLike): string[] {
32
+ if (!Array.isArray(value.observation_ids)) return []
33
+ return value.observation_ids.filter((id): id is string => typeof id === "string" && id.length > 0)
34
+ }
35
+
36
+ function rawObservationIds(rawFindings: CanonicalFinding[]): string[] {
37
+ return rawFindings
38
+ .map((finding) => finding.observation_id)
39
+ .filter((id): id is string => typeof id === "string" && id.length > 0)
40
+ }
41
+
42
+ export function validateFindingLineage(
43
+ rawFindings: CanonicalFinding[],
44
+ dedupedFindings: FindingLike[],
45
+ ): FindingLineageResult {
46
+ const rawIds = new Set(rawObservationIds(rawFindings))
47
+ const mappedIds: string[] = []
48
+ const seen = new Set<string>()
49
+ const duplicates = new Set<string>()
50
+ const countMismatches: LineageCountMismatch[] = []
51
+
52
+ for (const finding of dedupedFindings) {
53
+ const ids = observationIds(finding)
54
+ const suppliedCount = finding.observation_count
55
+
56
+ if (ids.length === 0 || (suppliedCount != null && suppliedCount !== ids.length)) {
57
+ countMismatches.push({
58
+ check: finding.check || finding.id || "(unknown finding)",
59
+ observation_count: typeof suppliedCount === "number" ? suppliedCount : undefined,
60
+ observation_ids_length: ids.length,
61
+ })
62
+ }
63
+
64
+ for (const id of ids) {
65
+ mappedIds.push(id)
66
+ if (seen.has(id)) {
67
+ duplicates.add(id)
68
+ }
69
+ seen.add(id)
70
+ }
71
+ }
72
+
73
+ const mappedSet = new Set(mappedIds)
74
+ const phantom = mappedIds.filter((id) => !rawIds.has(id))
75
+ const missing = Array.from(rawIds).filter((id) => !mappedSet.has(id))
76
+ const duplicateIds = sorted(duplicates)
77
+ const phantomIds = sorted(phantom)
78
+ const missingIds = sorted(missing)
79
+
80
+ countMismatches.sort((a, b) => a.check.localeCompare(b.check))
81
+
82
+ return {
83
+ valid:
84
+ duplicateIds.length === 0 &&
85
+ phantomIds.length === 0 &&
86
+ missingIds.length === 0 &&
87
+ countMismatches.length === 0 &&
88
+ mappedIds.length === rawIds.size,
89
+ raw_count: rawIds.size,
90
+ mapped_count: mappedIds.length,
91
+ duplicate_observation_ids: duplicateIds,
92
+ phantom_observation_ids: phantomIds,
93
+ missing_observation_ids: missingIds,
94
+ count_mismatches: countMismatches,
95
+ }
96
+ }
@@ -16,6 +16,8 @@ export interface ReportPathOptions {
16
16
  outputDir: string
17
17
  /** Optional run_id for run-scoped naming */
18
18
  runId?: string
19
+ /** Optional caller-supplied report revision. Base report is revision 1. */
20
+ revision?: number
19
21
  }
20
22
 
21
23
  export interface ResolvedReportPath {
@@ -46,7 +48,7 @@ export function sanitizeContractName(name: string): string {
46
48
  }
47
49
 
48
50
  export function resolveReportPath(options: ReportPathOptions): ResolvedReportPath {
49
- const { contractName, date, outputDir, runId } = options
51
+ const { contractName, date, outputDir, runId, revision } = options
50
52
 
51
53
  if (!contractName || contractName.trim() === "") {
52
54
  throw new ReportPathError("contractName must not be empty")
@@ -54,12 +56,16 @@ export function resolveReportPath(options: ReportPathOptions): ResolvedReportPat
54
56
  if (!outputDir || outputDir.trim() === "") {
55
57
  throw new ReportPathError("outputDir must not be empty")
56
58
  }
59
+ if (revision != null && (!Number.isInteger(revision) || revision < 2)) {
60
+ throw new ReportPathError("revision must be an integer greater than or equal to 2")
61
+ }
57
62
 
58
63
  const resolvedDate = date ?? new Date()
59
64
  const dateStr = formatReportDate(resolvedDate)
60
65
  const sanitizedName = sanitizeContractName(contractName)
61
66
  const runIdSuffix = runId ? `-${runId.substring(0, 8)}` : ""
62
- const filename = `${sanitizedName}-security-audit-${dateStr}${runIdSuffix}.md`
67
+ const revisionSuffix = revision == null ? "" : `-r${revision}`
68
+ const filename = `${sanitizedName}-security-audit-${dateStr}${runIdSuffix}${revisionSuffix}.md`
63
69
  const filePath = join(outputDir, filename)
64
70
  const canonicalId = runId ?? filename
65
71
 
@@ -246,9 +246,9 @@ export function normalizeToCanonicalFinding(
246
246
  : "manual"
247
247
 
248
248
  const reportedByAgentRaw =
249
+ options.reportedByAgent ??
249
250
  (typeof input.reported_by_agent === "string" ? input.reported_by_agent : undefined) ??
250
251
  (typeof input.reportedByAgent === "string" ? input.reportedByAgent : undefined) ??
251
- options.reportedByAgent ??
252
252
  "unknown"
253
253
  const reportedByAgent: ArgusAgentName = VALID_AGENTS.has(reportedByAgentRaw as ArgusAgentName)
254
254
  ? (reportedByAgentRaw as ArgusAgentName)
@@ -12,7 +12,9 @@ import {
12
12
  import type {
13
13
  AuditPhase,
14
14
  AuditState,
15
+ CoverageAttemptState,
15
16
  Finding,
17
+ FindingCounts,
16
18
  FuzzCounterexample,
17
19
  SoloditResult,
18
20
  ToolExecution,
@@ -99,6 +101,48 @@ function resolveToolSuccess(payload: Record<string, unknown>): boolean {
99
101
  return payload.success !== false
100
102
  }
101
103
 
104
+ const FINDING_COUNT_FIELDS = [
105
+ "rawObservations",
106
+ "recordedFindings",
107
+ "dedupedFindings",
108
+ "actionableFindings",
109
+ "nonActionableFindings",
110
+ ] as const
111
+
112
+ function asFindingCounts(value: unknown): FindingCounts | undefined {
113
+ if (!isRecord(value)) return undefined
114
+ const counts: FindingCounts = {}
115
+ for (const field of FINDING_COUNT_FIELDS) {
116
+ const count = value[field]
117
+ if (
118
+ typeof count === "number" &&
119
+ Number.isFinite(count) &&
120
+ Number.isInteger(count) &&
121
+ count >= 0
122
+ ) {
123
+ counts[field] = count
124
+ }
125
+ }
126
+ return Object.keys(counts).length > 0 ? counts : undefined
127
+ }
128
+
129
+ function asCoverageAttempt(value: unknown): CoverageAttemptState | undefined {
130
+ if (!isRecord(value)) return undefined
131
+ if (
132
+ value.status !== "pending" &&
133
+ value.status !== "run" &&
134
+ value.status !== "skipped" &&
135
+ value.status !== "failed"
136
+ ) {
137
+ return undefined
138
+ }
139
+ return {
140
+ status: value.status,
141
+ attemptedAt: typeof value.attemptedAt === "number" ? value.attemptedAt : undefined,
142
+ reason: typeof value.reason === "string" ? value.reason : undefined,
143
+ }
144
+ }
145
+
102
146
  function asStringArray(value: unknown): string[] | undefined {
103
147
  if (!Array.isArray(value)) return undefined
104
148
  return value.filter((item): item is string => typeof item === "string")
@@ -321,6 +365,7 @@ export function projectToolExecutions(events: AuditEvent[]): CanonicalToolExecut
321
365
  endTime: existing?.endTime,
322
366
  success: existing?.success ?? false,
323
367
  findingsCount: existing?.findingsCount ?? 0,
368
+ findingCounts: existing?.findingCounts,
324
369
  })
325
370
  continue
326
371
  }
@@ -340,6 +385,7 @@ export function projectToolExecutions(events: AuditEvent[]): CanonicalToolExecut
340
385
  endTime: event.timestamp,
341
386
  success: resolveToolSuccess(payload),
342
387
  findingsCount: resolveFindingsCount(payload),
388
+ findingCounts: asFindingCounts(payload.findingCounts),
343
389
  run_id: event.run_id,
344
390
  schema_version: event.schema_version,
345
391
  })
@@ -408,6 +454,8 @@ export function projectReportInput(
408
454
  asFuzzCounterexamples,
409
455
  )
410
456
  const coverageReport = extractLatestFromPayload(events, "coverageReport", asCoverageReport)
457
+ const coverageAttempt = extractLatestFromPayload(events, "coverageAttempt", asCoverageAttempt)
458
+ const findingCounts = extractLatestFromPayload(events, "findingCounts", asFindingCounts)
411
459
  const gasHotspots = extractLatestFromPayload(events, "gasHotspots", asGasHotspots)
412
460
  const proxyContracts = extractLatestFromPayload(events, "proxyContracts", asProxyContracts)
413
461
  const patternVersion = extractLatestFromPayload(events, "patternVersion", asString)
@@ -424,10 +472,12 @@ export function projectReportInput(
424
472
  projectDir,
425
473
  findings,
426
474
  toolsExecuted,
475
+ findingCounts,
427
476
  scope,
428
477
  soloditResults,
429
478
  fuzzCounterexamples,
430
479
  coverageReport,
480
+ coverageAttempt,
431
481
  gasHotspots,
432
482
  proxyContracts,
433
483
  patternVersion,
@@ -8,7 +8,9 @@ import {
8
8
  import type {
9
9
  ArgusAgentName,
10
10
  AuditPhase,
11
+ CoverageAttemptState,
11
12
  Finding,
13
+ FindingCounts,
12
14
  FindingSeverity,
13
15
  FuzzCounterexample,
14
16
  SoloditResult,
@@ -111,6 +113,7 @@ export interface ReportInput {
111
113
  projectDir: string
112
114
  findings: CanonicalFinding[]
113
115
  toolsExecuted: CanonicalToolExecution[]
116
+ findingCounts?: FindingCounts
114
117
  scope: string[]
115
118
  soloditResults?: SoloditResult[]
116
119
  fuzzCounterexamples?: FuzzCounterexample[]
@@ -120,6 +123,82 @@ export interface ReportInput {
120
123
  patternVersion?: string
121
124
  skillsLoaded?: string[]
122
125
  unavailableTools?: string[]
126
+ coverageAttempt?: CoverageAttemptState
127
+ }
128
+
129
+ const FINDING_COUNT_FIELDS = [
130
+ "rawObservations",
131
+ "recordedFindings",
132
+ "dedupedFindings",
133
+ "actionableFindings",
134
+ "nonActionableFindings",
135
+ ] as const
136
+
137
+ const COVERAGE_ATTEMPT_STATUSES = new Set(["pending", "run", "skipped", "failed"])
138
+
139
+ function pushFindingCountsErrors(errors: ValidationError[], raw: unknown, prefix: string): void {
140
+ if (raw == null) return
141
+ if (!isRecord(raw)) {
142
+ errors.push({
143
+ field: prefix,
144
+ code: "invalid",
145
+ message: `${prefix} must be an object when provided`,
146
+ })
147
+ return
148
+ }
149
+
150
+ for (const field of FINDING_COUNT_FIELDS) {
151
+ const value = raw[field]
152
+ if (value == null) continue
153
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
154
+ errors.push({
155
+ field: `${prefix}.${field}`,
156
+ code: "invalid",
157
+ message: `${prefix}.${field} must be a non-negative integer when provided`,
158
+ })
159
+ }
160
+ }
161
+ }
162
+
163
+ function pushCoverageAttemptErrors(errors: ValidationError[], raw: unknown): void {
164
+ if (raw == null) return
165
+ if (!isRecord(raw)) {
166
+ errors.push({
167
+ field: "coverageAttempt",
168
+ code: "invalid",
169
+ message: "coverageAttempt must be an object when provided",
170
+ })
171
+ return
172
+ }
173
+
174
+ if (typeof raw.status !== "string" || !COVERAGE_ATTEMPT_STATUSES.has(raw.status)) {
175
+ errors.push({
176
+ field: "coverageAttempt.status",
177
+ code: "enum",
178
+ message: "coverageAttempt.status must be one of: pending, run, skipped, failed",
179
+ })
180
+ }
181
+
182
+ if (
183
+ raw.attemptedAt != null &&
184
+ (typeof raw.attemptedAt !== "number" ||
185
+ !Number.isInteger(raw.attemptedAt) ||
186
+ raw.attemptedAt <= 0)
187
+ ) {
188
+ errors.push({
189
+ field: "coverageAttempt.attemptedAt",
190
+ code: "invalid",
191
+ message: "coverageAttempt.attemptedAt must be a positive integer when provided",
192
+ })
193
+ }
194
+
195
+ if (raw.reason != null && (typeof raw.reason !== "string" || raw.reason.trim().length === 0)) {
196
+ errors.push({
197
+ field: "coverageAttempt.reason",
198
+ code: "invalid",
199
+ message: "coverageAttempt.reason must be a non-empty string when provided",
200
+ })
201
+ }
123
202
  }
124
203
 
125
204
  function pushRequiredRootStringError(
@@ -253,7 +332,8 @@ export function validateCanonicalFinding(raw: unknown): ValidationResult<Canonic
253
332
  errors.push({
254
333
  field: "reported_by_agent",
255
334
  code: "enum",
256
- message: "reported_by_agent must be one of: argus, sentinel, pythia, scribe, unknown",
335
+ message:
336
+ "reported_by_agent must be one of: argus, sentinel, pythia, audit-specialist, scribe, unknown",
257
337
  })
258
338
  }
259
339
 
@@ -346,6 +426,8 @@ export function validateCanonicalToolExecution(
346
426
  })
347
427
  }
348
428
 
429
+ pushFindingCountsErrors(errors, raw.findingCounts, "findingCounts")
430
+
349
431
  if (typeof raw.run_id !== "string" || raw.run_id.trim().length === 0) {
350
432
  errors.push({
351
433
  field: "run_id",
@@ -400,6 +482,9 @@ export function validateReportInput(raw: unknown): ValidationResult<ReportInput>
400
482
  })
401
483
  }
402
484
 
485
+ pushFindingCountsErrors(errors, raw.findingCounts, "findingCounts")
486
+ pushCoverageAttemptErrors(errors, raw.coverageAttempt)
487
+
403
488
  if (!Array.isArray(raw.scope) || !raw.scope.every((item) => typeof item === "string")) {
404
489
  errors.push({
405
490
  field: "scope",
@@ -1,5 +1,11 @@
1
1
  export type FindingSeverity = "Critical" | "High" | "Medium" | "Low" | "Informational"
2
- export type ArgusAgentName = "argus" | "sentinel" | "pythia" | "scribe" | "unknown"
2
+ export type ArgusAgentName =
3
+ | "argus"
4
+ | "sentinel"
5
+ | "pythia"
6
+ | "audit-specialist"
7
+ | "scribe"
8
+ | "unknown"
3
9
  export type AuditPhase =
4
10
  | "reconnaissance"
5
11
  | "scanning"
@@ -88,6 +94,22 @@ export interface ToolExecution {
88
94
  endTime?: number
89
95
  success: boolean
90
96
  findingsCount: number
97
+ findingCounts?: FindingCounts
98
+ subagent_type?: string
99
+ }
100
+
101
+ export interface FindingCounts {
102
+ rawObservations?: number
103
+ recordedFindings?: number
104
+ dedupedFindings?: number
105
+ actionableFindings?: number
106
+ nonActionableFindings?: number
107
+ }
108
+
109
+ export interface CoverageAttemptState {
110
+ status: "pending" | "run" | "skipped" | "failed"
111
+ attemptedAt?: number
112
+ reason?: string
91
113
  }
92
114
 
93
115
  export interface AuditState {
@@ -105,7 +127,9 @@ export interface AuditState {
105
127
  skillsLoaded?: string[]
106
128
  unavailableTools?: string[]
107
129
  reportGenerated?: boolean
130
+ findingCounts?: FindingCounts
108
131
  knowledgeSynced?: { success: boolean; timestamp: number }
132
+ coverageAttempt?: CoverageAttemptState
109
133
  coverageReport?: {
110
134
  files: Array<{
111
135
  path: string