solidity-argus 0.1.8 → 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 (178) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +229 -13
  3. package/package.json +37 -8
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +72 -6
  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/checklists/cyfrin-defi-core/SKILL.md +3 -0
  22. package/skills/manifests/cyfrin.json +16 -0
  23. package/skills/manifests/defifofum.json +25 -0
  24. package/skills/manifests/kadenzipfel.json +48 -0
  25. package/skills/manifests/scvd.json +9 -0
  26. package/skills/manifests/smartbugs.json +9 -0
  27. package/skills/manifests/solodit.json +9 -0
  28. package/skills/manifests/sunweb3sec.json +9 -0
  29. package/skills/manifests/trailofbits.json +9 -0
  30. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  31. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  32. package/skills/references/exploit-reference/SKILL.md +3 -0
  33. package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
  34. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  35. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  36. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  37. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
  38. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  39. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  40. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  42. package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
  43. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  44. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  45. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
  46. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  47. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  48. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  49. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  50. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  51. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  52. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  54. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  55. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  56. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  57. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  59. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  60. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  61. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
  62. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
  64. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  65. package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
  66. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  67. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  68. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  70. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  71. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
  72. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  73. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  74. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  75. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  76. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  77. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  78. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  79. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  80. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  81. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  82. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  83. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  84. package/src/agents/argus-prompt.ts +27 -10
  85. package/src/agents/pythia-prompt.ts +7 -8
  86. package/src/agents/scribe-prompt.ts +10 -5
  87. package/src/agents/sentinel-prompt.ts +36 -7
  88. package/src/cli/cli-output.ts +16 -0
  89. package/src/cli/cli-program.ts +29 -22
  90. package/src/cli/commands/check-skills.ts +135 -0
  91. package/src/cli/commands/doctor.ts +303 -23
  92. package/src/cli/commands/init.ts +8 -6
  93. package/src/cli/commands/install.ts +10 -8
  94. package/src/cli/commands/lint-skills.ts +118 -0
  95. package/src/cli/index.ts +5 -5
  96. package/src/cli/tui-prompts.ts +4 -2
  97. package/src/cli/types.ts +3 -3
  98. package/src/config/index.ts +1 -1
  99. package/src/config/loader.ts +4 -6
  100. package/src/config/schema.ts +6 -5
  101. package/src/config/types.ts +2 -2
  102. package/src/constants/defaults.ts +2 -0
  103. package/src/create-hooks.ts +225 -29
  104. package/src/create-managers.ts +10 -8
  105. package/src/create-tools.ts +14 -8
  106. package/src/features/background-agent/background-manager.ts +93 -87
  107. package/src/features/background-agent/index.ts +1 -1
  108. package/src/features/context-monitor/context-monitor.ts +3 -3
  109. package/src/features/context-monitor/index.ts +2 -2
  110. package/src/features/error-recovery/session-recovery.ts +2 -4
  111. package/src/features/error-recovery/tool-error-recovery.ts +79 -19
  112. package/src/features/index.ts +5 -5
  113. package/src/features/persistent-state/audit-state-manager.ts +158 -52
  114. package/src/features/persistent-state/global-run-index.ts +38 -0
  115. package/src/features/persistent-state/index.ts +1 -1
  116. package/src/features/persistent-state/run-journal.ts +86 -0
  117. package/src/hooks/agent-tracker.ts +53 -0
  118. package/src/hooks/compaction-hook.ts +46 -37
  119. package/src/hooks/config-handler.ts +31 -11
  120. package/src/hooks/context-budget.ts +42 -0
  121. package/src/hooks/event-hook.ts +48 -23
  122. package/src/hooks/hook-system.ts +4 -4
  123. package/src/hooks/index.ts +5 -5
  124. package/src/hooks/knowledge-sync-hook.ts +19 -21
  125. package/src/hooks/recon-context-builder.ts +66 -0
  126. package/src/hooks/safe-create-hook.ts +9 -11
  127. package/src/hooks/system-prompt-hook.ts +128 -0
  128. package/src/hooks/tool-tracking-hook.ts +162 -29
  129. package/src/hooks/types.ts +2 -1
  130. package/src/index.ts +23 -13
  131. package/src/knowledge/retry.ts +53 -0
  132. package/src/knowledge/scvd-client.ts +103 -83
  133. package/src/knowledge/scvd-errors.ts +89 -0
  134. package/src/knowledge/scvd-index.ts +110 -62
  135. package/src/knowledge/scvd-sync.ts +223 -47
  136. package/src/knowledge/source-manifest.ts +102 -0
  137. package/src/managers/index.ts +1 -1
  138. package/src/managers/types.ts +19 -14
  139. package/src/plugin-interface.ts +19 -8
  140. package/src/shared/binary-utils.ts +44 -34
  141. package/src/shared/deep-merge.ts +55 -36
  142. package/src/shared/file-utils.ts +21 -19
  143. package/src/shared/index.ts +11 -5
  144. package/src/shared/jsonc-parser.ts +123 -28
  145. package/src/shared/logger.ts +91 -17
  146. package/src/shared/project-utils.ts +30 -0
  147. package/src/skills/analysis/cluster.ts +414 -0
  148. package/src/skills/analysis/gates.ts +227 -0
  149. package/src/skills/analysis/index.ts +33 -0
  150. package/src/skills/analysis/normalize.ts +217 -0
  151. package/src/skills/analysis/similarity.ts +224 -0
  152. package/src/skills/argus-skill-resolver.ts +237 -0
  153. package/src/skills/skill-schema.ts +99 -0
  154. package/src/solodit-lifecycle.ts +202 -0
  155. package/src/state/audit-state.ts +10 -8
  156. package/src/state/finding-store.ts +68 -55
  157. package/src/state/types.ts +96 -44
  158. package/src/tools/argus-skill-load-tool.ts +78 -0
  159. package/src/tools/contract-analyzer-tool.ts +60 -77
  160. package/src/tools/forge-coverage-tool.ts +226 -0
  161. package/src/tools/forge-fuzz-tool.ts +127 -127
  162. package/src/tools/forge-test-tool.ts +153 -157
  163. package/src/tools/gas-analysis-tool.ts +264 -0
  164. package/src/tools/pattern-checker-tool.ts +206 -167
  165. package/src/tools/pattern-loader.ts +77 -0
  166. package/src/tools/pattern-schema.ts +51 -0
  167. package/src/tools/proxy-detection-tool.ts +224 -0
  168. package/src/tools/report-generator-tool.ts +333 -142
  169. package/src/tools/slither-tool.ts +300 -210
  170. package/src/tools/solodit-search-tool.ts +255 -80
  171. package/src/tools/sync-knowledge-tool.ts +7 -11
  172. package/src/utils/audit-artifact-detector.ts +118 -0
  173. package/src/utils/dependency-scanner.ts +93 -0
  174. package/src/utils/project-detector.ts +175 -86
  175. package/src/utils/solidity-parser.ts +112 -67
  176. package/src/utils/solodit-health.ts +29 -0
  177. package/src/hooks/event-hook-v2.ts +0 -99
  178. package/src/state/plugin-state.ts +0 -14
