solidity-argus 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +34 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +34 -7
  75. package/src/agents/pythia-prompt.ts +13 -4
  76. package/src/agents/scribe-prompt.ts +20 -2
  77. package/src/agents/sentinel-prompt.ts +45 -5
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +6 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/system-prompt-hook.ts +18 -1
  114. package/src/hooks/tool-tracking-hook.ts +110 -51
  115. package/src/hooks/types.ts +2 -1
  116. package/src/index.ts +24 -37
  117. package/src/knowledge/retry.ts +22 -22
  118. package/src/knowledge/scvd-client.ts +88 -95
  119. package/src/knowledge/scvd-errors.ts +35 -35
  120. package/src/knowledge/scvd-index.ts +78 -80
  121. package/src/knowledge/scvd-sync.ts +106 -101
  122. package/src/managers/index.ts +1 -1
  123. package/src/managers/types.ts +19 -14
  124. package/src/plugin-interface.ts +7 -9
  125. package/src/shared/binary-utils.ts +44 -35
  126. package/src/shared/deep-merge.ts +55 -36
  127. package/src/shared/file-utils.ts +21 -19
  128. package/src/shared/index.ts +11 -5
  129. package/src/shared/jsonc-parser.ts +123 -28
  130. package/src/shared/logger.ts +16 -3
  131. package/src/shared/project-utils.ts +30 -0
  132. package/src/skills/analysis/cluster.ts +414 -0
  133. package/src/skills/analysis/gates.ts +227 -0
  134. package/src/skills/analysis/index.ts +33 -0
  135. package/src/skills/analysis/normalize.ts +217 -0
  136. package/src/skills/analysis/similarity.ts +224 -0
  137. package/src/skills/argus-skill-resolver.ts +17 -6
  138. package/src/skills/skill-schema.ts +11 -10
  139. package/src/solodit-lifecycle.ts +203 -0
  140. package/src/state/audit-state.ts +8 -8
  141. package/src/state/finding-store.ts +68 -55
  142. package/src/state/types.ts +88 -67
  143. package/src/tools/argus-skill-load-tool.ts +12 -7
  144. package/src/tools/contract-analyzer-tool.ts +142 -77
  145. package/src/tools/forge-coverage-tool.ts +226 -0
  146. package/src/tools/forge-fuzz-tool.ts +127 -127
  147. package/src/tools/forge-test-tool.ts +201 -158
  148. package/src/tools/gas-analysis-tool.ts +264 -0
  149. package/src/tools/pattern-checker-tool.ts +203 -191
  150. package/src/tools/pattern-loader.ts +5 -111
  151. package/src/tools/pattern-schema.ts +3 -0
  152. package/src/tools/proxy-detection-tool.ts +224 -0
  153. package/src/tools/report-generator-tool.ts +305 -206
  154. package/src/tools/slither-tool.ts +266 -218
  155. package/src/tools/solodit-search-tool.ts +235 -119
  156. package/src/tools/sync-knowledge-tool.ts +7 -11
  157. package/src/utils/audit-artifact-detector.ts +28 -29
  158. package/src/utils/dependency-scanner.ts +37 -37
  159. package/src/utils/project-detector.ts +111 -124
  160. package/src/utils/solidity-parser.ts +175 -75
  161. package/skills/patterns/access-control.yaml +0 -31
  162. package/skills/patterns/erc4626.yaml +0 -29
  163. package/skills/patterns/flash-loan.yaml +0 -20
  164. package/skills/patterns/oracle.yaml +0 -30
  165. package/skills/patterns/proxy.yaml +0 -30
  166. package/skills/patterns/reentrancy.yaml +0 -30
  167. package/skills/patterns/signature.yaml +0 -31
  168. package/src/hooks/event-hook-v2.ts +0 -99
  169. package/src/state/plugin-state.ts +0 -14
@@ -10,17 +10,17 @@ const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
10
10
  slither: {
11
11
  install: "pip install slither-analyzer",
12
12
  fallback:
13
- "Slither is unavailable. PROCEED with the audit using `argus_analyze_contract` for structural profiling and `argus_check_patterns` for vulnerability scanning. Note in the final report: \"Automated static analysis (Slither) was unavailable; manual review intensity increased.\"",
13
+ 'Slither is unavailable. PROCEED with the audit using `argus_analyze_contract` for structural profiling and `argus_check_patterns` for vulnerability scanning. Note in the final report: "Automated static analysis (Slither) was unavailable; manual review intensity increased."',
14
14
  },
