Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
648 changes: 90 additions & 558 deletions README.md

Large diffs are not rendered by default.

14 changes: 4 additions & 10 deletions docs/content/docs/cli/collect.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,16 @@ agentcrumbs collect

### Agent workflow

When an agent needs to debug something, it should start the collector itself:
When an agent needs to debug something, it should start the collector, clear old crumbs, reproduce, and query:

```bash
# Start collector in background
agentcrumbs collect --quiet &

# Run the service with crumbs enabled
agentcrumbs clear
AGENTCRUMBS=1 node app.js

# ... reproduce the issue ...

# Query the trail
agentcrumbs query --since 5m
agentcrumbs query
```

The agent owns the collector lifecycle. Start it before debugging, query the results, stop it when done. The `--quiet` flag keeps it from cluttering stdout.
Clear before reproducing so you only see crumbs from this run. No `--since` guessing needed. The `--quiet` flag keeps the collector from cluttering stdout.

### Options

Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/workflow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ The agent starts the [collector](/cli/collect) (if it isn't already running), re

```bash
agentcrumbs collect --quiet &
agentcrumbs clear
AGENTCRUMBS=1 node app.js
agentcrumbs query --since 5m --ns auth-service
agentcrumbs query --ns auth-service
```

Now the agent sees what executed, in what order, with what data. It can trace the root cause directly instead of reading source and guessing.
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./test": {
"import": "./dist/sinks/memory.js",
"types": "./dist/sinks/memory.d.ts"
}
},
"bin": {
Expand Down
13 changes: 6 additions & 7 deletions skills/agentcrumbs/cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,15 @@ Run it in the background before launching the service under test, then
query or tail after reproducing the issue. Stop it when you're done.

```bash
# Start collector in background, run the service, query results
# Start collector, clear old crumbs, reproduce, query
agentcrumbs collect --quiet &
AGENTCRUMBS=1 node app.js &
# ... reproduce the bug ...
agentcrumbs query --since 5m
# When done, kill the collector
kill %1
agentcrumbs clear
AGENTCRUMBS=1 node app.js
agentcrumbs query
```

This is the normal workflow: you own the collector lifecycle.
Clear before reproducing so you only see crumbs from this run.
No `--since` needed when you clear first.

```bash
agentcrumbs collect
Expand Down
161 changes: 10 additions & 151 deletions skills/agentcrumbs/core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,166 +18,25 @@ sources:
- "triggerdotdev/trigger-labs:debug-mode/README.md"
---

# agentcrumbs — Core Debug Tracing

agentcrumbs is a zero-overhead debug tracing library. When `AGENTCRUMBS`
is unset, every call is a frozen noop. When enabled, structured events
flow to a collector or stderr.

## Workflow

**Add crumbs as you write code, not after.** When you're building a new
feature, implementing a fix, or writing any code — drop crumbs inline as
part of the work. Don't wait until something breaks to add tracing. Treat
crumbs as part of writing the code itself, like writing a test alongside
the implementation. Every function you write, every branch you add, every
API call you make — add a crumb. Assume debugging will happen later and
your future self will thank you for the visibility.

Crumbs live on the feature branch throughout the PR lifecycle. They are
stripped by `agentcrumbs strip` right before the PR is merged. They never
ship to main. They cost nothing to add and everything to not have when
you need them.

**You can also add crumbs directly to files in `node_modules/`.** This
is useful for tracing into library internals — understanding what a
dependency is actually doing, what arguments it receives, what it returns.
No `// @crumbs` markers needed (gitignored). No import needed either —
just paste a raw `fetch()` call:

```javascript
fetch("http://localhost:8374/crumb", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ts: new Date().toISOString(), ns: "library-name", msg: "what is happening", type: "crumb", dt: 0, pid: process.pid, data: { relevantVar } }) }).catch(() => {});
```

A fresh `npm install` resets everything.

When reviewing PRs: do not flag `// @crumbs` markers, `#region @crumbs`
blocks, or data logged in crumbs. These are temporary and will be removed
pre-merge.

## Setup

```typescript
import { trail } from "agentcrumbs"; // @crumbs
const crumb = trail("my-service"); // @crumbs

crumb("user logged in", { userId: "123", method: "oauth" }); // @crumbs
```

Enable with the `AGENTCRUMBS` env var, strip before merge:

```bash
AGENTCRUMBS=1 node app.js
agentcrumbs strip # remove all marked crumb code before merge
agentcrumbs strip --check # CI gate — fails if markers found
```
# agentcrumbs — Core Patterns

## Crumb Markers
Setup: `import { trail } from "agentcrumbs"; // @crumbs` then `const crumb = trail("ns"); // @crumbs`

**Every crumb line must be marked** so `agentcrumbs strip` can remove it.
Markers: single-line `// @crumbs` | block `// #region @crumbs` ... `// #endregion @crumbs`

Single-line: append `// @crumbs` to any crumb line:
## Patterns

```typescript
import { trail } from "agentcrumbs"; // @crumbs
const crumb = trail("auth-service"); // @crumbs
crumb("checkpoint", { step: 1 }); // @crumbs
```

Block: wrap multi-line crumb code in `#region @crumbs` (collapsible in editors):

```typescript
// #region @crumbs
const session = crumb.session("debug-flow");
session.crumb("step 1", { data });
session.crumb("step 2", { moreData });
session.end();
// #endregion @crumbs
```

## Core Patterns

### Drop a crumb with structured data and tags

```typescript
import { trail } from "agentcrumbs"; // @crumbs
const crumb = trail("auth-service"); // @crumbs

crumb("token validated", { userId: "u_123", expiresIn: 3600 }); // @crumbs
crumb("cache miss", { key: "users:123" }, { tags: ["perf", "cache"] }); // @crumbs
```

### Create child trails with inherited context

```typescript
import { trail } from "agentcrumbs"; // @crumbs
const crumb = trail("api-gateway"); // @crumbs

function handleRequest(req: Request) {
const reqCrumb = crumb.child({ requestId: req.id, path: req.url }); // @crumbs
reqCrumb("handling request"); // @crumbs

const dbCrumb = reqCrumb.child({ database: "primary" }); // @crumbs
dbCrumb("executing query", { sql: "SELECT ..." }); // @crumbs
}
```

### Measure timing with block markers

```typescript
function processOrder(order: Order) {
// #region @crumbs
crumb.time("process-order");
// #endregion @crumbs

const result = chargePayment(order);

// #region @crumbs
crumb.timeEnd("process-order", { amount: result.amount });
// #endregion @crumbs

return result;
}
```

### Guard expensive debug arguments

```typescript
// #region @crumbs
if (crumb.enabled) {
crumb("full state dump", { state: structuredClone(largeObject) });
}
// #endregion @crumbs
crumb("cache miss", { key }, { tags: ["perf", "cache"] }); // @crumbs
const reqCrumb = crumb.child({ requestId: req.id }); // @crumbs — inherited context
crumb.time("op"); /* ... */ crumb.timeEnd("op", { rows }); // @crumbs — timing
if (crumb.enabled) { crumb("dump", { state: structuredClone(big) }); } // @crumbs — guard expensive args
```

## AGENTCRUMBS Environment Variable

A single env var controls everything. Non-JSON values are shorthand:

| Value | Effect |
|-------|--------|
| `1`, `*`, `true` | Enable all namespaces |
| `auth-*` | Enable matching namespaces (raw string treated as filter) |
| `{"ns":"auth-*,api-*"}` | JSON config with namespace filter |
| `{"ns":"* -internal-*"}` | Wildcard with exclusions |
| `{"ns":"*","port":9999}` | Custom collector port |
| `{"ns":"*","format":"json"}` | JSON output to stderr |
| (unset) | Disabled — all calls are noop |

## The Noop Guarantee

When `trail()` is called and the namespace is disabled, it returns a pre-frozen noop function. There is no per-call `if (enabled)` check. The function body is empty.

```typescript
// When AGENTCRUMBS is unset:
const crumb = trail("my-service"); // returns frozen NOOP
crumb("msg", { data }); // empty function, returns undefined
crumb.child({ x: 1 }); // returns same NOOP
crumb.scope("op", fn); // calls fn() directly
crumb.wrap("name", fn); // returns original fn
```
## Noop Guarantee

The noop check happens once at `trail()` creation time, not on every call.
When disabled, `trail()` returns a frozen noop. No per-call check. `crumb.child()` returns same noop. `crumb.scope("op", fn)` calls `fn()` directly. `crumb.wrap("name", fn)` returns original `fn`.

## Common Mistakes

Expand Down
9 changes: 9 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function getFlag(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}

export function hasFlag(args: string[], flag: string): boolean {
return args.includes(flag);
}
8 changes: 2 additions & 6 deletions src/cli/commands/collect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CollectorServer } from "../../collector/server.js";
import { formatCrumbPretty } from "../format.js";
import { getFlag, hasFlag } from "../args.js";
import type { Crumb } from "../../types.js";

