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
@@ -1,25 +1,25 @@
1
- import { mkdir, rename } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import type { AuditStateManager } from "../../managers/types";
4
- import { createAuditState } from "../../state/audit-state";
5
- import type { AuditState, PersistentAuditState } from "../../state/types";
6
- import { createLogger } from "../../shared/logger";
1
+ import { mkdir, rename } from "node:fs/promises"
2
+ import { dirname, join } from "node:path"
3
+ import type { AuditStateManager } from "../../managers/types"
4
+ import { createLogger } from "../../shared/logger"
5
+ import { createAuditState } from "../../state/audit-state"
6
+ import type { AuditState, PersistentAuditState } from "../../state/types"
7
7
 
8
- const STATE_FILE_DIR = ".opencode";
9
- const STATE_FILE_NAME = "argus-state.json";
10
- const STATE_VERSION = "1";
8
+ const STATE_FILE_DIR = ".opencode"
9
+ const STATE_FILE_NAME = "argus-state.json"
10
+ const STATE_VERSION = "2"
11
11
 
12
12
  function isObject(value: unknown): value is Record<string, unknown> {
13
- return typeof value === "object" && value !== null;
13
+ return typeof value === "object" && value !== null
14
14
  }
15
15
 
16
16
  function isStringArray(value: unknown): value is string[] {
17
- return Array.isArray(value) && value.every((item) => typeof item === "string");
17
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
18
18
  }
19
19
 
20
20
  function isAuditState(value: unknown): value is AuditState {
21
21
  if (!isObject(value)) {
22
- return false;
22
+ return false
23
23
  }
24
24
 
25
25
  return (
@@ -31,84 +31,189 @@ function isAuditState(value: unknown): value is AuditState {
31
31
  typeof value.currentPhase === "string" &&
32
32
  isStringArray(value.scope) &&
33
33
  typeof value.startTime === "number"
34
- );
34
+ )
35
35
  }
36
36
 
37
37
  function isPersistentAuditState(value: unknown): value is PersistentAuditState {
38
38
  if (!isAuditState(value) || !isObject(value)) {
39
- return false;
39
+ return false
40
40
  }
41
41
 
42
+ const hasSupportedVersion = value.version === "1" || value.version === "2"
43
+
42
44
  return (
43
- typeof value.savedAt === "number" &&
44
- typeof value.version === "string" &&
45
- typeof value.filePath === "string"
46
- );
45
+ typeof value.savedAt === "number" && hasSupportedVersion && typeof value.filePath === "string"
46
+ )
47
+ }
48
+
49
+ export function createDebouncedSave(
50
+ saveState: (state: AuditState) => Promise<void>,
51
+ delayMs = 5_000,
52
+ ): {
53
+ save: (state: AuditState) => void
54
+ flush: () => Promise<void>
55
+ } {
56
+ let timer: ReturnType<typeof setTimeout> | null = null
57
+ let pendingState: AuditState | null = null
58
+
59
+ async function persistPendingState(): Promise<void> {
60
+ if (!pendingState) {
61
+ return
62
+ }
63
+
64
+ const stateToPersist = pendingState
65
+ pendingState = null
66
+
67
+ try {
68
+ await saveState(stateToPersist)
69
+ } catch {
70
+ createLogger().debug("Debounced state persistence failed")
71
+ }
72
+ }
73
+
74
+ return {
75
+ save(state: AuditState): void {
76
+ pendingState = state
77
+
78
+ if (timer) {
79
+ clearTimeout(timer)
80
+ }
81
+
82
+ timer = setTimeout(() => {
83
+ timer = null
84
+ void persistPendingState()
85
+ }, delayMs)
86
+ },
87
+ async flush(): Promise<void> {
88
+ if (timer) {
89
+ clearTimeout(timer)
90
+ timer = null
91
+ }
92
+
93
+ await persistPendingState()
94
+ },
95
+ }
47
96
  }
48
97
 
