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
@@ -2,13 +2,28 @@ import { mkdir, rename } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
3
  import type { AuditStateManager } from "../../managers/types"
4
4
  import { createLogger } from "../../shared/logger"
5
+ import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
5
6
  import { createAuditState } from "../../state/audit-state"
7
+ import { projectAuditState, stableHash } from "../../state/projectors"
6
8
  import type { AuditState, PersistentAuditState } from "../../state/types"
9
+ import { readEvents } from "./event-sink"
7
10
 
8
- const STATE_FILE_DIR = ".opencode"
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,
@@ -95,14 +158,73 @@ export function createDebouncedSave(
95
158
  }
96
159
  }
97
160
 
98
- export function createAuditStateManager(projectDir: string): AuditStateManager {
161
+ export function createAuditStateManager(
162
+ projectDir: string,
163
+ resolver: ArgusRootResolver = defaultRootResolver,
164
+ ): AuditStateManager {
99
165
  const logger = createLogger()
100
- const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME)
166
+
167
+ const stateFilePath = join(resolver.writeRoot(projectDir), STATE_FILE_NAME)
101
168
  let currentState: AuditState = createAuditState(projectDir).state
102
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
+
103
222
  async function load(): Promise<AuditState | null> {
104
223
  try {
105
- const file = Bun.file(stateFilePath)
224
+ const resolvedPath = resolver.resolveReadPath(projectDir, STATE_FILE_NAME)
225
+ const readPath = resolvedPath ?? stateFilePath
226
+
227
+ const file = Bun.file(readPath)
106
228
  if (!(await file.exists())) {
107
229
  return null
108
230
  }
@@ -114,11 +236,19 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
114
236
 
115
237
  const parsed: unknown = JSON.parse(content)
116
238
  if (!isPersistentAuditState(parsed)) {
117
- logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
239
+ logger.warn("Persistent audit state is invalid, ignoring", readPath)
118
240
  return null
119
241
  }
120
242
 
121
- const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
243
+ const {
244
+ savedAt: _savedAt,
245
+ version,
246
+ filePath: _filePath,
247
+ source_of_truth: snapshotSourceOfTruth,
248
+ last_event_seq: snapshotSeq,
249
+ event_stream_hash: snapshotEventHash,
250
+ ...state
251
+ } = parsed
122
252
 
123
253
  if (version === "1") {
124
254
  if (!state.soloditResults) {
@@ -129,7 +259,32 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
129
259
  }
130
260
  }
131
261
 
132
- currentState = state
262
+ if (snapshotSeq !== undefined) {
263
+ logger.debug(`Loaded snapshot with last_event_seq=${snapshotSeq} from ${readPath}`)
264
+ }
265
+
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
133
288
  return currentState
134
289
  } catch (err) {
135
290
  logger.warn("Failed to load persisted audit state", err)
@@ -148,12 +303,23 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
148
303
  try {
149
304
  while (true) {
150
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
+ }
151
314
 
152
315
  const persistentState: PersistentAuditState = {
153
- ...stateToSave,
316
+ ...consistent.state,
154
317
  savedAt: Date.now(),
155
318
  version: STATE_VERSION,
156
319
  filePath: stateFilePath,
320
+ source_of_truth: consistent.sourceOfTruth,
321
+ last_event_seq: consistent.lastEventSeq,
322
+ event_stream_hash: consistent.eventStreamHash,
157
323
  }
158
324
 
159
325
  const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`
@@ -161,7 +327,7 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
161
327
  await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`)
162
328
  await rename(tempFilePath, stateFilePath)
163
329
 
164
- if (currentState === stateToSave) break
330
+ if (currentState === consistent.state) break
165
331
  }
166
332
  } catch (err) {
167
333
  logger.warn("Failed to persist audit state", err)
@@ -197,14 +363,18 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
197
363
 
198
364
  if (hasContent) {
199
365
  try {
366
+ const consistent = await deriveConsistentState(currentState)
200
367
  const archivesDir = join(dirname(stateFilePath), "archives")
201
368
  await mkdir(archivesDir, { recursive: true })
202
369
  const archivePath = join(archivesDir, `argus-state.${Date.now()}.json`)
203
370
  const persistentState: PersistentAuditState = {
204
- ...currentState,
371
+ ...consistent.state,
205
372
  savedAt: Date.now(),
206
373
  version: STATE_VERSION,
207
374
  filePath: archivePath,
375
+ source_of_truth: consistent.sourceOfTruth,
376
+ last_event_seq: consistent.lastEventSeq,
377
+ event_stream_hash: consistent.eventStreamHash,
208
378
  }
209
379
  await Bun.write(archivePath, `${JSON.stringify(persistentState, null, 2)}\n`)
210
380
  } catch {
@@ -1,5 +1,6 @@
1
1
  import { mkdir, rename } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
+ import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
3
4
  import type { AuditEvent, AuditEventType } from "../../state/schemas"
4
5
 
5
6
  export type EventSinkErrorCode = "SEQUENCE_CONFLICT" | "INVALID_EVENT" | "IO_ERROR"
@@ -52,8 +53,8 @@ function createMutex() {
52
53
  }
53
54
  }
54
55
 
55
- function buildJournalPath(runId: string, projectDir: string): string {
56
- return join(projectDir, ".opencode", "runs", runId, "events.jsonl")
56
+ function buildJournalPath(runId: string, projectDir: string, resolver: ArgusRootResolver): string {
57
+ return join(resolver.writeRoot(projectDir), "runs", runId, "events.jsonl")
57
58
  }
58
59
 
59
60
  async function readRawContent(path: string): Promise<string> {
@@ -85,8 +86,12 @@ function parseJournalLines(content: string): AuditEvent[] {
85
86
  /**
86
87
  * Replay-safe stateless read — returns all events for a run sorted by seq.
87
88
  */
88
- export async function readEvents(runId: string, projectDir: string): Promise<AuditEvent[]> {
89
- const journalPath = buildJournalPath(runId, projectDir)
89
+ export async function readEvents(
90
+ runId: string,
91
+ projectDir: string,
92
+ resolver: ArgusRootResolver = defaultRootResolver,
93
+ ): Promise<AuditEvent[]> {
94
+ const journalPath = buildJournalPath(runId, projectDir, resolver)
90
95
  const content = await readRawContent(journalPath)
91
96
  return parseJournalLines(content)
92
97
  }
@@ -95,8 +100,12 @@ export async function readEvents(runId: string, projectDir: string): Promise<Aud
95
100
  * Append-only event sink with monotonic seq allocation, in-process mutex,
96
101
  * and atomic temp-file-then-rename writes. Restart-safe via journal replay.
97
102
  */
98
- export function createEventSink(runId: string, projectDir: string): EventSink {
99
- const journalPath = buildJournalPath(runId, projectDir)
103
+ export function createEventSink(
104
+ runId: string,
105
+ projectDir: string,
106
+ resolver: ArgusRootResolver = defaultRootResolver,
107
+ ): EventSink {
108
+ const journalPath = buildJournalPath(runId, projectDir, resolver)
100
109
  const mutex = createMutex()
101
110
  let lastSeq = 0
102
111
  let initialized = false
@@ -0,0 +1,52 @@
1
+ import { mkdir, writeFile } from "node:fs/promises"
2
+ import { dirname } from "node:path"
3
+ import { createAuditArtifactResolver } from "../../shared/audit-artifact-resolver"
4
+ import { dedupeFindingsForFinalOutput } from "../../state/finding-aggregation"
5
+ import { projectFindings, projectToolExecutions, stableHash } from "../../state/projectors"
6
+ import type { CanonicalFinding, CanonicalToolExecution } from "../../state/schemas"
7
+ import { SCHEMA_VERSION } from "../../state/schemas"
8
+ import { readEvents } from "./event-sink"
9
+
10
+ export interface FindingsArtifact {
11
+ run_id: string
12
+ session_id: string
13
+ schema_version: string
14
+ seq_first: number
15
+ seq_last: number
16
+ event_count: number
17
+ content_hash: string
18
+ generated_at: number
19
+ findings: CanonicalFinding[]
20
+ toolsExecuted: CanonicalToolExecution[]
21
+ }
22
+
23
+ export async function materializeFindings(
24
+ runId: string,
25
+ projectDir: string,
26
+ sessionId?: string,
27
+ ): Promise<FindingsArtifact> {
28
+ const events = await readEvents(runId, projectDir)
29
+ const findings = dedupeFindingsForFinalOutput(projectFindings(events))
30
+ const toolsExecuted = projectToolExecutions(events)
31
+ const contentHash = stableHash(JSON.stringify(findings))
32
+ const generatedAt = events.at(-1)?.timestamp ?? 0
33
+
34
+ const artifact: FindingsArtifact = {
35
+ run_id: runId,
36
+ session_id: sessionId ?? events[0]?.session_id ?? "",
37
+ schema_version: SCHEMA_VERSION,
38
+ seq_first: events[0]?.seq ?? 0,
39
+ seq_last: events.at(-1)?.seq ?? 0,
40
+ event_count: events.length,
41
+ content_hash: contentHash,
42
+ generated_at: generatedAt,
43
+ findings,
44
+ toolsExecuted,
45
+ }
46
+
47
+ const findingsFile = createAuditArtifactResolver(runId, projectDir).paths().findingsFile
48
+ await mkdir(dirname(findingsFile), { recursive: true })
49
+ await writeFile(findingsFile, JSON.stringify(artifact, null, 2))
50
+
51
+ return artifact
52
+ }
@@ -1,3 +1,3 @@
1
1
  export { createAuditStateManager } from "./audit-state-manager"
2
- export { createEventSink, readEvents, EventSinkError } from "./event-sink"
3
2
  export type { EventSink, EventSinkErrorCode } from "./event-sink"
3
+ export { createEventSink, EventSinkError, readEvents } from "./event-sink"
@@ -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"
@@ -12,18 +13,23 @@ export type FinalizationResult = {
12
13
  timestamp: number
13
14
  }
14
15
 
15
- function hasSessionCreated(events: AuditEvent[]): boolean {
16
+ export function hasSessionCreated(events: AuditEvent[]): boolean {
16
17
  return events.some((event) => event.type === "session.created")
17
18
  }
18
19
 
19
- function hasSessionDeleted(events: AuditEvent[]): boolean {
20
+ export function hasSessionDeleted(events: AuditEvent[]): boolean {
20
21
  return events.some((event) => event.type === "session.deleted")
21
22
  }
22
23
 
23
- function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
24
+ export type ToolLifecycleCheckResult = {
25
+ orphanedToolCallIds: string[]
26
+ malformedEvents: string[]
27
+ }
28
+
29
+ export function collectToolLifecycleIssues(events: AuditEvent[]): ToolLifecycleCheckResult {
24
30
  const startedCallIds = new Set<string>()
25
31
  const completedCallIds = new Set<string>()
26
- const errors: string[] = []
32
+ const malformedEvents: string[] = []
27
33
 
28
34
  for (const event of events) {
29
35
  if (event.type !== "tool.started" && event.type !== "tool.completed") {
@@ -31,7 +37,7 @@ function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
31
37
  }
32
38
 
33
39
  if (typeof event.tool_call_id !== "string" || event.tool_call_id.length === 0) {
34
- errors.push(`${event.type} at seq ${event.seq} missing tool_call_id`)
40
+ malformedEvents.push(`${event.type} at seq ${event.seq} missing tool_call_id`)
35
41
  continue
36
42
  }
37
43
 
@@ -44,13 +50,25 @@ function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
44
50
  }
45
51
  }
46
52
 
53
+ const orphanedToolCallIds: string[] = []
47
54
  for (const toolCallId of startedCallIds) {
48
55
  if (!completedCallIds.has(toolCallId)) {
49
- errors.push(`orphaned tool.started without matching tool.completed: ${toolCallId}`)
56
+ orphanedToolCallIds.push(toolCallId)
50
57
  }
51
58
  }
52
59
 
53
- return errors
60
+ return {
61
+ orphanedToolCallIds,
62
+ malformedEvents,
63
+ }
64
+ }
65
+
66
+ function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
67
+ const { orphanedToolCallIds, malformedEvents } = collectToolLifecycleIssues(events)
68
+ const orphanedErrors = orphanedToolCallIds.map(
69
+ (toolCallId) => `orphaned tool.started without matching tool.completed: ${toolCallId}`,
70
+ )
71
+ return [...malformedEvents, ...orphanedErrors]
54
72
  }
55
73
 
56
74
  function asRecord(value: unknown): Record<string, unknown> | null {
@@ -161,6 +179,7 @@ export async function finalizeRun(
161
179
  invariantsPassed,
162
180
  errors,
163
181
  status: invariantsPassed ? "finalized" : "failed-finalization",
182
+ plugin_version: ARGUS_PLUGIN_VERSION,
164
183
  },
165
184
  })
166
185
  }
@@ -1,10 +1,10 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises"
2
2
  import { dirname, join } from "node:path"
3
3
  import { createLogger } from "../../shared/logger"
4
+ import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
4
5
 
5
6
  const logger = createLogger()
6
7
 
7
- const JOURNAL_DIR = ".opencode"
8
8
  const JOURNAL_FILE = "argus-journal.jsonl"
9
9
 
10
10
  export type JournalEvent =
@@ -15,7 +15,12 @@ export type JournalEvent =
15
15
  findingsCount: number
16
16
  toolsExecutedCount: number
17
17
  }
18
- | { type: "session.deleted"; timestamp: number; archived: boolean; finalizationPassed: boolean | null }
18
+ | {
19
+ type: "session.deleted"
20
+ timestamp: number
21
+ archived: boolean
22
+ finalizationPassed: boolean | null
23
+ }
19
24
  | {
20
25
  type: "tool.executed"
21
26
  tool: string
@@ -30,12 +35,15 @@ export type JournalEvent =
30
35
  findingsCount: number
31
36
  }
32
37
 
33
- export function createRunJournal(projectDir: string): {
38
+ export function createRunJournal(
39
+ projectDir: string,
40
+ resolver: ArgusRootResolver = defaultRootResolver,
41
+ ): {
34
42
  log(event: JournalEvent): void
35
43
  close(): Promise<void>
36
44
  getPath(): string
37
45
  } {
38
- const journalPath = join(projectDir, JOURNAL_DIR, JOURNAL_FILE)
46
+ const journalPath = join(resolver.writeRoot(projectDir), JOURNAL_FILE)
39
47
  let ensureDirPromise: Promise<void> | null = null
40
48
  const pendingWrites = new Set<Promise<void>>()
41
49
 
@@ -1,7 +1,8 @@
1
1
  import type { EventSink } from "../features/persistent-state/event-sink"
2
- import { finalizeRun } from "../features/persistent-state/run-finalizer"
3
2
  import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
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,6 +12,13 @@ const TOOL_SHORT_NAMES: Record<string, string> = {
12
12
  }
13
13
  const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
14
14
 
15
+ /** Maps unavailable-tool short names to their KEY_TOOLS counterpart */
16
+ const UNAVAILABLE_TO_KEY_TOOL: Record<string, string> = {
17
+ slither: "slither",
18
+ forge: "forge-test",
19
+ solodit: "solodit",
20
+ }
21
+
15
22
  export interface SystemPromptHookDeps {
16
23
  getAuditState: () => AuditState | null
17
24
  getAgentForSession: (sessionID: string) => string | undefined
@@ -69,8 +76,15 @@ export function buildDynamicContext(
69
76
  (t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
70
77
  ).join(" ")
71
78
  const unavailable = auditState.unavailableTools ?? []
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))
81
+ const gateStatus =
82
+ pendingKeyTools.length > 0
83
+ ? `REPORTING GATE: BLOCKED \u2014 key tools pending: ${pendingKeyTools.join(", ")}`
84
+ : "REPORTING GATE: ALLOWED"
72
85
  const lines: string[] = [
73
86
  `<argus-context agent="${agent}">`,
87
+ gateStatus,
74
88
  `Phase: ${auditState.currentPhase}`,
75
89
  `Contracts: ${auditState.contractsReviewed.length} reviewed`,
76
90
  `Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
@@ -91,6 +105,7 @@ export function buildDynamicContext(
91
105
  const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
92
106
  summary = [
93
107
  `<argus-context agent="${agent}">`,
108
+ gateStatus,
94
109
  `Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
95
110
  "</argus-context>",
96
111
  ].join("\n")