solidity-argus 0.2.0 → 0.3.0

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 (167) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +33 -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 +24 -7
  75. package/src/agents/pythia-prompt.ts +3 -4
  76. package/src/agents/scribe-prompt.ts +7 -2
  77. package/src/agents/sentinel-prompt.ts +32 -3
  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 +4 -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/tool-tracking-hook.ts +104 -50
  114. package/src/hooks/types.ts +2 -1
  115. package/src/index.ts +23 -36
  116. package/src/knowledge/retry.ts +22 -22
  117. package/src/knowledge/scvd-client.ts +88 -95
  118. package/src/knowledge/scvd-errors.ts +35 -35
  119. package/src/knowledge/scvd-index.ts +78 -80
  120. package/src/knowledge/scvd-sync.ts +106 -101
  121. package/src/managers/index.ts +1 -1
  122. package/src/managers/types.ts +19 -14
  123. package/src/plugin-interface.ts +7 -9
  124. package/src/shared/binary-utils.ts +44 -35
  125. package/src/shared/deep-merge.ts +55 -36
  126. package/src/shared/file-utils.ts +21 -19
  127. package/src/shared/index.ts +11 -5
  128. package/src/shared/jsonc-parser.ts +123 -28
  129. package/src/shared/logger.ts +16 -3
  130. package/src/shared/project-utils.ts +30 -0
  131. package/src/skills/analysis/cluster.ts +414 -0
  132. package/src/skills/analysis/gates.ts +227 -0
  133. package/src/skills/analysis/index.ts +33 -0
  134. package/src/skills/analysis/normalize.ts +217 -0
  135. package/src/skills/analysis/similarity.ts +224 -0
  136. package/src/skills/argus-skill-resolver.ts +17 -6
  137. package/src/skills/skill-schema.ts +11 -10
  138. package/src/solodit-lifecycle.ts +202 -0
  139. package/src/state/audit-state.ts +8 -8
  140. package/src/state/finding-store.ts +68 -55
  141. package/src/state/types.ts +88 -67
  142. package/src/tools/argus-skill-load-tool.ts +12 -7
  143. package/src/tools/contract-analyzer-tool.ts +60 -77
  144. package/src/tools/forge-coverage-tool.ts +226 -0
  145. package/src/tools/forge-fuzz-tool.ts +127 -127
  146. package/src/tools/forge-test-tool.ts +153 -157
  147. package/src/tools/gas-analysis-tool.ts +264 -0
  148. package/src/tools/pattern-checker-tool.ts +185 -190
  149. package/src/tools/pattern-loader.ts +5 -111
  150. package/src/tools/proxy-detection-tool.ts +224 -0
  151. package/src/tools/report-generator-tool.ts +268 -200
  152. package/src/tools/slither-tool.ts +266 -218
  153. package/src/tools/solodit-search-tool.ts +216 -119
  154. package/src/tools/sync-knowledge-tool.ts +7 -11
  155. package/src/utils/audit-artifact-detector.ts +28 -29
  156. package/src/utils/dependency-scanner.ts +37 -37
  157. package/src/utils/project-detector.ts +111 -124
  158. package/src/utils/solidity-parser.ts +103 -74
  159. package/skills/patterns/access-control.yaml +0 -31
  160. package/skills/patterns/erc4626.yaml +0 -29
  161. package/skills/patterns/flash-loan.yaml +0 -20
  162. package/skills/patterns/oracle.yaml +0 -30
  163. package/skills/patterns/proxy.yaml +0 -30
  164. package/skills/patterns/reentrancy.yaml +0 -30
  165. package/skills/patterns/signature.yaml +0 -31
  166. package/src/hooks/event-hook-v2.ts +0 -99
  167. package/src/state/plugin-state.ts +0 -14
@@ -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
  }