49
98
  export function createAuditStateManager(projectDir: string): AuditStateManager {
50
- const logger = createLogger();
51
- const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME);
52
- let currentState: AuditState = createAuditState(projectDir).state;
99
+ const logger = createLogger()
100
+ const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME)
101
+ let currentState: AuditState = createAuditState(projectDir).state
53
102
 
54
103
  async function load(): Promise<AuditState | null> {
55
104
  try {
56
- const file = Bun.file(stateFilePath);
105
+ const file = Bun.file(stateFilePath)
57
106
  if (!(await file.exists())) {
58
- return null;
107
+ return null
59
108
  }
60
109
 
61
- const content = await file.text();
110
+ const content = await file.text()
62
111
  if (!content.trim()) {
63
- return null;
112
+ return null
64
113
  }
65
114
 
66
- const parsed: unknown = JSON.parse(content);
115
+ const parsed: unknown = JSON.parse(content)
67
116
  if (!isPersistentAuditState(parsed)) {
68
- logger.warn("Persistent audit state is invalid, ignoring", stateFilePath);
69
- return null;
117
+ logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
118
+ return null
70
119
  }
71
120
 
72
- const { savedAt: _savedAt, version: _version, filePath: _filePath, ...state } = parsed;
73
- currentState = state;
74
- return currentState;
75
- } catch (_error) {
76
- return null;
121
+ const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
122
+
123
+ if (version === "1") {
124
+ if (!state.soloditResults) {
125
+ state.soloditResults = []
126
+ }
127
+ if (!state.fuzzCounterexamples) {
128
+ state.fuzzCounterexamples = []
129
+ }
130
+ }
131
+
132
+ currentState = state
133
+ return currentState
134
+ } catch (err) {
135
+ logger.warn("Failed to load persisted audit state", err)
136
+ return null
77
137
  }
78
138
  }
79
139
 
140
+ let saveInFlight = false
141
+
80
142
  async function save(state: AuditState): Promise<void> {
81
- currentState = state;
82
-
83
- const persistentState: PersistentAuditState = {
84
- ...state,
85
- savedAt: Date.now(),
86
- version: STATE_VERSION,
87
- filePath: stateFilePath,
88
- };
89
-
90
- const tempFilePath = `${stateFilePath}.tmp`;
91
- await mkdir(dirname(stateFilePath), { recursive: true });
92
- await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
93
- await rename(tempFilePath, stateFilePath);
143
+ currentState = state
144
+
145
+ if (saveInFlight) return
146
+ saveInFlight = true
147
+
148
+ try {
149
+ while (true) {
150
+ const stateToSave = currentState
151
+
152
+ const persistentState: PersistentAuditState = {
153
+ ...stateToSave,
154
+ savedAt: Date.now(),
155
+ version: STATE_VERSION,
156
+ filePath: stateFilePath,
157
+ }
158
+
159
+ const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`
160
+ await mkdir(dirname(stateFilePath), { recursive: true })
161
+ await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`)
162
+ await rename(tempFilePath, stateFilePath)
163
+
164
+ if (currentState === stateToSave) break
165
+ }
166
+ } catch (err) {
167
+ logger.warn("Failed to persist audit state", err)
168
+ throw err
169
+ } finally {
170
+ saveInFlight = false
171
+ }
94
172
  }
95
173
 
96
174
  function get(): AuditState {
97
- return currentState;
175
+ return currentState
98
176
  }
99
177
 
100
178
  async function update(patch: Partial<AuditState>): Promise<void> {
101
179
  currentState = {
102
180
  ...currentState,
103
181
  ...patch,
104
- };
182
+ }
105
183
 
106
- await save(currentState);
184
+ await save(currentState)
107
185
  }
108
186
 
