solidity-argus 0.3.7 → 0.5.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 (107) hide show
  1. package/AGENTS.md +13 -6
  2. package/README.md +24 -12
  3. package/package.json +7 -3
  4. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
  5. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
  6. package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
  7. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
  8. package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
  9. package/skills/checklists/general-audit/SKILL.md +1 -0
  10. package/skills/methodology/audit-workflow/SKILL.md +1 -0
  11. package/skills/methodology/report-template/SKILL.md +1 -0
  12. package/skills/methodology/severity-classification/SKILL.md +1 -0
  13. package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
  14. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
  15. package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
  16. package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
  17. package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
  18. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
  19. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
  20. package/src/agents/argus-prompt.ts +98 -33
  21. package/src/agents/pythia-prompt.ts +18 -1
  22. package/src/agents/scribe-prompt.ts +32 -10
  23. package/src/agents/sentinel-prompt.ts +19 -0
  24. package/src/agents/themis-prompt.ts +110 -0
  25. package/src/cli/commands/doctor.ts +29 -17
  26. package/src/config/loader.ts +29 -5
  27. package/src/config/schema.ts +45 -45
  28. package/src/constants/defaults.ts +1 -0
  29. package/src/create-hooks.ts +797 -148
  30. package/src/create-managers.ts +4 -2
  31. package/src/create-tools.ts +5 -1
  32. package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
  33. package/src/features/background-agent/background-manager.ts +32 -5
  34. package/src/features/error-recovery/tool-error-recovery.ts +1 -0
  35. package/src/features/persistent-state/audit-state-manager.ts +272 -29
  36. package/src/features/persistent-state/event-sink.ts +96 -25
  37. package/src/features/persistent-state/findings-materializer.ts +34 -2
  38. package/src/features/persistent-state/global-run-index.ts +86 -8
  39. package/src/features/persistent-state/index.ts +7 -1
  40. package/src/features/persistent-state/run-finalizer.ts +116 -7
  41. package/src/features/persistent-state/run-pruner.ts +93 -0
  42. package/src/hooks/agent-tracker.ts +14 -2
  43. package/src/hooks/compaction-hook.ts +7 -16
  44. package/src/hooks/config-handler.ts +83 -29
  45. package/src/hooks/context-budget.ts +4 -5
  46. package/src/hooks/event-hook.ts +213 -57
  47. package/src/hooks/knowledge-sync-hook.ts +2 -3
  48. package/src/hooks/safe-create-hook.ts +13 -1
  49. package/src/hooks/system-prompt-hook.ts +20 -39
  50. package/src/hooks/tool-tracking-hook.ts +597 -323
  51. package/src/index.ts +15 -1
  52. package/src/knowledge/scvd-client.ts +2 -4
  53. package/src/knowledge/scvd-errors.ts +25 -2
  54. package/src/knowledge/scvd-index.ts +7 -5
  55. package/src/knowledge/scvd-sync.ts +6 -6
  56. package/src/managers/types.ts +20 -2
  57. package/src/shared/agent-names.ts +23 -0
  58. package/src/shared/audit-artifact-resolver.ts +8 -3
  59. package/src/shared/audit-phases.ts +12 -0
  60. package/src/shared/cache-paths.ts +41 -0
  61. package/src/shared/drop-diagnostics.ts +2 -2
  62. package/src/shared/forge-errors.ts +31 -0
  63. package/src/shared/forge-runner.ts +30 -0
  64. package/src/shared/format-error.ts +3 -0
  65. package/src/shared/index.ts +9 -0
  66. package/src/shared/key-tools.ts +39 -0
  67. package/src/shared/logger.ts +7 -7
  68. package/src/shared/path-containment.ts +25 -0
  69. package/src/shared/path-utils.ts +11 -0
  70. package/src/shared/report-path-resolver.ts +4 -2
  71. package/src/shared/safe-emit.ts +24 -0
  72. package/src/shared/token-utils.ts +5 -0
  73. package/src/shared/type-guards.ts +8 -0
  74. package/src/shared/validation-constants.ts +52 -0
  75. package/src/skills/analysis/cluster.ts +1 -114
  76. package/src/skills/analysis/normalize.ts +2 -114
  77. package/src/skills/analysis/stopwords.ts +109 -0
  78. package/src/skills/argus-skill-resolver.ts +6 -3
  79. package/src/solodit-lifecycle.ts +153 -37
  80. package/src/state/adapters.ts +60 -66
  81. package/src/state/finding-aggregation.ts +6 -8
  82. package/src/state/finding-fingerprint.ts +1 -1
  83. package/src/state/finding-store.ts +31 -9
  84. package/src/state/index.ts +1 -1
  85. package/src/state/projectors.ts +27 -19
  86. package/src/state/schemas.ts +8 -32
  87. package/src/state/types.ts +3 -0
  88. package/src/tools/contract-analyzer-tool.ts +4 -6
  89. package/src/tools/forge-coverage-tool.ts +10 -35
  90. package/src/tools/forge-fuzz-tool.ts +21 -51
  91. package/src/tools/forge-test-tool.ts +25 -47
  92. package/src/tools/gas-analysis-tool.ts +12 -41
  93. package/src/tools/pattern-checker-tool.ts +37 -15
  94. package/src/tools/pattern-loader.ts +18 -4
  95. package/src/tools/persist-deduped-tool.ts +94 -0
  96. package/src/tools/proxy-detection-tool.ts +35 -34
  97. package/src/tools/read-findings-tool.ts +390 -0
  98. package/src/tools/record-finding-tool.ts +120 -25
  99. package/src/tools/report-generator-tool.ts +394 -328
  100. package/src/tools/report-preflight.ts +5 -1
  101. package/src/tools/slither-tool.ts +55 -16
  102. package/src/tools/solodit-search-tool.ts +260 -112
  103. package/src/tools/sync-knowledge-tool.ts +2 -3
  104. package/src/utils/solidity-parser.ts +39 -24
  105. package/src/features/migration/index.ts +0 -14
  106. package/src/features/migration/migration-adapter.ts +0 -151
  107. package/src/features/migration/parity-telemetry.ts +0 -133
