solidity-argus 0.2.0 → 0.3.2

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 (169) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +34 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +34 -7
  75. package/src/agents/pythia-prompt.ts +13 -4
  76. package/src/agents/scribe-prompt.ts +20 -2
  77. package/src/agents/sentinel-prompt.ts +45 -5
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +6 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/system-prompt-hook.ts +18 -1
  114. package/src/hooks/tool-tracking-hook.ts +110 -51
  115. package/src/hooks/types.ts +2 -1
  116. package/src/index.ts +24 -37
  117. package/src/knowledge/retry.ts +22 -22
  118. package/src/knowledge/scvd-client.ts +88 -95
  119. package/src/knowledge/scvd-errors.ts +35 -35
  120. package/src/knowledge/scvd-index.ts +78 -80
  121. package/src/knowledge/scvd-sync.ts +106 -101
  122. package/src/managers/index.ts +1 -1
  123. package/src/managers/types.ts +19 -14
  124. package/src/plugin-interface.ts +7 -9
  125. package/src/shared/binary-utils.ts +44 -35
  126. package/src/shared/deep-merge.ts +55 -36
  127. package/src/shared/file-utils.ts +21 -19
  128. package/src/shared/index.ts +11 -5
  129. package/src/shared/jsonc-parser.ts +123 -28
  130. package/src/shared/logger.ts +16 -3
  131. package/src/shared/project-utils.ts +30 -0
  132. package/src/skills/analysis/cluster.ts +414 -0
  133. package/src/skills/analysis/gates.ts +227 -0
  134. package/src/skills/analysis/index.ts +33 -0
  135. package/src/skills/analysis/normalize.ts +217 -0
  136. package/src/skills/analysis/similarity.ts +224 -0
  137. package/src/skills/argus-skill-resolver.ts +17 -6
  138. package/src/skills/skill-schema.ts +11 -10
  139. package/src/solodit-lifecycle.ts +203 -0
  140. package/src/state/audit-state.ts +8 -8
  141. package/src/state/finding-store.ts +68 -55
  142. package/src/state/types.ts +88 -67
  143. package/src/tools/argus-skill-load-tool.ts +12 -7
  144. package/src/tools/contract-analyzer-tool.ts +142 -77
  145. package/src/tools/forge-coverage-tool.ts +226 -0
  146. package/src/tools/forge-fuzz-tool.ts +127 -127
  147. package/src/tools/forge-test-tool.ts +201 -158
  148. package/src/tools/gas-analysis-tool.ts +264 -0
  149. package/src/tools/pattern-checker-tool.ts +203 -191
  150. package/src/tools/pattern-loader.ts +5 -111
  151. package/src/tools/pattern-schema.ts +3 -0
  152. package/src/tools/proxy-detection-tool.ts +224 -0
  153. package/src/tools/report-generator-tool.ts +305 -206
  154. package/src/tools/slither-tool.ts +266 -218
  155. package/src/tools/solodit-search-tool.ts +235 -119
  156. package/src/tools/sync-knowledge-tool.ts +7 -11
  157. package/src/utils/audit-artifact-detector.ts +28 -29
  158. package/src/utils/dependency-scanner.ts +37 -37
  159. package/src/utils/project-detector.ts +111 -124
  160. package/src/utils/solidity-parser.ts +175 -75
  161. package/skills/patterns/access-control.yaml +0 -31
  162. package/skills/patterns/erc4626.yaml +0 -29
  163. package/skills/patterns/flash-loan.yaml +0 -20
  164. package/skills/patterns/oracle.yaml +0 -30
  165. package/skills/patterns/proxy.yaml +0 -30
  166. package/skills/patterns/reentrancy.yaml +0 -30
  167. package/skills/patterns/signature.yaml +0 -31
  168. package/src/hooks/event-hook-v2.ts +0 -99
  169. package/src/state/plugin-state.ts +0 -14
@@ -1,33 +1,45 @@
1
- import type { AuditState } from "../state/types"
2
- import { createAuditState } from "../state/audit-state"
3
1
  import { createLogger } from "../shared/logger"
2
+ import { createAuditState } from "../state/audit-state"
3
+ import type { AuditState } from "../state/types"
4
+
5
+ export type AuditEventType =
6
+ | "session.created"
7
+ | "session.idle"
8
+ | "session.error"
9
+ | "session.deleted"
10
+ | "audit.phase-changed"
11
+ | "audit.finding-added"
12
+ | "audit.complete"
4
13
 