109
187
  async function reset(): Promise<void> {
110
- currentState = createAuditState(projectDir).state;
111
- await save(currentState);
188
+ currentState = createAuditState(projectDir).state
189
+ await save(currentState)
190
+ }
191
+
192
+ async function archive(): Promise<void> {
193
+ const hasContent =
194
+ currentState.findings.length > 0 ||
195
+ currentState.toolsExecuted.length > 0 ||
196
+ currentState.currentPhase !== "reconnaissance"
197
+
198
+ if (hasContent) {
199
+ try {
200
+ const archivesDir = join(dirname(stateFilePath), "archives")
201
+ await mkdir(archivesDir, { recursive: true })
202
+ const archivePath = join(archivesDir, `argus-state.${Date.now()}.json`)
203
+ const persistentState: PersistentAuditState = {
204
+ ...currentState,
205
+ savedAt: Date.now(),
206
+ version: STATE_VERSION,
207
+ filePath: archivePath,
208
+ }
209
+ await Bun.write(archivePath, `${JSON.stringify(persistentState, null, 2)}\n`)
210
+ } catch {
211
+ logger.debug("Failed to archive audit state")
212
+ }
213
+ }
214
+
215
+ currentState = createAuditState(projectDir).state
216
+ await save(currentState)
112
217
  }
113
218
 
114
219
  return {
@@ -117,5 +222,6 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
117
222
  get,
118
223
  update,
119
224
  reset,
120
- };
225
+ archive,
226
+ }
121
227
  }