@@ -0,0 +1,118 @@
1
+ import { readdirSync, readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { createLogger } from "../../shared/logger"
4
+ import type { CliCommand } from "../types"
5
+
6
+ const logger = createLogger()
7
+
8
+ import { loadArgusConfig } from "../../config/loader"
9
+ import { resolveSkillRoots } from "../../skills/argus-skill-resolver"
10
+ import { parseFrontmatter, validateSkillFrontmatter } from "../../skills/skill-schema"
11
+ import { cliOutput } from "../cli-output"
12
+
13
+ const GREEN = "\x1b[32m"
14
+ const RED = "\x1b[31m"
15
+ const RESET = "\x1b[0m"
16
+
17
+ function findSkillFiles(dir: string, maxDepth = 8): string[] {
18
+ const files: string[] = []
19
+ const stack: Array<{ path: string; depth: number }> = [{ path: dir, depth: 0 }]
20
+
21
+ while (stack.length > 0) {
22
+ const current = stack.pop()
23
+ if (!current || current.depth > maxDepth) continue
24
+
25
+ try {
26
+ const entries = readdirSync(current.path, { withFileTypes: true })
27
+ for (const entry of entries) {
28
+ const fullPath = join(current.path, entry.name)
29
+ if (entry.isDirectory()) {
30
+ stack.push({ path: fullPath, depth: current.depth + 1 })
31
+ } else if (entry.isFile() && entry.name.toUpperCase() === "SKILL.MD") {
32
+ files.push(fullPath)
33
+ }
34
+ }
35
+ } catch {}
36
+ }
37
+
38
+ return files
39
+ }
40
+
41
+ export interface LintResult {
42
+ valid: number
43
+ invalid: number
44
+ skipped: number
45
+ errors: Array<{ file: string; errors: string[] }>
46
+ }
47
+
48
+ export function lintSkillFiles(skillFiles: Array<{ path: string; content: string }>): LintResult {
49
+ let valid = 0
50
+ let invalid = 0
51
+ let skipped = 0
52
+ const errors: Array<{ file: string; errors: string[] }> = []
53
+
54
+ for (const { path, content } of skillFiles) {
55
+ const fm = parseFrontmatter(content)
56
+ if (!fm) {
57
+ skipped++
58
+ continue
59
+ }
60
+
61
+ const result = validateSkillFrontmatter(fm)
62
+ if (result.success) {
63
+ valid++
64
+ } else {
65
+ invalid++
66
+ errors.push({ file: path, errors: result.errors })
67
+ }
68
+ }
69
+
70
+ return { valid, invalid, skipped, errors }
71
+ }
72
+
73
+ export const lintSkillsCommand: CliCommand = {
74
+ name: "lint-skills",
75
+ description: "Validate all SKILL.md files against schema",
76
+ async execute(): Promise<number> {
77
+ const cwd = process.cwd()
78
+ let config: ReturnType<typeof loadArgusConfig> | undefined
79
+ try {
80
+ config = loadArgusConfig(cwd)
81
+ } catch {
82
+ logger.debug("Config load failed, using defaults")
83
+ }
84
+
85
+ const roots = resolveSkillRoots(cwd, config)
86
+ const skillFiles: Array<{ path: string; content: string }> = []
87
+
88
+ for (const root of roots) {
89
+ const files = findSkillFiles(root.path)
90
+ for (const file of files) {
91
+ try {
92
+ skillFiles.push({ path: file, content: readFileSync(file, "utf8") })
93
+ } catch {
94
+ logger.debug("Skipping unreadable skill file")
95
+ }
96
+ }
97
+ }
98
+
99
+ const result = lintSkillFiles(skillFiles)
100
+
101
+ cliOutput.log(
102
+ `Skill Lint: ${result.valid} valid, ${result.invalid} invalid, ${result.skipped} skipped (no frontmatter)`,
103
+ )
104
+
105
+ if (result.errors.length > 0) {
106
+ for (const { file, errors } of result.errors) {
107
+ cliOutput.log(`\n${RED}✗${RESET} ${file}`)
108
+ for (const err of errors) {
109
+ cliOutput.log(` - ${err}`)
110
+ }
111
+ }
112
+ } else if (result.valid > 0) {
113
+ cliOutput.log(`${GREEN}✓${RESET} All skills pass schema validation`)
114
+ }
115
+
116
+ return result.invalid > 0 ? 1 : 0
117
+ },
118
+ }
package/src/cli/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bun
2
- import { createCliProgram } from "./cli-program";
2
+ import { createCliProgram } from "./cli-program"
3
3
 
4
- const program = createCliProgram();
5
- const args = Bun.argv.slice(2);
6
- const exitCode = await program.dispatch(args);
4
+ const program = createCliProgram()
5
+ const args = Bun.argv.slice(2)
6
+ const exitCode = await program.dispatch(args)
7
7
 
8
8
  // Set exit code without process.exit() so stdout flushes before termination
9
- process.exitCode = exitCode;
9
+ process.exitCode = exitCode
@@ -1,3 +1,5 @@
1
+ import { cliOutput } from "./cli-output"
2
+
1
3
  const NON_INTERACTIVE =
2
4
  !process.stdin.isTTY || process.env.CI === "true" || process.env.ARGUS_NON_INTERACTIVE === "true"
3
5
 
@@ -29,10 +31,10 @@ export async function select(
29
31
  ): Promise<string> {
30
32
  if (NON_INTERACTIVE) return options[defaultIndex] ?? options[0] ?? ""
31
33
 
32
- console.log(message)
34
+ cliOutput.log(message)
33
35
  for (let i = 0; i < options.length; i++) {
34
36
  const marker = i === defaultIndex ? ">" : " "
35
- console.log(` ${marker} ${i + 1}. ${options[i]}`)
37
+ cliOutput.log(` ${marker} ${i + 1}. ${options[i]}`)
36
38
  }
37
39
 
38
40
  return new Promise((resolve) => {
package/src/cli/types.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Defines the contract for all CLI subcommands
4
4
  */
5
5
  export interface CliCommand {
6
- name: string;
7
- description: string;
8
- execute: (args: string[]) => Promise<number>;
6
+ name: string
7
+ description: string
8
+ execute: (args: string[]) => Promise<number>
9
9
  }
@@ -1,3 +1,3 @@
1
+ export { loadArgusConfig } from "./loader"
1
2
  export { ArgusConfigSchema } from "./schema"
2
3
  export type { ArgusConfig } from "./types"
3
- export { loadArgusConfig } from "./loader"
@@ -1,10 +1,10 @@
1
1
  import { homedir } from "node:os"
2
2
  import { join } from "node:path"
3
- import { ArgusConfigSchema } from "./schema"
4
- import type { ArgusConfig } from "./types"
5
- import { detectConfigFile, readJsoncFile } from "../shared/file-utils"
6
3
  import { deepMerge } from "../shared/deep-merge"
4
+ import { detectConfigFile, readJsoncFile } from "../shared/file-utils"
7
5
  import { createLogger } from "../shared/logger"
6
+ import { ArgusConfigSchema } from "./schema"
7
+ import type { ArgusConfig } from "./types"
8
8
 
9
9
  export function _mergeConfigs(
10
10
  userRaw: Record<string, unknown> | null,
@@ -28,9 +28,7 @@ export function loadArgusConfig(projectDir: string): ArgusConfig {
28
28
  const userRaw = userConfigInfo.path ? readJsoncFile(userConfigInfo.path) : null
29
29
 
30
30
  const projectConfigInfo = detectConfigFile(projectDir)
31
- const projectRaw = projectConfigInfo.path
32
- ? readJsoncFile(projectConfigInfo.path)
33
- : null
31
+ const projectRaw = projectConfigInfo.path ? readJsoncFile(projectConfigInfo.path) : null
34
32
 
35
33
  return _mergeConfigs(userRaw, projectRaw)
36
34
  }
@@ -2,6 +2,7 @@ import { z } from "zod"
2
2
 
3
3
  const AgentConfigSchema = z.object({
4
4
  model: z.string().optional(),
5
+ steps: z.number().positive().optional(),
5
6
  permission: z.record(z.string(), z.any()).optional(),
6
7
  tools: z.record(z.string(), z.boolean()).optional(),
7
8
  })
@@ -23,17 +24,16 @@ const KnowledgeConfigSchema = z.object({
23
24
  }),
24
25
  autoSync: z.boolean().default(true),
25
26
  customSkillsDir: z.string().optional(),
27
+ skillPrecedence: z.enum(["bundled-first", "custom-first"]).default("bundled-first"),
26
28
  })
27
29
 
28
30
  const ReportingConfigSchema = z.object({
29
31
  format: z.enum(["markdown"]).default("markdown"),
30
- severityThreshold: z
31
- .enum(["critical", "high", "medium", "low", "informational"])
32
- .default("low"),
32
+ severityThreshold: z.enum(["critical", "high", "medium", "low", "informational"]).default("low"),
33
33
  gasAnalysis: z.boolean().default(false),
34
34
  })
35
35
 
36
- const SolditConfigSchema = z.object({
36
+ const SoloditConfigSchema = z.object({
37
37
  enabled: z.boolean().default(true),
38
38
  port: z.number().default(3000),
39
39
  })
@@ -63,13 +63,14 @@ export const ArgusConfigSchema = z.object({
63
63
  apiUrl: "https://api.scvd.dev",
64
64
  },
65
65
  autoSync: true,
66
+ skillPrecedence: "bundled-first",
66
67
  }),
67
68
  reporting: ReportingConfigSchema.default({
68
69
  format: "markdown",
69
70
  severityThreshold: "low",
70
71
  gasAnalysis: false,
71
72
  }),
72
- solodit: SolditConfigSchema.default({
73
+ solodit: SoloditConfigSchema.default({
73
74
  enabled: true,
74
75
  port: 3000,
75
76
  }),
@@ -1,4 +1,4 @@
1
- import { z } from "zod"
2
- import { ArgusConfigSchema } from "./schema"
1
+ import type { z } from "zod"
2
+ import type { ArgusConfigSchema } from "./schema"
3
3
 
4
4
  export type ArgusConfig = z.infer<typeof ArgusConfigSchema>
@@ -4,3 +4,5 @@ export const DEFAULT_MODELS = {
4
4
  pythia: "anthropic/claude-sonnet-4-6",
5
5
  scribe: "anthropic/claude-sonnet-4-6",
6
6
  } as const
7
+
8
+ export const DEFAULT_STEPS = 50 as const
@@ -1,25 +1,71 @@
1
+ import { join } from "node:path"
1
2
  import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
3
  import type { ArgusConfig } from "./config/types"
3
- import type { Managers } from "./managers/types"
4
- import type { HookName } from "./hooks/types"
5
- import { createConfigHandler } from "./hooks/config-handler"
4
+ import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
5
+ import { createContextMonitor, createToolOutputTruncator } from "./features/context-monitor"
6
+ import {
7
+ createSessionRecoveryHandler,
8
+ createToolErrorRecoveryHandler,
9
+ } from "./features/error-recovery"
10
+ import { createDebouncedSave } from "./features/persistent-state/audit-state-manager"
11
+ import { recordRun } from "./features/persistent-state/global-run-index"
12
+ import { createRunJournal } from "./features/persistent-state/run-journal"
13
+ import { createAgentTracker } from "./hooks/agent-tracker"
6
14
  import { createCompactionHook } from "./hooks/compaction-hook"
7
- import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
8
- import { createEventHookV2 } from "./hooks/event-hook-v2"
15
+ import { createConfigHandler } from "./hooks/config-handler"
16
+ import { getTokenBudgetForAgent } from "./hooks/context-budget"
17
+ import { createEventHook } from "./hooks/event-hook"
18
+ import type { ReconContext } from "./hooks/recon-context-builder"
19
+ import { buildReconContextBlock } from "./hooks/recon-context-builder"
9
20
  import { safeCreateHook } from "./hooks/safe-create-hook"
10
- import { createToolOutputTruncator } from "./features/context-monitor"
11
- import { createSessionRecoveryHandler } from "./features/error-recovery"
12
- import { createToolErrorRecoveryHandler } from "./features/error-recovery"
21
+ import { createSystemPromptHook } from "./hooks/system-prompt-hook"
22
+ import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
23
+ import type { HookName } from "./hooks/types"
24
+ import type { Managers } from "./managers/types"
25
+ import { createLogger } from "./shared/logger"
26
+ import type { AuditState } from "./state/types"
27
+ import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
28
+ import { detectProject, type ProjectConfig } from "./utils/project-detector"
29
+
30
+ const logger = createLogger()
31
+
32
+ export type AgentTrackerRef = {
33
+ getAgentForSession(sessionID: string): string | undefined
34
+ isArgusAgent(sessionID: string): boolean
35
+ }
36
+
37
+ let _agentTrackerRef: AgentTrackerRef | undefined
38
+
39
+ export function getAgentForSession(sessionID: string): string | undefined {
40
+ return _agentTrackerRef?.getAgentForSession(sessionID)
41
+ }
42
+
43
+ export function isArgusAgent(sessionID: string): boolean {
44
+ return _agentTrackerRef?.isArgusAgent(sessionID) ?? false
45
+ }
13
46
 
14
47
  export type Hooks = Pick<
15
48
  PluginHooks,
16
49
  | "config"
50
+ | "chat.params"
51
+ | "chat.message"
17
52
  | "experimental.chat.system.transform"
18
53
  | "experimental.session.compacting"
19
54
  | "tool.execute.after"
20
55
  | "event"
21
56
  >
22
57
 
58
+ /**
59
+ * Creates the hook handlers for the Argus plugin.
60
+ *
61
+ * Context Delivery Strategy:
62
+ * - Prompt: Static agent identity (src/agents/*-prompt.ts) — methodology, personality, tool instructions
63
+ * - Hook: Dynamic state injection via experimental.chat.system.transform — audit progress, findings, phase
64
+ * - Skill-load: On-demand knowledge via argus_skill_load tool — vulnerability patterns, protocol knowledge
65
+ *
66
+ * The system.transform hook injects dynamic audit context only for Argus-family agents
67
+ * (argus, sentinel, pythia, scribe). Non-audit agents receive no injection.
68
+ */
23
69
  export function createHooks(args: {
24
70
  config: ArgusConfig
25
71
  managers: Managers
@@ -28,28 +74,118 @@ export function createHooks(args: {
28
74
  }): Hooks {
29
75
  const { config, managers, projectDir, isHookEnabled } = args
30
76
  const { auditStateManager, backgroundManager } = managers
77
+ const agentTracker = createAgentTracker()
78
+ _agentTrackerRef = agentTracker
31
79
 
32
- const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
33
- const toolErrorRecoveryHandler = createToolErrorRecoveryHandler()
34
- const outputTruncator = createToolOutputTruncator()
80
+ const contextMonitor = createContextMonitor()
81
+ const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
82
+ const debouncedSave = createDebouncedSave(auditStateManager.save)
83
+ const runJournal = createRunJournal(projectDir)
84
+ let auditStateGetter: (() => AuditState | null) | undefined
85
+ const toolErrorRecoveryHandler = createToolErrorRecoveryHandler(
86
+ () => auditStateGetter?.() ?? null,
87
+ (patch) => auditStateManager.update(patch),
88
+ )
89
+ const outputTruncator = createToolOutputTruncator()
35
90
 
36
- const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
37
- async ({ type, auditState, setAuditState: setState }) => {
91
+ // Sub-handlers run sequentially. The state persistence handler MUST be first:
92
+ // it loads persisted state on session.created, overriding the fresh default.
93
+ const {
94
+ hook: eventHook,
95
+ getAuditState,
96
+ setAuditState,
97
+ } = createEventHook(projectDir, [
98
+ async ({ type, sessionId, auditState, setAuditState: setState }) => {
38
99
  if (type === "session.created") {
39
- const recoveredState = await auditStateManager.load()
100
+ const timestamp = Date.now()
101
+ let recoveredState: AuditState | null = null
102
+
103
+ try {
104
+ recoveredState = await auditStateManager.load()
105
+ } finally {
106
+ runJournal.log({
107
+ type: "state.loaded",
108
+ timestamp,
109
+ success: recoveredState !== null,
110
+ findingsCount: recoveredState?.findings.length ?? 0,
111
+ })
112
+ }
113
+
40
114
  if (recoveredState) {
41
115
  setState(recoveredState)
42
116
  }
117
+
118
+ runJournal.log({
119
+ type: "session.created",
120
+ sessionId,
121
+ timestamp: Date.now(),
122
+ })
123
+
124
+ const effectiveState = recoveredState ?? auditStateManager.get()
125
+ if (effectiveState) {
126
+ void recordRun({
127
+ runId: effectiveState.sessionId,
128
+ opencodeSessionId: sessionId,
129
+ projectDir: effectiveState.projectDir,
130
+ statePath: join(effectiveState.projectDir, ".opencode", "argus-state.json"),
131
+ journalPath: join(effectiveState.projectDir, ".opencode", "argus-journal.jsonl"),
132
+ startedAt: effectiveState.startTime,
133
+ phase: effectiveState.currentPhase,
134
+ findingsCount: effectiveState.findings.length,
135
+ })
136
+ }
137
+
43
138
  return
44
139
  }
45
140
 
46
141
  if (type === "session.idle" && auditState) {
47
- await auditStateManager.save(auditState)
142
+ await debouncedSave.flush()
143
+
144
+ let saveSuccess = true
145
+ try {
146
+ await auditStateManager.save(auditState)
147
+ } catch {
148
+ saveSuccess = false
149
+ } finally {
150
+ runJournal.log({
151
+ type: "state.saved",
152
+ timestamp: Date.now(),
153
+ success: saveSuccess,
154
+ })
155
+ }
156
+
157
+ runJournal.log({
158
+ type: "session.idle",
159
+ timestamp: Date.now(),
160
+ findingsCount: auditState.findings.length,
161
+ toolsExecutedCount: auditState.toolsExecuted.length,
162
+ })
163
+
164
+ void recordRun({
165
+ runId: auditState.sessionId,
166
+ opencodeSessionId: sessionId,
167
+ projectDir: auditState.projectDir,
168
+ statePath: join(auditState.projectDir, ".opencode", "argus-state.json"),
169
+ journalPath: join(auditState.projectDir, ".opencode", "argus-journal.jsonl"),
170
+ startedAt: auditState.startTime,
171
+ phase: auditState.currentPhase,
172
+ findingsCount: auditState.findings.length,
173
+ })
174
+
48
175
  return
49
176
  }
50
177
 
51
178
  if (type === "session.deleted") {
52
- await auditStateManager.reset()
179
+ if (sessionId) {
180
+ agentTracker.clearSession(sessionId)
181
+ }
182
+
183
+ await auditStateManager.archive()
184
+ runJournal.log({
185
+ type: "session.deleted",
186
+ timestamp: Date.now(),
187
+ archived: true,
188
+ })
53
189
  }
54
190
  },
55
191
  async ({ type, sessionId, setAuditState: setState }) => {
@@ -62,26 +198,88 @@ export function createHooks(args: {
62
198
  },
63
199
  ])
64
200
 
65
- const initialState = auditStateManager.get()
66
- if (initialState) {
67
- setAuditState(initialState)
68
- }
201
+ auditStateGetter = getAuditState
202
+
203
+ const initialState = auditStateManager.get()
204
+ if (initialState) {
205
+ setAuditState(initialState)
206
+ }
207
+
208
+ const auditEnforcer = createAuditEnforcer()
209
+
210
+ const systemPromptHook = createSystemPromptHook({
211
+ getAuditState,
212
+ getAgentForSession: agentTracker.getAgentForSession,
213
+ isArgusAgent: agentTracker.isArgusAgent,
214
+ getContextPressure: (systemText: string) => {
215
+ const status = contextMonitor.getContextStatus(systemText, getAuditState())
216
+ return status.usage
217
+ },
218
+ getTokenBudget: getTokenBudgetForAgent,
219
+ getEnforcerReminder: auditEnforcer,
220
+ getReconBlock: () =>
221
+ buildReconContextBlock({
222
+ projectConfig: reconProjectConfig,
223
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
224
+ auditArtifacts: detectAuditArtifacts(projectDir),
225
+ }),
226
+ })
227
+
228
+ let reconProjectConfig: ProjectConfig | null = null
229
+
230
+ detectProject(projectDir)
231
+ .then((config) => {
232
+ reconProjectConfig = config
233
+ })
234
+ .catch(() => {
235
+ logger.debug("Project detection failed, using fallback recon context")
236
+ })
69
237
 
70
- const compactionHook = isHookEnabled("compaction")
71
- ? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
238
+ const getReconContext = (): ReconContext => ({
239
+ projectConfig: reconProjectConfig,
240
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
241
+ auditArtifacts: detectAuditArtifacts(projectDir),
242
+ })
243
+
244
+ const compactionHook = isHookEnabled("compaction")
245
+ ? safeCreateHook(() => createCompactionHook(getAuditState, getReconContext), "compaction")
72
246
  : undefined
73
247
 
74
248
  const toolTrackingHook = isHookEnabled("tool-tracking")
75
- ? safeCreateHook(() => createToolTrackingHook(getAuditState), "tool-tracking")
249
+ ? safeCreateHook(
250
+ () =>
251
+ createToolTrackingHook(getAuditState, ({ tool, findingsCount }) => {
252
+ const currentState = getAuditState()
253
+ if (currentState) {
254
+ debouncedSave.save(currentState)
255
+ }
256
+
257
+ runJournal.log({
258
+ type: "tool.executed",
259
+ tool,
260
+ timestamp: Date.now(),
261
+ findingsCount,
262
+ })
263
+ }),
264
+ "tool-tracking",
265
+ )
76
266
  : undefined
77
267
 
78
268
  const safeEventHook = isHookEnabled("event")
79
269
  ? safeCreateHook(() => eventHook, "event")
80
270
  : undefined
81
271
 
82
- return {
83
- config: createConfigHandler(config, projectDir),
84
- "experimental.chat.system.transform": undefined,
272
+ return {
273
+ config: createConfigHandler(config, projectDir),
274
+ "chat.params": async (input) => {
275
+ agentTracker.chatParamsHook(input)
276
+ },
277
+ "chat.message": async (input) => {
278
+ agentTracker.chatMessageHook(input)
279
+ },
280
+ "experimental.chat.system.transform": async (input, output) => {
281
+ await systemPromptHook(input, output)
282
+ },
85
283
  "experimental.session.compacting": compactionHook
86
284
  ? async (_input, output) => {
87
285
  const block = await compactionHook({ summary: output.context.join("\n") })
@@ -101,9 +299,7 @@ export function createHooks(args: {
101
299
  result: output.output,
102
300
  })
103
301
 
104
- const outputWithHint = recoveryHint
105
- ? `${output.output}${recoveryHint}`
106
- : output.output
302
+ const outputWithHint = recoveryHint ? `${output.output}${recoveryHint}` : output.output
107
303
  output.output = outputTruncator(outputWithHint)
108
304
  }
109
305
  : undefined,
@@ -1,23 +1,25 @@
1
1
  import type { ArgusConfig } from "./config/types"
2
- import type { Managers } from "./managers/types"
2
+ import type { Dispatcher } from "./features/background-agent/background-manager"
3
3
  import { createBackgroundManager } from "./features/background-agent/background-manager"
4
4
  import { createAuditStateManager } from "./features/persistent-state/audit-state-manager"
5
+ import type { Managers } from "./managers/types"
5
6
  import { createLogger } from "./shared/logger"
6
7
 
7
8
  export function createManagers(args: {
8
9
  projectDir: string
9
10
  config: ArgusConfig
11
+ backgroundDispatcher?: Dispatcher
10
12
  }): Managers {
11
- const { projectDir } = args
13
+ const { projectDir, config, backgroundDispatcher } = args
12
14
  const logger = createLogger()
13
15
 
14
16
  const backgroundManager = createBackgroundManager(
15
- async (agentName: string, prompt: string) => {
16
- logger.warn(
17
- `Background dispatch not wired: ${agentName} (${prompt.slice(0, 50)}...)`,
18
- )
19
- return `noop-${Date.now()}`
20
- },
17
+ backgroundDispatcher ??
18
+ (async (agentName: string, prompt: string) => {
19
+ logger.warn(`Background dispatch not wired: ${agentName} (${prompt.slice(0, 50)}...)`)
20
+ return `noop-${Date.now()}`
21
+ }),
22
+ { maxConcurrent: config.background?.max_concurrent ?? 3 },
21
23
  )
22
24
 
23
25
  const auditStateManager = createAuditStateManager(projectDir)
@@ -1,29 +1,35 @@
1
1
  import type { ToolDefinition } from "@opencode-ai/plugin"
2
2
  import type { ArgusConfig } from "./config/types"
3
- import { slitherTool } from "./tools/slither-tool"
4
- import { forgeTestTool } from "./tools/forge-test-tool"
5
- import { forgeFuzzTool } from "./tools/forge-fuzz-tool"
3
+ import { argusSkillLoadTool } from "./tools/argus-skill-load-tool"
6
4
  import { contractAnalyzerTool } from "./tools/contract-analyzer-tool"
5
+ import { forgeCoverageTool } from "./tools/forge-coverage-tool"
6
+ import { forgeFuzzTool } from "./tools/forge-fuzz-tool"
7
+ import { forgeTestTool } from "./tools/forge-test-tool"
8
+ import { gasAnalysisTool } from "./tools/gas-analysis-tool"
7
9
  import { patternCheckerTool } from "./tools/pattern-checker-tool"
8
- import { soloditSearchTool } from "./tools/solodit-search-tool"
10
+ import { proxyDetectionTool } from "./tools/proxy-detection-tool"
9
11
  import { reportGeneratorTool } from "./tools/report-generator-tool"
12
+ import { slitherTool } from "./tools/slither-tool"
13
+ import { createSoloditSearchTool } from "./tools/solodit-search-tool"
10
14
  import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
11
15
 
12
- export function createTools(
13
- config: ArgusConfig,
14
- ): Record<string, ToolDefinition> {
16
+ export function createTools(config: ArgusConfig): Record<string, ToolDefinition> {
15
17
  const tools: Record<string, ToolDefinition> = {
16
18
  argus_slither_analyze: slitherTool,
17
19
  argus_forge_test: forgeTestTool,
20
+ argus_gas_analysis: gasAnalysisTool,
18
21
  argus_forge_fuzz: forgeFuzzTool,
22
+ argus_forge_coverage: forgeCoverageTool,
19
23
  argus_analyze_contract: contractAnalyzerTool,
20
24
  argus_check_patterns: patternCheckerTool,
25
+ argus_proxy_detection: proxyDetectionTool,
26
+ argus_skill_load: argusSkillLoadTool,
21
27
  argus_generate_report: reportGeneratorTool,
22
28
  argus_sync_knowledge: syncKnowledgeTool,
23
29
  }
24
30
 
25
31
  if (config.solodit?.enabled !== false) {
26
- tools.argus_solodit_search = soloditSearchTool
32
+ tools.argus_solodit_search = createSoloditSearchTool(config.solodit?.port ?? 3000)
27
33
  }
28
34
 
29
35
  return tools