solidity-argus 0.3.4 → 0.3.6

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 (36) hide show
  1. package/package.json +4 -4
  2. package/src/agents/argus-prompt.ts +56 -2
  3. package/src/agents/pythia-prompt.ts +11 -0
  4. package/src/agents/scribe-prompt.ts +9 -4
  5. package/src/agents/sentinel-prompt.ts +10 -0
  6. package/src/cli/commands/init.ts +1 -1
  7. package/src/config/schema.ts +2 -2
  8. package/src/create-hooks.ts +95 -12
  9. package/src/create-tools.ts +2 -0
  10. package/src/features/audit-enforcer/audit-enforcer.ts +30 -2
  11. package/src/features/persistent-state/audit-state-manager.ts +180 -10
  12. package/src/features/persistent-state/event-sink.ts +15 -6
  13. package/src/features/persistent-state/findings-materializer.ts +52 -0
  14. package/src/features/persistent-state/index.ts +1 -1
  15. package/src/features/persistent-state/run-finalizer.ts +26 -7
  16. package/src/features/persistent-state/run-journal.ts +12 -4
  17. package/src/hooks/event-hook.ts +4 -1
  18. package/src/hooks/system-prompt-hook.ts +15 -0
  19. package/src/hooks/tool-tracking-hook.ts +168 -10
  20. package/src/shared/audit-artifact-resolver.ts +13 -12
  21. package/src/shared/file-utils.ts +7 -2
  22. package/src/shared/index.ts +8 -8
  23. package/src/shared/path-root-resolver.ts +34 -0
  24. package/src/shared/plugin-metadata.ts +23 -0
  25. package/src/shared/report-path-resolver.ts +3 -3
  26. package/src/state/adapters.ts +99 -5
  27. package/src/state/finding-aggregation.ts +100 -0
  28. package/src/state/finding-fingerprint.ts +47 -0
  29. package/src/state/finding-store.ts +19 -29
  30. package/src/state/projectors.ts +18 -4
  31. package/src/state/schemas.ts +145 -1
  32. package/src/state/types.ts +17 -1
  33. package/src/tools/record-finding-tool.ts +125 -0
  34. package/src/tools/report-generator-tool.ts +116 -7
  35. package/src/tools/report-preflight.ts +79 -0
  36. package/src/tools/solodit-search-tool.ts +6 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidity-argus",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Solidity smart contract security auditing plugin for OpenCode — 4 specialized agents, 12 tools (11 core + optional Solodit), and a curated vulnerability knowledge base",
