solidity-argus 0.3.5 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidity-argus",
3
- "version": "0.3.5",
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"
@@ -233,6 +233,9 @@ When building the final report or synthesizing findings:
233
233
  1. **Primary source**: \`toolsExecuted\` records, \`findings\` from state, and event stream data persisted via argus_* tool outputs.
234
234
  2. **Secondary source**: Tool transcript text (use only when durable evidence is unavailable or incomplete).
235
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).
236
239
 
237
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.
238
241
 
@@ -315,7 +318,12 @@ Your subagents have access to these specialized tools. Know when to delegate eac
315
318
  - **\`argus_generate_report\`**:
316
319
  - **Use**: During Reporting.
317
320
  - **Purpose**: Generates the final artifact.
318
- - **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.
319
327
 
320
328
  - **\`argus_sync_knowledge\`**:
321
329
  - **Use**: Maintenance.
@@ -481,7 +489,7 @@ ReportInput JSON (pass EXACTLY, no prose substitution):
481
489
  "session_id": "{session-id}",
482
490
  "tool_call_id": "{tool-call-id}",
483
491
  "source": "argus",
484
- "schema_version": "1.0.0",
492
+ "schema_version": "2.0.0",
485
493
  "projectDir": "{project-dir}",
486
494
  "findings": [canonical findings],
487
495
  "toolsExecuted": [canonical tool executions],
@@ -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,14 +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. **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. 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. **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:
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:
51
52
  - \`**Tool name**: [reason \u2014 unavailable/failed/timed out]. [Impact on finding coverage if any.]\`
52
53
  - Example: \`**argus_solodit_search**: External database was unavailable. Known-vulnerability cross-referencing was performed using local patterns only.\`
53
54
  - Never silently omit limitations — incomplete coverage must be disclosed.
54
- 5. Confirm the report was generated in your response to Argus: "Report generated via argus_generate_report: {filePath}".
55
+ 6. Confirm the report was generated in your response to Argus: "Report generated via argus_generate_report: {filePath}".
55
56
 
56
57
  ## SINGLE-WRITER POLICY
57
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.
@@ -310,12 +310,58 @@ export function createHooks(args: {
310
310
  {
311
311
  getEventSink: () => currentEventSink,
312
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
+ },
313
331
  },
314
332
  ),
315
333
  "tool-tracking",
316
334
  )
317
335
  : undefined
318
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
+
319
365
  const safeEventHook = isHookEnabled("event")
320
366
  ? safeCreateHook(
321
367
  () => async (input: Parameters<typeof eventHook>[0]) => {
@@ -393,6 +439,10 @@ export function createHooks(args: {
393
439
  result: output.output,
394
440
  })
395
441
 
442
+ if (input.tool === "argus_generate_report") {
443
+ await materializeCurrentFindings("tool.execute.after", true)
444
+ }
445
+
396
446
  const outputWithHint = recoveryHint ? `${output.output}${recoveryHint}` : output.output
397
447
  output.output = outputTruncator(outputWithHint)
398
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
  }
@@ -13,7 +13,6 @@ const PHASE_ORDER: AuditPhase[] = [
13
13
 
14
14
  const REPORTING_PHASES: AuditPhase[] = ["reporting", "complete"]
15
15
 
16
-
17
16
  const KEY_TOOL_FAMILIES: Array<{ family: string; prefixes: string[] }> = [
18
17
  { family: "slither", prefixes: ["argus_slither_analyze", "slither"] },
19
18
  { family: "forge_test", prefixes: ["argus_forge_test", "forge_test"] },
@@ -4,11 +4,26 @@ import type { AuditStateManager } from "../../managers/types"
4
4
  import { createLogger } from "../../shared/logger"
5
5
  import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
6
6
  import { createAuditState } from "../../state/audit-state"
7
+ import { projectAuditState, stableHash } from "../../state/projectors"
7
8
  import type { AuditState, PersistentAuditState } from "../../state/types"
9
+ import { readEvents } from "./event-sink"
8
10
 
9
11
  const STATE_FILE_NAME = "argus-state.json"
10
12
  const STATE_VERSION = "2"
11
13
 
14
+ type ProjectedAuditCore = Pick<
15
+ AuditState,
16
+ "contractsReviewed" | "findings" | "toolsExecuted" | "currentPhase" | "scope"
17
+ >
18
+
19
+ interface ConsistentStateResult {
20
+ state: AuditState
21
+ sourceOfTruth: "events" | "snapshot"
22
+ lastEventSeq?: number
23
+ eventStreamHash?: string
24
+ repaired: boolean
25
+ }
26
+
12
27
  function isObject(value: unknown): value is Record<string, unknown> {
13
28
  return typeof value === "object" && value !== null
14
29
  }
@@ -46,6 +61,54 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
46
61
  )
47
62
  }
48
63
 
64
+ function projectCoreState(
65
+ state: AuditState,
66
+ events: Awaited<ReturnType<typeof readEvents>>,
67
+ ): ProjectedAuditCore {
68
+ const projected = projectAuditState(events, state.projectDir)
69
+
70
+ return {
71
+ contractsReviewed: projected.contractsReviewed,
72
+ findings: projected.findings,
73
+ toolsExecuted: projected.toolsExecuted,
74
+ currentPhase: projected.currentPhase,
75
+ scope: projected.scope,
76
+ }
77
+ }
78
+
79
+ function hasProjectedCoreMismatch(state: AuditState, projectedCore: ProjectedAuditCore): boolean {
80
+ const stateCore: ProjectedAuditCore = {
81
+ contractsReviewed: state.contractsReviewed,
82
+ findings: state.findings,
83
+ toolsExecuted: state.toolsExecuted,
84
+ currentPhase: state.currentPhase,
85
+ scope: state.scope,
86
+ }
87
+
88
+ return stableHash(stateCore) !== stableHash(projectedCore)
89
+ }
90
+
91
+ function hasSnapshotStampMismatch(
92
+ snapshotSeq: number | undefined,
93
+ snapshotHash: string | undefined,
94
+ derivedSeq: number | undefined,
95
+ derivedHash: string | undefined,
96
+ ): boolean {
97
+ if (snapshotSeq === undefined && snapshotHash === undefined) {
98
+ return false
99
+ }
100
+
101
+ if (snapshotSeq !== undefined && derivedSeq !== undefined && snapshotSeq !== derivedSeq) {
102
+ return true
103
+ }
104
+
105
+ if (snapshotHash !== undefined && derivedHash !== undefined && snapshotHash !== derivedHash) {
106
+ return true
107
+ }
108
+
109
+ return false
110
+ }
111
+
49
112
  export function createDebouncedSave(
50
113
  saveState: (state: AuditState) => Promise<void>,
51
114
  delayMs = 5_000,
@@ -104,6 +167,58 @@ export function createAuditStateManager(
104
167
  const stateFilePath = join(resolver.writeRoot(projectDir), STATE_FILE_NAME)
105
168
  let currentState: AuditState = createAuditState(projectDir).state
106
169
 
170
+ async function deriveConsistentState(state: AuditState): Promise<ConsistentStateResult> {
171
+ if (!state.sessionId || !state.projectDir) {
172
+ return {
173
+ state,
174
+ sourceOfTruth: "snapshot",
175
+ repaired: false,
176
+ }
177
+ }
178
+
179
+ try {
180
+ const events = await readEvents(state.sessionId, state.projectDir, resolver)
181
+ const lastEventSeq = events.at(-1)?.seq ?? 0
182
+ const eventStreamHash = stableHash(events)
183
+
184
+ if (events.length === 0) {
185
+ return {
186
+ state,
187
+ sourceOfTruth: "events",
188
+ lastEventSeq,
189
+ eventStreamHash,
190
+ repaired: false,
191
+ }
192
+ }
193
+
194
+ const projectedCore = projectCoreState(state, events)
195
+ const repaired = hasProjectedCoreMismatch(state, projectedCore)
196
+
197
+ return {
198
+ state: repaired
199
+ ? {
200
+ ...state,
201
+ ...projectedCore,
202
+ }
203
+ : state,
204
+ sourceOfTruth: "events",
205
+ lastEventSeq,
206
+ eventStreamHash,
207
+ repaired,
208
+ }
209
+ } catch (error) {
210
+ logger.warn(
211
+ `Failed to derive state from events for run ${state.sessionId}; using snapshot fallback`,
212
+ error,
213
+ )
214
+ return {
215
+ state,
216
+ sourceOfTruth: "snapshot",
217
+ repaired: false,
218
+ }
219
+ }
220
+ }
221
+
107
222
  async function load(): Promise<AuditState | null> {
108
223
  try {
109
224
  const resolvedPath = resolver.resolveReadPath(projectDir, STATE_FILE_NAME)
@@ -129,9 +244,9 @@ export function createAuditStateManager(
129
244
  savedAt: _savedAt,
130
245
  version,
131
246
  filePath: _filePath,
132
- source_of_truth: _sourceOfTruth,
247
+ source_of_truth: snapshotSourceOfTruth,
133
248
  last_event_seq: snapshotSeq,
134
- event_stream_hash: _eventStreamHash,
249
+ event_stream_hash: snapshotEventHash,
135
250
  ...state
136
251
  } = parsed
137
252
 
@@ -144,12 +259,32 @@ export function createAuditStateManager(
144
259
  }
145
260
  }
146
261
 
147
-
148
262
  if (snapshotSeq !== undefined) {
149
263
  logger.debug(`Loaded snapshot with last_event_seq=${snapshotSeq} from ${readPath}`)
150
264
  }
151
265
 
152
- currentState = state
266
+ const consistent = await deriveConsistentState(state)
267
+ const stampMismatch =
268
+ consistent.sourceOfTruth === "events" &&
269
+ hasSnapshotStampMismatch(
270
+ snapshotSeq,
271
+ snapshotEventHash,
272
+ consistent.lastEventSeq,
273
+ consistent.eventStreamHash,
274
+ )
275
+
276
+ if (consistent.repaired || stampMismatch) {
277
+ const mismatchReason = consistent.repaired ? "projected core mismatch" : "stamp mismatch"
278
+ logger.warn(
279
+ `Recovered audit state from event stream for run ${state.sessionId}: ${mismatchReason}`,
280
+ )
281
+ } else if (snapshotSourceOfTruth === "events" && consistent.sourceOfTruth !== "events") {
282
+ logger.warn(
283
+ `Snapshot for run ${state.sessionId} was marked event-derived but could not be validated against events`,
284
+ )
285
+ }
286
+
287
+ currentState = consistent.state
153
288
  return currentState
154
289
  } catch (err) {
155
290
  logger.warn("Failed to load persisted audit state", err)
@@ -168,13 +303,23 @@ export function createAuditStateManager(
168
303
  try {
169
304
  while (true) {
170
305
  const stateToSave = currentState
306
+ const consistent = await deriveConsistentState(stateToSave)
307
+
308
+ if (consistent.repaired) {
309
+ logger.warn(
310
+ `State/core divergence detected for run ${stateToSave.sessionId}; auto-repairing`,
311
+ )
312
+ currentState = consistent.state
313
+ }
171
314
 
172
315
  const persistentState: PersistentAuditState = {
173
- ...stateToSave,
316
+ ...consistent.state,
174
317
  savedAt: Date.now(),
175
318
  version: STATE_VERSION,
176
319
  filePath: stateFilePath,
177
- source_of_truth: "events",
320
+ source_of_truth: consistent.sourceOfTruth,
321
+ last_event_seq: consistent.lastEventSeq,
322
+ event_stream_hash: consistent.eventStreamHash,
178
323
  }
179
324
 
180
325
  const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`
