solidity-argus 0.1.8 → 0.2.0

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 (84) hide show
  1. package/README.md +161 -1
  2. package/package.json +5 -2
  3. package/skills/README.md +63 -0
  4. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  5. package/skills/manifests/cyfrin.json +16 -0
  6. package/skills/manifests/defifofum.json +25 -0
  7. package/skills/manifests/kadenzipfel.json +48 -0
  8. package/skills/manifests/scvd.json +9 -0
  9. package/skills/manifests/smartbugs.json +11 -0
  10. package/skills/manifests/solodit.json +9 -0
  11. package/skills/manifests/sunweb3sec.json +11 -0
  12. package/skills/manifests/trailofbits.json +9 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  14. package/skills/patterns/access-control.yaml +31 -0
  15. package/skills/patterns/erc4626.yaml +29 -0
  16. package/skills/patterns/flash-loan.yaml +20 -0
  17. package/skills/patterns/oracle.yaml +30 -0
  18. package/skills/patterns/proxy.yaml +30 -0
  19. package/skills/patterns/reentrancy.yaml +30 -0
  20. package/skills/patterns/signature.yaml +31 -0
  21. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  22. package/skills/references/exploit-reference/SKILL.md +3 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
  24. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
  25. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
  26. package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
  27. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
  28. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
  29. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
  30. package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
  31. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
  32. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
  33. package/src/agents/argus-prompt.ts +4 -4
  34. package/src/agents/pythia-prompt.ts +4 -4
  35. package/src/agents/scribe-prompt.ts +3 -3
  36. package/src/agents/sentinel-prompt.ts +4 -4
  37. package/src/cli/cli-output.ts +16 -0
  38. package/src/cli/cli-program.ts +9 -5
  39. package/src/cli/commands/doctor.ts +274 -16
  40. package/src/cli/commands/init.ts +5 -5
  41. package/src/cli/commands/install.ts +5 -5
  42. package/src/cli/commands/lint-skills.ts +114 -0
  43. package/src/cli/tui-prompts.ts +4 -2
  44. package/src/config/schema.ts +2 -0
  45. package/src/create-hooks.ts +99 -14
  46. package/src/create-tools.ts +2 -0
  47. package/src/features/error-recovery/tool-error-recovery.ts +74 -19
  48. package/src/features/persistent-state/audit-state-manager.ts +36 -13
  49. package/src/hooks/agent-tracker.ts +53 -0
  50. package/src/hooks/compaction-hook.ts +46 -37
  51. package/src/hooks/config-handler.ts +3 -0
  52. package/src/hooks/context-budget.ts +45 -0
  53. package/src/hooks/event-hook.ts +5 -4
  54. package/src/hooks/knowledge-sync-hook.ts +2 -1
  55. package/src/hooks/recon-context-builder.ts +66 -0
  56. package/src/hooks/safe-create-hook.ts +4 -5
  57. package/src/hooks/system-prompt-hook.ts +128 -0
  58. package/src/hooks/tool-tracking-hook.ts +86 -7
  59. package/src/index.ts +24 -1
  60. package/src/knowledge/retry.ts +53 -0
  61. package/src/knowledge/scvd-client.ts +37 -10
  62. package/src/knowledge/scvd-errors.ts +89 -0
  63. package/src/knowledge/scvd-index.ts +53 -3
  64. package/src/knowledge/scvd-sync.ts +205 -34
  65. package/src/knowledge/source-manifest.ts +102 -0
  66. package/src/plugin-interface.ts +14 -1
  67. package/src/shared/binary-utils.ts +1 -0
  68. package/src/shared/logger.ts +78 -17
  69. package/src/skills/argus-skill-resolver.ts +226 -0
  70. package/src/skills/skill-schema.ts +98 -0
  71. package/src/state/audit-state.ts +2 -0
  72. package/src/state/types.ts +32 -1
  73. package/src/tools/argus-skill-load-tool.ts +73 -0
  74. package/src/tools/pattern-checker-tool.ts +56 -12
  75. package/src/tools/pattern-loader.ts +183 -0
  76. package/src/tools/pattern-schema.ts +51 -0
  77. package/src/tools/report-generator-tool.ts +134 -11
  78. package/src/tools/slither-tool.ts +61 -19
  79. package/src/tools/solodit-search-tool.ts +92 -14
  80. package/src/utils/audit-artifact-detector.ts +119 -0
  81. package/src/utils/dependency-scanner.ts +93 -0
  82. package/src/utils/project-detector.ts +128 -26
  83. package/src/utils/solidity-parser.ts +20 -4
  84. package/src/utils/solodit-health.ts +29 -0