5
5
  "keywords": [
6
6
  "solidity",
@@ -25,8 +25,8 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "bin": {
28
- "solidity-argus": "./src/cli/index.ts",
29
- "argus": "./src/cli/index.ts"
28
+ "solidity-argus": "src/cli/index.ts",
29
+ "argus": "src/cli/index.ts"
30
30
  },
31
31
  "files": [
32
32
  "src/",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "repository": {
68
68
  "type": "git",
69
- "url": "https://github.com/Apegurus/solidity-argus"
69
+ "url": "git+https://github.com/Apegurus/solidity-argus.git"
70
70
  },
71
71
  "engines": {
72
72
  "bun": ">=1.0.0"
@@ -225,6 +225,52 @@ Task(subagent_type="scribe", prompt="Generate the final audit report for Project
225
225
  \`\`\`
226
226
  - Wait for both to complete before synthesizing their results.
227
227
 
228
+ ### STATE-FIRST SYNTHESIS POLICY
229
+
230
+ **Synthesize and report from durable evidence — not transcript tails.**
231
+
232
+ When building the final report or synthesizing findings:
233
+ 1. **Primary source**: \`toolsExecuted\` records, \`findings\` from state, and event stream data persisted via argus_* tool outputs.
234
+ 2. **Secondary source**: Tool transcript text (use only when durable evidence is unavailable or incomplete).
235
+ 3. **Never** synthesize findings from ephemeral background transcript retrieval alone if durable state evidence exists.
236
+ 4. **Manual-finding durability**: If Argus, Sentinel, or Pythia identifies a finding outside analyzer tool payloads, they must call \
237
+ \`argus_record_finding\` before proceeding.
238
+ 5. **Report parity rule**: Scribe must not include findings in \`report_input\` unless they are event-backed (recorded via tools/events).
239
+
240
+ **Bounded background fan-out**: For deep audits, limit concurrent high-context background delegations to max 2 at a time. Split larger workloads into sequential waves. This prevents retrieval blind spots from simultaneous long-running tasks.
241
+
242
+ Example — correct fan-out:
243
+ - Wave 1: [Sentinel: slither + pattern check] + [Pythia: solodit search] (2 background tasks)
244
+ - Wait for both. Then Wave 2: [Sentinel: forge tests] (1 background task)
245
+
246
+ ## SYNTHESIS BARRIER: MUST NOT PROCEED WITHOUT DURABLE EVIDENCE
247
+
248
+ You **must not proceed** to synthesis or report generation until required durable evidence is confirmed present:
249
+ - \`toolsExecuted\` records exist for all planned tools
250
+ - Expected findings coverage is populated in state
251
+ - Lifecycle invariants are satisfied (no orphaned tool starts)
252
+
253
+ ### Adaptive Retrieval Budget
254
+
255
+ When waiting for background tasks, use bounded retrieval budgets by workload class:
256
+
257
+ | Class | Budget | Criteria |
258
+ |----------|---------|---------------------------------------------|
259
+ | quick | 60s | Single-tool or single-contract checks |
260
+ | standard | 180s | Multi-tool single-agent batches |
261
+ | deep | 600s | Multi-agent or synthesis-heavy runs |
262
+
263
+ Poll until the task reaches a terminal state: \`completed\`, \`error\`, \`cancelled\`, or \`interrupt\`.
264
+
265
+ ### Re-dispatch (LAST RESORT)
266
+
267
+ Re-dispatch is only justified when ALL of these are true:
268
+ 1. The task has reached terminal state OR retrieval budget has expired
269
+ 2. Required durable evidence is STILL missing from state/events
270
+ 3. The gap is specific and bounded (not a general "redo everything")
271
+
272
+ **When re-dispatching**: Target only missing evidence segments. Use \`run_in_background=false\` (foreground only) for re-dispatch pivots. Do NOT re-dispatch routinely after a single transcript retrieval miss if durable state evidence is already complete.
273
+
228
274
  ## TASK COMPLETION TRACKING
229
275
 
230
276
  You must track which audit phases are complete to avoid redundant work and tool re-execution.
@@ -272,7 +318,12 @@ Your subagents have access to these specialized tools. Know when to delegate eac
272
318
  - **\`argus_generate_report\`**:
273
319
  - **Use**: During Reporting.
274
320
  - **Purpose**: Generates the final artifact.
275
- - **Note**: Requires a versioned report_input JSON string matching the ReportInput contract (schema_version 1.0.0). Do not send natural-language-only findings to Scribe for tool invocation.
321
+ - **Note**: Requires a versioned report_input JSON string matching the ReportInput contract (schema_version 2.0.0). Do not send natural-language-only findings to Scribe for tool invocation.
322
+
323
+ - **\`argus_record_finding\`**:
324
+ - **Use**: Whenever a manual/non-tool finding is identified.
325
+ - **Purpose**: Persist manually identified findings as canonical event-backed observations before reporting.
326
+ - **Note**: Accepts a single finding or an array. Call it immediately when the finding is identified.
276
327
 
277
328
  - **\`argus_sync_knowledge\`**:
278
329
  - **Use**: Maintenance.
@@ -424,6 +475,8 @@ Tools may fail. You must be resilient.
424
475
 
425
476
  After you have synthesized your findings, build a canonical ReportInput payload and invoke Scribe:
426
477
 
478
+ **State-first requirement**: Before invoking Scribe, verify that \`toolsExecuted\` in your ReportInput contains entries for each tool you ran. Do NOT proceed to report generation if required tool coverage is missing from durable state — re-run the missing tool instead. Use \`preflight_policy: "strict-fail"\` for the final report invocation.
479
+
427
480
  \`\`\`
428
481
  Task(subagent_type="scribe", prompt="Generate the final security audit report.
429
482
 
@@ -436,7 +489,7 @@ ReportInput JSON (pass EXACTLY, no prose substitution):
436
489
  "session_id": "{session-id}",
437
490
  "tool_call_id": "{tool-call-id}",
438
491
  "source": "argus",
439
- "schema_version": "1.0.0",
492
+ "schema_version": "2.0.0",
440
493
  "projectDir": "{project-dir}",
441
494
  "findings": [canonical findings],
442
495
  "toolsExecuted": [canonical tool executions],
@@ -454,6 +507,7 @@ Scribe must call argus_generate_report with:
454
507
  - project_name: project name
455
508
  - scope: audited file list
456
509
  - report_input: serialized ReportInput JSON string
510
+ - preflight_policy: "strict-fail" (non-negotiable for final report)
457
511
 
458
512
  Legacy audit_state is transitional-only and deprecated.
459
513
 
@@ -49,6 +49,7 @@ You must follow this structured research process:
49
49
  ### 4. Reporting
50
50
  - **Objective**: Deliver actionable intelligence to Argus.
51
51
  - **Actions**:
52
+ - If you identify a manual finding from precedent/pattern reasoning, call \`argus_record_finding\` before reporting back.
52
53
  - Format findings clearly, citing the precedent (e.g., "Similar to the Cream Finance hack").
53
54
  - Assess severity based on the *likelihood* of exploitation in this specific context.
54
55
 
@@ -84,6 +85,16 @@ You have two primary tools. Master them.
84
85
  - Returns a list of matches with line numbers.
85
86
  - **Crucial**: You must verify the context. A regex match for \`selfdestruct\` is not a bug if it's in a test file or a legitimate upgrade mechanism (though still risky).
86
87
 
88
+ ### 3. \`argus_record_finding\`
89
+ **Purpose**: Persist research/manual findings into durable event-backed observations.
90
+ **When to use**:
91
+ - Whenever your finding is derived from precedent analysis or manual reasoning rather than a direct analyzer payload.
92
+ **Arguments**:
93
+ - \`finding\` (string): Serialized JSON object for one finding.
94
+ - \`findings\` (string): Serialized JSON array for multiple findings.
95
+ **Interpretation**:
96
+ - A finding is not report-ready until it has been recorded through this tool.
97
+
87
98
  ## EMPTY RESULTS STRATEGY
88
99
 
89
100
  When \`argus_solodit_search\` returns zero results for a query:
@@ -44,10 +44,15 @@ You must adhere to these strict writing standards:
44
44
  Argus passes you structured report data. Use that payload directly and keep it schema-accurate.
45
45
 
46
46
  **Your workflow**:
47
- 1. Validate Argus provided a serialized ReportInput JSON string (schema_version 1.0.0) with required fields: run_id, seq, session_id, tool_call_id, source, schema_version, projectDir, findings, toolsExecuted, scope.
48
- 2. Write the complete report in Markdown following the Report Structure and Output Format sections.
49
- 3. Call \`argus_generate_report\` with arguments { project_name, scope, report_input }. Use legacy \`audit_state\` only for transitional compatibility and treat it as deprecated.
50
- 4. Confirm the report was generated in your response to Argus: "Report generated via argus_generate_report: {filePath}".
47
+ 1. Validate Argus provided a serialized ReportInput JSON string (schema_version 2.0.0) with required fields: run_id, seq, session_id, tool_call_id, source, schema_version, projectDir, findings, toolsExecuted, scope. **Execution integrity check**: \`toolsExecuted\` must be non-empty for the audit to be considered complete. If \`toolsExecuted\` is empty or missing key tool families (slither, forge, patterns), add a \`## Limitations\` section to the report noting which tool coverage is absent.
48
+ 2. Enforce parity: do not include findings unless they are event-backed observations (recorded through tool/event flow, including \`argus_record_finding\`).
49
+ 3. Write the complete report in Markdown following the Report Structure and Output Format sections.
50
+ 4. Call \`argus_generate_report\` with arguments { project_name, scope, report_input }. Use legacy \`audit_state\` only for transitional compatibility and treat it as deprecated.
51
+ 5. **Limitations disclosure** (MANDATORY when tools fail): If any tool was unavailable, timed out, or failed, add a \`## Limitations\` section to the report BEFORE \`## Findings\`. Use this format:
52
+ - \`**Tool name**: [reason \u2014 unavailable/failed/timed out]. [Impact on finding coverage if any.]\`
53
+ - Example: \`**argus_solodit_search**: External database was unavailable. Known-vulnerability cross-referencing was performed using local patterns only.\`
54
+ - Never silently omit limitations — incomplete coverage must be disclosed.
55
+ 6. Confirm the report was generated in your response to Argus: "Report generated via argus_generate_report: {filePath}".
51
56
 
52
57
  ## SINGLE-WRITER POLICY
53
58
 
@@ -32,6 +32,7 @@ You operate in a loop of **Scan -> Analyze -> Verify**.
32
32
 
33
33
  4. **Reporting**:
34
34
  - Format your findings strictly according to the Output Format section.
35
+ - If you identify a manual finding outside analyzer payloads, call \`argus_record_finding\` immediately.
35
36
  - Report back to Argus with confirmed findings.
36
37
 
37
38
  ## POC VERIFICATION
@@ -127,6 +128,15 @@ You have access to a specific set of tools. Use them effectively.
127
128
  - High gas consumption often correlates with complex logic, unbounded loops, or storage-heavy operations.
128
129
  - Gas hotspots are prime candidates for DoS vulnerabilities.
129
130
 
131
+ ### 9. \`argus_record_finding\`
132
+ **Purpose**: Persist manual/non-tool findings as canonical event-backed observations.
133
+ **When to use**: Any time you manually confirm a finding that did not come from \`argus_slither_analyze\` or \`argus_check_patterns\` payloads.
134
+ **Arguments**:
135
+ - \`finding\` (string): Serialized JSON object for a single finding.
136
+ - \`findings\` (string): Serialized JSON array for multiple findings.
137
+ **Interpretation**:
138
+ - Recording is mandatory before handing findings to Argus for final synthesis.
139
+
130
140
  ## SKILL SYSTEM
131
141
 
132
142
  Use \`argus_skill_load\` only when specialized context is needed before deep verification work.
@@ -20,7 +20,7 @@ export const initCommand: CliCommand = {
20
20
  description: "Initialize Argus configuration for this project",
21
21
  async execute(_args: string[]): Promise<number> {
22
22
  const cwd = process.cwd()
23
- const configDir = join(cwd, ".opencode")
23
+ const configDir = join(cwd, ".argus")
24
24
  const configPath = join(configDir, "solidity-argus.json")
25
25
 
26
26
  if (existsSync(configPath)) {
@@ -31,7 +31,7 @@ const ReportingConfigSchema = z.object({
31
31
  format: z.enum(["markdown"]).default("markdown"),
32
32
  severityThreshold: z.enum(["critical", "high", "medium", "low", "informational"]).default("low"),
33
33
  gasAnalysis: z.boolean().default(false),
34
- output_dir: z.string().default(".opencode/reports/"),
34
+ output_dir: z.string().default(".argus/reports/"),
35
35
  })
36
36
 
37
37
  const SoloditConfigSchema = z.object({
@@ -74,7 +74,7 @@ export const ArgusConfigSchema = z.object({
74
74
  format: "markdown",
75
75
  severityThreshold: "low",
76
76
  gasAnalysis: false,
77
- output_dir: ".opencode/reports/",
77
+ output_dir: ".argus/reports/",
78
78
  }),
79
79
  solodit: SoloditConfigSchema.default({
80
80
  enabled: true,
@@ -1,4 +1,3 @@
1
- import { join } from "node:path"
2
1
  import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
3
2
  import type { ArgusConfig } from "./config/types"
4
3
  import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
@@ -7,9 +6,12 @@ import {
7
6
  createSessionRecoveryHandler,
8
7
  createToolErrorRecoveryHandler,
9
8
  } from "./features/error-recovery"
10
- import { createDebouncedSave } from "./features/persistent-state/audit-state-manager"
11
9
  import { getMigrationMode } from "./features/migration"
10
+ import { adaptLegacyFindings } from "./features/migration/migration-adapter"
11
+ import { computeParityMetrics, formatParityReport } from "./features/migration/parity-telemetry"
12
+ import { createDebouncedSave } from "./features/persistent-state/audit-state-manager"
12
13
  import { createEventSink, type EventSink } from "./features/persistent-state/event-sink"
14
+ import { materializeFindings } from "./features/persistent-state/findings-materializer"
13
15
  import { recordRun } from "./features/persistent-state/global-run-index"
14
16
  import { createRunJournal } from "./features/persistent-state/run-journal"
15
17
  import { createAgentTracker } from "./hooks/agent-tracker"
@@ -24,8 +26,8 @@ import { createSystemPromptHook } from "./hooks/system-prompt-hook"
24
26
  import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
25
27
  import type { HookName } from "./hooks/types"
26
28
  import type { Managers } from "./managers/types"
27
- import { createLogger } from "./shared/logger"
28
29
  import { createAuditArtifactResolver } from "./shared/audit-artifact-resolver"
30
+ import { createLogger } from "./shared/logger"
29
31
  import type { AuditState } from "./state/types"
30
32
  import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
31
33
  import { detectProject, type ProjectConfig } from "./utils/project-detector"
@@ -135,10 +137,8 @@ export function createHooks(args: {
135
137
 
136
138
  const effectiveState = recoveredState ?? auditStateManager.get()
137
139
  if (effectiveState) {
140
+ const resolver = createAuditArtifactResolver(effectiveState.sessionId, projectDir)
138
141
  try {
139
- // createAuditArtifactResolver is the canonical source for all run artifact paths.
140
- // The journal file path is: {projectDir}/.opencode/runs/{runId}/events.jsonl
141
- const resolver = createAuditArtifactResolver(effectiveState.sessionId, projectDir)
142
142
  const journalFile = resolver.paths().journalFile
143
143
  // createEventSink builds the same path internally; the resolver makes it explicit.
144
144
  currentEventSink = createEventSink(effectiveState.sessionId, projectDir)
@@ -149,13 +149,12 @@ export function createHooks(args: {
149
149
  `Failed to create event sink: ${error instanceof Error ? error.message : String(error)}`,
150
150
  )
151
151
  }
152
-
153
152
  void recordRun({
154
153
  runId: effectiveState.sessionId,
155
154
  opencodeSessionId: sessionId,
156
155
  projectDir: effectiveState.projectDir,
157
- statePath: join(effectiveState.projectDir, ".opencode", "argus-state.json"),
158
- journalPath: join(effectiveState.projectDir, ".opencode", "argus-journal.jsonl"),
156
+ statePath: resolver.paths().stateFile,
157
+ journalPath: resolver.paths().journalFile,
159
158
  startedAt: effectiveState.startTime,
160
159
  phase: effectiveState.currentPhase,
161
160
  findingsCount: effectiveState.findings.length,
@@ -188,17 +187,36 @@ export function createHooks(args: {
188
187
  toolsExecutedCount: auditState.toolsExecuted.length,
189
188
  })
190
189
 
190
+ const idleResolver = createAuditArtifactResolver(
191
+ auditState.sessionId,
192
+ auditState.projectDir,
193
+ )
191
194
  void recordRun({
192
195
  runId: auditState.sessionId,
193
196
  opencodeSessionId: sessionId,
194
197
  projectDir: auditState.projectDir,
195
- statePath: join(auditState.projectDir, ".opencode", "argus-state.json"),
196
- journalPath: join(auditState.projectDir, ".opencode", "argus-journal.jsonl"),
198
+ statePath: idleResolver.paths().stateFile,
199
+ journalPath: idleResolver.paths().journalFile,
197
200
  startedAt: auditState.startTime,
198
201
  phase: auditState.currentPhase,
199
202
  findingsCount: auditState.findings.length,
200
203
  })
201
204
 
205
+ if (migrationMode !== "legacy") {
206
+ try {
207
+ const { legacyFindings, canonicalFindings } = adaptLegacyFindings(
208
+ auditState,
209
+ migrationMode,
210
+ auditState.sessionId,
211
+ )
212
+ const parityMetrics = computeParityMetrics(legacyFindings, canonicalFindings)
213
+ logger.debug(formatParityReport(parityMetrics))
214
+ } catch (error) {
215
+ logger.warn(
216
+ `Migration parity check failed: ${error instanceof Error ? error.message : String(error)}`,
217
+ )
218
+ }
219
+ }
202
220
  return
203
221
  }
204
222
 
@@ -292,21 +310,82 @@ export function createHooks(args: {
292
310
  {
293
311
  getEventSink: () => currentEventSink,
294
312
  getSessionId: () => currentOpencodeSessionId,
313
+ getAgentName: () => {
314
+ if (!currentOpencodeSessionId) {
315
+ return undefined
316
+ }
317
+
318
+ const agent = agentTracker.getAgentForSession(currentOpencodeSessionId)
319
+ if (
320
+ agent === "argus" ||
321
+ agent === "sentinel" ||
322
+ agent === "pythia" ||
323
+ agent === "scribe" ||
324
+ agent === "unknown"
325
+ ) {
326
+ return agent
327
+ }
328
+
329
+ return "unknown"
330
+ },
295
331
  },
296
332
  ),
297
333
  "tool-tracking",
298
334
  )
299
335
  : undefined
300
336
 
337
+ const materializeCurrentFindings = async (
338
+ trigger: "session.idle" | "tool.execute.after",
339
+ failFast = false,
340
+ ): Promise<void> => {
341
+ const state = getAuditState()
342
+ if (!state || state.sessionId.length === 0) {
343
+ return
344
+ }
345
+
346
+ try {
347
+ await materializeFindings(
348
+ state.sessionId,
349
+ state.projectDir,
350
+ currentOpencodeSessionId.length > 0 ? currentOpencodeSessionId : undefined,
351
+ )
352
+ } catch (error) {
353
+ if (failFast) {
354
+ throw new Error(
355
+ `Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
356
+ )
357
+ }
358
+
359
+ logger.warn(
360
+ `Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
361
+ )
362
+ }
363
+ }
364
+
301
365
  const safeEventHook = isHookEnabled("event")
302
366
  ? safeCreateHook(
303
367
  () => async (input: Parameters<typeof eventHook>[0]) => {
304
368
  const isSessionDeleted = input.event.type === "session.deleted"
369
+ const finalizationBeforeDelete = isSessionDeleted ? getLastFinalizationResult() : null
305
370
 
306
371
  try {
307
372
  await eventHook(input)
308
373
  } finally {
309
374
  if (isSessionDeleted) {
375
+ const finalizationResult = getLastFinalizationResult()
376
+ const hasNewFinalization =
377
+ finalizationResult !== null && finalizationResult !== finalizationBeforeDelete
378
+
379
+ if (hasNewFinalization && finalizationResult.runId.length > 0) {
380
+ try {
381
+ await materializeFindings(finalizationResult.runId, projectDir)
382
+ } catch (error) {
383
+ logger.warn(
384
+ `Failed to materialize findings artifact for run ${finalizationResult.runId}: ${error instanceof Error ? error.message : String(error)}`,
385
+ )
386
+ }
387
+ }
388
+
310
389
  await auditStateManager.archive()
311
390
 
312
391
  const deletedSessionId = input.event.sessionId
@@ -318,7 +397,7 @@ export function createHooks(args: {
318
397
  type: "session.deleted",
319
398
  timestamp: Date.now(),
320
399
  archived: true,
321
- finalizationPassed: getLastFinalizationResult()?.invariantsPassed ?? null,
400
+ finalizationPassed: finalizationResult?.invariantsPassed ?? null,
322
401
  })
323
402
 
324
403
  currentEventSink = null
@@ -360,6 +439,10 @@ export function createHooks(args: {
360
439
  result: output.output,
361
440
  })
362
441
 
442
+ if (input.tool === "argus_generate_report") {
443
+ await materializeCurrentFindings("tool.execute.after", true)
444
+ }
445
+
363
446
  const outputWithHint = recoveryHint ? `${output.output}${recoveryHint}` : output.output
364
447
  output.output = outputTruncator(outputWithHint)
365
448
  }
@@ -8,6 +8,7 @@ import { forgeTestTool } from "./tools/forge-test-tool"
8
8
  import { gasAnalysisTool } from "./tools/gas-analysis-tool"
9
9
  import { patternCheckerTool } from "./tools/pattern-checker-tool"
10
10
  import { proxyDetectionTool } from "./tools/proxy-detection-tool"
11
+ import { recordFindingTool } from "./tools/record-finding-tool"
11
12
  import { reportGeneratorTool } from "./tools/report-generator-tool"
12
13
  import { slitherTool } from "./tools/slither-tool"
13
14
  import { createSoloditSearchTool } from "./tools/solodit-search-tool"
@@ -24,6 +25,7 @@ export function createTools(config: ArgusConfig): Record<string, ToolDefinition>
24
25
  argus_check_patterns: patternCheckerTool,
25
26
  argus_proxy_detection: proxyDetectionTool,
26
27
  argus_skill_load: argusSkillLoadTool,
28
+ argus_record_finding: recordFindingTool,
27
29
  argus_generate_report: reportGeneratorTool,
28
30
  argus_sync_knowledge: syncKnowledgeTool,
29
31
  }
@@ -11,6 +11,23 @@ const PHASE_ORDER: AuditPhase[] = [
11
11
  "complete",
12
12
  ]
13
13
 
14
+ const REPORTING_PHASES: AuditPhase[] = ["reporting", "complete"]
15
+
16
+ const KEY_TOOL_FAMILIES: Array<{ family: string; prefixes: string[] }> = [
17
+ { family: "slither", prefixes: ["argus_slither_analyze", "slither"] },
18
+ { family: "forge_test", prefixes: ["argus_forge_test", "forge_test"] },
19
+ { family: "forge_fuzz", prefixes: ["argus_forge_fuzz", "forge_fuzz"] },
20
+ { family: "forge_coverage", prefixes: ["argus_forge_coverage", "forge_coverage"] },
21
+ ]
22
+
23
+ function getMissingToolFamilies(auditState: AuditState): string[] {
24
+ const executedTools = auditState.toolsExecuted.map((t) => t.tool)
25
+ return KEY_TOOL_FAMILIES.filter(
26
+ ({ prefixes }) =>
27
+ !executedTools.some((tool) => prefixes.some((prefix) => tool.startsWith(prefix))),
28
+ ).map(({ family }) => family)
29
+ }
30
+
14
31
  function getNextPhase(current: AuditPhase): AuditPhase | null {
15
32
  const idx = PHASE_ORDER.indexOf(current)
16
33
  if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null
@@ -25,10 +42,21 @@ export function createAuditEnforcer() {
25
42
  const nextPhase = getNextPhase(auditState.currentPhase)
26
43
  if (!nextPhase) return null
27
44
 
28
- return [
45
+ const parts: string[] = [
29
46
  `[Argus Audit Enforcer] Audit in progress — current phase: ${auditState.currentPhase}.`,
30
47
  `Next phase: ${nextPhase}. Do not stop until audit is complete.`,
31
48
  `Progress: ${auditState.findings.length} findings, ${auditState.contractsReviewed.length} contracts reviewed.`,
32
- ].join(" ")
49
+ ]
50
+
51
+ if (REPORTING_PHASES.includes(auditState.currentPhase)) {
52
+ const missing = getMissingToolFamilies(auditState)
53
+ if (missing.length > 0) {
54
+ parts.push(
55
+ `\u26a0\ufe0f Tool coverage incomplete: ${missing.join(", ")} have not been executed. Do not proceed to report generation until required tools are complete.`,
56
+ )
57
+ }
58
+ }
59
+
60
+ return parts.join(" ")
33
61
  }
34
62
  }