@@ -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)
@@ -326,8 +326,62 @@ export function createToolTrackingHook(
326
326
  case "argus_forge_fuzz":
327
327
  processFuzzResult(record, auditState)
328
328
  break
329
+ case "argus_generate_report": {
330
+ auditState.reportGenerated = true
331
+ break
332
+ }
333
+ case "argus_sync_knowledge": {
334
+ const success = record.success === true
335
+ auditState.knowledgeSynced = { success, timestamp: Date.now() }
336
+ break
337
+ }
338
+ case "argus_forge_coverage": {
339
+ const reportObj = toRecord(record.report)
340
+ const files = reportObj?.files
341
+ if (Array.isArray(files)) {
342
+ auditState.coverageReport = {
343
+ files: files
344
+ .filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
345
+ .map((f) => ({
346
+ path: typeof f.path === "string" ? f.path : "unknown",
347
+ linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
348
+ statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
349
+ branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
350
+ functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
351
+ })),
352
+ }
353
+ }
354
+ break
355
+ }
356
+ case "argus_proxy_detection": {
357
+ if (record.isProxy === true) {
358
+ auditState.proxyContracts ??= []
359
+ auditState.proxyContracts.push({
360
+ file: typeof record.file === "string" ? record.file : "unknown",
361
+ proxyType: typeof record.proxyType === "string" ? record.proxyType : "unknown",
362
+ indicators: Array.isArray(record.indicators)
363
+ ? record.indicators.filter((i): i is string => typeof i === "string")
364
+ : [],
365
+ })
366
+ }
367
+ break
368
+ }
369
+ case "argus_gas_analysis": {
370
+ const hotspots = record.hotspots
371
+ if (Array.isArray(hotspots)) {
372
+ auditState.gasHotspots = hotspots
373
+ .filter((h): h is Record<string, unknown> => !!h && typeof h === "object")
374
+ .map((h) => ({
375
+ contract: typeof h.contract === "string" ? h.contract : "unknown",
376
+ function: typeof h.function === "string" ? h.function : "unknown",
377
+ avgGas: typeof h.avgGas === "number" ? h.avgGas : 0,
378
+ }))
379
+ }
380
+ break
381
+ }
329
382
  }
330
383
 
331
384
  recordToolExecution(auditState, input.tool, findingsCount)
385
+ onStateChanged?.({ tool: input.tool, findingsCount })
332
386
  }
333
387
  }
@@ -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
16
  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
 
@@ -1,47 +1,47 @@
1
1
  export interface RetryOptions<T> {
2
- maxAttempts: number;
3
- baseDelayMs: number;
4
- shouldRetry: (error: unknown) => boolean;
5
- onRetry?: (attempt: number, error: unknown) => void;
6
- _valueType?: T;
2
+ maxAttempts: number
3
+ baseDelayMs: number
4
+ shouldRetry: (error: unknown) => boolean
5
+ onRetry?: (attempt: number, error: unknown) => void
6
+ _valueType?: T
7
7
  }
8
8
 
9
9
  export interface RetryResult<T> {
10
- success: boolean;
11
- value?: T;
12
- error?: unknown;
13
- attempts: number;
10
+ success: boolean
11
+ value?: T
12
+ error?: unknown
13
+ attempts: number
14
14
  }
15
15
 
16
16
  function sleep(delayMs: number): Promise<void> {
17
- return new Promise((resolve) => setTimeout(resolve, delayMs));
17
+ return new Promise((resolve) => setTimeout(resolve, delayMs))
18
18
  }
19
19
 
20
20
  export async function withRetry<T>(
21
21
  fn: () => Promise<T>,
22
- options: RetryOptions<T>
22
+ options: RetryOptions<T>,
23
23
  ): Promise<RetryResult<T>> {
24
- const maxAttempts = options.maxAttempts > 0 ? options.maxAttempts : 1;
25
- let lastError: unknown;
24
+ const maxAttempts = options.maxAttempts > 0 ? options.maxAttempts : 1
25
+ let lastError: unknown
26
26
 
27
27
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
28
28
  try {
29
- const value = await fn();
30
- return { success: true, value, attempts: attempt };
29
+ const value = await fn()
30
+ return { success: true, value, attempts: attempt }
31
31
  } catch (error) {
32
- lastError = error;
33
- const canRetry = attempt < maxAttempts && options.shouldRetry(error);
32
+ lastError = error
33
+ const canRetry = attempt < maxAttempts && options.shouldRetry(error)
34
34
 
35
35
  if (!canRetry) {
36
- return { success: false, error, attempts: attempt };
36
+ return { success: false, error, attempts: attempt }
37
37
  }
38
38
 
39
39
  if (options.onRetry) {
40
- options.onRetry(attempt, error);
40
+ options.onRetry(attempt, error)
41
41
  }
42
42
 
43
- const delay = options.baseDelayMs * 2 ** (attempt - 1);
44
- await sleep(delay);
43
+ const delay = options.baseDelayMs * 2 ** (attempt - 1)
44
+ await sleep(delay)
45
45
  }
46
46
  }
47
47
 
@@ -49,5 +49,5 @@ export async function withRetry<T>(
49
49
  success: false,
50
50
  error: lastError,
51
51
  attempts: maxAttempts,
52
- };
52
+ }
53
53
  }