15
15
  forge: {
16
16
  install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
17
17
  fallback:
18
- "Foundry/Forge is unavailable. SKIP automated testing and fuzzing. Verify findings through manual code tracing and static analysis. Note in the final report: \"Dynamic testing (Forge) was unavailable; findings verified via manual analysis.\"",
18
+ 'Foundry/Forge is unavailable. SKIP automated testing and fuzzing. Verify findings through manual code tracing and static analysis. Note in the final report: "Dynamic testing (Forge) was unavailable; findings verified via manual analysis."',
19
19
  },
20
20
  solodit: {
21
21
  install: "",
22
22
  fallback:
23
- "Solodit API is unreachable. PROCEED using `argus_check_patterns` with local vulnerability rules. Note in the final report: \"External vulnerability databases were inaccessible; research limited to local patterns.\"",
23
+ 'Solodit API is unreachable. PROCEED using `argus_check_patterns` with local vulnerability rules. Note in the final report: "External vulnerability databases were inaccessible; research limited to local patterns."',
24
24
  },
25
25
  scvd: {
26
26
  install: "",
@@ -54,6 +54,7 @@ function resolveToolBase(tool: string): string {
54
54
 
55
55
  export function createToolErrorRecoveryHandler(
56
56
  getAuditState?: () => AuditState | null,
57
+ updateAuditState?: (patch: Partial<AuditState>) => Promise<void>,
57
58
  ) {
58
59
  const logger = createLogger()
59
60
 
@@ -80,12 +81,16 @@ export function createToolErrorRecoveryHandler(
80
81
 
81
82
  const unavailable = isToolUnavailable(lowerResult)
82
83
 
83
- if (unavailable && getAuditState) {
84
+ if (unavailable && getAuditState && updateAuditState) {
84
85
  const state = getAuditState()
85
86
  if (state) {
86
- state.unavailableTools ??= []
87
- if (!state.unavailableTools.includes(toolBase)) {
88
- state.unavailableTools.push(toolBase)
87
+ const existing = state.unavailableTools ?? []
88
+ if (!existing.includes(toolBase)) {
89
+ void updateAuditState({
90
+ unavailableTools: [...existing, toolBase],
91
+ }).catch((error: unknown) => {
92
+ logger.warn(`Failed to persist unavailable tool state for ${toolBase}`, error)
93
+ })
89
94
  logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
90
95
  }
91
96
  }
@@ -1,5 +1,5 @@
1
- export * from './background-agent'
2
- export * from './persistent-state'
3
- export * from './context-monitor'
4
- export * from './audit-enforcer'
5
- export * from './error-recovery'
1
+ export * from "./audit-enforcer"
2
+ export * from "./background-agent"
3
+ export * from "./context-monitor"
4
+ export * from "./error-recovery"
5
+ export * from "./persistent-state"
@@ -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 = "2";
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,107 +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";
42
+ const hasSupportedVersion = value.version === "1" || value.version === "2"
43
43
 
44
44
  return (
45
- typeof value.savedAt === "number" &&
46
- hasSupportedVersion &&
47
- typeof value.filePath === "string"
48
- );
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
+ }
49
96
  }
50
97
 
51
98
  export function createAuditStateManager(projectDir: string): AuditStateManager {
52
- const logger = createLogger();
53
- const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME);
54
- 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
55
102
 
56
103
  async function load(): Promise<AuditState | null> {
57
104
  try {
58
- const file = Bun.file(stateFilePath);
105
+ const file = Bun.file(stateFilePath)
59
106
  if (!(await file.exists())) {
60
- return null;
107
+ return null
61
108
  }
62
109
 
63
- const content = await file.text();
110
+ const content = await file.text()
64
111
  if (!content.trim()) {
65
- return null;
112
+ return null
66
113
  }
67
114
 
68
- const parsed: unknown = JSON.parse(content);
115
+ const parsed: unknown = JSON.parse(content)
69
116
  if (!isPersistentAuditState(parsed)) {
70
- logger.warn("Persistent audit state is invalid, ignoring", stateFilePath);
71
- return null;
117
+ logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
118
+ return null
72
119
  }
73
120
 
74
- const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed;
121
+ const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
75
122
 
76
123
  if (version === "1") {
77
124
  if (!state.soloditResults) {
78
- state.soloditResults = [];
125
+ state.soloditResults = []
79
126
  }
80
127
  if (!state.fuzzCounterexamples) {
81
- state.fuzzCounterexamples = [];
128
+ state.fuzzCounterexamples = []
82
129
  }
83
130
  }
84
131
 
85
- currentState = state;
86
- return currentState;
87
- } catch (_error) {
88
- return null;
132
+ currentState = state
133
+ return currentState
134
+ } catch (err) {
135
+ logger.warn("Failed to load persisted audit state", err)
136
+ return null
89
137
  }
90
138
  }
91
139
 
92
- let saveInFlight = false;
140
+ let saveInFlight = false
93
141
 
94
142
  async function save(state: AuditState): Promise<void> {
95
- currentState = state;
143
+ currentState = state
96
144
 
97
- if (saveInFlight) return;
98
- saveInFlight = true;
145
+ if (saveInFlight) return
146
+ saveInFlight = true
99
147
 
100
148
  try {
101
- const persistentState: PersistentAuditState = {
102
- ...state,
103
- savedAt: Date.now(),
104
- version: STATE_VERSION,
105
- filePath: stateFilePath,
106
- };
107
-
108
- const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`;
109
- await mkdir(dirname(stateFilePath), { recursive: true });
110
- await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
111
- await rename(tempFilePath, stateFilePath);
112
- } catch {
113
- // Non-critical: state persistence is best-effort
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
114
169
  } finally {
115
- saveInFlight = false;
170
+ saveInFlight = false
116
171
  }
117
172
  }
118
173
 
119
174
  function get(): AuditState {
120
- return currentState;
175
+ return currentState
121
176
  }
122
177
 
123
178
  async function update(patch: Partial<AuditState>): Promise<void> {
124
179
  currentState = {
125
180
  ...currentState,
126
181
  ...patch,
127
- };
182
+ }
128
183
 
129
- await save(currentState);
184
+ await save(currentState)
130
185
  }
131
186
 
132
187
  async function reset(): Promise<void> {
133
- currentState = createAuditState(projectDir).state;
134
- 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)
135
217
  }
136
218
 
137
219
  return {
@@ -140,5 +222,6 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
140
222
  get,
141
223
  update,
142
224
  reset,
143
- };
225
+ archive,
226
+ }
144
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
+ }
@@ -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,14 +96,18 @@ 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",
96
111
  argus_skill_load: "allow",
97
112
  skill: "allow",
98
113
  },
@@ -100,6 +115,7 @@ export function createConfigHandler(
100
115
  pythia: {
101
116
  mode: "subagent",
102
117
  model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
118
+ steps: argusConfig.agents?.pythia?.steps ?? DEFAULT_STEPS,
103
119
  description: "Vulnerability researcher",
104
120
  prompt: PYTHIA_PROMPT,
105
121
  permission: {
@@ -112,6 +128,7 @@ export function createConfigHandler(
112
128
  scribe: {
113
129
  mode: "subagent",
114
130
  model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
131
+ steps: argusConfig.agents?.scribe?.steps ?? DEFAULT_STEPS,
115
132
  description: "Audit report writer",
116
133
  prompt: SCRIBE_PROMPT,
117
134
  permission: {
@@ -10,7 +10,7 @@
10
10
 
11
11
  const ARGUS_BUDGET = 2000
12
12
  const SUBAGENT_BUDGET = 1000
13
- const PRESSURE_THRESHOLD = 0.70
13
+ const PRESSURE_THRESHOLD = 0.7
14
14
  const PRESSURE_REDUCTION = 0.5
15
15
 
16
16
  const ARGUS_AGENTS = new Set(["argus"])
@@ -23,10 +23,7 @@ const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
23
23
  * @param contextPressure - Current context usage ratio (0.0–1.0), from ContextMonitor
24
24
  * @returns Token budget in tokens. 0 for non-Argus agents.
25
25
  */
26
- export function getTokenBudgetForAgent(
27
- agent: string,
28
- contextPressure: number = 0,
29
- ): number {
26
+ export function getTokenBudgetForAgent(agent: string, contextPressure: number = 0): number {
30
27
  let budget: number
31
28
 
32
29
  if (ARGUS_AGENTS.has(agent)) {