5
14
  export type EventHookFn = (input: {
6
- event: { type: string; sessionId?: string }
15
+ event: { type: string; sessionId?: string; properties?: Record<string, unknown> }
7
16
  }) => Promise<void>
8
17
 
9
- /**
10
- * Creates a session lifecycle event hook that manages audit state.
11
- *
12
- * Returns the hook function plus accessors for reading/writing the
13
- * closure-held audit state. Other hooks (compaction, tool tracking,
14
- * system prompt) share the same state instance via these accessors.
15
- */
16
- export function createEventHook(projectDir?: string): {
18
+ export type EventSubHandler = (event: {
19
+ type: string
20
+ sessionId?: string
21
+ auditState: AuditState | null
22
+ setAuditState: (state: AuditState | null) => void
23
+ }) => Promise<void>
24
+
25
+ export function createEventHook(
26
+ projectDir?: string,
27
+ subHandlers: EventSubHandler[] = [],
28
+ ): {
17
29
  hook: EventHookFn
18
30
  getAuditState: () => AuditState | null
19
31
  setAuditState: (state: AuditState | null) => void
20
32
  } {
33
+ const logger = createLogger()
21
34
  let currentAuditState: AuditState | null = null
22
35
 
23
36
  const getAuditState = (): AuditState | null => currentAuditState
24
-
25
37
  const setAuditState = (state: AuditState | null): void => {
26
38
  currentAuditState = state
27
39
  }
28
40
 
29
41
  const hook: EventHookFn = async (input): Promise<void> => {
30
- const { type } = input.event
42
+ const { type, sessionId } = input.event
31
43
 
32
44
  switch (type) {
33
45
  case "session.created": {
@@ -38,23 +50,23 @@ export function createEventHook(projectDir?: string): {
38
50
  }
39
51
 
40
52
  case "session.idle": {
41
- if (currentAuditState) {
42
- createLogger().debug(
43
- `[state] Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}, contracts: ${currentAuditState.contractsReviewed.length}`
44
- )
45
- }
46
- break
47
- }
53
+ if (currentAuditState) {
54
+ logger.debug(
55
+ `Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}`,
56
+ )
57
+ }
58
+ break
59
+ }
48
60
 
49
61
  case "session.error": {
50
62
  if (currentAuditState) {
51
- createLogger().error(
63
+ logger.error(
52
64
  `Session error — state snapshot: ${JSON.stringify({
53
65
  sessionId: currentAuditState.sessionId,
54
66
  phase: currentAuditState.currentPhase,
55
67
  findingsCount: currentAuditState.findings.length,
56
68
  contractsReviewed: currentAuditState.contractsReviewed,
57
- })}`
69
+ })}`,
58
70
  )
59
71
  }
60
72
  break
@@ -65,10 +77,22 @@ export function createEventHook(projectDir?: string): {
65
77
  break
66
78
  }
67
79
 
68
- // Unknown events: no-op — never throw
69
80
  default:
70
81
  break
71
82
  }
83
+
84
+ for (const handler of subHandlers) {
85
+ try {
86
+ await handler({
87
+ type,
88
+ sessionId,
89
+ auditState: currentAuditState,
90
+ setAuditState,
91
+ })
92
+ } catch (error) {
93
+ logger.error(`Sub-handler failed for event ${type}:`, error)
94
+ }
95
+ }
72
96
  }
73
97
 
74
98
  return { hook, getAuditState, setAuditState }
@@ -1,9 +1,9 @@
1
- import type { HookName } from "./types";
1
+ import type { HookName } from "./types"
2
2
 
3
3
  export function createHookGuard(disabledHooks: string[]) {
4
- const disabledSet = new Set(disabledHooks);
4
+ const disabledSet = new Set(disabledHooks)
5
5
 
6
6
  return function isHookEnabled(name: HookName): boolean {
7
- return !disabledSet.has(name);
8
- };
7
+ return !disabledSet.has(name)
8
+ }
9
9
  }
@@ -1,5 +1,5 @@
1
- export { createHookGuard } from "./hook-system";
2
- export { safeCreateHook } from "./safe-create-hook";
3
- export { createEventHookV2 } from "./event-hook-v2";
4
- export type { HookName } from "./types";
5
- export type { EventHookV2Fn, AuditEventType, EventSubHandler } from "./event-hook-v2";
1
+ export type { AuditEventType, EventHookFn, EventSubHandler } from "./event-hook"
2
+ export { createEventHook } from "./event-hook"
3
+ export { createHookGuard } from "./hook-system"
4
+ export { safeCreateHook } from "./safe-create-hook"
5
+ export type { HookName } from "./types"
@@ -1,8 +1,8 @@
1
1
  import os from "node:os"
2
2
  import path from "node:path"
3
- import { ScvdClient } from "../knowledge/scvd-client"
4
- import { syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
5
3
  import type { ArgusConfig } from "../config/types"
4
+ import { ScvdClient } from "../knowledge/scvd-client"
5
+ import { type SyncResult, syncIncremental } from "../knowledge/scvd-sync"
6
6
  import { createLogger } from "../shared/logger"
7
7
 
8
8
  export type KnowledgeSyncDependencies = {
@@ -19,14 +19,14 @@ function defaultDependencies(): Required<KnowledgeSyncDependencies> {
19
19
  syncIncrementalFn: async (client: unknown, indexPath: string) =>
20
20
  syncIncremental(client as ScvdClient, indexPath),
21
21
  log: (message: string) => {
22
- createLogger().info(message)
23
- },
22
+ createLogger().info(message)
23
+ },
24
24
  }
25
25
  }
26
26
 
27
27
  export function createKnowledgeSyncHook(
28
28
  argusConfig: ArgusConfig,
29
- deps: KnowledgeSyncDependencies = {}
29
+ deps: KnowledgeSyncDependencies = {},
30
30
  ): () => void {
31
31
  const dependencies = { ...defaultDependencies(), ...deps }
32
32
 
@@ -36,23 +36,20 @@ export function createKnowledgeSyncHook(
36
36
  }
37
37
 
38
38
  const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
39
- const indexPath = path.join(
40
- os.homedir(),
41
- ".cache",
42
- "solidity-argus",
43
- "scvd-index.json"
44
- )
39
+ const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
45
40
 
46
41
  Promise.resolve().then(async () => {
47
- try {
48
- const client = dependencies.createClient(apiUrl)
49
- const result = await dependencies.syncIncrementalFn(client, indexPath)
50
- if (result.newFindings > 0) {
51
- dependencies.log(
52
- `[argus] SCVD index updated: ${result.newFindings} new findings (total: ${result.totalIndexed})`
53
- )
54
- }
55
- } catch (_e) { /* non-critical: sync errors are logged above */ }
56
- })
42
+ try {
43
+ const client = dependencies.createClient(apiUrl)
44
+ const result = await dependencies.syncIncrementalFn(client, indexPath)
45
+ if (result.newFindings > 0) {
46
+ dependencies.log(
47
+ `[argus] SCVD index updated: ${result.newFindings} new findings (total: ${result.totalIndexed})`,
48
+ )
49
+ }
50
+ } catch (_e) {
51
+ createLogger().debug("Knowledge sync failed during auto-sync")
52
+ }
53
+ })
57
54
  }
58
55
  }
@@ -1,6 +1,6 @@
1
- import type { ProjectConfig } from "../utils/project-detector"
2
- import type { DependencyRisk } from "../utils/dependency-scanner"
3
1
  import type { AuditArtifact } from "../utils/audit-artifact-detector"
2
+ import type { DependencyRisk } from "../utils/dependency-scanner"
3
+ import type { ProjectConfig } from "../utils/project-detector"
4
4
 
5
5
  export interface ReconContext {
6
6
  projectConfig: ProjectConfig | null
@@ -1,14 +1,13 @@
1
1
  import { createLogger } from "../shared/logger"
2
2
 
3
- export function safeCreateHook<T>(
4
- factory: () => T,
5
- hookName: string
6
- ): T | undefined {
3
+ export function safeCreateHook<T>(factory: () => T, hookName: string): T | undefined {
7
4
  try {
8
- return factory();
5
+ return factory()
9
6
  } catch (error) {
10
7
  const logger = createLogger()
11
- logger.error(`Failed to create hook "${hookName}": ${error instanceof Error ? error.message : String(error)}`)
12
- return undefined;
8
+ logger.error(
9
+ `Failed to create hook "${hookName}": ${error instanceof Error ? error.message : String(error)}`,
10
+ )
11
+ return undefined
13
12
  }
14
13
  }
@@ -3,6 +3,15 @@ import type { AuditState, FindingSeverity } from "../state/types"
3
3
  const DEFAULT_TOKEN_BUDGET = 2000
4
4
  const TOKENS_PER_CHAR = 4
5
5
 
6
+ const TOOL_SHORT_NAMES: Record<string, string> = {
7
+ argus_slither_analyze: "slither",
8
+ argus_forge_test: "forge-test",
9
+ argus_check_patterns: "patterns",
10
+ argus_solodit_search: "solodit",
11
+ argus_analyze_contract: "analyzer",
12
+ }
13
+ const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
14
+
6
15
  export interface SystemPromptHookDeps {
7
16
  getAuditState: () => AuditState | null
8
17
  getAgentForSession: (sessionID: string) => string | undefined
@@ -52,7 +61,13 @@ export function buildDynamicContext(
52
61
  severityCounts[finding.severity]++
53
62
  }
54
63
 
64
+ const executedToolNames = new Set(
65
+ auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
66
+ )
55
67
  const tools = auditState.toolsExecuted.map((tool) => tool.tool).join(", ") || "none"
68
+ const taskStatus = KEY_TOOLS.map(
69
+ (t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
70
+ ).join(" ")
56
71
  const unavailable = auditState.unavailableTools ?? []
57
72
  const lines: string[] = [
58
73
  `<argus-context agent="${agent}">`,
@@ -60,6 +75,7 @@ export function buildDynamicContext(
60
75
  `Contracts: ${auditState.contractsReviewed.length} reviewed`,
61
76
  `Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
62
77
  `Tools: ${tools}`,
78
+ `Tasks: ${taskStatus}`,
63
79
  ]
64
80
 
65
81
  if (unavailable.length > 0) {
@@ -72,9 +88,10 @@ export function buildDynamicContext(
72
88
  let summary = lines.join("\n")
73
89
 
74
90
  if (estimateTokens(summary) > tokenBudget) {
91
+ const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
75
92
  summary = [
76
93
  `<argus-context agent="${agent}">`,
77
- `Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length}`,
94
+ `Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
78
95
  "</argus-context>",
79
96
  ].join("\n")
80
97
  }
@@ -1,6 +1,6 @@
1
- import type { AuditState, FindingSeverity, FuzzCounterexample, SoloditResult } from "../state/types"
2
1
  import type { FindingStore } from "../state/finding-store"
3
2
  import { createFindingStore } from "../state/finding-store"
3
+ import type { AuditState, FindingSeverity, FuzzCounterexample, SoloditResult } from "../state/types"
4
4
 
5
5
  type ToolHookInput = {
6
6
  tool: string
@@ -8,6 +8,11 @@ type ToolHookInput = {
8
8
  result: string
9
9
  }
10
10
 
11
+ type ToolExecutionMetadata = {
12
+ tool: string
13
+ findingsCount: number
14
+ }
15
+
11
16
  const VALID_SEVERITIES: ReadonlySet<string> = new Set([
12
17
  "Critical",
13
18
  "High",
@@ -16,11 +21,7 @@ const VALID_SEVERITIES: ReadonlySet<string> = new Set([
16
21
  "Informational",
17
22
  ])
18
23
 
19
- const VALID_CONFIDENCES: ReadonlySet<string> = new Set([
20
- "High",
21
- "Medium",
22
- "Low",
23
- ])
24
+ const VALID_CONFIDENCES: ReadonlySet<string> = new Set(["High", "Medium", "Low"])
24
25
 
25
26
  function toSeverity(value: unknown): FindingSeverity {
26
27
  if (typeof value === "string" && VALID_SEVERITIES.has(value)) {
@@ -55,10 +56,7 @@ function toRecord(value: unknown): Record<string, unknown> | undefined {
55
56
  return undefined
56
57
  }
57
58
 
58
- function processSlitherResult(
59
- parsed: Record<string, unknown>,
60
- store: FindingStore
61
- ): number {
59
+ function processSlitherResult(parsed: Record<string, unknown>, store: FindingStore): number {
62
60
  const findings = parsed.findings
63
61
  if (!Array.isArray(findings)) return 0
64
62
 
@@ -96,10 +94,7 @@ function processSlitherResult(
96
94
  return count
97
95
  }
98
96
 
99
- function processPatternResult(
100
- parsed: Record<string, unknown>,
101
- store: FindingStore
102
- ): number {
97
+ function processPatternResult(parsed: Record<string, unknown>, store: FindingStore): number {
103
98
  const sources = parsed.sources
104
99
  if (!Array.isArray(sources)) return 0
105
100
 
@@ -145,10 +140,7 @@ function processPatternResult(
145
140
  return count
146
141
  }
147
142
 
148
- function processContractAnalyzerResult(
149
- parsed: Record<string, unknown>,
150
- state: AuditState
151
- ): void {
143
+ function processContractAnalyzerResult(parsed: Record<string, unknown>, state: AuditState): void {
152
144
  // Handle direct ContractProfile format (actual tool output)
153
145
  if (typeof parsed.filePath === "string") {
154
146
  if (!state.contractsReviewed.includes(parsed.filePath)) {
@@ -166,15 +158,11 @@ function processContractAnalyzerResult(
166
158
  }
167
159
  }
168
160
 
169
- function processFuzzResult(
170
- parsed: Record<string, unknown>,
171
- state: AuditState
172
- ): void {
161
+ function processFuzzResult(parsed: Record<string, unknown>, state: AuditState): void {
173
162
  const counterexamples = parsed.counterexamples
174
163
  if (!Array.isArray(counterexamples) || counterexamples.length === 0) return
175
164
 
176
- const totalRuns =
177
- typeof parsed.totalRuns === "number" ? parsed.totalRuns : 0
165
+ const totalRuns = typeof parsed.totalRuns === "number" ? parsed.totalRuns : 0
178
166
 
179
167
  state.fuzzCounterexamples ??= []
180
168
 
@@ -185,8 +173,13 @@ function processFuzzResult(
185
173
  const testName = ce.testName
186
174
  if (typeof testName !== "string") continue
187
175
 
188
- const rawInputs = toRecord(ce.inputs)
189
- const inputs = rawInputs ? Object.values(rawInputs).map(String) : []
176
+ const rawInputs = ce.inputs
177
+ const inputs = Array.isArray(rawInputs)
178
+ ? rawInputs.map(String)
179
+ : (() => {
180
+ const rec = toRecord(rawInputs)
181
+ return rec ? Object.values(rec).map(String) : []
182
+ })()
190
183
 
191
184
  const entry: FuzzCounterexample = {
192
185
  testName,
@@ -203,26 +196,20 @@ function processFuzzResult(
203
196
  }
204
197
  }
205
198
 
206
- function processSoloditResult(
207
- parsed: Record<string, unknown>,
208
- state: AuditState
209
- ): void {
199
+ function processSoloditResult(parsed: Record<string, unknown>, state: AuditState): void {
210
200
  const query = typeof parsed.query === "string" ? parsed.query : ""
211
201
  const results = Array.isArray(parsed.results) ? parsed.results : []
212
- const totalFound =
213
- typeof parsed.totalFound === "number" ? parsed.totalFound : results.length
214
-
215
- const topResults: SoloditResult["topResults"] = results
216
- .slice(0, 5)
217
- .map((raw) => {
218
- const r = toRecord(raw)
219
- return {
220
- title: typeof r?.title === "string" ? r.title : "",
221
- severity: typeof r?.severity === "string" ? r.severity : "",
222
- url: typeof r?.url === "string" ? r.url : "",
223
- protocol: typeof r?.protocol === "string" ? r.protocol : "",
224
- }
225
- })
202
+ const totalFound = typeof parsed.totalFound === "number" ? parsed.totalFound : results.length
203
+
204
+ const topResults: SoloditResult["topResults"] = results.slice(0, 5).map((raw) => {
205
+ const r = toRecord(raw)
206
+ return {
207
+ title: typeof r?.title === "string" ? r.title : "",
208
+ severity: typeof r?.severity === "string" ? r.severity : "",
209
+ url: typeof r?.url === "string" ? r.url : "",
210
+ protocol: typeof r?.protocol === "string" ? r.protocol : "",
211
+ }
212
+ })
226
213
 
227
214
  state.soloditResults ??= []
228
215
  state.soloditResults.push({
@@ -246,11 +233,7 @@ function processSoloditResult(
246
233
  * architecture. For accurate timing, the hook would need to fire in tool.execute.before
247
234
  * and tool.execute.after phases separately.
248
235
  */
249
- function recordToolExecution(
250
- state: AuditState,
251
- toolName: string,
252
- findingsCount: number
253
- ): void {
236
+ function recordToolExecution(state: AuditState, toolName: string, findingsCount: number): void {
254
237
  const now = Date.now()
255
238
  state.toolsExecuted.push({
256
239
  tool: toolName,
@@ -269,7 +252,8 @@ function recordToolExecution(
269
252
  * Findings are deduplicated via the FindingStore (by check+file+lines).
270
253
  */
271
254
  export function createToolTrackingHook(
272
- getAuditState: () => AuditState | null
255
+ getAuditState: () => AuditState | null,
256
+ onStateChanged?: (metadata: ToolExecutionMetadata) => void,
273
257
  ): (input: ToolHookInput) => Promise<void> {
274
258
  const storesByState = new WeakMap<AuditState, FindingStore>()
275
259
 
@@ -296,6 +280,22 @@ export function createToolTrackingHook(
296
280
 
297
281
  const { state: auditState, store } = resolved
298
282
 
283
+ // Handle argus_skill_load first — it returns markdown, not JSON
284
+ if (input.tool === "argus_skill_load") {
285
+ // Extract skill name from markdown header: "## Argus Skill: {name} [Source: ...]"
286
+ const nameMatch = input.result.match(/^##\s+Argus Skill:\s+(.+?)(?:\s+\[|$)/m)
287
+ const skillName = nameMatch?.[1]?.trim()
288
+ if (skillName) {
289
+ auditState.skillsLoaded ??= []
290
+ if (!auditState.skillsLoaded.includes(skillName)) {
291
+ auditState.skillsLoaded.push(skillName)
292
+ }
293
+ }
294
+ recordToolExecution(auditState, input.tool, 0)
295
+ onStateChanged?.({ tool: input.tool, findingsCount: 0 })
296
+ return
297
+ }
298
+
299
299
  let parsed: unknown
300
300
  try {
301
301
  parsed = JSON.parse(input.result)
@@ -321,13 +321,72 @@ export function createToolTrackingHook(
321
321
  case "argus_solodit_search":
322
322
  processSoloditResult(record, auditState)
323
323
  break
324
- case "argus_forge_test":
324
+ case "argus_forge_test": {
325
+ const summary = toRecord(record.summary)
326
+ if (summary && typeof summary.failed === "number") {
327
+ findingsCount = summary.failed
328
+ }
325
329
  break
330
+ }
326
331
  case "argus_forge_fuzz":
327
332
  processFuzzResult(record, auditState)
328
333
  break
334
+ case "argus_generate_report": {
335
+ auditState.reportGenerated = true
336
+ break
337
+ }
338
+ case "argus_sync_knowledge": {
339
+ const success = record.success === true
340
+ auditState.knowledgeSynced = { success, timestamp: Date.now() }
341
+ break
342
+ }
343
+ case "argus_forge_coverage": {
344
+ const reportObj = toRecord(record.report)
345
+ const files = reportObj?.files
346
+ if (Array.isArray(files)) {
347
+ auditState.coverageReport = {
348
+ files: files
349
+ .filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
350
+ .map((f) => ({
351
+ path: typeof f.path === "string" ? f.path : "unknown",
352
+ linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
353
+ statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
354
+ branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
355
+ functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
356
+ })),
357
+ }
358
+ }
359
+ break
360
+ }
361
+ case "argus_proxy_detection": {
362
+ if (record.isProxy === true) {
363
+ auditState.proxyContracts ??= []
364
+ auditState.proxyContracts.push({
365
+ file: typeof record.file === "string" ? record.file : "unknown",
366
+ proxyType: typeof record.proxyType === "string" ? record.proxyType : "unknown",
367
+ indicators: Array.isArray(record.indicators)
368
+ ? record.indicators.filter((i): i is string => typeof i === "string")
369
+ : [],
370
+ })
371
+ }
372
+ break
373
+ }
374
+ case "argus_gas_analysis": {
375
+ const hotspots = record.hotspots
376
+ if (Array.isArray(hotspots)) {
377
+ auditState.gasHotspots = hotspots
378
+ .filter((h): h is Record<string, unknown> => !!h && typeof h === "object")
379
+ .map((h) => ({
380
+ contract: typeof h.contract === "string" ? h.contract : "unknown",
381
+ function: typeof h.function === "string" ? h.function : "unknown",
382
+ avgGas: typeof h.avgGas === "number" ? h.avgGas : 0,
383
+ }))
384
+ }
385
+ break
386
+ }
329
387
  }
330
388
 
331
389
  recordToolExecution(auditState, input.tool, findingsCount)
390
+ onStateChanged?.({ tool: input.tool, findingsCount })
332
391
  }
333
392
  }
@@ -12,4 +12,5 @@ export type HookName =
12
12
  | "tool-error-recovery"
13
13
  | "context-window-monitor"
14
14
  | "tool-output-truncator"
15
- | "audit-continuation";
15
+ | "audit-continuation"
16
+ | "system-prompt"
package/src/index.ts CHANGED
@@ -1,54 +1,41 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
2
  import { loadArgusConfig } from "./config/loader"
3
- import { createHookGuard } from "./hooks/hook-system"
4
- import { createTools } from "./create-tools"
5
3
  import { createHooks } from "./create-hooks"
6
4
  import { createManagers } from "./create-managers"
5
+ import { createTools } from "./create-tools"
6
+ import type { Dispatcher } from "./features/background-agent/background-manager"
7
+ import { createHookGuard } from "./hooks/hook-system"
7
8
  import { createPluginInterface } from "./plugin-interface"
8
- import { checkSoloditHealth } from "./utils/solodit-health"
9
- import { createLogger } from "./shared/logger"
10
-
11
- async function startSoloditMcp(port: number): Promise<void> {
12
- const logger = createLogger()
13
-
14
- // Health check before spawn: if already reachable, skip spawn
15
- const health = await checkSoloditHealth(port, true)
16
- if (health.reachable) {
17
- logger.debug(`Solodit MCP already running on port ${port} — skipping spawn`)
18
- return
19
- }
20
-
21
- const child = Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
22
- stdin: "ignore",
23
- stdout: "ignore",
24
- stderr: "ignore",
25
- env: { ...process.env, PORT: String(port) },
26
- })
27
- child.unref()
28
-
29
- // Health check after spawn: wait 2s, then ping
30
- setTimeout(async () => {
31
- const health = await checkSoloditHealth(port, true)
32
- if (!health.reachable) {
33
- logger.debug(`Solodit MCP not yet reachable on port ${port} — will retry on first use`)
34
- } else {
35
- logger.debug(`Solodit MCP healthy on port ${port}`)
36
- }
37
- }, 2000)
38
- }
9
+ import { startSoloditMcp } from "./solodit-lifecycle"
39
10
 
40
11
  const ArgusPlugin: Plugin = async (ctx) => {
41
12
  const projectDir = ctx.directory ?? process.cwd()
42
13
  const config = loadArgusConfig(projectDir)
43
14
 
44
15
  if (config.solodit?.enabled !== false) {
45
- // Fire-and-forget: startSoloditMcp is now async but we don't await
46
- // to avoid blocking plugin initialization
47
- startSoloditMcp(config.solodit?.port ?? 3000)
16
+ await startSoloditMcp(config.solodit?.port ?? 3000)
48
17
  }
49
18
 
50
19
  const isHookEnabled = createHookGuard(config.disabled_hooks)
51
- const managers = createManagers({ projectDir, config })
20
+ const taskCandidate = (ctx as Record<string, unknown>).task
21
+ const backgroundDispatcher: Dispatcher | undefined =
22
+ typeof taskCandidate === "function"
23
+ ? async (agentName: string, prompt: string) => {
24
+ const result = await taskCandidate(agentName, prompt)
25
+ if (typeof result === "string") {
26
+ return result
27
+ }
28
+ if (typeof result === "object" && result !== null) {
29
+ const taskId = (result as Record<string, unknown>).task_id
30
+ if (typeof taskId === "string") {
31
+ return taskId
32
+ }
33
+ }
34
+ return `task-${Date.now()}`
35
+ }
36
+ : undefined
37
+
38
+ const managers = createManagers({ projectDir, config, backgroundDispatcher })
52
39
  const tools = createTools(config)
53
40
  const hooks = createHooks({ config, managers, projectDir, isHookEnabled })
54
41