@@ -182,7 +327,7 @@ export function createAuditStateManager(
182
327
  await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`)
183
328
  await rename(tempFilePath, stateFilePath)
184
329
 
185
- if (currentState === stateToSave) break
330
+ if (currentState === consistent.state) break
186
331
  }
187
332
  } catch (err) {
188
333
  logger.warn("Failed to persist audit state", err)
@@ -218,15 +363,18 @@ export function createAuditStateManager(
218
363
 
219
364
  if (hasContent) {
220
365
  try {
366
+ const consistent = await deriveConsistentState(currentState)
221
367
  const archivesDir = join(dirname(stateFilePath), "archives")
222
368
  await mkdir(archivesDir, { recursive: true })
223
369
  const archivePath = join(archivesDir, `argus-state.${Date.now()}.json`)
224
370
  const persistentState: PersistentAuditState = {
225
- ...currentState,
371
+ ...consistent.state,
226
372
  savedAt: Date.now(),
227
373
  version: STATE_VERSION,
228
374
  filePath: archivePath,
229
- source_of_truth: "events",
375
+ source_of_truth: consistent.sourceOfTruth,
376
+ last_event_seq: consistent.lastEventSeq,
377
+ event_stream_hash: consistent.eventStreamHash,
230
378
  }
231
379
  await Bun.write(archivePath, `${JSON.stringify(persistentState, null, 2)}\n`)
232
380
  } catch {
@@ -1,9 +1,6 @@
1
1
  import { mkdir, rename } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
- import {
4
- type ArgusRootResolver,
5
- defaultRootResolver,
6
- } from "../../shared/path-root-resolver"
3
+ import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
7
4
  import type { AuditEvent, AuditEventType } from "../../state/schemas"
8
5
 
9
6
  export type EventSinkErrorCode = "SEQUENCE_CONFLICT" | "INVALID_EVENT" | "IO_ERROR"
@@ -89,7 +86,11 @@ function parseJournalLines(content: string): AuditEvent[] {
89
86
  /**
90
87
  * Replay-safe stateless read — returns all events for a run sorted by seq.
91
88
  */
92
- export async function readEvents(runId: string, projectDir: string, resolver: ArgusRootResolver = defaultRootResolver): Promise<AuditEvent[]> {
89
+ export async function readEvents(
90
+ runId: string,
91
+ projectDir: string,
92
+ resolver: ArgusRootResolver = defaultRootResolver,
93
+ ): Promise<AuditEvent[]> {
93
94
  const journalPath = buildJournalPath(runId, projectDir, resolver)
94
95
  const content = await readRawContent(journalPath)
95
96
  return parseJournalLines(content)
@@ -99,7 +100,11 @@ export async function readEvents(runId: string, projectDir: string, resolver: Ar
99
100
  * Append-only event sink with monotonic seq allocation, in-process mutex,
100
101
  * and atomic temp-file-then-rename writes. Restart-safe via journal replay.
101
102
  */
102
- export function createEventSink(runId: string, projectDir: string, resolver: ArgusRootResolver = defaultRootResolver): EventSink {
103
+ export function createEventSink(
104
+ runId: string,
105
+ projectDir: string,
106
+ resolver: ArgusRootResolver = defaultRootResolver,
107
+ ): EventSink {
103
108
  const journalPath = buildJournalPath(runId, projectDir, resolver)
104
109
  const mutex = createMutex()
105
110
  let lastSeq = 0
@@ -1,6 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises"
2
2
  import { dirname } from "node:path"
3
3
  import { createAuditArtifactResolver } from "../../shared/audit-artifact-resolver"
4
+ import { dedupeFindingsForFinalOutput } from "../../state/finding-aggregation"
4
5
  import { projectFindings, projectToolExecutions, stableHash } from "../../state/projectors"
5
6
  import type { CanonicalFinding, CanonicalToolExecution } from "../../state/schemas"
6
7
  import { SCHEMA_VERSION } from "../../state/schemas"
@@ -25,7 +26,7 @@ export async function materializeFindings(
25
26
  sessionId?: string,
26
27
  ): Promise<FindingsArtifact> {
27
28
  const events = await readEvents(runId, projectDir)
28
- const findings = projectFindings(events)
29
+ const findings = dedupeFindingsForFinalOutput(projectFindings(events))
29
30
  const toolsExecuted = projectToolExecutions(events)
30
31
  const contentHash = stableHash(JSON.stringify(findings))
31
32
  const generatedAt = events.at(-1)?.timestamp ?? 0
@@ -1,3 +1,4 @@
1
+ import { ARGUS_PLUGIN_VERSION } from "../../shared/plugin-metadata"
1
2
  import { validateEventSequence } from "../../state/projectors"
2
3
  import type { AuditEvent } from "../../state/schemas"
3
4
  import { SCHEMA_VERSION } from "../../state/schemas"
@@ -178,6 +179,7 @@ export async function finalizeRun(
178
179
  invariantsPassed,
179
180
  errors,
180
181
  status: invariantsPassed ? "finalized" : "failed-finalization",
182
+ plugin_version: ARGUS_PLUGIN_VERSION,
181
183
  },
182
184
  })
183
185
  }
@@ -1,10 +1,7 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
- import {
4
- type ArgusRootResolver,
5
- defaultRootResolver,
6
- } from "../../shared/path-root-resolver"
7
3
  import { createLogger } from "../../shared/logger"
4
+ import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
8
5
 
9
6
  const logger = createLogger()
10
7
 
@@ -2,6 +2,7 @@ import type { EventSink } from "../features/persistent-state/event-sink"
2
2
  import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
3
3
  import { finalizeRun } from "../features/persistent-state/run-finalizer"
4
4
  import { createLogger } from "../shared/logger"
5
+ import { ARGUS_PLUGIN_VERSION } from "../shared/plugin-metadata"
5
6
  import { createAuditState } from "../state/audit-state"
6
7
  import type { AuditEvent } from "../state/schemas"
7
8
  import { SCHEMA_VERSION } from "../state/schemas"
@@ -140,6 +141,7 @@ export function createEventHook(
140
141
  await emitToSink("session.created", currentAuditState.sessionId, sessionId, {
141
142
  projectDir: currentAuditState.projectDir,
142
143
  sessionId: currentAuditState.sessionId,
144
+ plugin_version: ARGUS_PLUGIN_VERSION,
143
145
  })
144
146
  }
145
147
  break
@@ -160,6 +162,7 @@ export function createEventHook(
160
162
  if (preDeleteState) {
161
163
  await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
162
164
  archived: true,
165
+ plugin_version: ARGUS_PLUGIN_VERSION,
163
166
  })
164
167
 
165
168
  if (eventSink) {
@@ -12,7 +12,6 @@ const TOOL_SHORT_NAMES: Record<string, string> = {
12
12
  }
13
13
  const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
14
14
 
15
-
16
15
  /** Maps unavailable-tool short names to their KEY_TOOLS counterpart */
17
16
  const UNAVAILABLE_TO_KEY_TOOL: Record<string, string> = {
18
17
  slither: "slither",
@@ -77,12 +76,8 @@ export function buildDynamicContext(
77
76
  (t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
78
77
  ).join(" ")
79
78
  const unavailable = auditState.unavailableTools ?? []
80
- const excusedTools = new Set(
81
- unavailable.map((t) => UNAVAILABLE_TO_KEY_TOOL[t]).filter(Boolean),
82
- )
83
- const pendingKeyTools = KEY_TOOLS.filter(
84
- (t) => !executedToolNames.has(t) && !excusedTools.has(t),
85
- )
79
+ const excusedTools = new Set(unavailable.map((t) => UNAVAILABLE_TO_KEY_TOOL[t]).filter(Boolean))
80
+ const pendingKeyTools = KEY_TOOLS.filter((t) => !executedToolNames.has(t) && !excusedTools.has(t))
86
81
  const gateStatus =
87
82
  pendingKeyTools.length > 0
88
83
  ? `REPORTING GATE: BLOCKED \u2014 key tools pending: ${pendingKeyTools.join(", ")}`