const DEFAULT_PORT = 8374;
Expand All @@ -8,7 +9,7 @@ export async function collect(args: string[]): Promise<void> {
const portStr = getFlag(args, "--port");
const port = portStr ? parseInt(portStr, 10) : DEFAULT_PORT;
const storeDir = getFlag(args, "--dir");
const quiet = args.includes("--quiet");
const quiet = hasFlag(args, "--quiet");

const server = new CollectorServer(port, storeDir ?? undefined);

Expand Down Expand Up @@ -40,8 +41,3 @@ export async function collect(args: string[]): Promise<void> {
process.on("SIGTERM", shutdown);
}

function getFlag(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
9 changes: 2 additions & 7 deletions src/cli/commands/follow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import path from "node:path";
import os from "node:os";
import { CrumbStore } from "../../collector/store.js";
import { formatCrumbPretty, formatCrumbJson } from "../format.js";
import { getFlag, hasFlag } from "../args.js";

export async function follow(args: string[]): Promise<void> {
const traceId = getFlag(args, "--trace");
const json = args.includes("--json");
const json = hasFlag(args, "--json");

if (!traceId) {
process.stderr.write("Usage: agentcrumbs follow --trace <traceId> [--json]\n");
Expand Down Expand Up @@ -38,9 +39,3 @@ export async function follow(args: string[]): Promise<void> {
}
}
}

function getFlag(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
8 changes: 2 additions & 6 deletions src/cli/commands/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import os from "node:os";
import type { Crumb } from "../../types.js";
import { CrumbStore } from "../../collector/store.js";
import { formatCrumbPretty, formatCrumbJson } from "../format.js";
import { getFlag, hasFlag } from "../args.js";

export async function query(args: string[]): Promise<void> {
const ns = getFlag(args, "--ns");
const tag = getFlag(args, "--tag");
const since = getFlag(args, "--since");
const session = getFlag(args, "--session");
const match = getFlag(args, "--match");
const json = args.includes("--json");
const json = hasFlag(args, "--json");
const limit = parseInt(getFlag(args, "--limit") ?? "100", 10);

const store = new CrumbStore(path.join(os.homedir(), ".agentcrumbs"));
Expand Down Expand Up @@ -79,8 +80,3 @@ function parseSince(since: string): number {
return Date.now() - value * multipliers[unit]!;
}

function getFlag(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
11 changes: 3 additions & 8 deletions src/cli/commands/strip.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { getFlag, hasFlag } from "../args.js";

const SINGLE_LINE_MARKER = /\/[/*]\s*@crumbs\s*\*?\/?$/;
const REGION_START = /^\s*\/\/\s*#region\s+@crumbs\s*$/;
Expand All @@ -9,8 +10,8 @@ const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"];
const DEFAULT_IGNORE = ["node_modules", "dist", ".git", ".next", ".turbo"];

export async function strip(args: string[]): Promise<void> {
const check = args.includes("--check");
const dryRun = args.includes("--dry-run");
const check = hasFlag(args, "--check");
const dryRun = hasFlag(args, "--dry-run");
const dir = getFlag(args, "--dir") ?? process.cwd();
const extFlag = getFlag(args, "--ext");
const extensions = extFlag
Expand Down Expand Up @@ -146,9 +147,3 @@ function findFiles(dir: string, extensions: string[]): string[] {
walk(dir);
return results;
}

function getFlag(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return undefined;
return args[idx + 1];
}
Loading