@@ -1,8 +1,10 @@
1
1
  import type { EventSink } from "../features/persistent-state/event-sink"
2
+ import { updateRunStatus } from "../features/persistent-state/global-run-index"
2
3
  import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
3
4
  import { finalizeRun } from "../features/persistent-state/run-finalizer"
4
5
  import { createLogger } from "../shared/logger"
5
6
  import { ARGUS_PLUGIN_VERSION } from "../shared/plugin-metadata"
7
+ import { safeEmitToSink } from "../shared/safe-emit"
6
8
  import { createAuditState } from "../state/audit-state"
7
9
  import type { AuditEvent } from "../state/schemas"
8
10
  import { SCHEMA_VERSION } from "../state/schemas"
@@ -18,9 +20,41 @@ export type AuditEventType =
18
20
  | "audit.complete"
19
21
 
20
22
  export type EventHookFn = (input: {
21
- event: { type: string; sessionId?: string; properties?: Record<string, unknown> }
23
+ event: { type: string; properties?: Record<string, unknown> }
22
24
  }) => Promise<void>
23
25
 
26
+ /**
27
+ * Extract the OpenCode session ID from an SDK Event object.
28
+ *
29
+ * The OpenCode SDK Event union uses different shapes depending on event type:
30
+ * - session.created / session.deleted → { properties: { info: { id: string } } }
31
+ * - session.idle / session.error → { properties: { sessionID: string } }
32
+ * - Other events may have properties.sessionID or none at all.
33
+ */
34
+ export function extractSessionId(event: {
35
+ type: string
36
+ properties?: Record<string, unknown>
37
+ }): string | undefined {
38
+ const props = event.properties
39
+ if (!props) return undefined
40
+
41
+ // session.idle, session.error, and many other events use properties.sessionID
42
+ if (typeof props.sessionID === "string" && props.sessionID.length > 0) {
43
+ return props.sessionID
44
+ }
45
+
46
+ // session.created and session.deleted wrap a Session object at properties.info
47
+ const info = props.info
48
+ if (info && typeof info === "object" && info !== null) {
49
+ const infoRecord = info as Record<string, unknown>
50
+ if (typeof infoRecord.id === "string" && infoRecord.id.length > 0) {
51
+ return infoRecord.id
52
+ }
53
+ }
54
+
55
+ return undefined
56
+ }
57
+
24
58
  export type EventSubHandler = (event: {
25
59
  type: string
26
60
  sessionId?: string
@@ -33,78 +67,158 @@ export function createEventHook(
33
67
  subHandlers: EventSubHandler[] = [],
34
68
  ): {
35
69
  hook: EventHookFn
36
- getAuditState: () => AuditState | null
37
- setAuditState: (state: AuditState | null) => void
38
- setEventSink: (sink: EventSink | null) => void
70
+ getAuditState: (sessionId?: string) => AuditState | null
71
+ setAuditState: (state: AuditState | null, sessionId?: string) => void
72
+ setEventSink: (sink: EventSink | null, sessionId?: string) => void
73
+ getEventSink: (sessionId?: string) => EventSink | null
39
74
  getLastFinalizationResult: () => FinalizationResult | null
40
75
  } {
41
76
  const logger = createLogger()
42
- let currentAuditState: AuditState | null = null
43
- let eventSink: EventSink | null = null
77
+ const statesBySessionId = new Map<string, AuditState>()
78
+ const sinksBySessionId = new Map<string, EventSink>()
79
+
80
+ const MAX_SESSION_STATES = 500
81
+
82
+ function setSessionState(sessionId: string, state: AuditState): void {
83
+ if (statesBySessionId.size >= MAX_SESSION_STATES && !statesBySessionId.has(sessionId)) {
84
+ const oldest = statesBySessionId.keys().next().value
85
+ if (oldest) statesBySessionId.delete(oldest)
86
+ }
87
+ statesBySessionId.set(sessionId, state)
88
+ }
89
+
90
+ function setSessionSink(sessionId: string, sink: EventSink): void {
91
+ if (sinksBySessionId.size >= MAX_SESSION_STATES && !sinksBySessionId.has(sessionId)) {
92
+ const oldest = sinksBySessionId.keys().next().value
93
+ if (oldest) sinksBySessionId.delete(oldest)
94
+ }
95
+ sinksBySessionId.set(sessionId, sink)
96
+ }
97
+
98
+ let fallbackAuditState: AuditState | null = null
99
+ let fallbackEventSink: EventSink | null = null
100
+ let activeSessionId = ""
44
101
  let lastFinalizationResult: FinalizationResult | null = null
45
102
 
46
- const getAuditState = (): AuditState | null => currentAuditState
47
- const setAuditState = (state: AuditState | null): void => {
48
- currentAuditState = state
103
+ const getAuditState = (sessionId?: string): AuditState | null => {
104
+ if (sessionId && sessionId.length > 0) {
105
+ const sessionState = statesBySessionId.get(sessionId)
106
+ if (sessionState) {
107
+ return sessionState
108
+ }
109
+ // Fall through to activeSessionId — child sessions (e.g. sentinel)
110
+ // may not have their own state entry but share the parent's state.
111
+ }
112
+
113
+ if (activeSessionId.length > 0) {
114
+ return statesBySessionId.get(activeSessionId) ?? fallbackAuditState
115
+ }
116
+
117
+ return fallbackAuditState
49
118
  }
50
- const setEventSink = (sink: EventSink | null): void => {
51
- eventSink = sink
119
+
120
+ const setAuditState = (state: AuditState | null, sessionId?: string): void => {
121
+ if (sessionId && sessionId.length > 0) {
122
+ if (state) {
123
+ setSessionState(sessionId, state)
124
+ activeSessionId = sessionId
125
+ } else {
126
+ statesBySessionId.delete(sessionId)
127
+ }
128
+ return
129
+ }
130
+
131
+ fallbackAuditState = state
132
+ }
133
+
134
+ const getEventSink = (sessionId?: string): EventSink | null => {
135
+ if (sessionId && sessionId.length > 0) {
136
+ const sessionSink = sinksBySessionId.get(sessionId)
137
+ if (sessionSink) {
138
+ return sessionSink
139
+ }
140
+ return fallbackEventSink
141
+ }
142
+
143
+ if (activeSessionId.length > 0) {
144
+ return sinksBySessionId.get(activeSessionId) ?? fallbackEventSink
145
+ }
146
+
147
+ return fallbackEventSink
148
+ }
149
+
150
+ const setEventSink = (sink: EventSink | null, sessionId?: string): void => {
151
+ if (sessionId && sessionId.length > 0) {
152
+ if (sink) {
153
+ setSessionSink(sessionId, sink)
154
+ } else {
155
+ sinksBySessionId.delete(sessionId)
156
+ }
157
+ return
158
+ }
159
+
160
+ fallbackEventSink = sink
52
161
  }
53
162
 
54
163
  async function emitToSink(
164
+ sink: EventSink | null,
55
165
  type: AuditEvent["type"],
56
166
  runId: string,
57
167
  sessionId: string | undefined,
58
168
  payload: unknown,
59
169
  ): Promise<void> {
60
- if (!eventSink) return
61
- try {
62
- await eventSink.append({
63
- type,
64
- run_id: runId,
65
- seq: 0, // auto-assigned by sink
66
- session_id: sessionId ?? "",
67
- source: "event-hook",
68
- schema_version: SCHEMA_VERSION,
69
- timestamp: Date.now(),
70
- payload,
71
- })
72
- } catch (error) {
73
- logger.error(
74
- `Failed to emit ${type} event to sink: ${error instanceof Error ? error.message : String(error)}`,
75
- )
76
- }
170
+ if (!sink) return
171
+ await safeEmitToSink(sink, {
172
+ type,
173
+ run_id: runId,
174
+ seq: 0, // auto-assigned by sink
175
+ session_id: sessionId ?? "",
176
+ source: "event-hook",
177
+ schema_version: SCHEMA_VERSION,
178
+ timestamp: Date.now(),
179
+ payload,
180
+ })
77
181
  }
78
182
 
79
183
  const hook: EventHookFn = async (input): Promise<void> => {
80
- const { type, sessionId } = input.event
184
+ const type = input.event.type
185
+ const sessionId = extractSessionId(input.event)
186
+ const sessionKey = sessionId && sessionId.length > 0 ? sessionId : activeSessionId
187
+ let stateForSession = getAuditState(sessionKey)
81
188
  let preDeleteState: AuditState | null = null
189
+ const preDeleteSink = getEventSink(sessionKey)
82
190
 
83
191
  switch (type) {
84
192
  case "session.created": {
85
193
  const dir = projectDir ?? process.cwd()
86
194
  const { state } = createAuditState(dir)
87
- currentAuditState = state
195
+ if (sessionId && sessionId.length > 0) {
196
+ setSessionState(sessionId, state)
197
+ activeSessionId = sessionId
198
+ } else {
199
+ fallbackAuditState = state
200
+ }
201
+ stateForSession = state
88
202
  break
89
203
  }
90
204
 
91
205
  case "session.idle": {
92
- if (currentAuditState) {
206
+ if (stateForSession) {
93
207
  logger.debug(
94
- `Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}`,
208
+ `Session idle — phase: ${stateForSession.currentPhase}, findings: ${stateForSession.findings.length}`,
95
209
  )
96
210
  }
97
211
  break
98
212
  }
99
213
 
100
214
  case "session.error": {
101
- if (currentAuditState) {
215
+ if (stateForSession) {
102
216
  logger.error(
103
217
  `Session error — state snapshot: ${JSON.stringify({
104
- sessionId: currentAuditState.sessionId,
105
- phase: currentAuditState.currentPhase,
106
- findingsCount: currentAuditState.findings.length,
107
- contractsReviewed: currentAuditState.contractsReviewed,
218
+ sessionId: stateForSession.sessionId,
219
+ phase: stateForSession.currentPhase,
220
+ findingsCount: stateForSession.findings.length,
221
+ contractsReviewed: stateForSession.contractsReviewed,
108
222
  })}`,
109
223
  )
110
224
  }
@@ -112,7 +226,7 @@ export function createEventHook(
112
226
  }
113
227
 
114
228
  case "session.deleted": {
115
- preDeleteState = currentAuditState
229
+ preDeleteState = stateForSession
116
230
  break
117
231
  }
118
232
 
@@ -122,11 +236,16 @@ export function createEventHook(
122
236
 
123
237
  for (const handler of subHandlers) {
124
238
  try {
239
+ const setStateForSession = (state: AuditState | null): void => {
240
+ setAuditState(state, sessionKey)
241
+ stateForSession = state
242
+ }
243
+
125
244
  await handler({
126
245
  type,
127
246
  sessionId,
128
- auditState: currentAuditState,
129
- setAuditState,
247
+ auditState: stateForSession,
248
+ setAuditState: setStateForSession,
130
249
  })
131
250
  } catch (error) {
132
251
  logger.error(`Sub-handler failed for event ${type}:`, error)
@@ -134,24 +253,32 @@ export function createEventHook(
134
253
  }
135
254
 
136
255
  // Emit canonical events to sink (after sub-handlers, so sink may have been set during session.created)
256
+ const sinkForSession = getEventSink(sessionKey)
137
257
  switch (type) {
138
258
  case "session.created": {
139
- if (currentAuditState) {
140
- await emitToSink("session.created", currentAuditState.sessionId, sessionId, {
141
- projectDir: currentAuditState.projectDir,
142
- sessionId: currentAuditState.sessionId,
143
- plugin_version: ARGUS_PLUGIN_VERSION,
144
- })
259
+ if (stateForSession) {
260
+ await emitToSink(
261
+ sinkForSession,
262
+ "session.created",
263
+ stateForSession.sessionId,
264
+ sessionId,
265
+ {
266
+ projectDir: stateForSession.projectDir,
267
+ sessionId: stateForSession.sessionId,
268
+ plugin_version: ARGUS_PLUGIN_VERSION,
269
+ scope: stateForSession.scope,
270
+ },
271
+ )
145
272
  }
146
273
  break
147
274
  }
148
275
 
149
276
  case "session.idle": {
150
- if (currentAuditState) {
151
- await emitToSink("session.idle", currentAuditState.sessionId, sessionId, {
152
- findingsCount: currentAuditState.findings.length,
153
- toolsExecutedCount: currentAuditState.toolsExecuted.length,
154
- phase: currentAuditState.currentPhase,
277
+ if (stateForSession) {
278
+ await emitToSink(sinkForSession, "session.idle", stateForSession.sessionId, sessionId, {
279
+ findingsCount: stateForSession.findings.length,
280
+ toolsExecutedCount: stateForSession.toolsExecuted.length,
281
+ phase: stateForSession.currentPhase,
155
282
  })
156
283
  }
157
284
  break
@@ -159,17 +286,29 @@ export function createEventHook(
159
286
 
160
287
  case "session.deleted": {
161
288
  if (preDeleteState) {
162
- await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
289
+ await emitToSink(preDeleteSink, "session.deleted", preDeleteState.sessionId, sessionId, {
163
290
  archived: true,
164
291
  plugin_version: ARGUS_PLUGIN_VERSION,
165
292
  })
166
293
 
167
- if (eventSink) {
294
+ const hasSiblingSessionForRun =
295
+ typeof sessionKey === "string" && sessionKey.length > 0
296
+ ? Array.from(sinksBySessionId.entries()).some(
297
+ ([mappedSessionId, mappedSink]) =>
298
+ mappedSessionId !== sessionKey && mappedSink.runId === preDeleteState.sessionId,
299
+ )
300
+ : false
301
+
302
+ if (preDeleteSink && !preDeleteSink.isFinalized && !hasSiblingSessionForRun) {
168
303
  try {
169
304
  lastFinalizationResult = await finalizeRun(
170
305
  preDeleteState.sessionId,
171
306
  preDeleteState.projectDir,
172
- eventSink,
307
+ preDeleteSink,
308
+ )
309
+ void updateRunStatus(
310
+ preDeleteState.sessionId,
311
+ lastFinalizationResult.invariantsPassed ? "finalized" : "failed",
173
312
  )
174
313
  } catch (error) {
175
314
  logger.error(
@@ -178,8 +317,18 @@ export function createEventHook(
178
317
  }
179
318
  }
180
319
  }
181
- currentAuditState = null
182
- eventSink = null
320
+
321
+ if (sessionKey && sessionKey.length > 0) {
322
+ statesBySessionId.delete(sessionKey)
323
+ sinksBySessionId.delete(sessionKey)
324
+ if (activeSessionId === sessionKey) {
325
+ const nextSession = statesBySessionId.keys().next().value
326
+ activeSessionId = typeof nextSession === "string" ? nextSession : ""
327
+ }
328
+ } else {
329
+ fallbackAuditState = null
330
+ fallbackEventSink = null
331
+ }
183
332
  break
184
333
  }
185
334
 
@@ -190,5 +339,12 @@ export function createEventHook(
190
339
 
191
340
  const getLastFinalizationResult = (): FinalizationResult | null => lastFinalizationResult
192
341
 
193
- return { hook, getAuditState, setAuditState, setEventSink, getLastFinalizationResult }
342
+ return {
343
+ hook,
344
+ getAuditState,
345
+ setAuditState,
346
+ setEventSink,
347
+ getEventSink,
348
+ getLastFinalizationResult,
349
+ }
194
350
  }
@@ -1,8 +1,7 @@
1
- import os from "node:os"
2
- import path from "node:path"
3
1
  import type { ArgusConfig } from "../config/types"
4
2
  import { ScvdClient } from "../knowledge/scvd-client"
5
3
  import { type SyncResult, syncIncremental } from "../knowledge/scvd-sync"
4
+ import { getScvdIndexPath } from "../shared/cache-paths"
6
5
  import { createLogger } from "../shared/logger"
7
6
 
8
7
  export type KnowledgeSyncDependencies = {
@@ -36,7 +35,7 @@ export function createKnowledgeSyncHook(
36
35
  }
37
36
 
38
37
  const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
39
- const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
38
+ const indexPath = getScvdIndexPath()
40
39
 
41
40
  Promise.resolve().then(async () => {
42
41
  try {
@@ -1,6 +1,15 @@
1
1
  import { createLogger } from "../shared/logger"
2
2
 
3
- export function safeCreateHook<T>(factory: () => T, hookName: string): T | undefined {
3
+ export interface SafeCreateHookOptions {
4
+ critical?: boolean
5
+ }
6
+
7
+ export function safeCreateHook<T>(
8
+ factory: () => T,
9
+ hookName: string,
10
+ options: SafeCreateHookOptions = {},
11
+ ): T | undefined {
12
+ const { critical = false } = options
4
13
  try {
5
14
  return factory()
6
15
  } catch (error) {
@@ -8,6 +17,9 @@ export function safeCreateHook<T>(factory: () => T, hookName: string): T | undef
8
17
  logger.error(
9
18
  `Failed to create hook "${hookName}": ${error instanceof Error ? error.message : String(error)}`,
10
19
  )
20
+ if (critical) {
21
+ throw error
22
+ }
11
23
  return undefined
12
24
  }
13
25
  }
@@ -1,29 +1,17 @@
1
- import type { AuditState, FindingSeverity } from "../state/types"
1
+ import { computeMissingKeyTools, KEY_TOOLS, TOOL_SHORT_NAMES } from "../shared/key-tools"
2
+ import { estimateTokens } from "../shared/token-utils"
3
+ import { countBySeverity } from "../shared/validation-constants"
4
+ import type { AuditState } from "../state/types"
2
5
 
3
- const DEFAULT_TOKEN_BUDGET = 2000
4
- const TOKENS_PER_CHAR = 4
5
-
6
- const TOOL_SHORT_NAMES: Record<string, string> = {
7
- argus_slither_analyze: "slither",
8
- argus_forge_test: "forge-test",
9
- argus_check_patterns: "patterns",
10
- argus_solodit_search: "solodit",
11
- argus_analyze_contract: "analyzer",
12
- }
13
- const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
6
+ export { estimateTokens }
14
7
 
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
- }
8
+ const DEFAULT_TOKEN_BUDGET = 2000
21
9
 
22
10
  export interface SystemPromptHookDeps {
23
- getAuditState: () => AuditState | null
11
+ getAuditState: (sessionId?: string) => AuditState | null
24
12
  getAgentForSession: (sessionID: string) => string | undefined
25
13
  isArgusAgent: (sessionID: string) => boolean
26
- getContextPressure?: (systemText: string) => number
14
+ getContextPressure?: (systemText: string, sessionId?: string) => number
27
15
  getTokenBudget?: (agent: string, contextPressure: number) => number
28
16
  getEnforcerReminder?: (state: AuditState) => string | null
29
17
  getReconBlock?: () => string | null
@@ -47,26 +35,12 @@ export function buildFallbackDirectives(unavailableTools: string[]): string[] {
47
35
  return directives
48
36
  }
49
37
 
50
- export function estimateTokens(text: string): number {
51
- return Math.ceil(text.length / TOKENS_PER_CHAR)
52
- }
53
-
54
38
  export function buildDynamicContext(
55
39
  auditState: AuditState,
56
40
  agent: string,
57
41
  tokenBudget: number = DEFAULT_TOKEN_BUDGET,
58
42
  ): string {
59
- const severityCounts: Record<FindingSeverity, number> = {
60
- Critical: 0,
61
- High: 0,
62
- Medium: 0,
63
- Low: 0,
64
- Informational: 0,
65
- }
66
-
67
- for (const finding of auditState.findings) {
68
- severityCounts[finding.severity]++
69
- }
43
+ const severityCounts = countBySeverity(auditState.findings)
70
44
 
71
45
  const executedToolNames = new Set(
72
46
  auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
@@ -76,14 +50,14 @@ export function buildDynamicContext(
76
50
  (t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
77
51
  ).join(" ")
78
52
  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))
53
+ const pendingKeyTools = computeMissingKeyTools(auditState.toolsExecuted, unavailable)
81
54
  const gateStatus =
82
55
  pendingKeyTools.length > 0
83
56
  ? `REPORTING GATE: BLOCKED \u2014 key tools pending: ${pendingKeyTools.join(", ")}`
84
57
  : "REPORTING GATE: ALLOWED"
85
58
  const lines: string[] = [
86
59
  `<argus-context agent="${agent}">`,
60
+ ...(auditState.sessionId ? [`run_id: ${auditState.sessionId}`] : []),
87
61
  gateStatus,
88
62
  `Phase: ${auditState.currentPhase}`,
89
63
  `Contracts: ${auditState.contractsReviewed.length} reviewed`,
@@ -97,6 +71,12 @@ export function buildDynamicContext(
97
71
  lines.push(...buildFallbackDirectives(unavailable))
98
72
  }
99
73
 
74
+ if (auditState.currentPhase === "reporting" && !auditState.reportGenerated) {
75
+ lines.push(
76
+ "REPORT GENERATION: INCOMPLETE — Scribe was dispatched but argus_generate_report was not called. Re-dispatch Scribe or call argus_generate_report directly.",
77
+ )
78
+ }
79
+
100
80
  lines.push("</argus-context>")
101
81
 
102
82
  let summary = lines.join("\n")
@@ -105,6 +85,7 @@ export function buildDynamicContext(
105
85
  const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
106
86
  summary = [
107
87
  `<argus-context agent="${agent}">`,
88
+ ...(auditState.sessionId ? [`run_id: ${auditState.sessionId}`] : []),
108
89
  gateStatus,
109
90
  `Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
110
91
  "</argus-context>",
@@ -127,7 +108,7 @@ export function createSystemPromptHook(deps: SystemPromptHookDeps) {
127
108
  return
128
109
  }
129
110
 
130
- const auditState = deps.getAuditState()
111
+ const auditState = deps.getAuditState(input.sessionID)
131
112
  if (!auditState) {
132
113
  return
133
114
  }
@@ -138,7 +119,7 @@ export function createSystemPromptHook(deps: SystemPromptHookDeps) {
138
119
  }
139
120
 
140
121
  const currentSystem = output.system.join("\n")
141
- const pressure = deps.getContextPressure?.(currentSystem) ?? 0
122
+ const pressure = deps.getContextPressure?.(currentSystem, input.sessionID) ?? 0
142
123
  const budget = deps.getTokenBudget?.(agent, pressure) ?? DEFAULT_TOKEN_BUDGET
143
124
 
144
125
  output.system.push(buildDynamicContext(auditState, agent, budget))