solidity-argus 0.1.8 → 0.3.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 (178) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +229 -13
  3. package/package.json +37 -8
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +72 -6
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  22. package/skills/manifests/cyfrin.json +16 -0
  23. package/skills/manifests/defifofum.json +25 -0
  24. package/skills/manifests/kadenzipfel.json +48 -0
  25. package/skills/manifests/scvd.json +9 -0
  26. package/skills/manifests/smartbugs.json +9 -0
  27. package/skills/manifests/solodit.json +9 -0
  28. package/skills/manifests/sunweb3sec.json +9 -0
  29. package/skills/manifests/trailofbits.json +9 -0
  30. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  31. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  32. package/skills/references/exploit-reference/SKILL.md +3 -0
  33. package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
  34. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  35. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  36. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  37. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
  38. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  39. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  40. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  42. package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
  43. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  44. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  45. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
  46. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  47. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  48. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  49. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  50. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  51. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  52. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  54. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  55. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  56. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  57. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  59. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  60. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  61. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
  62. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
  64. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  65. package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
  66. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  67. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  68. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  70. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  71. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
  72. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  73. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  74. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  75. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  76. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  77. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  78. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  79. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  80. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  81. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  82. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  83. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  84. package/src/agents/argus-prompt.ts +27 -10
  85. package/src/agents/pythia-prompt.ts +7 -8
  86. package/src/agents/scribe-prompt.ts +10 -5
  87. package/src/agents/sentinel-prompt.ts +36 -7
  88. package/src/cli/cli-output.ts +16 -0
  89. package/src/cli/cli-program.ts +29 -22
  90. package/src/cli/commands/check-skills.ts +135 -0
  91. package/src/cli/commands/doctor.ts +303 -23
  92. package/src/cli/commands/init.ts +8 -6
  93. package/src/cli/commands/install.ts +10 -8
  94. package/src/cli/commands/lint-skills.ts +118 -0
  95. package/src/cli/index.ts +5 -5
  96. package/src/cli/tui-prompts.ts +4 -2
  97. package/src/cli/types.ts +3 -3
  98. package/src/config/index.ts +1 -1
  99. package/src/config/loader.ts +4 -6
  100. package/src/config/schema.ts +6 -5
  101. package/src/config/types.ts +2 -2
  102. package/src/constants/defaults.ts +2 -0
  103. package/src/create-hooks.ts +225 -29
  104. package/src/create-managers.ts +10 -8
  105. package/src/create-tools.ts +14 -8
  106. package/src/features/background-agent/background-manager.ts +93 -87
  107. package/src/features/background-agent/index.ts +1 -1
  108. package/src/features/context-monitor/context-monitor.ts +3 -3
  109. package/src/features/context-monitor/index.ts +2 -2
  110. package/src/features/error-recovery/session-recovery.ts +2 -4
  111. package/src/features/error-recovery/tool-error-recovery.ts +79 -19
  112. package/src/features/index.ts +5 -5
  113. package/src/features/persistent-state/audit-state-manager.ts +158 -52
  114. package/src/features/persistent-state/global-run-index.ts +38 -0
  115. package/src/features/persistent-state/index.ts +1 -1
  116. package/src/features/persistent-state/run-journal.ts +86 -0
  117. package/src/hooks/agent-tracker.ts +53 -0
  118. package/src/hooks/compaction-hook.ts +46 -37
  119. package/src/hooks/config-handler.ts +31 -11
  120. package/src/hooks/context-budget.ts +42 -0
  121. package/src/hooks/event-hook.ts +48 -23
  122. package/src/hooks/hook-system.ts +4 -4
  123. package/src/hooks/index.ts +5 -5
  124. package/src/hooks/knowledge-sync-hook.ts +19 -21
  125. package/src/hooks/recon-context-builder.ts +66 -0
  126. package/src/hooks/safe-create-hook.ts +9 -11
  127. package/src/hooks/system-prompt-hook.ts +128 -0
  128. package/src/hooks/tool-tracking-hook.ts +162 -29
  129. package/src/hooks/types.ts +2 -1
  130. package/src/index.ts +23 -13
  131. package/src/knowledge/retry.ts +53 -0
  132. package/src/knowledge/scvd-client.ts +103 -83
  133. package/src/knowledge/scvd-errors.ts +89 -0
  134. package/src/knowledge/scvd-index.ts +110 -62
  135. package/src/knowledge/scvd-sync.ts +223 -47
  136. package/src/knowledge/source-manifest.ts +102 -0
  137. package/src/managers/index.ts +1 -1
  138. package/src/managers/types.ts +19 -14
  139. package/src/plugin-interface.ts +19 -8
  140. package/src/shared/binary-utils.ts +44 -34
  141. package/src/shared/deep-merge.ts +55 -36
  142. package/src/shared/file-utils.ts +21 -19
  143. package/src/shared/index.ts +11 -5
  144. package/src/shared/jsonc-parser.ts +123 -28
  145. package/src/shared/logger.ts +91 -17
  146. package/src/shared/project-utils.ts +30 -0
  147. package/src/skills/analysis/cluster.ts +414 -0
  148. package/src/skills/analysis/gates.ts +227 -0
  149. package/src/skills/analysis/index.ts +33 -0
  150. package/src/skills/analysis/normalize.ts +217 -0
  151. package/src/skills/analysis/similarity.ts +224 -0
  152. package/src/skills/argus-skill-resolver.ts +237 -0
  153. package/src/skills/skill-schema.ts +99 -0
  154. package/src/solodit-lifecycle.ts +202 -0
  155. package/src/state/audit-state.ts +10 -8
  156. package/src/state/finding-store.ts +68 -55
  157. package/src/state/types.ts +96 -44
  158. package/src/tools/argus-skill-load-tool.ts +78 -0
  159. package/src/tools/contract-analyzer-tool.ts +60 -77
  160. package/src/tools/forge-coverage-tool.ts +226 -0
  161. package/src/tools/forge-fuzz-tool.ts +127 -127
  162. package/src/tools/forge-test-tool.ts +153 -157
  163. package/src/tools/gas-analysis-tool.ts +264 -0
  164. package/src/tools/pattern-checker-tool.ts +206 -167
  165. package/src/tools/pattern-loader.ts +77 -0
  166. package/src/tools/pattern-schema.ts +51 -0
  167. package/src/tools/proxy-detection-tool.ts +224 -0
  168. package/src/tools/report-generator-tool.ts +333 -142
  169. package/src/tools/slither-tool.ts +300 -210
  170. package/src/tools/solodit-search-tool.ts +255 -80
  171. package/src/tools/sync-knowledge-tool.ts +7 -11
  172. package/src/utils/audit-artifact-detector.ts +118 -0
  173. package/src/utils/dependency-scanner.ts +93 -0
  174. package/src/utils/project-detector.ts +175 -86
  175. package/src/utils/solidity-parser.ts +112 -67
  176. package/src/utils/solodit-health.ts +29 -0
  177. package/src/hooks/event-hook-v2.ts +0 -99
  178. package/src/state/plugin-state.ts +0 -14
