> For the complete documentation index, see [llms.txt](https://docs.ergo.services/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ergo.services/tools/argus.md).

# Actor Model Vet Tool

The actor model rests on rules the Go compiler cannot check. A message must not share memory with its sender. A callback must not block without a bound. A goroutine started inside a callback must not reach back into actor state. Break any of these and the code compiles, the tests pass, and the failure arrives later - as a race under load, as a mailbox that never drains, as a node that reports itself healthy while serving nothing.

`argus` reads your packages and reports those breaks. It is a vet tool, so it runs the way `go vet` runs: over the build graph, with your build tags, cached per package.

## Installation

```
go install ergo.tools/argus@latest
```

## Running it

In production, as a vet tool. The go command drives it and caches the results:

```
go vet -vettool=$(which argus) ./...
```

During development, directly over package patterns:

```
argus ./...
```

Package patterns resolve against the main module of the working directory, exactly as the go command resolves them. To analyse a module you are not standing in, move there first:

```
argus -C ../../application/observer ./...
```

## Reading a finding

```
gen/cron_action.go:103:4: [tier2] [A2011] []any is passed to Spawn and shares
unsynchronized memory with the parent: []interface{} -> any; the child runs on its
own goroutine, so pass a copy or a type that guards itself
```

Every finding carries a tier and a rule ID. The tier says how much to trust it, and decides whether it fails your build:

| Tier | Default | Meaning                                                                     |
| ---- | ------- | --------------------------------------------------------------------------- |
| 1    | error   | The invariant is broken. The failure is a matter of timing, not of whether. |
| 2    | warning | The construct is wrong often enough to look at, and legitimate sometimes.   |
| 3    | off     | Style and hygiene. Opt in when you want it.                                 |

The rule ID explains itself:

```
argus help          every rule in this build
argus help A1002    one rule in full - what it reports, what it deliberately does not
```

Read that second command before arguing with a finding. Each rule documents its own blind spots, and several of them are narrower than their titles suggest.

## Rules

Forty-four rules in this build, grouped by what they protect.

**A1xxx - the actor model itself.** Shared memory in a message (A1001), an unbounded wait in a callback (A1002), a call to your own identity (A1003), a goroutine touching actor state (A1004), meta state written from both meta goroutines (A1005), sending a field of your own state (A1006), internal memory escaping across an actor boundary (A1007), `HandleCall` returning an error in the reason slot (A1008), a goroutine with no panic boundary (A1010), a blocking request in `Init` against the init budget (A1011), routing through the node handle from inside an actor (A1012).

**A2xxx - using the framework as it works.** A state-gated API called where its state forbids it (A2001), a discarded result hiding a certain failure (A2001a), a call the runtime refuses on its arguments whatever the state (A2026), a `HandleCall` that can never reply (A2002) or replies twice (A2003), a supervisor spec the framework rejects at init (A2005), router or pool options it rejects the same way (A2025), timer misuse (A2006), a self-timer chain armed from two places (A2015), an event that can never be published (A2007), a web request never completed (A2008), a spawn argument shared with the parent (A2011), logging a transient failure and then returning it (A2012), discarding the buffered events a subscription returns (A2013), a round trip in `Terminate` (A2014), `Terminate` dereferencing what `Init` may never have assigned (A2014a), identity established in a handler `Init` sent to itself (A2016), `Init` returning a framework sentinel as control flow (A2017), an application `Init` that returns an error after taking a resource nothing releases (A2023), `Notify` set while the producer handles neither start nor stop (A2018), a meta `Start` that returns at once and so ends the meta (A2027).

Two of that group are about how long a callback holds its mailbox. A round trip inside a callback parks the mailbox for everyone queued behind it (A2024). A request made inside `HandleCall` on the default timeout has no budget at all, because the caller is waiting on the same five seconds and will have given up before the reply exists (A2028) - the fix is either a smaller inner timeout or the deferred reply of [Sync Request Handling](https://github.com/ergo-services/ergo/tree/master/docs/advanced/handle-sync.md).

Five are about what survives the trip to another node: a message type that cannot be serialized (A2004), a type registration that fails at startup (A2009), a type reachable from a registered one that nobody registers (A2021), an `fmt.Errorf` value carrying `%w` put on the wire, which arrives as text with its identity gone (A2020), and a sentinel put on the wire that this node never registered (A2022).

**A3xxx - conventions.** A message type with no marker (A3001), a message type missing from the registration list (A3003), suppression debt (A3005), prose that should be a marker (A3006), a compression threshold below the framework floor (A3007).

The numbering has gaps: `A1009`, `A2010`, `A2019`, `A3002` and `A3004` are not in this build. An identifier is permanent once published, because it appears in configuration files, baselines and source directives, so a rule that is retired - or designed and not shipped - keeps its number rather than having it reused.

## Adopting it on existing code

The first run on a codebase that has never seen the tool reports findings that are all true and none of which are getting fixed today. Take a baseline, and the run goes quiet until something new appears:

```
argus -argusmodel.keys ./... 2>&1 | argus baseline > argus-baseline.json
```

Findings already in the baseline stay silent. Anything new is reported.

## What to expect on real code

A codebase written before the tool existed reports plenty. `application/observer` reports A1001 broadly, because any message carrying a `map[string]any` or a slice shares memory on local delivery, and local delivery does not copy. Those findings are correct; whether they matter depends on whether the sender touches the value afterwards.

That is the tool's shape in general: it reports what it can prove about the construct, and leaves the judgement about your specific case to you. Read new findings as signal, not as verdicts.