@@ -6,20 +6,53 @@ import { createConfigHandler } from "./hooks/config-handler"
6
6
  import { createCompactionHook } from "./hooks/compaction-hook"
7
7
  import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
8
8
  import { createEventHookV2 } from "./hooks/event-hook-v2"
9
+ import { createAgentTracker } from "./hooks/agent-tracker"
10
+ import { createSystemPromptHook } from "./hooks/system-prompt-hook"
9
11
  import { safeCreateHook } from "./hooks/safe-create-hook"
10
- import { createToolOutputTruncator } from "./features/context-monitor"
12
+ import { createContextMonitor, createToolOutputTruncator } from "./features/context-monitor"
13
+ import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
14
+ import { getTokenBudgetForAgent } from "./hooks/context-budget"
11
15
  import { createSessionRecoveryHandler } from "./features/error-recovery"
12
16
  import { createToolErrorRecoveryHandler } from "./features/error-recovery"
17
+ import { detectProject } from "./utils/project-detector"
18
+ import type { ProjectConfig } from "./utils/project-detector"
19
+ import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
20
+ import type { ReconContext } from "./hooks/recon-context-builder"
21
+ import { buildReconContextBlock } from "./hooks/recon-context-builder"
22
+ import type { AuditState } from "./state/types"
23
+
24
+ let latestAgentTracker: ReturnType<typeof createAgentTracker> | undefined
25
+
26
+ export function getAgentForSession(sessionID: string): string | undefined {
27
+ return latestAgentTracker?.getAgentForSession(sessionID)
28
+ }
29
+
30
+ export function isArgusAgent(sessionID: string): boolean {
31
+ return latestAgentTracker?.isArgusAgent(sessionID) ?? false
32
+ }
13
33
 
14
34
  export type Hooks = Pick<
15
35
  PluginHooks,
16
36
  | "config"
37
+ | "chat.params"
38
+ | "chat.message"
17
39
  | "experimental.chat.system.transform"
18
40
  | "experimental.session.compacting"
19
41
  | "tool.execute.after"
20
42
  | "event"
21
43
  >
22
44
 