@@ -1,6 +1,6 @@
1
- import type { AuditState, FindingSeverity } from "../state/types"
2
1
  import type { FindingStore } from "../state/finding-store"
3
2
  import { createFindingStore } from "../state/finding-store"
3
+ import type { AuditState, FindingSeverity, FuzzCounterexample, SoloditResult } from "../state/types"
4
4
 
5
5
  type ToolHookInput = {
6
6
  tool: string
@@ -8,6 +8,11 @@ type ToolHookInput = {
8
8
  result: string
9
9
  }
10
10
 
11
+ type ToolExecutionMetadata = {
12
+ tool: string
13
+ findingsCount: number
14
+ }
15
+
11
16
  const VALID_SEVERITIES: ReadonlySet<string> = new Set([
12
17
  "Critical",
13
18
  "High",
@@ -16,11 +21,7 @@ const VALID_SEVERITIES: ReadonlySet<string> = new Set([
16
21
  "Informational",
17
22
  ])
18
23
 
19
- const VALID_CONFIDENCES: ReadonlySet<string> = new Set([
20
- "High",
21
- "Medium",
22
- "Low",
23
- ])
24
+ const VALID_CONFIDENCES: ReadonlySet<string> = new Set(["High", "Medium", "Low"])
24
25
 
25
26
  function toSeverity(value: unknown): FindingSeverity {
26
27
  if (typeof value === "string" && VALID_SEVERITIES.has(value)) {
@@ -55,10 +56,7 @@ function toRecord(value: unknown): Record<string, unknown> | undefined {
55
56
  return undefined
56
57
  }
57
58
 
58
- function processSlitherResult(
59
- parsed: Record<string, unknown>,
60
- store: FindingStore
61
- ): number {
59
+ function processSlitherResult(parsed: Record<string, unknown>, store: FindingStore): number {
62
60
  const findings = parsed.findings
63
61
  if (!Array.isArray(findings)) return 0
64
62
 
@@ -96,10 +94,7 @@ function processSlitherResult(
96
94
  return count
97
95
  }
98
96
 
99
- function processPatternResult(
100
- parsed: Record<string, unknown>,
101
- store: FindingStore
102
- ): number {
97
+ function processPatternResult(parsed: Record<string, unknown>, store: FindingStore): number {
103
98
  const sources = parsed.sources
104
99
  if (!Array.isArray(sources)) return 0
105
100
 
@@ -145,10 +140,7 @@ function processPatternResult(
145
140
  return count
146
141
  }
147
142
 
148
- function processContractAnalyzerResult(
149
- parsed: Record<string, unknown>,
150
- state: AuditState
151
- ): void {
143
+ function processContractAnalyzerResult(parsed: Record<string, unknown>, state: AuditState): void {
152
144
  // Handle direct ContractProfile format (actual tool output)
153
145
  if (typeof parsed.filePath === "string") {
154
146
  if (!state.contractsReviewed.includes(parsed.filePath)) {
@@ -166,16 +158,82 @@ function processContractAnalyzerResult(
166
158
  }
167
159
  }
168
160
 
169
- function recordToolExecution(
170
- state: AuditState,
171
- toolName: string,
172
- findingsCount: number
173
- ): void {
174
- const alreadyRecorded = state.toolsExecuted.some(
175
- (execution) => execution.tool === toolName
176
- )
177
- if (alreadyRecorded) return
161
+ function processFuzzResult(parsed: Record<string, unknown>, state: AuditState): void {
162
+ const counterexamples = parsed.counterexamples
163
+ if (!Array.isArray(counterexamples) || counterexamples.length === 0) return
164
+
165
+ const totalRuns = typeof parsed.totalRuns === "number" ? parsed.totalRuns : 0
166
+
167
+ state.fuzzCounterexamples ??= []
168
+
169
+ for (const raw of counterexamples) {
170
+ const ce = toRecord(raw)
171
+ if (!ce) continue
172
+
173
+ const testName = ce.testName
174
+ if (typeof testName !== "string") continue
175
+
176
+ const rawInputs = ce.inputs
177
+ const inputs = Array.isArray(rawInputs)
178
+ ? rawInputs.map(String)
179
+ : (() => {
180
+ const rec = toRecord(rawInputs)
181
+ return rec ? Object.values(rec).map(String) : []
182
+ })()
183
+
184
+ const entry: FuzzCounterexample = {
185
+ testName,
186
+ inputs,
187
+ runs: totalRuns,
188
+ timestamp: Date.now(),
189
+ }
190
+
191
+ if (typeof ce.revertReason === "string") {
192
+ entry.revertReason = ce.revertReason
193
+ }
194
+
195
+ state.fuzzCounterexamples.push(entry)
196
+ }
197
+ }
198
+
199
+ function processSoloditResult(parsed: Record<string, unknown>, state: AuditState): void {
200
+ const query = typeof parsed.query === "string" ? parsed.query : ""
201
+ const results = Array.isArray(parsed.results) ? parsed.results : []
202
+ const totalFound = typeof parsed.totalFound === "number" ? parsed.totalFound : results.length
203
+
204
+ const topResults: SoloditResult["topResults"] = results.slice(0, 5).map((raw) => {
205
+ const r = toRecord(raw)
206
+ return {
207
+ title: typeof r?.title === "string" ? r.title : "",
208
+ severity: typeof r?.severity === "string" ? r.severity : "",
209
+ url: typeof r?.url === "string" ? r.url : "",
210
+ protocol: typeof r?.protocol === "string" ? r.protocol : "",
211
+ }
212
+ })
178
213
 
214
+ state.soloditResults ??= []
215
+ state.soloditResults.push({
216
+ query,
217
+ timestamp: Date.now(),
218
+ resultCount: totalFound,
219
+ topResults,
220
+ })
221
+ }
222
+
223
+ /**
224
+ * Records a tool execution in the audit state.
225
+ *
226
+ * Multiple entries per tool name are allowed — if the same tool runs multiple times
227
+ * (e.g., argus_slither_analyze on different targets), each execution is recorded
228
+ * with its own findingsCount.
229
+ *
230
+ * Timing limitation: startTime and endTime are both set to Date.now() because this
231
+ * hook fires in the tool.execute.after phase, after execution has already completed.
232
+ * We cannot capture the actual start time. This is a known limitation of the hook
233
+ * architecture. For accurate timing, the hook would need to fire in tool.execute.before
234
+ * and tool.execute.after phases separately.
235
+ */
236
+ function recordToolExecution(state: AuditState, toolName: string, findingsCount: number): void {
179
237
  const now = Date.now()
180
238
  state.toolsExecuted.push({
181
239
  tool: toolName,
@@ -194,7 +252,8 @@ function recordToolExecution(
194
252
  * Findings are deduplicated via the FindingStore (by check+file+lines).
195
253
  */
196
254
  export function createToolTrackingHook(
197
- getAuditState: () => AuditState | null
255
+ getAuditState: () => AuditState | null,
256
+ onStateChanged?: (metadata: ToolExecutionMetadata) => void,
198
257
  ): (input: ToolHookInput) => Promise<void> {
199
258
  const storesByState = new WeakMap<AuditState, FindingStore>()
200
259
 
@@ -221,6 +280,22 @@ export function createToolTrackingHook(
221
280
 
222
281
  const { state: auditState, store } = resolved
223
282
 
283
+ // Handle argus_skill_load first — it returns markdown, not JSON
284
+ if (input.tool === "argus_skill_load") {
285
+ // Extract skill name from markdown header: "## Argus Skill: {name} [Source: ...]"
286
+ const nameMatch = input.result.match(/^##\s+Argus Skill:\s+(.+?)(?:\s+\[|$)/m)
287
+ const skillName = nameMatch?.[1]?.trim()
288
+ if (skillName) {
289
+ auditState.skillsLoaded ??= []
290
+ if (!auditState.skillsLoaded.includes(skillName)) {
291
+ auditState.skillsLoaded.push(skillName)
292
+ }
293
+ }
294
+ recordToolExecution(auditState, input.tool, 0)
295
+ onStateChanged?.({ tool: input.tool, findingsCount: 0 })
296
+ return
297
+ }
298
+
224
299
  let parsed: unknown
225
300
  try {
226
301
  parsed = JSON.parse(input.result)
@@ -243,12 +318,70 @@ export function createToolTrackingHook(
243
318
  case "argus_analyze_contract":
244
319
  processContractAnalyzerResult(record, auditState)
245
320
  break
321
+ case "argus_solodit_search":
322
+ processSoloditResult(record, auditState)
323
+ break
246
324
  case "argus_forge_test":
325
+ break
247
326
  case "argus_forge_fuzz":
248
- // No findings to extract — counterexamples are informational
327
+ processFuzzResult(record, auditState)
328
+ break
329
+ case "argus_generate_report": {
330
+ auditState.reportGenerated = true
331
+ break
332
+ }
333
+ case "argus_sync_knowledge": {
334
+ const success = record.success === true
335
+ auditState.knowledgeSynced = { success, timestamp: Date.now() }
336
+ break
337
+ }
338
+ case "argus_forge_coverage": {
339
+ const reportObj = toRecord(record.report)
340
+ const files = reportObj?.files
341
+ if (Array.isArray(files)) {
342
+ auditState.coverageReport = {
343
+ files: files
344
+ .filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
345
+ .map((f) => ({
346
+ path: typeof f.path === "string" ? f.path : "unknown",
347
+ linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
348
+ statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
349
+ branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
350
+ functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
351
+ })),
352
+ }
353
+ }
249
354
  break
355
+ }
356
+ case "argus_proxy_detection": {
357
+ if (record.isProxy === true) {
358
+ auditState.proxyContracts ??= []
359
+ auditState.proxyContracts.push({
360
+ file: typeof record.file === "string" ? record.file : "unknown",
361
+ proxyType: typeof record.proxyType === "string" ? record.proxyType : "unknown",
362
+ indicators: Array.isArray(record.indicators)
363
+ ? record.indicators.filter((i): i is string => typeof i === "string")
364
+ : [],
365
+ })
366
+ }
367
+ break
368
+ }
369
+ case "argus_gas_analysis": {
370
+ const hotspots = record.hotspots
371
+ if (Array.isArray(hotspots)) {
372
+ auditState.gasHotspots = hotspots
373
+ .filter((h): h is Record<string, unknown> => !!h && typeof h === "object")
374
+ .map((h) => ({
375
+ contract: typeof h.contract === "string" ? h.contract : "unknown",
376
+ function: typeof h.function === "string" ? h.function : "unknown",
377
+ avgGas: typeof h.avgGas === "number" ? h.avgGas : 0,
378
+ }))
379
+ }
380
+ break
381
+ }
250
382
  }
251
383
 
252
384
  recordToolExecution(auditState, input.tool, findingsCount)
385
+ onStateChanged?.({ tool: input.tool, findingsCount })
253
386
  }
254
387
  }
@@ -12,4 +12,5 @@ export type HookName =
12
12
  | "tool-error-recovery"
13
13
  | "context-window-monitor"
14
14
  | "tool-output-truncator"
15
- | "audit-continuation";
15
+ | "audit-continuation"
16
+ | "system-prompt"
package/src/index.ts CHANGED
@@ -1,20 +1,12 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
2
  import { loadArgusConfig } from "./config/loader"
3
- import { createHookGuard } from "./hooks/hook-system"
4
- import { createTools } from "./create-tools"
5
3
  import { createHooks } from "./create-hooks"
6
4
  import { createManagers } from "./create-managers"
5
+ import { createTools } from "./create-tools"
6
+ import type { Dispatcher } from "./features/background-agent/background-manager"
7
+ import { createHookGuard } from "./hooks/hook-system"
7
8
  import { createPluginInterface } from "./plugin-interface"
8
-
9
- function startSoloditMcp(port: number): void {
10
- const child = Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
11
- stdin: "ignore",
12
- stdout: "ignore",
13
- stderr: "ignore",
14
- env: { ...process.env, PORT: String(port) },
15
- })
16
- child.unref()
17
- }
9
+ import { startSoloditMcp } from "./solodit-lifecycle"
18
10
 
19
11
  const ArgusPlugin: Plugin = async (ctx) => {
20
12
  const projectDir = ctx.directory ?? process.cwd()
@@ -25,7 +17,25 @@ const ArgusPlugin: Plugin = async (ctx) => {
25
17
  }
26
18
 
27
19
  const isHookEnabled = createHookGuard(config.disabled_hooks)
28
- const managers = createManagers({ projectDir, config })
20
+ const taskCandidate = (ctx as Record<string, unknown>).task
21
+ const backgroundDispatcher: Dispatcher | undefined =
22
+ typeof taskCandidate === "function"
23
+ ? async (agentName: string, prompt: string) => {
24
+ const result = await taskCandidate(agentName, prompt)
25
+ if (typeof result === "string") {
26
+ return result
27
+ }
28
+ if (typeof result === "object" && result !== null) {
29
+ const taskId = (result as Record<string, unknown>).task_id
30
+ if (typeof taskId === "string") {
31
+ return taskId
32
+ }
33
+ }
34
+ return `task-${Date.now()}`
35
+ }
36
+ : undefined
37
+
38
+ const managers = createManagers({ projectDir, config, backgroundDispatcher })
29
39
  const tools = createTools(config)
30
40
  const hooks = createHooks({ config, managers, projectDir, isHookEnabled })
31
41
 
@@ -0,0 +1,53 @@
1
+ export interface RetryOptions<T> {
2
+ maxAttempts: number
3
+ baseDelayMs: number
4
+ shouldRetry: (error: unknown) => boolean
5
+ onRetry?: (attempt: number, error: unknown) => void
6
+ _valueType?: T
7
+ }
8
+
9
+ export interface RetryResult<T> {
10
+ success: boolean
11
+ value?: T
12
+ error?: unknown
13
+ attempts: number
14
+ }
15
+
16
+ function sleep(delayMs: number): Promise<void> {
17
+ return new Promise((resolve) => setTimeout(resolve, delayMs))
18
+ }
19
+
20
+ export async function withRetry<T>(
21
+ fn: () => Promise<T>,
22
+ options: RetryOptions<T>,
23
+ ): Promise<RetryResult<T>> {
24
+ const maxAttempts = options.maxAttempts > 0 ? options.maxAttempts : 1
25
+ let lastError: unknown
26
+
27
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
28
+ try {
29
+ const value = await fn()
30
+ return { success: true, value, attempts: attempt }
31
+ } catch (error) {
32
+ lastError = error
33
+ const canRetry = attempt < maxAttempts && options.shouldRetry(error)
34
+
35
+ if (!canRetry) {
36
+ return { success: false, error, attempts: attempt }
37
+ }
38
+
39
+ if (options.onRetry) {
40
+ options.onRetry(attempt, error)
41
+ }
42
+
43
+ const delay = options.baseDelayMs * 2 ** (attempt - 1)
44
+ await sleep(delay)
45
+ }
46
+ }
47
+
48
+ return {
49
+ success: false,
50
+ error: lastError,
51
+ attempts: maxAttempts,
52
+ }
53
+ }