solidity-argus 0.3.6 → 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 +851 -142
  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 +57 -3
  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 +606 -326
  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 +396 -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,8 +226,7 @@ export function createEventHook(
112
226
  }
113
227
 
114
228
  case "session.deleted": {
115
- preDeleteState = currentAuditState
116
- currentAuditState = null
229
+ preDeleteState = stateForSession
117
230
  break
118
231
  }
119
232
 
@@ -123,11 +236,16 @@ export function createEventHook(
123
236
 
124
237
  for (const handler of subHandlers) {
125
238
  try {
239
+ const setStateForSession = (state: AuditState | null): void => {
240
+ setAuditState(state, sessionKey)
241
+ stateForSession = state
242
+ }
243
+
126
244
  await handler({
127
245
  type,
128
246
  sessionId,
129
- auditState: currentAuditState,
130
- setAuditState,
247
+ auditState: stateForSession,
248
+ setAuditState: setStateForSession,
131
249
  })
132
250
  } catch (error) {
133
251
  logger.error(`Sub-handler failed for event ${type}:`, error)
@@ -135,24 +253,32 @@ export function createEventHook(
135
253
  }
136
254
 
137
255
  // Emit canonical events to sink (after sub-handlers, so sink may have been set during session.created)
256
+ const sinkForSession = getEventSink(sessionKey)
138
257
  switch (type) {
139
258
  case "session.created": {
140
- if (currentAuditState) {
141
- await emitToSink("session.created", currentAuditState.sessionId, sessionId, {
142
- projectDir: currentAuditState.projectDir,
143
- sessionId: currentAuditState.sessionId,
144
- plugin_version: ARGUS_PLUGIN_VERSION,
145
- })
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
+ )
146
272
  }
147
273
  break
148
274
  }
149
275
 
150
276
  case "session.idle": {
151
- if (currentAuditState) {
152
- await emitToSink("session.idle", currentAuditState.sessionId, sessionId, {
153
- findingsCount: currentAuditState.findings.length,
154
- toolsExecutedCount: currentAuditState.toolsExecuted.length,
155
- 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,
156
282
  })
157
283
  }
158
284
  break
@@ -160,17 +286,29 @@ export function createEventHook(
160
286
 
161
287
  case "session.deleted": {
162
288
  if (preDeleteState) {
163
- await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
289
+ await emitToSink(preDeleteSink, "session.deleted", preDeleteState.sessionId, sessionId, {
164
290
  archived: true,
165
291
  plugin_version: ARGUS_PLUGIN_VERSION,
166
292
  })
167
293
 
168
- 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) {
169
303
  try {
170
304
  lastFinalizationResult = await finalizeRun(
171
305
  preDeleteState.sessionId,
172
306
  preDeleteState.projectDir,
173
- eventSink,
307
+ preDeleteSink,
308
+ )
309
+ void updateRunStatus(
310
+ preDeleteState.sessionId,
311
+ lastFinalizationResult.invariantsPassed ? "finalized" : "failed",
174
312
  )
175
313
  } catch (error) {
176
314
  logger.error(
@@ -179,7 +317,18 @@ export function createEventHook(
179
317
  }
180
318
  }
181
319
  }
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))