@@ -0,0 +1,38 @@
1
+ import { appendFileSync } from "node:fs"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { homedir } from "node:os"
4
+ import { join } from "node:path"
5
+ import { createLogger } from "../../shared/logger"
6
+
7
+ const logger = createLogger()
8
+
9
+ const CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "runs")
10
+ const INDEX_FILE = join(CACHE_DIR, "index.jsonl")
11
+
12
+ export type RunIndexEntry = {
13
+ runId: string
14
+ opencodeSessionId?: string
15
+ projectDir: string
16
+ statePath: string
17
+ journalPath: string
18
+ startedAt: number
19
+ phase: string
20
+ findingsCount: number
21
+ }
22
+
23
+ let dirEnsured = false
24
+
25
+ async function ensureDir(): Promise<void> {
26
+ if (dirEnsured) return
27
+ await mkdir(CACHE_DIR, { recursive: true })
28
+ dirEnsured = true
29
+ }
30
+
31
+ export async function recordRun(entry: RunIndexEntry): Promise<void> {
32
+ try {
33
+ await ensureDir()
34
+ appendFileSync(INDEX_FILE, `${JSON.stringify(entry)}\n`)
35
+ } catch {
36
+ logger.debug("Failed to write global run index entry")
37
+ }
38
+ }
@@ -1 +1 @@
1
- export { createAuditStateManager } from "./audit-state-manager";
1
+ export { createAuditStateManager } from "./audit-state-manager"
@@ -0,0 +1,86 @@
1
+ import { appendFile, mkdir } from "node:fs/promises"
2
+ import { dirname, join } from "node:path"
3
+ import { createLogger } from "../../shared/logger"
4
+
5
+ const logger = createLogger()
6
+
7
+ const JOURNAL_DIR = ".opencode"
8
+ const JOURNAL_FILE = "argus-journal.jsonl"
9
+
10
+ export type JournalEvent =
11
+ | { type: "session.created"; sessionId?: string; timestamp: number }
12
+ | {
13
+ type: "session.idle"
14
+ timestamp: number
15
+ findingsCount: number
16
+ toolsExecutedCount: number
17
+ }
18
+ | { type: "session.deleted"; timestamp: number; archived: boolean }
19
+ | {
20
+ type: "tool.executed"
21
+ tool: string
22
+ timestamp: number
23
+ findingsCount: number
24
+ }
25
+ | { type: "state.saved"; timestamp: number; success: boolean }
26
+ | {
27
+ type: "state.loaded"
28
+ timestamp: number
29
+ success: boolean
30
+ findingsCount: number
31
+ }
32
+
33
+ export function createRunJournal(projectDir: string): {
34
+ log(event: JournalEvent): void
35
+ close(): Promise<void>
36
+ getPath(): string
37
+ } {
38
+ const journalPath = join(projectDir, JOURNAL_DIR, JOURNAL_FILE)
39
+ let ensureDirPromise: Promise<void> | null = null
40
+ const pendingWrites = new Set<Promise<void>>()
41
+
42
+ function ensureDirectory(): Promise<void> {
43
+ if (!ensureDirPromise) {
44
+ ensureDirPromise = (async () => {
45
+ try {
46
+ await mkdir(dirname(journalPath), { recursive: true })
47
+ } catch {
48
+ logger.debug("Failed to create run journal directory")
49
+ }
50
+ })()
51
+ }
52
+
53
+ return ensureDirPromise
54
+ }
55
+
56
+ function trackWrite(writePromise: Promise<void>): void {
57
+ pendingWrites.add(writePromise)
58
+ void writePromise.finally(() => {
59
+ pendingWrites.delete(writePromise)
60
+ })
61
+ }
62
+
63
+ function log(event: JournalEvent): void {
64
+ const line = `${JSON.stringify(event)}\n`
65
+
66
+ const writePromise = ensureDirectory()
67
+ .then(async () => {
68
+ await appendFile(journalPath, line, "utf8")
69
+ })
70
+ .catch(() => {
71
+ logger.debug("Failed to append run journal event")
72
+ })
73
+
74
+ trackWrite(writePromise)
75
+ }
76
+
77
+ async function close(): Promise<void> {
78
+ await Promise.allSettled(Array.from(pendingWrites))
79
+ }
80
+
81
+ return {
82
+ log,
83
+ close,
84
+ getPath: () => journalPath,
85
+ }
86
+ }
@@ -0,0 +1,53 @@
1
+ import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
+
3
+ type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
4
+ agent?: string
5
+ }
6
+ type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
7
+
8
+ const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
9
+
10
+ export type AgentTracker = ReturnType<typeof createAgentTracker>
11
+
12
+ export function createAgentTracker() {
13
+ const sessions = new Map<string, string>()
14
+
15
+ const trackSession = (sessionID: string, agent?: string): void => {
16
+ if (!agent) {
17
+ return
18
+ }
19
+
20
+ sessions.set(sessionID, agent)
21
+ }
22
+
23
+ return {
24
+ chatParamsHook: (input: ChatParamsInput): void => {
25
+ trackSession(input.sessionID, input.agent)
26
+ },
27
+
28
+ chatMessageHook: (input: ChatMessageInput): void => {
29
+ trackSession(input.sessionID, input.agent)
30
+ },
31
+
32
+ getAgentForSession: (sessionID: string): string | undefined => {
33
+ return sessions.get(sessionID)
34
+ },
35
+
36
+ isArgusAgent: (sessionID: string): boolean => {
37
+ const agent = sessions.get(sessionID)
38
+ if (!agent) {
39
+ return false
40
+ }
41
+
42
+ return ARGUS_FAMILY.has(agent)
43
+ },
44
+
45
+ clearSession: (sessionID: string): void => {
46
+ sessions.delete(sessionID)
47
+ },
48
+
49
+ getTrackedSessions: (): Map<string, string> => {
50
+ return sessions
51
+ },
52
+ }
53
+ }
@@ -1,50 +1,59 @@
1
1
  import type { AuditState, FindingSeverity } from "../state/types"
2
+ import type { ReconContext } from "./recon-context-builder"
3
+ import { buildReconContextBlock } from "./recon-context-builder"
2
4
 