45
+ /**
46
+ * Creates the hook handlers for the Argus plugin.
47
+ *
48
+ * Context Delivery Strategy:
49
+ * - Prompt: Static agent identity (src/agents/*-prompt.ts) — methodology, personality, tool instructions
50
+ * - Hook: Dynamic state injection via experimental.chat.system.transform — audit progress, findings, phase
51
+ * - Skill-load: On-demand knowledge via argus_skill_load tool — vulnerability patterns, protocol knowledge
52
+ *
53
+ * The system.transform hook injects dynamic audit context only for Argus-family agents
54
+ * (argus, sentinel, pythia, scribe). Non-audit agents receive no injection.
55
+ */
23
56
  export function createHooks(args: {
24
57
  config: ArgusConfig
25
58
  managers: Managers
@@ -28,13 +61,17 @@ export function createHooks(args: {
28
61
  }): Hooks {
29
62
  const { config, managers, projectDir, isHookEnabled } = args
30
63
  const { auditStateManager, backgroundManager } = managers
64
+ const agentTracker = createAgentTracker()
65
+ latestAgentTracker = agentTracker
31
66
 
32
- const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
33
- const toolErrorRecoveryHandler = createToolErrorRecoveryHandler()
34
- const outputTruncator = createToolOutputTruncator()
67
+ const contextMonitor = createContextMonitor()
68
+ const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
69
+ let auditStateGetter: (() => AuditState | null) | undefined
70
+ const toolErrorRecoveryHandler = createToolErrorRecoveryHandler(() => auditStateGetter?.() ?? null)
71
+ const outputTruncator = createToolOutputTruncator()
35
72
 
36
73
  const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
37
- async ({ type, auditState, setAuditState: setState }) => {
74
+ async ({ type, sessionId, auditState, setAuditState: setState }) => {
38
75
  if (type === "session.created") {
39
76
  const recoveredState = await auditStateManager.load()
40
77
  if (recoveredState) {
@@ -49,6 +86,9 @@ export function createHooks(args: {
49
86
  }
50
87
 
51
88
  if (type === "session.deleted") {
89
+ if (sessionId) {
90
+ agentTracker.clearSession(sessionId)
91
+ }
52
92
  await auditStateManager.reset()
53
93
  }
54
94
  },
@@ -62,13 +102,50 @@ export function createHooks(args: {
62
102
  },
63
103
  ])
64
104
 
65
- const initialState = auditStateManager.get()
66
- if (initialState) {
67
- setAuditState(initialState)
68
- }
105
+ auditStateGetter = getAuditState
106
+
107
+ const initialState = auditStateManager.get()
108
+ if (initialState) {
109
+ setAuditState(initialState)
110
+ }
111
+
112
+ const auditEnforcer = createAuditEnforcer()
69
113
 
70
- const compactionHook = isHookEnabled("compaction")
71
- ? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
114
+ const systemPromptHook = createSystemPromptHook({
115
+ getAuditState,
116
+ getAgentForSession: agentTracker.getAgentForSession,
117
+ isArgusAgent: agentTracker.isArgusAgent,
118
+ getContextPressure: (systemText: string) => {
119
+ const status = contextMonitor.getContextStatus(systemText, getAuditState())
120
+ return status.usage
121
+ },
122
+ getTokenBudget: getTokenBudgetForAgent,
123
+ getEnforcerReminder: auditEnforcer,
124
+ getReconBlock: () => buildReconContextBlock({
125
+ projectConfig: reconProjectConfig,
126
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
127
+ auditArtifacts: detectAuditArtifacts(projectDir),
128
+ }),
129
+ })
130
+
131
+ let reconProjectConfig: ProjectConfig | null = null
132
+
133
+ detectProject(projectDir)
134
+ .then((config) => {
135
+ reconProjectConfig = config
136
+ })
137
+ .catch(() => {
138
+ // Silent fallback — audit artifacts remain available
139
+ })
140
+
141
+ const getReconContext = (): ReconContext => ({
142
+ projectConfig: reconProjectConfig,
143
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
144
+ auditArtifacts: detectAuditArtifacts(projectDir),
145
+ })
146
+
147
+ const compactionHook = isHookEnabled("compaction")
148
+ ? safeCreateHook(() => createCompactionHook(getAuditState, getReconContext), "compaction")
72
149
  : undefined
73
150
 
74
151
  const toolTrackingHook = isHookEnabled("tool-tracking")
@@ -79,9 +156,17 @@ export function createHooks(args: {
79
156
  ? safeCreateHook(() => eventHook, "event")
80
157
  : undefined
81
158
 
82
- return {
83
- config: createConfigHandler(config, projectDir),
84
- "experimental.chat.system.transform": undefined,
159
+ return {
160
+ config: createConfigHandler(config, projectDir),
161
+ "chat.params": async (input) => {
162
+ agentTracker.chatParamsHook(input)
163
+ },
164
+ "chat.message": async (input) => {
165
+ agentTracker.chatMessageHook(input)
166
+ },
167
+ "experimental.chat.system.transform": async (input, output) => {
168
+ await systemPromptHook(input, output)
169
+ },
85
170
  "experimental.session.compacting": compactionHook
86
171
  ? async (_input, output) => {
87
172
  const block = await compactionHook({ summary: output.context.join("\n") })
@@ -8,6 +8,7 @@ import { patternCheckerTool } from "./tools/pattern-checker-tool"
8
8
  import { soloditSearchTool } from "./tools/solodit-search-tool"
9
9
  import { reportGeneratorTool } from "./tools/report-generator-tool"
10
10
  import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
11
+ import { argusSkillLoadTool } from "./tools/argus-skill-load-tool"
11
12
 
12
13
  export function createTools(
13
14
  config: ArgusConfig,
@@ -18,6 +19,7 @@ export function createTools(
18
19
  argus_forge_fuzz: forgeFuzzTool,
19
20
  argus_analyze_contract: contractAnalyzerTool,
20
21
  argus_check_patterns: patternCheckerTool,
22
+ argus_skill_load: argusSkillLoadTool,
21
23
  argus_generate_report: reportGeneratorTool,
22
24
  argus_sync_knowledge: syncKnowledgeTool,
23
25
  }
@@ -1,15 +1,60 @@
1
1
  import { createLogger } from "../../shared/logger"
2
+ import type { AuditState } from "../../state/types"
2
3
 
3
- const RECOVERY_HINTS: Record<string, string> = {
4
- slither: "Install Slither: pip install slither-analyzer",
5
- forge: "Install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup",
6
- solodit: "Check network connectivity or Solodit API status",
7
- scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
4
+ type ToolFallbackEntry = {
5
+ install: string
6
+ fallback: string
8
7
  }
9
8
 
10
- const VIA_IR_HINT = "Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
9
+ const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
10
+ slither: {
11
+ install: "pip install slither-analyzer",
12
+ fallback:
13
+ "Slither is unavailable. PROCEED with the audit using `argus_analyze_contract` for structural profiling and `argus_check_patterns` for vulnerability scanning. Note in the final report: \"Automated static analysis (Slither) was unavailable; manual review intensity increased.\"",
14
+ },
15
+ forge: {
16
+ install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
17
+ fallback:
18
+ "Foundry/Forge is unavailable. SKIP automated testing and fuzzing. Verify findings through manual code tracing and static analysis. Note in the final report: \"Dynamic testing (Forge) was unavailable; findings verified via manual analysis.\"",
19
+ },
20
+ solodit: {
21
+ install: "",
22
+ fallback:
23
+ "Solodit API is unreachable. PROCEED using `argus_check_patterns` with local vulnerability rules. Note in the final report: \"External vulnerability databases were inaccessible; research limited to local patterns.\"",
24
+ },
25
+ scvd: {
26
+ install: "",
27
+ fallback:
28
+ "SCVD API is unavailable. PROCEED with local patterns and Solodit search if available.",
29
+ },
30
+ }
31
+
32
+ const VIA_IR_HINT =
33
+ "Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
34
+
35
+ function isToolUnavailable(lowerResult: string): boolean {
36
+ return (
37
+ lowerResult.includes("enoent") ||
38
+ lowerResult.includes("not found") ||
39
+ lowerResult.includes("not installed")
40
+ )
41
+ }
42
+
43
+ function isToolError(lowerResult: string): boolean {
44
+ return (
45
+ isToolUnavailable(lowerResult) ||
46
+ lowerResult.includes("command failed") ||
47
+ lowerResult.includes("error:")
48
+ )
49
+ }
50
+
51
+ function resolveToolBase(tool: string): string {
52
+ return tool.replace("argus_", "").split("_")[0] ?? ""
53
+ }
11
54
 
12
- export function createToolErrorRecoveryHandler() {
55
+ export function createToolErrorRecoveryHandler(
56
+ getAuditState?: () => AuditState | null,
57
+ ) {
13
58
  const logger = createLogger()
14
59
 
15
60
  return (toolResult: { tool: string; result: string }): string | null => {
@@ -27,22 +72,32 @@ export function createToolErrorRecoveryHandler() {
27
72
  return `\n[Argus Recovery Hint] ${VIA_IR_HINT}`
28
73
  }
29
74
 
30
- const isError =
31
- lowerResult.includes("enoent") ||
32
- lowerResult.includes("not found") ||
33
- lowerResult.includes("command failed") ||
34
- lowerResult.includes("error:")
75
+ if (!isToolError(lowerResult)) return null
35
76
 
36
- if (!isError) return null
77
+ const toolBase = resolveToolBase(tool)
78
+ const entry = TOOL_FALLBACKS[toolBase]
79
+ if (!entry) return null
37
80
 
38
- const toolBase = tool.replace("argus_", "").split("_")[0] ?? ""
39
- const hint = RECOVERY_HINTS[toolBase]
81
+ const unavailable = isToolUnavailable(lowerResult)
82
+
83
+ if (unavailable && getAuditState) {
84
+ const state = getAuditState()
85
+ if (state) {
86
+ state.unavailableTools ??= []
87
+ if (!state.unavailableTools.includes(toolBase)) {
88
+ state.unavailableTools.push(toolBase)
89
+ logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
90
+ }
91
+ }
92
+ }
40
93
 
41
- if (hint) {
42
- logger.info(`Tool error recovery hint for ${tool}: ${hint}`)
43
- return `\n[Argus Recovery Hint] ${hint}`
94
+ if (unavailable) {
95
+ logger.info(`Tool unavailable fallback for ${tool}`)
96
+ return `\n[Argus Fallback] ${entry.fallback}`
44
97
  }
45
98
 
46
- return null
99
+ const installHint = entry.install ? ` (install: ${entry.install})` : ""
100
+ logger.info(`Tool error recovery hint for ${tool}`)
101
+ return `\n[Argus Recovery Hint] ${toolBase} error${installHint}. ${entry.fallback}`
47
102
  }
48
103
  }
@@ -7,7 +7,7 @@ import { createLogger } from "../../shared/logger";
7
7
 
8
8
  const STATE_FILE_DIR = ".opencode";
9
9
  const STATE_FILE_NAME = "argus-state.json";
10
- const STATE_VERSION = "1";
10
+ const STATE_VERSION = "2";
11
11
 
12
12
  function isObject(value: unknown): value is Record<string, unknown> {
13
13
  return typeof value === "object" && value !== null;
@@ -39,9 +39,11 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
39
39
  return false;
40
40
  }
41
41
 
42
+ const hasSupportedVersion = value.version === "1" || value.version === "2";
43
+
42
44
  return (
43
45
  typeof value.savedAt === "number" &&
44
- typeof value.version === "string" &&
46
+ hasSupportedVersion &&
45
47
  typeof value.filePath === "string"
46
48
  );
47
49
  }
@@ -69,7 +71,17 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
69
71
  return null;
70
72
  }
71
73
 
72
- const { savedAt: _savedAt, version: _version, filePath: _filePath, ...state } = parsed;
74
+ const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed;
75
+
76
+ if (version === "1") {
77
+ if (!state.soloditResults) {
78
+ state.soloditResults = [];
79
+ }
80
+ if (!state.fuzzCounterexamples) {
81
+ state.fuzzCounterexamples = [];
82
+ }
83
+ }
84
+
73
85
  currentState = state;
74
86
  return currentState;
75
87
  } catch (_error) {
@@ -77,20 +89,31 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
77
89
  }
78
90
  }
79
91
 
92
+ let saveInFlight = false;
93
+
80
94
  async function save(state: AuditState): Promise<void> {
81
95
  currentState = state;
82
96
 
83
- const persistentState: PersistentAuditState = {
84
- ...state,
85
- savedAt: Date.now(),
86
- version: STATE_VERSION,
87
- filePath: stateFilePath,
88
- };
97
+ if (saveInFlight) return;
98
+ saveInFlight = true;
89
99
 
90
- const tempFilePath = `${stateFilePath}.tmp`;
91
- await mkdir(dirname(stateFilePath), { recursive: true });
92
- await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
93
- await rename(tempFilePath, stateFilePath);
100
+ try {
101
+ const persistentState: PersistentAuditState = {
102
+ ...state,
103
+ savedAt: Date.now(),
104
+ version: STATE_VERSION,
105
+ filePath: stateFilePath,
106
+ };
107
+
108
+ const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`;
109
+ await mkdir(dirname(stateFilePath), { recursive: true });
110
+ await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
111
+ await rename(tempFilePath, stateFilePath);
112
+ } catch {
113
+ // Non-critical: state persistence is best-effort
114
+ } finally {
115
+ saveInFlight = false;
116
+ }
94
117
  }
95
118
 
96
119
  function get(): AuditState {
@@ -0,0 +1,53 @@
1
+ import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
+
3
+ type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
4
+ agent?: string
5
+ }
6
+ type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
7
+
8
+ const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
9
+
10
+ export type AgentTracker = ReturnType<typeof createAgentTracker>
11
+
12
+ export function createAgentTracker() {
13
+ const sessions = new Map<string, string>()
14
+
15
+ const trackSession = (sessionID: string, agent?: string): void => {
16
+ if (!agent) {
17
+ return
18
+ }
19
+
20
+ sessions.set(sessionID, agent)
21
+ }
22
+
23
+ return {
24
+ chatParamsHook: (input: ChatParamsInput): void => {
25
+ trackSession(input.sessionID, input.agent)
26
+ },
27
+
28
+ chatMessageHook: (input: ChatMessageInput): void => {
29
+ trackSession(input.sessionID, input.agent)
30
+ },
31
+
32
+ getAgentForSession: (sessionID: string): string | undefined => {
33
+ return sessions.get(sessionID)
34
+ },
35
+
36
+ isArgusAgent: (sessionID: string): boolean => {
37
+ const agent = sessions.get(sessionID)
38
+ if (!agent) {
39
+ return false
40
+ }
41
+
42
+ return ARGUS_FAMILY.has(agent)
43
+ },
44
+
45
+ clearSession: (sessionID: string): void => {
46
+ sessions.delete(sessionID)
47
+ },
48
+
49
+ getTrackedSessions: (): Map<string, string> => {
50
+ return sessions
51
+ },
52
+ }
53
+ }
@@ -1,50 +1,59 @@
1
1
  import type { AuditState, FindingSeverity } from "../state/types"
2
+ import type { ReconContext } from "./recon-context-builder"
3
+ import { buildReconContextBlock } from "./recon-context-builder"
2
4
 
3
- /**
4
- * Creates a compaction hook that serializes audit state into XML format
5
- * so findings survive context window compression.
6
- *
7
- * The returned hook is called by OpenCode's `experimental.session.compacting`
8
- * event, receiving `{ summary: string }` and returning the enriched summary.
9
- */
10
5
  export function createCompactionHook(
11
- getAuditState: () => AuditState | null
6
+ getAuditState: () => AuditState | null,
7
+ getReconContext?: () => ReconContext | null,
12
8
  ): (input: { summary: string }) => Promise<string | null> {
13
9
  return async (_input: { summary: string }): Promise<string | null> => {
14
10
  const state = getAuditState()
15
- if (!state) {
16
- return null
17
- }
18
11
 
19
- const severityCounts: Record<FindingSeverity, number> = {
20
- Critical: 0,
21
- High: 0,
22
- Medium: 0,
23
- Low: 0,
24
- Informational: 0,
12
+ const parts: string[] = []
13
+
14
+ if (state) {
15
+ const severityCounts: Record<FindingSeverity, number> = {
16
+ Critical: 0,
17
+ High: 0,
18
+ Medium: 0,
19
+ Low: 0,
20
+ Informational: 0,
21
+ }
22
+
23
+ for (const finding of state.findings) {
24
+ severityCounts[finding.severity]++
25
+ }
26
+
27
+ const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
28
+ const contracts = state.contractsReviewed.join(", ")
29
+ const started = new Date(state.startTime).toISOString()
30
+
31
+ parts.push(
32
+ [
33
+ "<argus-audit-state>",
34
+ `Phase: ${state.currentPhase}`,
35
+ `Contracts Reviewed: ${contracts}`,
36
+ "Findings:",
37
+ ` Critical: ${severityCounts.Critical}`,
38
+ ` High: ${severityCounts.High}`,
39
+ ` Medium: ${severityCounts.Medium}`,
40
+ ` Low: ${severityCounts.Low}`,
41
+ ` Informational: ${severityCounts.Informational}`,
42
+ `Tools Executed: ${toolNames}`,
43
+ `Started: ${started}`,
44
+ "</argus-audit-state>",
45
+ ].join("\n"),
46
+ )
25
47
  }
26
48
 
27
- for (const finding of state.findings) {
28
- severityCounts[finding.severity]++
49
+ if (getReconContext) {
50
+ const recon = getReconContext()
51
+ if (recon) {
52
+ const reconBlock = buildReconContextBlock(recon)
53
+ if (reconBlock) parts.push(reconBlock)
54
+ }
29
55
  }
30
56
 
31
- const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
32
- const contracts = state.contractsReviewed.join(", ")
33
- const started = new Date(state.startTime).toISOString()
34
-
35
- return [
36
- "<argus-audit-state>",
37
- `Phase: ${state.currentPhase}`,
38
- `Contracts Reviewed: ${contracts}`,
39
- "Findings:",
40
- ` Critical: ${severityCounts.Critical}`,
41
- ` High: ${severityCounts.High}`,
42
- ` Medium: ${severityCounts.Medium}`,
43
- ` Low: ${severityCounts.Low}`,
44
- ` Informational: ${severityCounts.Informational}`,
45
- `Tools Executed: ${toolNames}`,
46
- `Started: ${started}`,
47
- "</argus-audit-state>",
48
- ].join("\n")
57
+ return parts.length > 0 ? parts.join("\n") : null
49
58
  }
50
59
  }
@@ -93,6 +93,7 @@ export function createConfigHandler(
93
93
  argus_forge_fuzz: "allow",
94
94
  argus_analyze_contract: "allow",
95
95
  argus_check_patterns: "allow",
96
+ argus_skill_load: "allow",
96
97
  skill: "allow",
97
98
  },
98
99
  },
@@ -104,6 +105,7 @@ export function createConfigHandler(
104
105
  permission: {
105
106
  argus_solodit_search: "allow",
106
107
  argus_check_patterns: "allow",
108
+ argus_skill_load: "allow",
107
109
  skill: "allow",
108
110
  },
109
111
  },
@@ -114,6 +116,7 @@ export function createConfigHandler(
114
116
  prompt: SCRIBE_PROMPT,
115
117
  permission: {
116
118
  argus_generate_report: "allow",
119
+ argus_skill_load: "allow",
117
120
  skill: "allow",
118
121
  },
119
122
  },
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Context budget allocation for Argus agents.
3
+ *
4
+ * Provides per-agent token budgets for system-prompt injection sizing.
5
+ * Under context pressure (>70%), budgets are reduced by 50% to prevent
6
+ * context window overflow during long audits.
7
+ *
8
+ * Decoupled from system-prompt-hook.ts — consumed by the hook when available.
9
+ */
10
+
11
+ const ARGUS_BUDGET = 2000
12
+ const SUBAGENT_BUDGET = 1000
13
+ const PRESSURE_THRESHOLD = 0.70
14
+ const PRESSURE_REDUCTION = 0.5
15
+
16
+ const ARGUS_AGENTS = new Set(["argus"])
17
+ const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
18
+
19
+ /**
20
+ * Returns the token budget for a given agent, adjusted for context pressure.
21
+ *
22
+ * @param agent - Agent name (e.g. "argus", "sentinel", "pythia", "scribe")
23
+ * @param contextPressure - Current context usage ratio (0.0–1.0), from ContextMonitor
24
+ * @returns Token budget in tokens. 0 for non-Argus agents.
25
+ */
26
+ export function getTokenBudgetForAgent(
27
+ agent: string,
28
+ contextPressure: number = 0,
29
+ ): number {
30
+ let budget: number
31
+
32
+ if (ARGUS_AGENTS.has(agent)) {
33
+ budget = ARGUS_BUDGET
34
+ } else if (SUBAGENTS.has(agent)) {
35
+ budget = SUBAGENT_BUDGET
36
+ } else {
37
+ return 0
38
+ }
39
+
40
+ if (contextPressure > PRESSURE_THRESHOLD) {
41
+ budget = Math.floor(budget * PRESSURE_REDUCTION)
42
+ }
43
+
44
+ return budget
45
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AuditState } from "../state/types"
2
2
  import { createAuditState } from "../state/audit-state"
3
+ import { createLogger } from "../shared/logger"
3
4
 
4
5
  export type EventHookFn = (input: {
5
6
  event: { type: string; sessionId?: string }
@@ -38,8 +39,8 @@ export function createEventHook(projectDir?: string): {
38
39
 
39
40
  case "session.idle": {
40
41
  if (currentAuditState) {
41
- console.error(
42
- `[argus-state] Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}, contracts: ${currentAuditState.contractsReviewed.length}`
42
+ createLogger().debug(
43
+ `[state] Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}, contracts: ${currentAuditState.contractsReviewed.length}`
43
44
  )
44
45
  }
45
46
  break
@@ -47,8 +48,8 @@ export function createEventHook(projectDir?: string): {
47
48
 
48
49
  case "session.error": {
49
50
  if (currentAuditState) {
50
- console.error(
51
- `[argus-error] Session error — state snapshot: ${JSON.stringify({
51
+ createLogger().error(
52
+ `Session error — state snapshot: ${JSON.stringify({
52
53
  sessionId: currentAuditState.sessionId,
53
54
  phase: currentAuditState.currentPhase,
54
55
  findingsCount: currentAuditState.findings.length,
@@ -3,6 +3,7 @@ import path from "node:path"
3
3
  import { ScvdClient } from "../knowledge/scvd-client"
4
4
  import { syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
5
5
  import type { ArgusConfig } from "../config/types"
6
+ import { createLogger } from "../shared/logger"
6
7
 
7
8
  export type KnowledgeSyncDependencies = {
8
9
  createClient?: (apiUrl: string) => unknown
@@ -18,7 +19,7 @@ function defaultDependencies(): Required<KnowledgeSyncDependencies> {
18
19
  syncIncrementalFn: async (client: unknown, indexPath: string) =>
19
20
  syncIncremental(client as ScvdClient, indexPath),
20
21
  log: (message: string) => {
21
- console.error(message)
22
+ createLogger().info(message)
22
23
  },
23
24
  }
24
25
  }