3
- /**
4
- * Creates a compaction hook that serializes audit state into XML format
5
- * so findings survive context window compression.
6
- *
7
- * The returned hook is called by OpenCode's `experimental.session.compacting`
8
- * event, receiving `{ summary: string }` and returning the enriched summary.
9
- */
10
5
  export function createCompactionHook(
11
- getAuditState: () => AuditState | null
6
+ getAuditState: () => AuditState | null,
7
+ getReconContext?: () => ReconContext | null,
12
8
  ): (input: { summary: string }) => Promise<string | null> {
13
9
  return async (_input: { summary: string }): Promise<string | null> => {
14
10
  const state = getAuditState()
15
- if (!state) {
16
- return null
17
- }
18
11
 
19
- const severityCounts: Record<FindingSeverity, number> = {
20
- Critical: 0,
21
- High: 0,
22
- Medium: 0,
23
- Low: 0,
24
- Informational: 0,
12
+ const parts: string[] = []
13
+
14
+ if (state) {
15
+ const severityCounts: Record<FindingSeverity, number> = {
16
+ Critical: 0,
17
+ High: 0,
18
+ Medium: 0,
19
+ Low: 0,
20
+ Informational: 0,
21
+ }
22
+
23
+ for (const finding of state.findings) {
24
+ severityCounts[finding.severity]++
25
+ }
26
+
27
+ const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
28
+ const contracts = state.contractsReviewed.join(", ")
29
+ const started = new Date(state.startTime).toISOString()
30
+
31
+ parts.push(
32
+ [
33
+ "<argus-audit-state>",
34
+ `Phase: ${state.currentPhase}`,
35
+ `Contracts Reviewed: ${contracts}`,
36
+ "Findings:",
37
+ ` Critical: ${severityCounts.Critical}`,
38
+ ` High: ${severityCounts.High}`,
39
+ ` Medium: ${severityCounts.Medium}`,
40
+ ` Low: ${severityCounts.Low}`,
41
+ ` Informational: ${severityCounts.Informational}`,
42
+ `Tools Executed: ${toolNames}`,
43
+ `Started: ${started}`,
44
+ "</argus-audit-state>",
45
+ ].join("\n"),
46
+ )
25
47
  }
26
48
 
27
- for (const finding of state.findings) {
28
- severityCounts[finding.severity]++
49
+ if (getReconContext) {
50
+ const recon = getReconContext()
51
+ if (recon) {
52
+ const reconBlock = buildReconContextBlock(recon)
53
+ if (reconBlock) parts.push(reconBlock)
54
+ }
29
55
  }
30
56
 
31
- const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
32
- const contracts = state.contractsReviewed.join(", ")
33
- const started = new Date(state.startTime).toISOString()
34
-
35
- return [
36
- "<argus-audit-state>",
37
- `Phase: ${state.currentPhase}`,
38
- `Contracts Reviewed: ${contracts}`,
39
- "Findings:",
40
- ` Critical: ${severityCounts.Critical}`,
41
- ` High: ${severityCounts.High}`,
42
- ` Medium: ${severityCounts.Medium}`,
43
- ` Low: ${severityCounts.Low}`,
44
- ` Informational: ${severityCounts.Informational}`,
45
- `Tools Executed: ${toolNames}`,
46
- `Started: ${started}`,
47
- "</argus-audit-state>",
48
- ].join("\n")
57
+ return parts.length > 0 ? parts.join("\n") : null
49
58
  }
50
59
  }
@@ -1,17 +1,19 @@
1
- import { resolve, join } from "node:path"
2
1
  import { existsSync, readdirSync } from "node:fs"
3
2
  import { homedir } from "node:os"
3
+ import { join, resolve } from "node:path"
4
4
  import type { Config } from "@opencode-ai/sdk/v2"
5
- import type { ArgusConfig } from "../config/types"
6
- import { DEFAULT_MODELS } from "../constants/defaults"
7
- import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
8
5
  import { ARGUS_PROMPT } from "../agents/argus-prompt"
9
- import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
10
6
  import { PYTHIA_PROMPT } from "../agents/pythia-prompt"
11
7
  import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
8
+ import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
9
+ import type { ArgusConfig } from "../config/types"
10
+ import { DEFAULT_MODELS, DEFAULT_STEPS } from "../constants/defaults"
11
+ import { createLogger } from "../shared/logger"
12
+ import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
12
13
 
13
14
  const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
14
15
  const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
16
+ const TOB_BRANCH = "main"
15
17
  let tobCloneInFlight = false
16
18
 
17
19
  function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
@@ -40,24 +42,32 @@ function ensureTrailOfBitsSkills(): string[] {
40
42
  if (!tobCloneInFlight) {
41
43
  tobCloneInFlight = true
42
44
  const cloneProcess = Bun.spawn(
43
- ["git", "clone", "--depth", "1", TOB_REPO_URL, TOB_CACHE_DIR],
45
+ ["git", "clone", "--depth", "1", "--branch", TOB_BRANCH, TOB_REPO_URL, TOB_CACHE_DIR],
44
46
  {
45
47
  stdin: "ignore",
46
48
  stdout: "ignore",
47
49
  stderr: "ignore",
50
+ signal: AbortSignal.timeout(60_000),
48
51
  },
49
52
  )
50
- cloneProcess.exited.finally(() => {
51
- tobCloneInFlight = false
52
- })
53
+ cloneProcess.exited
54
+ .then((code) => {
55
+ if (code !== 0) {
56
+ const logger = createLogger()
57
+ logger.warn(`Trail of Bits skills clone failed with exit code ${code}`)
58
+ }
59
+ })
60
+ .finally(() => {
61
+ tobCloneInFlight = false
62
+ })
53
63
  }
54
64
 
55
- return []
65
+ return []
56
66
  }
57
67
 
58
68
  export function createConfigHandler(
59
69
  argusConfig: ArgusConfig,
60
- projectDir: string = process.cwd()
70
+ projectDir: string = process.cwd(),
61
71
  ): (config: Config) => Promise<void> {
62
72
  const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
63
73
 
@@ -67,6 +77,7 @@ export function createConfigHandler(
67
77
  argus: {
68
78
  mode: "primary",
69
79
  model: argusConfig.agents?.argus?.model ?? DEFAULT_MODELS.argus,
80
+ steps: argusConfig.agents?.argus?.steps ?? DEFAULT_STEPS,
70
81
  description: "Solidity security auditor — the All-Seeing Guardian",
71
82
  prompt: ARGUS_PROMPT,
72
83
  tools: {
@@ -85,35 +96,44 @@ export function createConfigHandler(
85
96
  sentinel: {
86
97
  mode: "subagent",
87
98
  model: argusConfig.agents?.sentinel?.model ?? DEFAULT_MODELS.sentinel,
99
+ steps: argusConfig.agents?.sentinel?.steps ?? DEFAULT_STEPS,
88
100
  description: "Static analysis and testing specialist",
89
101
  prompt: SENTINEL_PROMPT,
90
102
  permission: {
91
103
  argus_slither_analyze: "allow",
92
104
  argus_forge_test: "allow",
105
+ argus_gas_analysis: "allow",
93
106
  argus_forge_fuzz: "allow",
94
107
  argus_analyze_contract: "allow",
95
108
  argus_check_patterns: "allow",
109
+ argus_proxy_detection: "allow",
110
+ argus_forge_coverage: "allow",
111
+ argus_skill_load: "allow",
96
112
  skill: "allow",
97
113
  },
98
114
  },
99
115
  pythia: {
100
116
  mode: "subagent",
101
117
  model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
118
+ steps: argusConfig.agents?.pythia?.steps ?? DEFAULT_STEPS,
102
119
  description: "Vulnerability researcher",
103
120
  prompt: PYTHIA_PROMPT,
104
121
  permission: {
105
122
  argus_solodit_search: "allow",
106
123
  argus_check_patterns: "allow",
124
+ argus_skill_load: "allow",
107
125
  skill: "allow",
108
126
  },
109
127
  },
110
128
  scribe: {
111
129
  mode: "subagent",
112
130
  model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
131
+ steps: argusConfig.agents?.scribe?.steps ?? DEFAULT_STEPS,
113
132
  description: "Audit report writer",
114
133
  prompt: SCRIBE_PROMPT,
115
134
  permission: {
116
135
  argus_generate_report: "allow",
136
+ argus_skill_load: "allow",
117
137
  skill: "allow",
118
138
  },
119
139
  },